From 378bf64fd5c6aa8e3afc1e3faf544eeed6940dac Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 1 Sep 2025 09:40:21 +0200 Subject: [PATCH 1/5] Replace rebar3_format with efmt, improve emacs indentation script Revert partially "Add Makefile targets to format and indent source code" This reverts commit 3bda8582252c7a07fc848003e8facb0fad566431. --- Makefile.in | 11 +++++++--- configure.ac | 2 ++ rebar.config | 2 +- tools/emacs-indent.sh | 29 ++++++++++++++++++++----- tools/rebar3-format.sh | 49 +++++++++++------------------------------- 5 files changed, 47 insertions(+), 46 deletions(-) diff --git a/Makefile.in b/Makefile.in index cf7480702..305c12826 100644 --- a/Makefile.in +++ b/Makefile.in @@ -13,6 +13,7 @@ SED = @SED@ ERL = @ERL@ EPMD = @EPMD@ IEX = @IEX@ +EMACS = @EMACS@ INSTALLUSER=@INSTALLUSER@ INSTALLGROUP=@INSTALLGROUP@ @@ -266,11 +267,15 @@ _build/edoc/logo.png: edoc_compile #' format / indent # +.SILENT: format indent + +FORMAT_LOG=/tmp/ejabberd-format.log + format: - tools/rebar3-format.sh $(REBAR3) + tools/rebar3-format.sh $(FORMAT_LOG) $(REBAR3) indent: - tools/emacs-indent.sh + tools/emacs-indent.sh $(FORMAT_LOG) $(EMACS) #. #' copy-files @@ -714,7 +719,7 @@ help: @echo " translations Extract translation files" @echo " TAGS Generate tags file for text editors" @echo "" - @echo " format Format source code using rebar3_format" + @echo " format Format source code using efmt [rebar3]" @echo " indent Indent source code using erlang-mode [emacs]" @echo "" @echo " dialyzer Run Dialyzer static analyzer" diff --git a/configure.ac b/configure.ac index b91595dc5..d3b549ea2 100644 --- a/configure.ac +++ b/configure.ac @@ -88,6 +88,8 @@ AC_PATH_PROG([ESCRIPT], [escript], [], [$ERLANG_ROOT_DIR/bin]) #locating make AC_CHECK_PROG([MAKE], [make], [make], []) +AC_PATH_TOOL(EMACS, emacs, , [${extra_erl_path}$PATH]) + if test "x$ESCRIPT" = "x"; then AC_MSG_ERROR(['escript' was not found]) fi diff --git a/rebar.config b/rebar.config index e3d42edce..a35b3bf0f 100644 --- a/rebar.config +++ b/rebar.config @@ -164,7 +164,7 @@ {branch, "consolidation_fix"}}} }]}}. {if_rebar3, {project_plugins, [configure_deps, - {if_var_true, tools, rebar3_format}, + {if_var_true, tools, rebar3_efmt}, {if_var_true, tools, {rebar3_lint, "4.1.1"}} ]}}. {if_not_rebar3, {plugins, [ diff --git a/tools/emacs-indent.sh b/tools/emacs-indent.sh index f3caecf4c..73d84470d 100755 --- a/tools/emacs-indent.sh +++ b/tools/emacs-indent.sh @@ -1,18 +1,29 @@ -#!/bin/bash +#!/bin/sh # To indent and remove tabs, surround the piece of code with: # %% @indent-begin # %% @indent-end # +# Install Emacs and erlang-mode. For example in Debian: +# apt-get install emacs erlang-mode +# # Then run: # make indent # -# Please note this script only indents the first occurrence. +# Please note this script only indents the first occurrence per file -FILES=$(git grep --name-only @indent-begin src/) +FILES=$(git grep --name-only @indent-begin include/ src/) +LOG=${1:-/tmp/ejabberd-format.log} +EMACS=${2:-emacs} + +if [ ! "$EMACS" ] || [ ! -x "$EMACS" ] +then + echo "==> Cannot indent source code because Emacs is not installed" + exit 1 +fi for FILENAME in $FILES; do - echo "==> Indenting $FILENAME..." + echo "==> Indenting $FILENAME..." >>$LOG emacs -batch $FILENAME \ -f "erlang-mode" \ --eval "(goto-char (point-min))" \ @@ -23,5 +34,13 @@ for FILENAME in $FILES; do --eval "(erlang-indent-region begin end)" \ --eval "(untabify begin end)" \ -f "delete-trailing-whitespace" \ - -f "save-buffer" + -f "save-buffer" >>$LOG 2>&1 done + +grep -q 'Error' $LOG \ + && cat $LOG +grep -q 'Error: void-function (erlang-mode)' $LOG \ + && echo \ + && echo "==> Maybe you need to install erlang-mode system package" \ + && exit 1 +rm $LOG diff --git a/tools/rebar3-format.sh b/tools/rebar3-format.sh index 2181b6870..2126f0a57 100755 --- a/tools/rebar3-format.sh +++ b/tools/rebar3-format.sh @@ -1,41 +1,16 @@ -#!/bin/bash +#!/bin/sh -# To start formatting a file, add a line that contains: -# @format-begin -# Formatting in that file can later be disabled adding another line with: -# @format-end -# -# It can be reenabled again later in the file. -# -# Finally, call: make format +# To format the source code, simply run: +# make format -REBAR=$1 +LOG=${1:-/tmp/ejabberd-format.log} +REBAR3=${2:-rebar3} -FORMAT() -{ -FPATH=$1 -ERLS=$(git grep --name-only @format-begin "$FPATH"/) +$REBAR3 efmt -w --parallel >$LOG 2>&1 -for ERL in $ERLS; do - perl -n -e 'sub o { open(OUT, ">", sprintf("%s-format-%02d", $f, $n++));}; BEGIN{($f)=@ARGV;o()}; o() if /\@format-/; print OUT $_;' $ERL -done - -EFMTS=$(find "$FPATH"/*-format-* -type f -exec grep --files-with-matches "@format-begin" '{}' ';') -EFMTS2="" -for EFMT in $EFMTS; do - EFMTS2="$EFMTS2 --files $EFMT" -done -$REBAR format $EFMTS2 - -for ERL in $ERLS; do - SPLITS=$(find $ERL-format-* -type f) - rm $ERL - for SPLIT in $SPLITS; do - cat $SPLIT >> $ERL - rm $SPLIT - done -done -} - -FORMAT src -FORMAT test +if ! grep -q 'All files were formatted correctly' $LOG +then + cat $LOG + exit 1 +fi +rm $LOG From 2ec8fe2bc39b191f862d1c763d147b39d26aa8f6 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 1 Sep 2025 10:25:45 +0200 Subject: [PATCH 2/5] Indent instead of format some pieces of code that include pretty print --- include/ejabberd_commands.hrl | 2 ++ include/ejabberd_http.hrl | 2 ++ include/ejabberd_oauth.hrl | 2 ++ include/ejabberd_router.hrl | 3 +++ include/eldap.hrl | 2 ++ include/mod_antispam.hrl | 2 ++ include/mod_caps.hrl | 2 ++ include/mod_mam.hrl | 2 ++ include/mod_muc_room.hrl | 2 ++ include/mod_offline.hrl | 2 ++ include/mod_privacy.hrl | 2 ++ include/mod_push.hrl | 3 +++ include/mod_roster.hrl | 2 ++ include/mqtt.hrl | 3 +++ include/pubsub.hrl | 2 ++ src/ejabberd_auth_ldap.erl | 7 +++++++ src/ejabberd_bosh.erl | 8 ++++++++ src/ejabberd_ctl.erl | 7 +++++++ src/ejabberd_http_ws.erl | 10 +++++++++- src/ejabberd_piefxis.erl | 8 ++++++++ src/ejabberd_sql.erl | 8 ++++++++ src/ejabberd_update.erl | 7 +++++++ src/eldap.erl | 8 ++++++++ src/mod_antispam.erl | 8 ++++++++ src/mod_http_upload.erl | 8 ++++++++ src/mod_mqtt_mnesia.erl | 8 ++++++++ src/mod_stun_disco.erl | 8 ++++++++ src/mod_vcard_ldap.erl | 8 ++++++++ src/mqtt_codec.erl | 8 ++++++++ 29 files changed, 143 insertions(+), 1 deletion(-) diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 14d19d2e1..c08ec514b 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -type aterm() :: {atom(), atype()}. -type atype() :: integer | string | binary | any | atom | diff --git a/include/ejabberd_http.hrl b/include/ejabberd_http.hrl index 9e1373ce6..98ee4ee25 100644 --- a/include/ejabberd_http.hrl +++ b/include/ejabberd_http.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(request, {method :: method(), diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl index 4798d9070..0b6bbbfe8 100644 --- a/include/ejabberd_oauth.hrl +++ b/include/ejabberd_oauth.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(oauth_token, { token = <<"">> :: binary() | '_', diff --git a/include/ejabberd_router.hrl b/include/ejabberd_router.hrl index 060ab79a1..231d92f72 100644 --- a/include/ejabberd_router.hrl +++ b/include/ejabberd_router.hrl @@ -1,3 +1,6 @@ +%% @efmt:off +%% @indent-begin + -define(ROUTES_CACHE, routes_cache). -type local_hint() :: integer() | {apply, atom(), atom()}. diff --git a/include/eldap.hrl b/include/eldap.hrl index 0b6dc97e5..3eb9a7628 100644 --- a/include/eldap.hrl +++ b/include/eldap.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -define(LDAP_PORT, 389). diff --git a/include/mod_antispam.hrl b/include/mod_antispam.hrl index c30f24620..7aa512df1 100644 --- a/include/mod_antispam.hrl +++ b/include/mod_antispam.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -define(MODULE_ANTISPAM, mod_antispam). diff --git a/include/mod_caps.hrl b/include/mod_caps.hrl index ee1bbe44e..0046b5d79 100644 --- a/include/mod_caps.hrl +++ b/include/mod_caps.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(caps_features, {node_pair = {<<"">>, <<"">>} :: {binary(), binary()}, diff --git a/include/mod_mam.hrl b/include/mod_mam.hrl index 77ea54a5e..8d9e8e887 100644 --- a/include/mod_mam.hrl +++ b/include/mod_mam.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(archive_msg, {us = {<<"">>, <<"">>} :: {binary(), binary()}, diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index 1e90a7912..82f777880 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -define(MAX_USERS_DEFAULT, 200). diff --git a/include/mod_offline.hrl b/include/mod_offline.hrl index e1bb236f6..045eba08b 100644 --- a/include/mod_offline.hrl +++ b/include/mod_offline.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(offline_msg, {us = {<<"">>, <<"">>} :: {binary(), binary()}, diff --git a/include/mod_privacy.hrl b/include/mod_privacy.hrl index 8118a6de6..2da48552c 100644 --- a/include/mod_privacy.hrl +++ b/include/mod_privacy.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(privacy, {us = {<<"">>, <<"">>} :: {binary(), binary()}, default = none :: none | binary(), diff --git a/include/mod_push.hrl b/include/mod_push.hrl index 8a9de102b..f238a42f1 100644 --- a/include/mod_push.hrl +++ b/include/mod_push.hrl @@ -16,6 +16,9 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin + -record(push_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, timestamp = erlang:timestamp() :: erlang:timestamp(), diff --git a/include/mod_roster.hrl b/include/mod_roster.hrl index a056dd22c..5b807c11d 100644 --- a/include/mod_roster.hrl +++ b/include/mod_roster.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin -record(roster, { diff --git a/include/mqtt.hrl b/include/mqtt.hrl index bf910368f..a91e56804 100644 --- a/include/mqtt.hrl +++ b/include/mqtt.hrl @@ -15,6 +15,9 @@ %%% limitations under the License. %%% %%%------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin + -define(MQTT_VERSION_4, 4). -define(MQTT_VERSION_5, 5). diff --git a/include/pubsub.hrl b/include/pubsub.hrl index 316be342a..593c1175b 100644 --- a/include/pubsub.hrl +++ b/include/pubsub.hrl @@ -17,6 +17,8 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- +%% @efmt:off +%% @indent-begin %% ------------------------------- %% Pubsub constants diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 091e567a8..8656684aa 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -44,6 +44,9 @@ -include("eldap.hrl"). +%% +%% @efmt:off +%% @indent-begin -record(state, {host = <<"">> :: binary(), eldap_id = <<"">> :: binary(), @@ -61,6 +64,10 @@ deref_aliases = never :: never | searching | finding | always, dn_filter :: binary() | undefined, dn_filter_attrs = [] :: [binary()]}). +%% @indent-end +%% @efmt:on +%% + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), diff --git a/src/ejabberd_bosh.erl b/src/ejabberd_bosh.erl index 8d1dbd595..1e29114bd 100644 --- a/src/ejabberd_bosh.erl +++ b/src/ejabberd_bosh.erl @@ -81,6 +81,10 @@ -export_type([bosh_socket/0]). +%% +%% @efmt:off +%% @indent-begin + -record(state, {host = <<"">> :: binary(), sid = <<"">> :: binary(), @@ -109,6 +113,10 @@ els = [] :: [fxml_stream:xml_stream_el()], size = 0 :: non_neg_integer()}). +%% @indent-end +%% @efmt:on +%% + start(#body{attrs = Attrs} = Body, IP, SID) -> XMPPDomain = get_attr(to, Attrs), SupervisorProc = gen_mod:get_module_proc(XMPPDomain, mod_bosh), diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 88bd5e785..6422da520 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -774,6 +774,10 @@ print_usage_tags(Tag, MaxC, ShCode, Version) -> %% Print usage of 'help' command %%----------------------------- +%% +%% @efmt:off +%% @indent-begin + print_usage_help(MaxC, ShCode) -> LongDesc = ["This special ", ?C("help"), " command provides help of ejabberd commands.\n\n" @@ -812,6 +816,9 @@ print_usage_help(MaxC, ShCode) -> result = {help, string}}, print(get_usage_command2("help", C, MaxC, ShCode), []). +%% @indent-end +%% @efmt:on +%% %%----------------------------- %% Print usage command diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index c14ed2d58..0a7134349 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -40,6 +40,10 @@ -include("ejabberd_http.hrl"). +%% +%% @efmt:off +%% @indent-begin + -record(state, {socket :: ws_socket(), ping_interval :: non_neg_integer(), @@ -52,7 +56,11 @@ c2s_pid :: pid(), ws :: {#ws{}, pid()}}). -%-define(DBGFSM, true). +%% @indent-end +%% @efmt:on +%% + +%%%-define(DBGFSM, true). -ifdef(DBGFSM). diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 789be7359..caa8cfca0 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -56,12 +56,20 @@ -define(NS_PIEFXIS, <<"http://www.xmpp.org/extensions/xep-0227.html#ns">>). -define(NS_XI, <<"http://www.w3.org/2001/XInclude">>). +%% +%% @efmt:off +%% @indent-begin + -record(state, {xml_stream_state :: fxml_stream:xml_stream_state() | undefined, user = <<"">> :: binary(), server = <<"">> :: binary(), fd = self() :: file:io_device(), dir = <<"">> :: binary()}). +%% @indent-end +%% @efmt:on +%% + -type state() :: #state{}. %%File could be large.. we read it in chunks diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index 0c29f039e..bbb0b6765 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -91,6 +91,10 @@ -include("ejabberd_sql_pt.hrl"). -include("ejabberd_stacktrace.hrl"). +%% +%% @efmt:off +%% @indent-begin + -record(state, {db_ref :: undefined | db_ref_pid() | odbc_connection_reference(), db_type = odbc :: pgsql | mysql | sqlite | odbc | mssql, @@ -101,6 +105,10 @@ overload_reported :: undefined | integer(), timeout :: pos_integer()}). +%% @indent-end +%% @efmt:on +%% + -define(STATE_KEY, ejabberd_sql_state). -define(NESTING_KEY, ejabberd_sql_nesting_level). -define(TOP_LEVEL_TXN, 0). diff --git a/src/ejabberd_update.erl b/src/ejabberd_update.erl index 6c80fe8e7..ea2be1566 100644 --- a/src/ejabberd_update.erl +++ b/src/ejabberd_update.erl @@ -155,6 +155,10 @@ build_script(Dir, UpdatedBeams) -> end, {Script, LowLevelScript, Check1}. +%% +%% @efmt:off +%% @indent-begin + %% Copied from Erlang/OTP file: lib/sasl/src/systools.hrl -ifdef(SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL). -record(application, @@ -222,6 +226,9 @@ build_script(Dir, UpdatedBeams) -> }). -endif. +%% @indent-end +%% @efmt:on +%% make_script(UpdatedBeams) -> lists:map( diff --git a/src/eldap.erl b/src/eldap.erl index 3676bd09a..a0a2a5422 100644 --- a/src/eldap.erl +++ b/src/eldap.erl @@ -122,6 +122,10 @@ -type handle() :: pid() | atom() | binary(). +%% +%% @efmt:off +%% @indent-begin + -record(eldap, {version = ?LDAP_VERSION :: non_neg_integer(), hosts = [] :: [binary()], @@ -142,6 +146,10 @@ dict = dict:new() :: dict:dict(), req_q = queue:new() :: queue:queue()}). +%% @indent-end +%% @efmt:on +%% + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- diff --git a/src/mod_antispam.erl b/src/mod_antispam.erl index b2e7c5937..e07fb2197 100644 --- a/src/mod_antispam.erl +++ b/src/mod_antispam.erl @@ -71,6 +71,10 @@ -include_lib("xmpp/include/xmpp.hrl"). +%% +%% @efmt:off +%% @indent-begin + -record(state, {host = <<>> :: binary(), dump_fd = undefined :: file:io_device() | undefined, @@ -86,6 +90,10 @@ whitelist_domains = #{} :: #{binary() => false} }). +%% @indent-end +%% @efmt:on +%% + -type state() :: #state{}. -define(COMMAND_TIMEOUT, timer:seconds(30)). diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index faa085811..dfa194525 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -91,6 +91,10 @@ -include("logger.hrl"). -include("translate.hrl"). +%% +%% @efmt:off +%% @indent-begin + -record(state, {server_host = <<>> :: binary(), hosts = [] :: [binary()], @@ -116,6 +120,10 @@ height :: integer(), width :: integer()}). +%% @indent-end +%% @efmt:on +%% + -type state() :: #state{}. -type slot() :: [binary(), ...]. -type slots() :: #{slot() => {pos_integer(), reference()}}. diff --git a/src/mod_mqtt_mnesia.erl b/src/mod_mqtt_mnesia.erl index 5c2902d2b..62437d597 100644 --- a/src/mod_mqtt_mnesia.erl +++ b/src/mod_mqtt_mnesia.erl @@ -28,6 +28,10 @@ -include("logger.hrl"). -include("mqtt.hrl"). +%% +%% @efmt:off +%% @indent-begin + -record(mqtt_pub, {topic_server :: {binary(), binary()}, user :: binary(), resource :: binary(), @@ -50,6 +54,10 @@ pid :: pid() | '_', timestamp :: erlang:timestamp() | '_'}). +%% @indent-end +%% @efmt:on +%% + %%%=================================================================== %%% API %%%=================================================================== diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl index c210868e7..61942019e 100644 --- a/src/mod_stun_disco.erl +++ b/src/mod_stun_disco.erl @@ -62,6 +62,10 @@ -type host_or_hash() :: binary() | {hash, binary()}. -type service_type() :: stun | stuns | turn | turns | undefined. +%% +%% @efmt:off +%% @indent-begin + -record(request, {host :: binary() | inet:ip_address() | undefined, port :: 0..65535 | undefined, @@ -75,6 +79,10 @@ secret :: binary(), ttl :: non_neg_integer()}). +%% @indent-end +%% @efmt:on +%% + -type request() :: #request{}. -type state() :: #state{}. diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 3ecf39ba1..0d67fb564 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -45,6 +45,10 @@ -define(PROCNAME, ejabberd_mod_vcard_ldap). +%% +%% @efmt:off +%% @indent-begin + -record(state, {serverhost = <<"">> :: binary(), myhosts = [] :: [binary()], @@ -68,6 +72,10 @@ deref_aliases = never :: never | searching | finding | always, matches = 0 :: non_neg_integer()}). +%% @indent-end +%% @efmt:on +%% + %%%=================================================================== %%% API %%%=================================================================== diff --git a/src/mqtt_codec.erl b/src/mqtt_codec.erl index 1ef474dd5..c67adfb12 100644 --- a/src/mqtt_codec.erl +++ b/src/mqtt_codec.erl @@ -31,6 +31,10 @@ -define(MAX_UINT32, 4294967295). -define(MAX_VARINT, 268435456). +%% +%% @efmt:off +%% @indent-begin + -record(codec_state, {version :: undefined | mqtt_version(), type :: undefined | non_neg_integer(), flags :: undefined | non_neg_integer(), @@ -58,6 +62,10 @@ {{bad_flag, atom()}, char(), term()} | {{bad_flags, atom()}, char(), char()}. +%% @indent-end +%% @efmt:on +%% + -opaque state() :: #codec_state{}. -export_type([state/0, error_reason/0]). From da482a780aaa75ee029b38cb6b3510d3f0ce8ba2 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 1 Sep 2025 12:16:37 +0200 Subject: [PATCH 3/5] Help the code formatter when dealing with some lists and tuples --- .../configure_deps/src/configure_deps_prv.erl | 3 +- rebar.config | 45 ++++++++++++------- src/ejabberd_web_admin.erl | 4 +- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/_checkouts/configure_deps/src/configure_deps_prv.erl b/_checkouts/configure_deps/src/configure_deps_prv.erl index 91f2a3adc..9d91f7acd 100644 --- a/_checkouts/configure_deps/src/configure_deps_prv.erl +++ b/_checkouts/configure_deps/src/configure_deps_prv.erl @@ -10,7 +10,8 @@ %% =================================================================== -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> - Provider = providers:create([ + Provider = providers:create( + [ {namespace, default}, {name, ?PROVIDER}, % The 'user friendly' name of the task {module, ?MODULE}, % The module implementation of the task diff --git a/rebar.config b/rebar.config index a35b3bf0f..6cbd4d7b7 100644 --- a/rebar.config +++ b/rebar.config @@ -23,59 +23,74 @@ %%% {deps, [{if_not_rebar3, - {if_version_below, "24", + {if_version_below, + "24", {base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}} }}, {cache_tab, "~> 1.0.33", {git, "https://github.com/processone/cache_tab", {tag, "1.0.33"}}}, {eimp, "~> 1.0.26", {git, "https://github.com/processone/eimp", {tag, "1.0.26"}}}, - {if_var_true, pam, + {if_var_true, + pam, {epam, "~> 1.0.14", {git, "https://github.com/processone/epam", {tag, "1.0.14"}}}}, - {if_var_true, redis, + {if_var_true, + redis, {if_not_rebar3, {eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}} }}, - {if_var_true, redis, + {if_var_true, + redis, {if_rebar3, - {if_version_below, "21", + {if_version_below, + "21", {eredis, "1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}, {eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}} }}}, - {if_var_true, sip, + {if_var_true, + sip, {esip, "~> 1.0.59", {git, "https://github.com/processone/esip", {tag, "1.0.59"}}}}, - {if_var_true, zlib, + {if_var_true, + zlib, {ezlib, "~> 1.0.15", {git, "https://github.com/processone/ezlib", {tag, "1.0.15"}}}}, {fast_tls, "~> 1.1.25", {git, "https://github.com/processone/fast_tls", {tag, "1.1.25"}}}, {fast_xml, "~> 1.1.57", {git, "https://github.com/processone/fast_xml", {tag, "1.1.57"}}}, {fast_yaml, "~> 1.0.39", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.39"}}}, {idna, "~> 6.0", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}}, - {if_version_below, "27", + {if_version_below, + "27", {jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}} }, - {if_version_above, "23", + {if_version_above, + "23", {jose, "~> 1.11.10", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.10"}}}, {jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} }, - {if_version_below, "22", + {if_version_below, + "22", {lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}} }, - {if_var_true, lua, + {if_var_true, + lua, {if_version_below, "21", {luerl, "1.0.0", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}}, {luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}} }}, {mqtree, "~> 1.0.19", {git, "https://github.com/processone/mqtree", {tag, "1.0.19"}}}, {p1_acme, "~> 1.0.28", {git, "https://github.com/processone/p1_acme", {tag, "1.0.28"}}}, - {if_var_true, mysql, + {if_var_true, + mysql, {p1_mysql, "~> 1.0.26", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.26"}}}}, {p1_oauth2, "~> 0.6.14", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.14"}}}, - {if_var_true, pgsql, + {if_var_true, + pgsql, {p1_pgsql, "~> 1.1.35", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.35"}}}}, {p1_utils, "~> 1.0.28", {git, "https://github.com/processone/p1_utils", {tag, "1.0.28"}}}, {pkix, "~> 1.0.10", {git, "https://github.com/processone/pkix", {tag, "1.0.10"}}}, - {if_var_true, sqlite, + {if_var_true, + sqlite, {sqlite3, "~> 1.1.15", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.15"}}}}, {stringprep, "~> 1.0.33", {git, "https://github.com/processone/stringprep", {tag, "1.0.33"}}}, - {if_var_true, stun, + {if_var_true, + stun, {stun, "~> 1.2.21", {git, "https://github.com/processone/stun", {tag, "1.2.21"}}}}, {xmpp, "~> 1.11.1", {git, "https://github.com/processone/xmpp", {tag, "1.11.1"}}}, {yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}} diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index ea63d817a..64e93b38a 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -445,7 +445,9 @@ process_admin(global, #request{path = [], lang = Lang} = Request, AJID) -> [?XA(<<"img">>, [{<<"src">>, <<"logo.png">>}, {<<"style">>, <<"border-radius:10px; background:#49cbc1; padding: 1.1em;">>}]) ]) - ] ++ Title ++ [ + ] ++ + Title ++ + [ ?XAE(<<"blockquote">>, [{<<"id">>, <<"welcome">>}], [?XC(<<"p">>, <<"Welcome to ejabberd's WebAdmin!">>), From 6b7d15f0271686b6902a0edc82f407e529b35a90 Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 3 Sep 2025 12:39:11 +0200 Subject: [PATCH 4/5] Result of running "make format indent" for the first time --- _checkouts/configure_deps/rebar.config | 2 +- .../configure_deps/src/configure_deps.app.src | 5 +- .../configure_deps/src/configure_deps.erl | 1 + .../configure_deps/src/configure_deps_prv.erl | 38 +- include/ELDAPv3.hrl | 126 +- include/bosh.hrl | 25 +- include/ejabberd_auth.hrl | 6 +- include/ejabberd_commands.hrl | 1 - include/ejabberd_sm.hrl | 11 +- include/ejabberd_sql.hrl | 92 +- include/ejabberd_web_admin.hrl | 92 +- include/http_bind.hrl | 25 +- include/logger.hrl | 93 +- include/mod_announce.hrl | 12 +- include/mod_last.hrl | 8 +- include/mod_matrix_gw.hrl | 32 +- include/mod_muc.hrl | 32 +- include/mod_private.hrl | 12 +- include/mod_proxy65.hrl | 8 +- include/mod_shared_roster.hrl | 12 +- include/mod_vcard.hrl | 38 +- plugins/configure_deps.erl | 3 +- plugins/deps_erl_opts.erl | 5 +- plugins/override_deps_versions2.erl | 104 +- plugins/override_opts.erl | 58 +- rebar.config | 194 +- rel/files/install_upgrade.escript | 27 +- rel/relive.escript | 2 + src/ELDAPv3.erl | 4190 ++++---- src/acl.erl | 209 +- src/econf.erl | 467 +- src/ejabberd.erl | 112 +- src/ejabberd_access_permissions.erl | 402 +- src/ejabberd_acme.erl | 742 +- src/ejabberd_admin.erl | 2012 ++-- src/ejabberd_app.erl | 207 +- src/ejabberd_auth.erl | 1556 +-- src/ejabberd_auth_anonymous.erl | 185 +- src/ejabberd_auth_external.erl | 80 +- src/ejabberd_auth_jwt.erl | 81 +- src/ejabberd_auth_ldap.erl | 357 +- src/ejabberd_auth_mnesia.erl | 370 +- src/ejabberd_auth_pam.erl | 55 +- src/ejabberd_auth_sql.erl | 561 +- src/ejabberd_backend_sup.erl | 2 + src/ejabberd_batch.erl | 179 +- src/ejabberd_bosh.erl | 1379 +-- src/ejabberd_c2s.erl | 1447 +-- src/ejabberd_c2s_config.erl | 22 +- src/ejabberd_captcha.erl | 812 +- src/ejabberd_cluster.erl | 93 +- src/ejabberd_cluster_mnesia.erl | 69 +- src/ejabberd_commands.erl | 272 +- src/ejabberd_commands_doc.erl | 558 +- src/ejabberd_config.erl | 858 +- src/ejabberd_config_transformer.erl | 770 +- src/ejabberd_ctl.erl | 899 +- src/ejabberd_db_sup.erl | 2 + src/ejabberd_doc.erl | 270 +- src/ejabberd_hooks.erl | 614 +- src/ejabberd_http.erl | 1167 ++- src/ejabberd_http_ws.erl | 309 +- src/ejabberd_iq.erl | 117 +- src/ejabberd_listener.erl | 939 +- src/ejabberd_local.erl | 91 +- src/ejabberd_logger.erl | 359 +- src/ejabberd_mnesia.erl | 537 +- src/ejabberd_oauth.erl | 605 +- src/ejabberd_oauth_mnesia.erl | 61 +- src/ejabberd_oauth_rest.erl | 88 +- src/ejabberd_oauth_sql.erl | 155 +- src/ejabberd_old_config.erl | 400 +- src/ejabberd_option.erl | 437 +- src/ejabberd_options.erl | 204 +- src/ejabberd_options_doc.erl | 1424 ++- src/ejabberd_piefxis.erl | 435 +- src/ejabberd_pkix.erl | 382 +- src/ejabberd_redis.erl | 689 +- src/ejabberd_redis_sup.erl | 82 +- src/ejabberd_regexp.erl | 34 +- src/ejabberd_router.erl | 562 +- src/ejabberd_router_mnesia.erl | 289 +- src/ejabberd_router_multicast.erl | 217 +- src/ejabberd_router_redis.erl | 181 +- src/ejabberd_router_sql.erl | 165 +- src/ejabberd_s2s.erl | 737 +- src/ejabberd_s2s_in.erl | 395 +- src/ejabberd_s2s_out.erl | 424 +- src/ejabberd_service.erl | 296 +- src/ejabberd_shaper.erl | 143 +- src/ejabberd_sip.erl | 52 +- src/ejabberd_sm.erl | 1224 ++- src/ejabberd_sm_mnesia.erl | 101 +- src/ejabberd_sm_redis.erl | 210 +- src/ejabberd_sm_sql.erl | 191 +- src/ejabberd_sql.erl | 1284 +-- src/ejabberd_sql_pt.erl | 733 +- src/ejabberd_sql_schema.erl | 1113 ++- src/ejabberd_sql_sup.erl | 190 +- src/ejabberd_stun.erl | 172 +- src/ejabberd_sup.erl | 77 +- src/ejabberd_system_monitor.erl | 307 +- src/ejabberd_systemd.erl | 125 +- src/ejabberd_tmp_sup.erl | 9 +- src/ejabberd_update.erl | 302 +- src/ejabberd_web.erl | 66 +- src/ejabberd_web_admin.erl | 1881 ++-- src/ejabberd_websocket.erl | 292 +- src/ejabberd_websocket_codec.erl | 226 +- src/ejabberd_xmlrpc.erl | 390 +- src/ejd2sql.erl | 152 +- src/eldap.erl | 1033 +- src/eldap_filter.erl | 63 +- src/eldap_pool.erl | 83 +- src/eldap_utils.erl | 272 +- src/elixir_logger_backend.erl | 26 +- src/ext_mod.erl | 909 +- src/extauth.erl | 185 +- src/extauth_sup.erl | 106 +- src/gen_iq_handler.erl | 100 +- src/gen_mod.erl | 735 +- src/gen_pubsub_node.erl | 231 +- src/gen_pubsub_nodetree.erl | 87 +- src/jd2ejd.erl | 226 +- src/misc.erl | 751 +- src/mod_adhoc.erl | 328 +- src/mod_adhoc_api.erl | 426 +- src/mod_adhoc_api_opt.erl | 2 +- src/mod_adhoc_opt.erl | 2 +- src/mod_admin_extra.erl | 3368 ++++--- src/mod_admin_update_sql.erl | 180 +- src/mod_announce.erl | 1184 ++- src/mod_announce_mnesia.erl | 104 +- src/mod_announce_opt.erl | 7 +- src/mod_announce_sql.erl | 140 +- src/mod_antispam.erl | 527 +- src/mod_antispam_dump.erl | 33 +- src/mod_antispam_files.erl | 52 +- src/mod_antispam_filter.erl | 80 +- src/mod_antispam_opt.erl | 11 +- src/mod_antispam_rtbl.erl | 83 +- src/mod_auth_fast.erl | 160 +- src/mod_auth_fast_mnesia.erl | 123 +- src/mod_auth_fast_opt.erl | 4 +- src/mod_avatar.erl | 634 +- src/mod_avatar_opt.erl | 3 +- src/mod_block_strangers.erl | 299 +- src/mod_block_strangers_opt.erl | 7 +- src/mod_blocking.erl | 344 +- src/mod_bosh.erl | 426 +- src/mod_bosh_mnesia.erl | 191 +- src/mod_bosh_opt.erl | 12 +- src/mod_bosh_redis.erl | 122 +- src/mod_bosh_sql.erl | 92 +- src/mod_caps.erl | 780 +- src/mod_caps_mnesia.erl | 57 +- src/mod_caps_opt.erl | 6 +- src/mod_caps_sql.erl | 74 +- src/mod_carboncopy.erl | 292 +- src/mod_client_state.erl | 487 +- src/mod_client_state_opt.erl | 4 +- src/mod_configure.erl | 2665 ++--- src/mod_configure_opt.erl | 2 +- src/mod_conversejs.erl | 215 +- src/mod_conversejs_opt.erl | 11 +- src/mod_delegation.erl | 529 +- src/mod_delegation_opt.erl | 4 +- src/mod_disco.erl | 564 +- src/mod_disco_opt.erl | 6 +- src/mod_fail2ban.erl | 241 +- src/mod_fail2ban_opt.erl | 4 +- src/mod_host_meta.erl | 98 +- src/mod_host_meta_opt.erl | 3 +- src/mod_http_api.erl | 573 +- src/mod_http_api_opt.erl | 2 +- src/mod_http_fileserver.erl | 534 +- src/mod_http_fileserver_opt.erl | 14 +- src/mod_http_upload.erl | 1381 +-- src/mod_http_upload_opt.erl | 21 +- src/mod_http_upload_quota.erl | 436 +- src/mod_http_upload_quota_opt.erl | 4 +- src/mod_jidprep.erl | 90 +- src/mod_jidprep_opt.erl | 2 +- src/mod_last.erl | 297 +- src/mod_last_mnesia.erl | 57 +- src/mod_last_opt.erl | 6 +- src/mod_last_sql.erl | 78 +- src/mod_legacy_auth.erl | 156 +- src/mod_mam.erl | 2677 ++--- src/mod_mam_mnesia.erl | 446 +- src/mod_mam_opt.erl | 13 +- src/mod_mam_sql.erl | 1300 +-- src/mod_matrix_gw.erl | 636 +- src/mod_matrix_gw_opt.erl | 10 +- src/mod_matrix_gw_room.erl | 2125 ++-- src/mod_matrix_gw_s2s.erl | 278 +- src/mod_matrix_gw_sup.erl | 38 +- src/mod_metrics.erl | 161 +- src/mod_metrics_opt.erl | 5 +- src/mod_mix.erl | 947 +- src/mod_mix_mnesia.erl | 214 +- src/mod_mix_opt.erl | 6 +- src/mod_mix_pam.erl | 500 +- src/mod_mix_pam_mnesia.erl | 61 +- src/mod_mix_pam_opt.erl | 6 +- src/mod_mix_pam_sql.erl | 140 +- src/mod_mix_sql.erl | 395 +- src/mod_mqtt.erl | 440 +- src/mod_mqtt_bridge.erl | 297 +- src/mod_mqtt_bridge_opt.erl | 5 +- src/mod_mqtt_bridge_session.erl | 588 +- src/mod_mqtt_mnesia.erl | 319 +- src/mod_mqtt_opt.erl | 19 +- src/mod_mqtt_session.erl | 833 +- src/mod_mqtt_sql.erl | 130 +- src/mod_mqtt_ws.erl | 156 +- src/mod_muc.erl | 1908 ++-- src/mod_muc_admin.erl | 2735 +++--- src/mod_muc_admin_opt.erl | 2 +- src/mod_muc_log.erl | 1323 +-- src/mod_muc_log_opt.erl | 18 +- src/mod_muc_mnesia.erl | 535 +- src/mod_muc_occupantid.erl | 41 +- src/mod_muc_opt.erl | 37 +- src/mod_muc_room.erl | 8572 ++++++++++------- src/mod_muc_rtbl.erl | 361 +- src/mod_muc_rtbl_opt.erl | 3 +- src/mod_muc_sql.erl | 815 +- src/mod_muc_sup.erl | 47 +- src/mod_multicast.erl | 982 +- src/mod_multicast_opt.erl | 9 +- src/mod_offline.erl | 1622 ++-- src/mod_offline_mnesia.erl | 308 +- src/mod_offline_opt.erl | 10 +- src/mod_offline_sql.erl | 394 +- src/mod_ping.erl | 277 +- src/mod_ping_opt.erl | 5 +- src/mod_pres_counter.erl | 154 +- src/mod_pres_counter_opt.erl | 3 +- src/mod_privacy.erl | 1101 ++- src/mod_privacy_mnesia.erl | 191 +- src/mod_privacy_opt.erl | 6 +- src/mod_privacy_sql.erl | 708 +- src/mod_private.erl | 803 +- src/mod_private_mnesia.erl | 138 +- src/mod_private_opt.erl | 6 +- src/mod_private_sql.erl | 177 +- src/mod_privilege.erl | 637 +- src/mod_privilege_opt.erl | 13 +- src/mod_providers.erl | 169 +- src/mod_providers_opt.erl | 16 +- src/mod_proxy65.erl | 132 +- src/mod_proxy65_lib.erl | 70 +- src/mod_proxy65_mnesia.erl | 176 +- src/mod_proxy65_opt.erl | 16 +- src/mod_proxy65_redis.erl | 255 +- src/mod_proxy65_service.erl | 295 +- src/mod_proxy65_sql.erl | 195 +- src/mod_proxy65_stream.erl | 271 +- src/mod_pubsub.erl | 6745 +++++++------ src/mod_pubsub_mnesia.erl | 1 + src/mod_pubsub_opt.erl | 24 +- src/mod_pubsub_serverinfo.erl | 205 +- src/mod_pubsub_serverinfo_opt.erl | 2 +- src/mod_pubsub_sql.erl | 203 +- src/mod_push.erl | 901 +- src/mod_push_keepalive.erl | 156 +- src/mod_push_keepalive_opt.erl | 4 +- src/mod_push_mnesia.erl | 232 +- src/mod_push_opt.erl | 9 +- src/mod_push_sql.erl | 347 +- src/mod_register.erl | 892 +- src/mod_register_opt.erl | 13 +- src/mod_register_web.erl | 817 +- src/mod_roster.erl | 1285 ++- src/mod_roster_mnesia.erl | 204 +- src/mod_roster_opt.erl | 9 +- src/mod_roster_sql.erl | 524 +- src/mod_s2s_bidi.erl | 127 +- src/mod_s2s_dialback.erl | 340 +- src/mod_s2s_dialback_opt.erl | 2 +- src/mod_scram_upgrade.erl | 142 +- src/mod_scram_upgrade_opt.erl | 2 +- src/mod_service_log.erl | 48 +- src/mod_service_log_opt.erl | 2 +- src/mod_shared_roster.erl | 1601 +-- src/mod_shared_roster_ldap.erl | 938 +- src/mod_shared_roster_ldap_opt.erl | 33 +- src/mod_shared_roster_mnesia.erl | 161 +- src/mod_shared_roster_opt.erl | 6 +- src/mod_shared_roster_sql.erl | 248 +- src/mod_sic.erl | 72 +- src/mod_sip.erl | 402 +- src/mod_sip_opt.erl | 9 +- src/mod_sip_proxy.erl | 522 +- src/mod_sip_registrar.erl | 782 +- src/mod_stats.erl | 288 +- src/mod_stream_mgmt.erl | 1082 ++- src/mod_stream_mgmt_opt.erl | 9 +- src/mod_stun_disco.erl | 856 +- src/mod_stun_disco_opt.erl | 6 +- src/mod_time.erl | 28 +- src/mod_vcard.erl | 722 +- src/mod_vcard_ldap.erl | 583 +- src/mod_vcard_ldap_opt.erl | 26 +- src/mod_vcard_mnesia.erl | 264 +- src/mod_vcard_mnesia_opt.erl | 2 +- src/mod_vcard_opt.erl | 13 +- src/mod_vcard_sql.erl | 418 +- src/mod_vcard_xupdate.erl | 178 +- src/mod_vcard_xupdate_opt.erl | 5 +- src/mod_version.erl | 68 +- src/mod_version_opt.erl | 2 +- src/mqtt_codec.erl | 681 +- src/node_flat.erl | 1165 ++- src/node_flat_sql.erl | 1442 +-- src/node_pep.erl | 299 +- src/node_pep_sql.erl | 257 +- src/nodetree_tree.erl | 313 +- src/nodetree_tree_sql.erl | 507 +- src/nodetree_virtual.erl | 73 +- src/prosody2ejabberd.erl | 809 +- src/proxy_protocol.erl | 280 +- src/pubsub_db_sql.erl | 197 +- src/pubsub_index.erl | 44 +- src/pubsub_migrate.erl | 819 +- src/pubsub_subscription.erl | 269 +- src/pubsub_subscription_sql.erl | 208 +- src/rest.erl | 174 +- src/str.erl | 82 +- src/translate.erl | 341 +- src/win32_dns.erl | 91 +- src/xml_compress.erl | 1645 ++-- test/announce_tests.erl | 21 +- test/antispam_tests.erl | 98 +- test/carbons_tests.erl | 178 +- test/commands_tests.erl | 207 +- test/configtest_tests.erl | 38 + test/csi_tests.erl | 165 +- test/ejabberd_SUITE.erl | 962 +- test/ejabberd_test_options.erl | 8 +- test/example_tests.erl | 14 +- test/jidprep_tests.erl | 23 +- test/json_test.erl | 40 +- test/ldap_srv.erl | 251 +- test/mam_tests.erl | 672 +- test/mod_configtest.erl | 7 + test/muc_tests.erl | 1973 ++-- test/offline_tests.erl | 585 +- test/privacy_tests.erl | 850 +- test/private_tests.erl | 122 +- test/proxy65_tests.erl | 45 +- test/pubsub_tests.erl | 795 +- test/push_tests.erl | 150 +- test/replaced_tests.erl | 20 +- test/roster_tests.erl | 718 +- test/sm_tests.erl | 74 +- test/stundisco_tests.erl | 210 +- test/suite.erl | 1050 +- test/suite.hrl | 32 +- test/upload_tests.erl | 214 +- test/vcard_tests.erl | 148 +- test/webadmin_tests.erl | 121 +- tools/xml_compress_gen.erl | 809 +- 364 files changed, 86832 insertions(+), 59666 deletions(-) diff --git a/_checkouts/configure_deps/rebar.config b/_checkouts/configure_deps/rebar.config index f618f3e40..2656fd554 100644 --- a/_checkouts/configure_deps/rebar.config +++ b/_checkouts/configure_deps/rebar.config @@ -1,2 +1,2 @@ {erl_opts, [debug_info]}. -{deps, []}. \ No newline at end of file +{deps, []}. diff --git a/_checkouts/configure_deps/src/configure_deps.app.src b/_checkouts/configure_deps/src/configure_deps.app.src index 6ef9e0763..2fc31bec2 100644 --- a/_checkouts/configure_deps/src/configure_deps.app.src +++ b/_checkouts/configure_deps/src/configure_deps.app.src @@ -3,7 +3,6 @@ {vsn, "0.0.1"}, {registered, []}, {applications, [kernel, stdlib]}, - {env,[]}, + {env, []}, {modules, []}, - {links, []} - ]}. + {links, []}]}. diff --git a/_checkouts/configure_deps/src/configure_deps.erl b/_checkouts/configure_deps/src/configure_deps.erl index 5ec5beb45..85f0efbee 100644 --- a/_checkouts/configure_deps/src/configure_deps.erl +++ b/_checkouts/configure_deps/src/configure_deps.erl @@ -2,6 +2,7 @@ -export([init/1]). + -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> {ok, State1} = configure_deps_prv:init(State), diff --git a/_checkouts/configure_deps/src/configure_deps_prv.erl b/_checkouts/configure_deps/src/configure_deps_prv.erl index 9d91f7acd..cd9b67763 100644 --- a/_checkouts/configure_deps/src/configure_deps_prv.erl +++ b/_checkouts/configure_deps/src/configure_deps_prv.erl @@ -3,7 +3,8 @@ -export([init/1, do/1, format_error/1]). -define(PROVIDER, 'configure-deps'). --define(DEPS, [install_deps]). +-define(DEPS, [install_deps]). + %% =================================================================== %% Public API @@ -11,17 +12,15 @@ -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create( - [ - {namespace, default}, - {name, ?PROVIDER}, % The 'user friendly' name of the task - {module, ?MODULE}, % The module implementation of the task - {bare, true}, % The task can be run by the user, always true - {deps, ?DEPS}, % The list of dependencies - {example, "rebar3 configure-deps"}, % How to use the plugin - {opts, []}, % list of options understood by the plugin - {short_desc, "Explicitly run ./configure for dependencies"}, - {desc, "A rebar plugin to allow explicitly running ./configure on dependencies. Useful if dependencies might change prior to compilation when configure is run."} - ]), + [{namespace, default}, + {name, ?PROVIDER}, % The 'user friendly' name of the task + {module, ?MODULE}, % The module implementation of the task + {bare, true}, % The task can be run by the user, always true + {deps, ?DEPS}, % The list of dependencies + {example, "rebar3 configure-deps"}, % How to use the plugin + {opts, []}, % list of options understood by the plugin + {short_desc, "Explicitly run ./configure for dependencies"}, + {desc, "A rebar plugin to allow explicitly running ./configure on dependencies. Useful if dependencies might change prior to compilation when configure is run."}]), {ok, rebar_state:add_provider(State, Provider)}. @@ -31,25 +30,30 @@ do(State) -> lists:foreach(fun do_app/1, Apps), {ok, State}. + exec_configure({'configure-deps', Cmd}, Dir) -> - rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, true}]); + rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, true}]); exec_configure(_, Acc) -> Acc. + parse_pre_hooks({pre_hooks, PreHooks}, Acc) -> lists:foldl(fun exec_configure/2, Acc, PreHooks); parse_pre_hooks(_, Acc) -> Acc. + parse_additions({add, App, Additions}, {MyApp, Dir}) when App == MyApp -> lists:foldl(fun parse_pre_hooks/2, Dir, Additions), - {MyApp, Dir}; + {MyApp, Dir}; parse_additions(_, Acc) -> Acc. + do_app(App) -> Dir = rebar_app_info:dir(App), - Opts = rebar_app_info:opts(App), - Overrides = rebar_opts:get(Opts, overrides), + Opts = rebar_app_info:opts(App), + Overrides = rebar_opts:get(Opts, overrides), lists:foldl(fun parse_additions/2, {binary_to_atom(rebar_app_info:name(App), utf8), Dir}, Overrides). --spec format_error(any()) -> iolist(). + +-spec format_error(any()) -> iolist(). format_error(Reason) -> io_lib:format("~p", [Reason]). diff --git a/include/ELDAPv3.hrl b/include/ELDAPv3.hrl index 41d698cfc..f636c5dd0 100644 --- a/include/ELDAPv3.hrl +++ b/include/ELDAPv3.hrl @@ -3,79 +3,101 @@ %% SEQUENCE and SET, and macro definitions for each value %% definition,in module ELDAPv3 +-record('LDAPMessage', { + messageID, protocolOp, controls = asn1_NOVALUE + }). +-record('AttributeValueAssertion', { + attributeDesc, assertionValue + }). --record('LDAPMessage',{ -messageID, protocolOp, controls = asn1_NOVALUE}). +-record('Attribute', { + type, vals + }). --record('AttributeValueAssertion',{ -attributeDesc, assertionValue}). +-record('LDAPResult', { + resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE + }). --record('Attribute',{ -type, vals}). +-record('Control', { + controlType, criticality = asn1_DEFAULT, controlValue = asn1_NOVALUE + }). --record('LDAPResult',{ -resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE}). +-record('BindRequest', { + version, name, authentication + }). --record('Control',{ -controlType, criticality = asn1_DEFAULT, controlValue = asn1_NOVALUE}). +-record('SaslCredentials', { + mechanism, credentials = asn1_NOVALUE + }). --record('BindRequest',{ -version, name, authentication}). +-record('BindResponse', { + resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, serverSaslCreds = asn1_NOVALUE + }). --record('SaslCredentials',{ -mechanism, credentials = asn1_NOVALUE}). +-record('SearchRequest', { + baseObject, scope, derefAliases, sizeLimit, timeLimit, typesOnly, filter, attributes + }). --record('BindResponse',{ -resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, serverSaslCreds = asn1_NOVALUE}). +-record('SubstringFilter', { + type, substrings + }). --record('SearchRequest',{ -baseObject, scope, derefAliases, sizeLimit, timeLimit, typesOnly, filter, attributes}). +-record('MatchingRuleAssertion', { + matchingRule = asn1_NOVALUE, type = asn1_NOVALUE, matchValue, dnAttributes = asn1_DEFAULT + }). --record('SubstringFilter',{ -type, substrings}). +-record('SearchResultEntry', { + objectName, attributes + }). --record('MatchingRuleAssertion',{ -matchingRule = asn1_NOVALUE, type = asn1_NOVALUE, matchValue, dnAttributes = asn1_DEFAULT}). +-record('PartialAttributeList_SEQOF', { + type, vals + }). --record('SearchResultEntry',{ -objectName, attributes}). +-record('ModifyRequest', { + object, modification + }). --record('PartialAttributeList_SEQOF',{ -type, vals}). +-record('ModifyRequest_modification_SEQOF', { + operation, modification + }). --record('ModifyRequest',{ -object, modification}). +-record('AttributeTypeAndValues', { + type, vals + }). --record('ModifyRequest_modification_SEQOF',{ -operation, modification}). +-record('AddRequest', { + entry, attributes + }). --record('AttributeTypeAndValues',{ -type, vals}). +-record('AttributeList_SEQOF', { + type, vals + }). --record('AddRequest',{ -entry, attributes}). +-record('ModifyDNRequest', { + entry, newrdn, deleteoldrdn, newSuperior = asn1_NOVALUE + }). --record('AttributeList_SEQOF',{ -type, vals}). +-record('CompareRequest', { + entry, ava + }). --record('ModifyDNRequest',{ -entry, newrdn, deleteoldrdn, newSuperior = asn1_NOVALUE}). +-record('ExtendedRequest', { + requestName, requestValue = asn1_NOVALUE + }). --record('CompareRequest',{ -entry, ava}). +-record('ExtendedResponse', { + resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, responseName = asn1_NOVALUE, response = asn1_NOVALUE + }). --record('ExtendedRequest',{ -requestName, requestValue = asn1_NOVALUE}). +-record('PasswdModifyRequestValue', { + userIdentity = asn1_NOVALUE, oldPasswd = asn1_NOVALUE, newPasswd = asn1_NOVALUE + }). --record('ExtendedResponse',{ -resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, responseName = asn1_NOVALUE, response = asn1_NOVALUE}). +-record('PasswdModifyResponseValue', { + genPasswd = asn1_NOVALUE + }). --record('PasswdModifyRequestValue',{ -userIdentity = asn1_NOVALUE, oldPasswd = asn1_NOVALUE, newPasswd = asn1_NOVALUE}). - --record('PasswdModifyResponseValue',{ -genPasswd = asn1_NOVALUE}). - --define('maxInt', 2147483647). --define('passwdModifyOID', [49,46,51,46,54,46,49,46,52,46,49,46,52,50,48,51,46,49,46,49,49,46,49]). +-define('maxInt', 2147483647). +-define('passwdModifyOID', [49, 46, 51, 46, 54, 46, 49, 46, 52, 46, 49, 46, 52, 50, 48, 51, 46, 49, 46, 49, 49, 46, 49]). diff --git a/include/bosh.hrl b/include/bosh.hrl index dd9f1b6a1..4fa2f548a 100644 --- a/include/bosh.hrl +++ b/include/bosh.hrl @@ -19,33 +19,36 @@ %%%---------------------------------------------------------------------- -define(CT_XML, - {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). + {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). -define(CT_PLAIN, - {<<"Content-Type">>, <<"text/plain">>}). + {<<"Content-Type">>, <<"text/plain">>}). -define(CT_JSON, {<<"Content-Type">>, <<"application/json">>}). -define(AC_ALLOW_ORIGIN, - {<<"Access-Control-Allow-Origin">>, <<"*">>}). + {<<"Access-Control-Allow-Origin">>, <<"*">>}). -define(AC_ALLOW_METHODS, - {<<"Access-Control-Allow-Methods">>, - <<"GET, POST, OPTIONS">>}). + {<<"Access-Control-Allow-Methods">>, + <<"GET, POST, OPTIONS">>}). -define(AC_ALLOW_HEADERS, - {<<"Access-Control-Allow-Headers">>, - <<"Content-Type">>}). + {<<"Access-Control-Allow-Headers">>, + <<"Content-Type">>}). -define(AC_MAX_AGE, - {<<"Access-Control-Max-Age">>, <<"86400">>}). + {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(OPTIONS_HEADER, - [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, - ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). + [?CT_PLAIN, + ?AC_ALLOW_ORIGIN, + ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, + ?AC_MAX_AGE]). -define(HEADER(CType), - [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). + [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). -define(BOSH_CACHE, bosh_cache). diff --git a/include/ejabberd_auth.hrl b/include/ejabberd_auth.hrl index bf7660d3f..e2fcdbfbe 100644 --- a/include/ejabberd_auth.hrl +++ b/include/ejabberd_auth.hrl @@ -18,5 +18,7 @@ %%% %%%---------------------------------------------------------------------- --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1', - password = <<"">> :: binary() | scram() | '_'}). +-record(passwd, { + us = {<<"">>, <<"">>} :: {binary(), binary()} | {binary(), binary(), atom()} | '$1', + password = <<"">> :: binary() | scram() | '_' + }). diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index c08ec514b..5e7ba7782 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -106,4 +106,3 @@ args_example :: none | [any()] | '_', result_example :: any() }. - diff --git a/include/ejabberd_sm.hrl b/include/ejabberd_sm.hrl index 54a828e1a..9d21d8992 100644 --- a/include/ejabberd_sm.hrl +++ b/include/ejabberd_sm.hrl @@ -27,10 +27,13 @@ -record(session_counter, {vhost, count}). -type sid() :: {erlang:timestamp(), pid()}. -type ip() :: {inet:ip_address(), inet:port_number()} | undefined. --type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()} - | {oor, boolean()} | {auth_module, atom()} - | {num_stanzas_in, non_neg_integer()} - | {atom(), term()}]. +-type info() :: [{conn, atom()} | + {ip, ip()} | + {node, atom()} | + {oor, boolean()} | + {auth_module, atom()} | + {num_stanzas_in, non_neg_integer()} | + {atom(), term()}]. -type prio() :: undefined | integer(). -endif. diff --git a/include/ejabberd_sql.hrl b/include/ejabberd_sql.hrl index d0ab55cba..8e76f485e 100644 --- a/include/ejabberd_sql.hrl +++ b/include/ejabberd_sql.hrl @@ -30,46 +30,62 @@ -define(SQL_INSERT(Table, Fields), ?SQL_INSERT_MARK(Table, Fields)). -ifdef(COMPILER_REPORTS_ONLY_LINES). --record(sql_query, {hash :: binary(), - format_query :: fun(), - format_res :: fun(), - args :: fun(), - flags :: non_neg_integer(), - loc :: {module(), pos_integer()}}). +-record(sql_query, { + hash :: binary(), + format_query :: fun(), + format_res :: fun(), + args :: fun(), + flags :: non_neg_integer(), + loc :: {module(), pos_integer()} + }). -else. --record(sql_query, {hash :: binary(), - format_query :: fun(), - format_res :: fun(), - args :: fun(), - flags :: non_neg_integer(), - loc :: {module(), {pos_integer(), pos_integer()}}}). +-record(sql_query, { + hash :: binary(), + format_query :: fun(), + format_res :: fun(), + args :: fun(), + flags :: non_neg_integer(), + loc :: {module(), {pos_integer(), pos_integer()}} + }). -endif. --record(sql_escape, {string :: fun((binary()) -> binary()), - integer :: fun((integer()) -> binary()), - boolean :: fun((boolean()) -> binary()), - in_array_string :: fun((binary()) -> binary()), - like_escape :: fun(() -> binary())}). +-record(sql_escape, { + string :: fun((binary()) -> binary()), + integer :: fun((integer()) -> binary()), + boolean :: fun((boolean()) -> binary()), + in_array_string :: fun((binary()) -> binary()), + like_escape :: fun(() -> binary()) + }). +-record(sql_index, { + columns, + unique = false :: boolean(), + meta = #{} + }). +-record(sql_column, { + name :: binary(), + type, + default = false, + opts = [] + }). +-record(sql_table, { + name :: binary(), + columns :: [#sql_column{}], + indices = [] :: [#sql_index{}], + post_create + }). +-record(sql_schema, { + version :: integer(), + tables :: [#sql_table{}], + update = [] + }). +-record(sql_references, { + table :: binary(), + column :: binary() + }). --record(sql_index, {columns, - unique = false :: boolean(), - meta = #{}}). --record(sql_column, {name :: binary(), - type, - default = false, - opts = []}). --record(sql_table, {name :: binary(), - columns :: [#sql_column{}], - indices = [] :: [#sql_index{}], - post_create}). --record(sql_schema, {version :: integer(), - tables :: [#sql_table{}], - update = []}). --record(sql_references, {table :: binary(), - column :: binary()}). - --record(sql_schema_info, - {db_type :: pgsql | mysql | sqlite, - db_version :: any(), - new_schema = true :: boolean()}). +-record(sql_schema_info, { + db_type :: pgsql | mysql | sqlite, + db_version :: any(), + new_schema = true :: boolean() + }). diff --git a/include/ejabberd_web_admin.hrl b/include/ejabberd_web_admin.hrl index 45e4beada..b86b5eeef 100644 --- a/include/ejabberd_web_admin.hrl +++ b/include/ejabberd_web_admin.hrl @@ -19,35 +19,35 @@ %%%---------------------------------------------------------------------- -define(X(Name), - #xmlel{name = Name, attrs = [], children = []}). + #xmlel{name = Name, attrs = [], children = []}). -define(XA(Name, Attrs), - #xmlel{name = Name, attrs = Attrs, children = []}). + #xmlel{name = Name, attrs = Attrs, children = []}). -define(XE(Name, Els), - #xmlel{name = Name, attrs = [], children = Els}). + #xmlel{name = Name, attrs = [], children = Els}). -define(XAE(Name, Attrs, Els), - #xmlel{name = Name, attrs = Attrs, children = Els}). + #xmlel{name = Name, attrs = Attrs, children = Els}). -define(C(Text), {xmlcdata, Text}). -define(XC(Name, Text), ?XE(Name, [?C(Text)])). -define(XAC(Name, Attrs, Text), - ?XAE(Name, Attrs, [?C(Text)])). + ?XAE(Name, Attrs, [?C(Text)])). -define(CT(Text), ?C((translate:translate(Lang, Text)))). -define(XCT(Name, Text), ?XC(Name, (translate:translate(Lang, Text)))). -define(XACT(Name, Attrs, Text), - ?XAC(Name, Attrs, (translate:translate(Lang, Text)))). + ?XAC(Name, Attrs, (translate:translate(Lang, Text)))). -define(LI(Els), ?XE(<<"li">>, Els)). -define(A(URL, Els), - ?XAE(<<"a">>, [{<<"href">>, URL}], Els)). + ?XAE(<<"a">>, [{<<"href">>, URL}], Els)). -define(AC(URL, Text), ?A(URL, [?C(Text)])). @@ -58,69 +58,79 @@ -define(BR, ?X(<<"br">>)). -define(INPUT(Type, Name, Value), - ?XA(<<"input">>, - [{<<"type">>, Type}, {<<"name">>, Name}, - {<<"value">>, Value}])). + ?XA(<<"input">>, + [{<<"type">>, Type}, + {<<"name">>, Name}, + {<<"value">>, Value}])). -define(INPUTPH(Type, Name, Value, PlaceHolder), - ?XA(<<"input">>, - [{<<"type">>, Type}, {<<"name">>, Name}, - {<<"value">>, Value}, {<<"placeholder">>, PlaceHolder}])). + ?XA(<<"input">>, + [{<<"type">>, Type}, + {<<"name">>, Name}, + {<<"value">>, Value}, + {<<"placeholder">>, PlaceHolder}])). -define(INPUTT(Type, Name, Value), - ?INPUT(Type, Name, (translate:translate(Lang, Value)))). + ?INPUT(Type, Name, (translate:translate(Lang, Value)))). -define(INPUTD(Type, Name, Value), - ?XA(<<"input">>, - [{<<"type">>, Type}, {<<"name">>, Name}, - {<<"class">>, <<"btn-danger">>}, {<<"value">>, Value}])). + ?XA(<<"input">>, + [{<<"type">>, Type}, + {<<"name">>, Name}, + {<<"class">>, <<"btn-danger">>}, + {<<"value">>, Value}])). -define(INPUTTD(Type, Name, Value), - ?INPUTD(Type, Name, (translate:translate(Lang, Value)))). + ?INPUTD(Type, Name, (translate:translate(Lang, Value)))). -define(INPUTS(Type, Name, Value, Size), - ?XA(<<"input">>, - [{<<"type">>, Type}, {<<"name">>, Name}, - {<<"value">>, Value}, {<<"size">>, Size}])). + ?XA(<<"input">>, + [{<<"type">>, Type}, + {<<"name">>, Name}, + {<<"value">>, Value}, + {<<"size">>, Size}])). -define(INPUTST(Type, Name, Value, Size), - ?INPUT(Type, Name, (translate:translate(Lang, Value)), Size)). + ?INPUT(Type, Name, (translate:translate(Lang, Value)), Size)). -define(ACLINPUT(Text), - ?XE(<<"td">>, - [?INPUT(<<"text">>, <<"value", ID/binary>>, Text)])). + ?XE(<<"td">>, + [?INPUT(<<"text">>, <<"value", ID/binary>>, Text)])). -define(TEXTAREA(Name, Rows, Cols, Value), - ?XAC(<<"textarea">>, - [{<<"name">>, Name}, {<<"rows">>, Rows}, - {<<"cols">>, Cols}], - Value)). + ?XAC(<<"textarea">>, + [{<<"name">>, Name}, + {<<"rows">>, Rows}, + {<<"cols">>, Cols}], + Value)). %% Build an xmlelement for result -define(XRES(Text), - ?XAC(<<"p">>, [{<<"class">>, <<"result">>}], Text)). + ?XAC(<<"p">>, [{<<"class">>, <<"result">>}], Text)). -define(DIVRES(Elements), - ?XAE(<<"div">>, [{<<"class">>, <<"result">>}], Elements)). + ?XAE(<<"div">>, [{<<"class">>, <<"result">>}], Elements)). %% Guide Link -define(XREST(Text), ?XRES((translate:translate(Lang, Text)))). -define(GL(Ref, Title), - ?XAE(<<"div">>, [{<<"class">>, <<"guidelink">>}], - [?XAE(<<"a">>, - [{<<"href">>, <<"https://docs.ejabberd.im/", Ref/binary>>}, - {<<"target">>, <<"_blank">>}], - [?C(<<"docs: ", Title/binary>>)])])). + ?XAE(<<"div">>, + [{<<"class">>, <<"guidelink">>}], + [?XAE(<<"a">>, + [{<<"href">>, <<"https://docs.ejabberd.im/", Ref/binary>>}, + {<<"target">>, <<"_blank">>}], + [?C(<<"docs: ", Title/binary>>)])])). %% h1 with a Guide Link -define(H1GLraw(Name, Ref, Title), - [?XC(<<"h1">>, Name), ?GL(Ref, Title), ?BR, ?BR]). + [?XC(<<"h1">>, Name), ?GL(Ref, Title), ?BR, ?BR]). -define(H1GL(Name, RefConf, Title), - ?H1GLraw(Name, <<"admin/configuration/", RefConf/binary>>, Title)). + ?H1GLraw(Name, <<"admin/configuration/", RefConf/binary>>, Title)). -define(ANCHORL(Ref), - ?XAE(<<"div">>, [{<<"class">>, <<"anchorlink">>}], - [?XAE(<<"a">>, - [{<<"href">>, <<"#", Ref/binary>>}], - [?C(unicode:characters_to_binary("¶"))])])). + ?XAE(<<"div">>, + [{<<"class">>, <<"anchorlink">>}], + [?XAE(<<"a">>, + [{<<"href">>, <<"#", Ref/binary>>}], + [?C(unicode:characters_to_binary("¶"))])])). diff --git a/include/http_bind.hrl b/include/http_bind.hrl index ab1294e7d..dbe46f079 100644 --- a/include/http_bind.hrl +++ b/include/http_bind.hrl @@ -19,31 +19,34 @@ %%%---------------------------------------------------------------------- -define(CT_XML, - {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). + {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). -define(CT_PLAIN, - {<<"Content-Type">>, <<"text/plain">>}). + {<<"Content-Type">>, <<"text/plain">>}). -define(AC_ALLOW_ORIGIN, - {<<"Access-Control-Allow-Origin">>, <<"*">>}). + {<<"Access-Control-Allow-Origin">>, <<"*">>}). -define(AC_ALLOW_METHODS, - {<<"Access-Control-Allow-Methods">>, - <<"GET, POST, OPTIONS">>}). + {<<"Access-Control-Allow-Methods">>, + <<"GET, POST, OPTIONS">>}). -define(AC_ALLOW_HEADERS, - {<<"Access-Control-Allow-Headers">>, - <<"Content-Type">>}). + {<<"Access-Control-Allow-Headers">>, + <<"Content-Type">>}). -define(AC_MAX_AGE, - {<<"Access-Control-Max-Age">>, <<"86400">>}). + {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(NO_CACHE, {<<"Cache-Control">>, <<"max-age=0, no-cache, no-store">>}). -define(OPTIONS_HEADER, - [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, - ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). + [?CT_PLAIN, + ?AC_ALLOW_ORIGIN, + ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, + ?AC_MAX_AGE]). -define(HEADER, - [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS, ?NO_CACHE]). + [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS, ?NO_CACHE]). diff --git a/include/logger.hrl b/include/logger.hrl index e41ab73dd..3ceea6f89 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -23,62 +23,87 @@ -compile([{parse_transform, lager_transform}]). -define(DEBUG(Format, Args), - begin lager:debug(Format, Args), ok end). + begin lager:debug(Format, Args), ok end). -define(INFO_MSG(Format, Args), - begin lager:info(Format, Args), ok end). + begin lager:info(Format, Args), ok end). -define(WARNING_MSG(Format, Args), - begin lager:warning(Format, Args), ok end). + begin lager:warning(Format, Args), ok end). -define(ERROR_MSG(Format, Args), - begin lager:error(Format, Args), ok end). + begin lager:error(Format, Args), ok end). -define(CRITICAL_MSG(Format, Args), - begin lager:critical(Format, Args), ok end). + begin lager:critical(Format, Args), ok end). -else. -include_lib("kernel/include/logger.hrl"). --define(CLEAD, "\e[1"). % bold --define(CMID, "\e[0"). % normal --define(CCLEAN, "\e[0m"). % clean +-define(CLEAD, "\e[1"). % bold +-define(CMID, "\e[0"). % normal +-define(CCLEAN, "\e[0m"). % clean --define(CDEFAULT, ";49;95m"). % light magenta --define(CDEBUG, ";49;90m"). % dark gray --define(CINFO, ";49;92m"). % green --define(CWARNING, ";49;93m"). % light yellow --define(CERROR, ";49;91m"). % light magenta --define(CCRITICAL,";49;31m"). % light red +-define(CDEFAULT, ";49;95m"). % light magenta +-define(CDEBUG, ";49;90m"). % dark gray +-define(CINFO, ";49;92m"). % green +-define(CWARNING, ";49;93m"). % light yellow +-define(CERROR, ";49;91m"). % light magenta +-define(CCRITICAL, ";49;31m"). % light red -define(DEBUG(Format, Args), - begin ?LOG_DEBUG(Format, Args, - #{clevel => ?CLEAD ++ ?CDEBUG, - ctext => ?CMID ++ ?CDEBUG}), - ok end). + begin + ?LOG_DEBUG(Format, + Args, + #{ + clevel => ?CLEAD ++ ?CDEBUG, + ctext => ?CMID ++ ?CDEBUG + }), + ok + end). -define(INFO_MSG(Format, Args), - begin ?LOG_INFO(Format, Args, - #{clevel => ?CLEAD ++ ?CINFO, - ctext => ?CCLEAN}), - ok end). + begin + ?LOG_INFO(Format, + Args, + #{ + clevel => ?CLEAD ++ ?CINFO, + ctext => ?CCLEAN + }), + ok + end). -define(WARNING_MSG(Format, Args), - begin ?LOG_WARNING(Format, Args, - #{clevel => ?CLEAD ++ ?CWARNING, - ctext => ?CMID ++ ?CWARNING}), - ok end). + begin + ?LOG_WARNING(Format, + Args, + #{ + clevel => ?CLEAD ++ ?CWARNING, + ctext => ?CMID ++ ?CWARNING + }), + ok + end). -define(ERROR_MSG(Format, Args), - begin ?LOG_ERROR(Format, Args, - #{clevel => ?CLEAD ++ ?CERROR, - ctext => ?CMID ++ ?CERROR}), - ok end). + begin + ?LOG_ERROR(Format, + Args, + #{ + clevel => ?CLEAD ++ ?CERROR, + ctext => ?CMID ++ ?CERROR + }), + ok + end). -define(CRITICAL_MSG(Format, Args), - begin ?LOG_CRITICAL(Format, Args, - #{clevel => ?CLEAD++ ?CCRITICAL, - ctext => ?CMID ++ ?CCRITICAL}), - ok end). + begin + ?LOG_CRITICAL(Format, + Args, + #{ + clevel => ?CLEAD ++ ?CCRITICAL, + ctext => ?CMID ++ ?CCRITICAL + }), + ok + end). -endif. %% Use only when trying to troubleshoot test problem with ExUnit diff --git a/include/mod_announce.hrl b/include/mod_announce.hrl index 77badf90e..830603e25 100644 --- a/include/mod_announce.hrl +++ b/include/mod_announce.hrl @@ -18,8 +18,12 @@ %%% %%%---------------------------------------------------------------------- --record(motd, {server = <<"">> :: binary(), - packet = #xmlel{} :: xmlel()}). +-record(motd, { + server = <<"">> :: binary(), + packet = #xmlel{} :: xmlel() + }). --record(motd_users, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - dummy = [] :: [] | '_'}). +-record(motd_users, { + us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', + dummy = [] :: [] | '_' + }). diff --git a/include/mod_last.hrl b/include/mod_last.hrl index b1c13621a..fd2da7d4c 100644 --- a/include/mod_last.hrl +++ b/include/mod_last.hrl @@ -18,6 +18,8 @@ %%% %%%---------------------------------------------------------------------- --record(last_activity, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - timestamp = 0 :: non_neg_integer(), - status = <<"">> :: binary()}). +-record(last_activity, { + us = {<<"">>, <<"">>} :: {binary(), binary()}, + timestamp = 0 :: non_neg_integer(), + status = <<"">> :: binary() + }). diff --git a/include/mod_matrix_gw.hrl b/include/mod_matrix_gw.hrl index cdb272e8e..048eaf2e8 100644 --- a/include/mod_matrix_gw.hrl +++ b/include/mod_matrix_gw.hrl @@ -18,19 +18,19 @@ %%% %%%---------------------------------------------------------------------- --record(room_version, - {id :: binary(), - %% use the same field names as in Synapse - enforce_key_validity :: boolean(), - special_case_aliases_auth :: boolean(), - strict_canonicaljson :: boolean(), - limit_notifications_power_levels :: boolean(), - knock_join_rule :: boolean(), - restricted_join_rule :: boolean(), - restricted_join_rule_fix :: boolean(), - knock_restricted_join_rule :: boolean(), - enforce_int_power_levels :: boolean(), - implicit_room_creator :: boolean(), - updated_redaction_rules :: boolean(), - hydra :: boolean() - }). +-record(room_version, { + id :: binary(), + %% use the same field names as in Synapse + enforce_key_validity :: boolean(), + special_case_aliases_auth :: boolean(), + strict_canonicaljson :: boolean(), + limit_notifications_power_levels :: boolean(), + knock_join_rule :: boolean(), + restricted_join_rule :: boolean(), + restricted_join_rule_fix :: boolean(), + knock_restricted_join_rule :: boolean(), + enforce_int_power_levels :: boolean(), + implicit_room_creator :: boolean(), + updated_redaction_rules :: boolean(), + hydra :: boolean() + }). diff --git a/include/mod_muc.hrl b/include/mod_muc.hrl index f801b29e1..34805687a 100644 --- a/include/mod_muc.hrl +++ b/include/mod_muc.hrl @@ -18,19 +18,25 @@ %%% %%%---------------------------------------------------------------------- --record(muc_room, {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | - {'_', binary()}, - opts = [] :: list() | '_'}). +-record(muc_room, { + name_host = {<<"">>, <<"">>} :: {binary(), binary()} | + {'_', binary()}, + opts = [] :: list() | '_' + }). --record(muc_registered, - {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', - nick = <<"">> :: binary()}). +-record(muc_registered, { + us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', + nick = <<"">> :: binary() + }). --record(muc_online_room, - {name_host :: {binary(), binary()} | '$1' | {'_', binary()} | '_', - pid :: pid() | '$2' | '_' | '$1'}). +-record(muc_online_room, { + name_host :: {binary(), binary()} | '$1' | {'_', binary()} | '_', + pid :: pid() | '$2' | '_' | '$1' + }). --record(muc_online_users, {us :: {binary(), binary()}, - resource :: binary() | '_', - room :: binary() | '_' | '$1', - host :: binary() | '_' | '$2'}). +-record(muc_online_users, { + us :: {binary(), binary()}, + resource :: binary() | '_', + room :: binary() | '_' | '$1', + host :: binary() | '_' | '$2' + }). diff --git a/include/mod_private.hrl b/include/mod_private.hrl index 05adc7d8b..e1fa1801a 100644 --- a/include/mod_private.hrl +++ b/include/mod_private.hrl @@ -18,7 +18,11 @@ %%% %%%---------------------------------------------------------------------- --record(private_storage, - {usns = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | - '$1' | '_'}, - xml = #xmlel{} :: xmlel() | '_' | '$1'}). +-record(private_storage, { + usns = {<<"">>, <<"">>, <<"">>} :: {binary(), + binary(), + binary() | + '$1' | + '_'}, + xml = #xmlel{} :: xmlel() | '_' | '$1' + }). diff --git a/include/mod_proxy65.hrl b/include/mod_proxy65.hrl index 4f017124a..d3a087997 100644 --- a/include/mod_proxy65.hrl +++ b/include/mod_proxy65.hrl @@ -68,6 +68,8 @@ %% RFC 1928 defined timeout. -define(SOCKS5_REPLY_TIMEOUT, 10000). --record(s5_request, {rsv = 0 :: integer(), - cmd = connect :: connect | udp, - sha1 = <<"">> :: binary()}). +-record(s5_request, { + rsv = 0 :: integer(), + cmd = connect :: connect | udp, + sha1 = <<"">> :: binary() + }). diff --git a/include/mod_shared_roster.hrl b/include/mod_shared_roster.hrl index 4c35878e8..84dc46d76 100644 --- a/include/mod_shared_roster.hrl +++ b/include/mod_shared_roster.hrl @@ -18,8 +18,12 @@ %%% %%%---------------------------------------------------------------------- --record(sr_group, {group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()}, - opts = [] :: list() | '_' | '$2'}). +-record(sr_group, { + group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()}, + opts = [] :: list() | '_' | '$2' + }). --record(sr_user, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - group_host = {<<"">>, <<"">>} :: {binary(), binary()}}). +-record(sr_user, { + us = {<<"">>, <<"">>} :: {binary(), binary()}, + group_host = {<<"">>, <<"">>} :: {binary(), binary()} + }). diff --git a/include/mod_vcard.hrl b/include/mod_vcard.hrl index d97e5c900..3c9725b69 100644 --- a/include/mod_vcard.hrl +++ b/include/mod_vcard.hrl @@ -18,11 +18,35 @@ %%% %%%---------------------------------------------------------------------- --record(vcard_search, - {us, user, luser, fn, lfn, family, lfamily, given, - lgiven, middle, lmiddle, nickname, lnickname, bday, - lbday, ctry, lctry, locality, llocality, email, lemail, - orgname, lorgname, orgunit, lorgunit}). +-record(vcard_search, { + us, + user, + luser, + fn, + lfn, + family, + lfamily, + given, + lgiven, + middle, + lmiddle, + nickname, + lnickname, + bday, + lbday, + ctry, + lctry, + locality, + llocality, + email, + lemail, + orgname, + lorgname, + orgunit, + lorgunit + }). --record(vcard, {us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(), - vcard = #xmlel{} :: xmlel()}). +-record(vcard, { + us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(), + vcard = #xmlel{} :: xmlel() + }). diff --git a/plugins/configure_deps.erl b/plugins/configure_deps.erl index 181da0b02..7963b1b4e 100644 --- a/plugins/configure_deps.erl +++ b/plugins/configure_deps.erl @@ -1,5 +1,6 @@ -module(configure_deps). -export(['configure-deps'/2]). + 'configure-deps'(Config, Vals) -> - {ok, Config}. + {ok, Config}. diff --git a/plugins/deps_erl_opts.erl b/plugins/deps_erl_opts.erl index 725802664..94b9b712b 100644 --- a/plugins/deps_erl_opts.erl +++ b/plugins/deps_erl_opts.erl @@ -1,6 +1,7 @@ -module(deps_erl_opts). -export([preprocess/2]). + preprocess(Config, Dirs) -> ExtraOpts = rebar_config:get(Config, deps_erl_opts, []), Opts = rebar_config:get(Config, erl_opts, []), @@ -8,5 +9,7 @@ preprocess(Config, Dirs) -> lists:keystore(element(1, Opt), 1, Acc, Opt); (Opt, Acc) -> [Opt | lists:delete(Opt, Acc)] - end, Opts, ExtraOpts), + end, + Opts, + ExtraOpts), {ok, rebar_config:set(Config, erl_opts, NewOpts), []}. diff --git a/plugins/override_deps_versions2.erl b/plugins/override_deps_versions2.erl index de08b5482..0922ca694 100644 --- a/plugins/override_deps_versions2.erl +++ b/plugins/override_deps_versions2.erl @@ -1,22 +1,25 @@ -module(override_deps_versions2). -export([preprocess/2, 'pre_update-deps'/2, new_replace/1, new_replace/0]). + preprocess(Config, _Dirs) -> update_deps(Config). + update_deps(Config) -> LocalDeps = rebar_config:get_local(Config, deps, []), TopDeps = case rebar_config:get_xconf(Config, top_deps, []) of - [] -> LocalDeps; - Val -> Val - end, + [] -> LocalDeps; + Val -> Val + end, Config2 = rebar_config:set_xconf(Config, top_deps, TopDeps), NewDeps = lists:map(fun({Name, _, _} = Dep) -> - case lists:keyfind(Name, 1, TopDeps) of - false -> Dep; - TopDep -> TopDep - end - end, LocalDeps), + case lists:keyfind(Name, 1, TopDeps) of + false -> Dep; + TopDep -> TopDep + end + end, + LocalDeps), %io:format("LD ~p~n", [LocalDeps]), %io:format("TD ~p~n", [TopDeps]), @@ -28,53 +31,59 @@ update_deps(Config) -> {ok, Config2, _} = update_deps(Config), case code:is_loaded(old_rebar_config) of - false -> - {_, Beam, _} = code:get_object_code(rebar_config), - NBeam = rename(Beam, old_rebar_config), - code:load_binary(old_rebar_config, "blank", NBeam), - replace_mod(Beam); - _ -> - ok + false -> + {_, Beam, _} = code:get_object_code(rebar_config), + NBeam = rename(Beam, old_rebar_config), + code:load_binary(old_rebar_config, "blank", NBeam), + replace_mod(Beam); + _ -> + ok end, {ok, Config2}. + new_replace() -> old_rebar_config:new(). + + new_replace(Config) -> NC = old_rebar_config:new(Config), {ok, Conf, _} = update_deps(NC), Conf. + replace_mod(Beam) -> {ok, {_, [{exports, Exports}]}} = beam_lib:chunks(Beam, [exports]), Funcs = lists:filtermap( - fun({module_info, _}) -> - false; - ({Name, Arity}) -> - Args = args(Arity), - Call = case Name of - new -> - [erl_syntax:application( - erl_syntax:abstract(override_deps_versions2), - erl_syntax:abstract(new_replace), - Args)]; - _ -> - [erl_syntax:application( - erl_syntax:abstract(old_rebar_config), - erl_syntax:abstract(Name), - Args)] - end, - {true, erl_syntax:function(erl_syntax:abstract(Name), - [erl_syntax:clause(Args, none, - Call)])} - end, Exports), + fun({module_info, _}) -> + false; + ({Name, Arity}) -> + Args = args(Arity), + Call = case Name of + new -> + [erl_syntax:application( + erl_syntax:abstract(override_deps_versions2), + erl_syntax:abstract(new_replace), + Args)]; + _ -> + [erl_syntax:application( + erl_syntax:abstract(old_rebar_config), + erl_syntax:abstract(Name), + Args)] + end, + {true, erl_syntax:function(erl_syntax:abstract(Name), + [erl_syntax:clause(Args, + none, + Call)])} + end, + Exports), Forms0 = ([erl_syntax:attribute(erl_syntax:abstract(module), - [erl_syntax:abstract(rebar_config)])] - ++ Funcs), - Forms = [erl_syntax:revert(Form) || Form <- Forms0], + [erl_syntax:abstract(rebar_config)])] ++ + Funcs), + Forms = [ erl_syntax:revert(Form) || Form <- Forms0 ], %io:format("--------------------------------------------------~n" - % "~s~n", - % [[erl_pp:form(Form) || Form <- Forms]]), + % "~s~n", + % [[erl_pp:form(Form) || Form <- Forms]]), {ok, Mod, Bin} = compile:forms(Forms, [report, export_all]), code:purge(rebar_config), {module, Mod} = code:load_binary(rebar_config, "mock", Bin). @@ -83,15 +92,18 @@ replace_mod(Beam) -> args(0) -> []; args(N) -> - [arg(N) | args(N-1)]. + [arg(N) | args(N - 1)]. + arg(N) -> - erl_syntax:variable(list_to_atom("A"++integer_to_list(N))). + erl_syntax:variable(list_to_atom("A" ++ integer_to_list(N))). + rename(BeamBin0, Name) -> BeamBin = replace_in_atab(BeamBin0, Name), update_form_size(BeamBin). + %% Replace the first atom of the atom table with the new name replace_in_atab(<<"Atom", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> replace_first_atom(<<"Atom">>, Cnk, CnkSz0, Rest, latin1, Name); @@ -100,6 +112,7 @@ replace_in_atab(<<"AtU8", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> replace_in_atab(<>, Name) -> <>. + replace_first_atom(CnkName, Cnk, CnkSz0, Rest, Encoding, Name) -> <> = Cnk, NumPad0 = num_pad_bytes(CnkSz0), @@ -116,11 +129,12 @@ replace_first_atom(CnkName, Cnk, CnkSz0, Rest, Encoding, Name) -> %% BinSize to be an even multiple of ?beam_num_bytes_alignment. num_pad_bytes(BinSize) -> case 4 - (BinSize rem 4) of - 4 -> 0; - N -> N + 4 -> 0; + N -> N end. + %% Update the size within the top-level form update_form_size(<<"FOR1", _OldSz:32, Rest/binary>> = Bin) -> Sz = size(Bin) - 8, -<<"FOR1", Sz:32, Rest/binary>>. + <<"FOR1", Sz:32, Rest/binary>>. diff --git a/plugins/override_opts.erl b/plugins/override_opts.erl index 818f53e87..d578b1ea9 100644 --- a/plugins/override_opts.erl +++ b/plugins/override_opts.erl @@ -1,43 +1,53 @@ -module(override_opts). -export([preprocess/2]). + override_opts(override, Config, Opts) -> lists:foldl(fun({Opt, Value}, Conf) -> - rebar_config:set(Conf, Opt, Value) - end, Config, Opts); + rebar_config:set(Conf, Opt, Value) + end, + Config, + Opts); override_opts(add, Config, Opts) -> lists:foldl(fun({Opt, Value}, Conf) -> - V = rebar_config:get_local(Conf, Opt, []), - rebar_config:set(Conf, Opt, V ++ Value) - end, Config, Opts); + V = rebar_config:get_local(Conf, Opt, []), + rebar_config:set(Conf, Opt, V ++ Value) + end, + Config, + Opts); override_opts(del, Config, Opts) -> lists:foldl(fun({Opt, Value}, Conf) -> - V = rebar_config:get_local(Conf, Opt, []), - rebar_config:set(Conf, Opt, V -- Value) - end, Config, Opts). + V = rebar_config:get_local(Conf, Opt, []), + rebar_config:set(Conf, Opt, V -- Value) + end, + Config, + Opts). + preprocess(Config, _Dirs) -> Overrides = rebar_config:get_local(Config, overrides, []), TopOverrides = case rebar_config:get_xconf(Config, top_overrides, []) of - [] -> Overrides; - Val -> Val - end, + [] -> Overrides; + Val -> Val + end, Config2 = rebar_config:set_xconf(Config, top_overrides, TopOverrides), try Config3 = case rebar_app_utils:load_app_file(Config2, _Dirs) of - {ok, C, AppName, _AppData} -> - lists:foldl(fun({Type, AppName2, Opts}, Conf1) when - AppName2 == AppName -> - override_opts(Type, Conf1, Opts); - ({Type, Opts}, Conf1a) -> - override_opts(Type, Conf1a, Opts); - (_, Conf2) -> - Conf2 - end, C, TopOverrides); - _ -> - Config2 - end, - {ok, Config3, []} + {ok, C, AppName, _AppData} -> + lists:foldl(fun({Type, AppName2, Opts}, Conf1) + when AppName2 == AppName -> + override_opts(Type, Conf1, Opts); + ({Type, Opts}, Conf1a) -> + override_opts(Type, Conf1a, Opts); + (_, Conf2) -> + Conf2 + end, + C, + TopOverrides); + _ -> + Config2 + end, + {ok, Config3, []} catch error:badarg -> {ok, Config2, []} end. diff --git a/rebar.config b/rebar.config index 6cbd4d7b7..f0eb5950e 100644 --- a/rebar.config +++ b/rebar.config @@ -25,8 +25,7 @@ {deps, [{if_not_rebar3, {if_version_below, "24", - {base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}} - }}, + {base64url, "~> 1.0", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}}}, {cache_tab, "~> 1.0.33", {git, "https://github.com/processone/cache_tab", {tag, "1.0.33"}}}, {eimp, "~> 1.0.26", {git, "https://github.com/processone/eimp", {tag, "1.0.26"}}}, {if_var_true, @@ -35,16 +34,14 @@ {if_var_true, redis, {if_not_rebar3, - {eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}} - }}, + {eredis, "~> 1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}}}, {if_var_true, redis, {if_rebar3, {if_version_below, "21", {eredis, "1.2.0", {git, "https://github.com/wooga/eredis/", {tag, "v1.2.0"}}}, - {eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}} - }}}, + {eredis, "~> 1.7.1", {git, "https://github.com/Nordix/eredis/", {tag, "v1.7.1"}}}}}}, {if_var_true, sip, {esip, "~> 1.0.59", {git, "https://github.com/processone/esip", {tag, "1.0.59"}}}}, @@ -57,23 +54,19 @@ {idna, "~> 6.0", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}}, {if_version_below, "27", - {jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}} - }, + {jiffy, "~> 1.1.1", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}}}, {if_version_above, "23", {jose, "~> 1.11.10", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.10"}}}, - {jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} - }, + {jose, "1.11.1", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}}, {if_version_below, "22", - {lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}} - }, + {lager, "~> 3.9.1", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}}}, {if_var_true, lua, {if_version_below, "21", - {luerl, "1.0.0", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}}, - {luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}} - }}, + {luerl, "1.0.0", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}}, + {luerl, "~> 1.2.0", {git, "https://github.com/rvirding/luerl", {tag, "1.2"}}}}}, {mqtree, "~> 1.0.19", {git, "https://github.com/processone/mqtree", {tag, "1.0.19"}}}, {p1_acme, "~> 1.0.28", {git, "https://github.com/processone/p1_acme", {tag, "1.0.28"}}}, {if_var_true, @@ -93,32 +86,31 @@ stun, {stun, "~> 1.2.21", {git, "https://github.com/processone/stun", {tag, "1.2.21"}}}}, {xmpp, "~> 1.11.1", {git, "https://github.com/processone/xmpp", {tag, "1.11.1"}}}, - {yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}} - ]}. + {yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}}]}. {gitonly_deps, [ejabberd_po]}. {if_var_true, latest_deps, - {floating_deps, [cache_tab, - eimp, - epam, - esip, - ezlib, - fast_tls, - fast_xml, - fast_yaml, - mqtree, - p1_acme, - p1_mysql, - p1_oauth2, - p1_pgsql, - p1_utils, - pkix, - sqlite3, - stringprep, - stun, - xmpp, - yconf]}}. + {floating_deps, [cache_tab, + eimp, + epam, + esip, + ezlib, + fast_tls, + fast_xml, + fast_yaml, + mqtree, + p1_acme, + p1_mysql, + p1_oauth2, + p1_pgsql, + p1_utils, + pkix, + sqlite3, + stringprep, + stun, + xmpp, + yconf]}}. %%% %%% Compile @@ -136,8 +128,8 @@ {"stringprep", []}]}. {erl_first_files, ["src/ejabberd_sql_pt.erl", "src/ejabberd_config.erl", - "src/gen_mod.erl", "src/mod_muc_room.erl", - "src/mod_push.erl", "src/xmpp_socket.erl"]}. + "src/gen_mod.erl", "src/mod_muc_room.erl", + "src/mod_push.erl", "src/xmpp_socket.erl"]}. {erl_opts, [nowarn_deprecated_function, {i, "include"}, @@ -174,22 +166,17 @@ %% https://github.com/Supersonido/rebar_mix/issues/27#issuecomment-894873335 %% Let's use this fixed rebar_mix fork, see its PR: %% https://github.com/Supersonido/rebar_mix/pull/31 - {if_var_true, elixir, {rebar_mix, ".*", - {git, "https://github.com/bsanyi/rebar_mix.git", - {branch, "consolidation_fix"}}} - }]}}. + {if_var_true, elixir, + {rebar_mix, ".*", + {git, "https://github.com/bsanyi/rebar_mix.git", + {branch, "consolidation_fix"}}}}]}}. {if_rebar3, {project_plugins, [configure_deps, {if_var_true, tools, rebar3_efmt}, - {if_var_true, tools, {rebar3_lint, "4.1.1"}} - ]}}. -{if_not_rebar3, {plugins, [ - deps_erl_opts, override_deps_versions2, override_opts, configure_deps - ]}}. + {if_var_true, tools, {rebar3_lint, "4.1.1"}}]}}. +{if_not_rebar3, {plugins, [deps_erl_opts, override_deps_versions2, override_opts, configure_deps]}}. {if_rebar3, {if_var_true, elixir, - {provider_hooks, [ - {post, [{compile, {mix, consolidate_protocols}}]} - ]}}}. + {provider_hooks, [{post, [{compile, {mix, consolidate_protocols}}]}]}}}. %% Compiling Jose 1.11.10 with Erlang/OTP 27.0 throws warnings on public_key deprecated functions {if_rebar3, {overrides, [{del, jose, [{erl_opts, [warnings_as_errors]}]}]}}. @@ -207,16 +194,13 @@ {if_rebar3, {xref_checks, [deprecated_function_calls, deprecated_functions, locals_not_used, - undefined_function_calls, undefined_functions]} -}. + undefined_function_calls, undefined_functions]}}. {if_not_rebar3, {xref_checks, [deprecated_function_calls, deprecated_functions, - undefined_function_calls, undefined_functions]} -}. + undefined_function_calls, undefined_functions]}}. -{xref_exclusions, [ - "(\"gen_transport\":_/_)", +{xref_exclusions, ["(\"gen_transport\":_/_)", "(\"eprof\":_/_)", {if_var_false, elixir, "(\"Elixir.*\":_/_)"}, {if_var_false, http, "(\"lhttpc\":_/_)"}, @@ -234,37 +218,62 @@ {eunit_compile_opts, [{i, "tools"}, {i, "include"}]}. -{dialyzer, [{get_warnings, false}, % Show warnings of dependencies +{dialyzer, [{get_warnings, false}, % Show warnings of dependencies {if_version_above, "25", - {plt_extra_apps, - [asn1, odbc, public_key, stdlib, syntax_tools, - idna, jose, - cache_tab, eimp, fast_tls, fast_xml, fast_yaml, - mqtree, p1_acme, p1_oauth2, p1_utils, pkix, - stringprep, xmpp, yconf, - {if_version_below, "27", jiffy}, - {if_var_true, pam, epam}, - {if_var_true, redis, eredis}, - {if_var_true, sip, esip}, - {if_var_true, zlib, ezlib}, - {if_var_true, lua, luerl}, - {if_var_true, mysql, p1_mysql}, - {if_var_true, pgsql, p1_pgsql}, - {if_var_true, stun, stun}, - {if_var_true, sqlite, sqlite3}]}, - {plt_extra_apps, % For Erlang/OTP 25 and older - [cache_tab, eimp, fast_tls, fast_xml, fast_yaml, - mqtree, p1_acme, p1_oauth2, p1_utils, pkix, stringprep, xmpp, yconf, - {if_var_true, pam, epam}, - {if_var_true, redis, eredis}, - {if_var_true, sip, esip}, - {if_var_true, zlib, ezlib}, - {if_var_true, lua, luerl}, - {if_var_true, mysql, p1_mysql}, - {if_var_true, pgsql, p1_pgsql}, - {if_var_true, stun, stun}, - {if_var_true, sqlite, sqlite3}]} - } ]}. + {plt_extra_apps, + [asn1, + odbc, + public_key, + stdlib, + syntax_tools, + idna, + jose, + cache_tab, + eimp, + fast_tls, + fast_xml, + fast_yaml, + mqtree, + p1_acme, + p1_oauth2, + p1_utils, + pkix, + stringprep, + xmpp, + yconf, + {if_version_below, "27", jiffy}, + {if_var_true, pam, epam}, + {if_var_true, redis, eredis}, + {if_var_true, sip, esip}, + {if_var_true, zlib, ezlib}, + {if_var_true, lua, luerl}, + {if_var_true, mysql, p1_mysql}, + {if_var_true, pgsql, p1_pgsql}, + {if_var_true, stun, stun}, + {if_var_true, sqlite, sqlite3}]}, + {plt_extra_apps, % For Erlang/OTP 25 and older + [cache_tab, + eimp, + fast_tls, + fast_xml, + fast_yaml, + mqtree, + p1_acme, + p1_oauth2, + p1_utils, + pkix, + stringprep, + xmpp, + yconf, + {if_var_true, pam, epam}, + {if_var_true, redis, eredis}, + {if_var_true, sip, esip}, + {if_var_true, zlib, ezlib}, + {if_var_true, lua, luerl}, + {if_var_true, mysql, p1_mysql}, + {if_var_true, pgsql, p1_pgsql}, + {if_var_true, stun, stun}, + {if_var_true, sqlite, sqlite3}]}}]}. {ct_opts, [{keep_logs, 20}]}. @@ -279,7 +288,7 @@ %%% {relx, [{release, {ejabberd, {cmd, "grep {vsn, vars.config | sed 's|{vsn, \"||;s|\"}.||' | tr -d '\012'"}}, - [ejabberd]}, + [ejabberd]}, {sys_config, "./rel/sys.config"}, {vm_args, "./rel/vm.args"}, {overlay_vars, "vars.config"}, @@ -292,12 +301,10 @@ {copy, "{{base_dir}}/consolidated/*", "lib/ejabberd-{{release_version}}/ebin/"}, {copy, "rel/overlays/iex", "releases/{{release_version}}/"}, {if_var_true, elixir, - {template, "rel/overlays/elixir", "releases/{{release_version}}/elixir"} - }, + {template, "rel/overlays/elixir", "releases/{{release_version}}/elixir"}}, {copy, "inetrc", "conf/inetrc"}, {copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"}, - {copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]} - ]}. + {copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]}]}. {profiles, [{prod, [{relx, [{debug_info, strip}, {dev_mode, false}, @@ -326,8 +333,7 @@ --config rel/relive.config \ --eval sync:go(). \ --script rel/relive.escript \ - --name ejabberd@localhost"}]} -]}. + --name ejabberd@localhost"}]}]}. %% Local Variables: %% mode: erlang diff --git a/rel/files/install_upgrade.escript b/rel/files/install_upgrade.escript index 56cea1963..1a842b725 100644 --- a/rel/files/install_upgrade.escript +++ b/rel/files/install_upgrade.escript @@ -4,23 +4,34 @@ %% ex: ft=erlang ts=4 sw=4 et -define(TIMEOUT, 60000). --define(INFO(Fmt,Args), io:format(Fmt,Args)). +-define(INFO(Fmt, Args), io:format(Fmt, Args)). + main([NodeName, Cookie, ReleasePackage]) -> TargetNode = start_distribution(NodeName, Cookie), - {ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release, - [ReleasePackage], ?TIMEOUT), + {ok, Vsn} = rpc:call(TargetNode, + release_handler, + unpack_release, + [ReleasePackage], + ?TIMEOUT), ?INFO("Unpacked Release ~p~n", [Vsn]), - {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, - check_install_release, [Vsn], ?TIMEOUT), - {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, - install_release, [Vsn], ?TIMEOUT), + {ok, OtherVsn, Desc} = rpc:call(TargetNode, + release_handler, + check_install_release, + [Vsn], + ?TIMEOUT), + {ok, OtherVsn, Desc} = rpc:call(TargetNode, + release_handler, + install_release, + [Vsn], + ?TIMEOUT), ?INFO("Installed Release ~p~n", [Vsn]), ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), ?INFO("Made Release ~p Permanent~n", [Vsn]); main(_) -> init:stop(1). + start_distribution(NodeName, Cookie) -> MyNode = make_script_node(NodeName), {ok, _Pid} = net_kernel:start([MyNode, shortnames]), @@ -36,9 +47,11 @@ start_distribution(NodeName, Cookie) -> end, TargetNode. + make_target_node(Node) -> [_, Host] = string:tokens(atom_to_list(node()), "@"), list_to_atom(lists:concat([Node, "@", Host])). + make_script_node(Node) -> list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])). diff --git a/rel/relive.escript b/rel/relive.escript index 3ee2de0f3..b55e208f2 100644 --- a/rel/relive.escript +++ b/rel/relive.escript @@ -1,5 +1,6 @@ #!/usr/bin/env escript + main(_) -> Base = "_build/relive", prepare(Base, "", none), @@ -9,6 +10,7 @@ main(_) -> c:erlangrc([os:cmd("echo -n $HOME")]), ok. + prepare(BaseDir, SuffixDir, MFA) -> Dir = filename:join(BaseDir, SuffixDir), case file:make_dir(Dir) of diff --git a/src/ELDAPv3.erl b/src/ELDAPv3.erl index 3c102e7ec..4a6eb0f7d 100644 --- a/src/ELDAPv3.erl +++ b/src/ELDAPv3.erl @@ -5,270 +5,265 @@ -compile(nowarn_unused_vars). -dialyzer(no_match). -include("ELDAPv3.hrl"). --asn1_info([{vsn,'2.0.1'}, - {module,'ELDAPv3'}, - {options,[{i,"src"},{outdir,"src"},noobj,{i,"."},{i,"asn1"}]}]). +-asn1_info([{vsn, '2.0.1'}, + {module, 'ELDAPv3'}, + {options, [{i, "src"}, {outdir, "src"}, noobj, {i, "."}, {i, "asn1"}]}]). --export([encoding_rule/0,bit_string_format/0]). --export([ -'enc_LDAPMessage'/2, -'enc_MessageID'/2, -'enc_LDAPString'/2, -'enc_LDAPOID'/2, -'enc_LDAPDN'/2, -'enc_RelativeLDAPDN'/2, -'enc_AttributeType'/2, -'enc_AttributeDescription'/2, -'enc_AttributeDescriptionList'/2, -'enc_AttributeValue'/2, -'enc_AttributeValueAssertion'/2, -'enc_AssertionValue'/2, -'enc_Attribute'/2, -'enc_MatchingRuleId'/2, -'enc_LDAPResult'/2, -'enc_Referral'/2, -'enc_LDAPURL'/2, -'enc_Controls'/2, -'enc_Control'/2, -'enc_BindRequest'/2, -'enc_AuthenticationChoice'/2, -'enc_SaslCredentials'/2, -'enc_BindResponse'/2, -'enc_UnbindRequest'/2, -'enc_SearchRequest'/2, -'enc_Filter'/2, -'enc_SubstringFilter'/2, -'enc_MatchingRuleAssertion'/2, -'enc_SearchResultEntry'/2, -'enc_PartialAttributeList'/2, -'enc_SearchResultReference'/2, -'enc_SearchResultDone'/2, -'enc_ModifyRequest'/2, -'enc_AttributeTypeAndValues'/2, -'enc_ModifyResponse'/2, -'enc_AddRequest'/2, -'enc_AttributeList'/2, -'enc_AddResponse'/2, -'enc_DelRequest'/2, -'enc_DelResponse'/2, -'enc_ModifyDNRequest'/2, -'enc_ModifyDNResponse'/2, -'enc_CompareRequest'/2, -'enc_CompareResponse'/2, -'enc_AbandonRequest'/2, -'enc_ExtendedRequest'/2, -'enc_ExtendedResponse'/2, -'enc_PasswdModifyRequestValue'/2, -'enc_PasswdModifyResponseValue'/2 -]). +-export([encoding_rule/0, bit_string_format/0]). +-export(['enc_LDAPMessage'/2, + 'enc_MessageID'/2, + 'enc_LDAPString'/2, + 'enc_LDAPOID'/2, + 'enc_LDAPDN'/2, + 'enc_RelativeLDAPDN'/2, + 'enc_AttributeType'/2, + 'enc_AttributeDescription'/2, + 'enc_AttributeDescriptionList'/2, + 'enc_AttributeValue'/2, + 'enc_AttributeValueAssertion'/2, + 'enc_AssertionValue'/2, + 'enc_Attribute'/2, + 'enc_MatchingRuleId'/2, + 'enc_LDAPResult'/2, + 'enc_Referral'/2, + 'enc_LDAPURL'/2, + 'enc_Controls'/2, + 'enc_Control'/2, + 'enc_BindRequest'/2, + 'enc_AuthenticationChoice'/2, + 'enc_SaslCredentials'/2, + 'enc_BindResponse'/2, + 'enc_UnbindRequest'/2, + 'enc_SearchRequest'/2, + 'enc_Filter'/2, + 'enc_SubstringFilter'/2, + 'enc_MatchingRuleAssertion'/2, + 'enc_SearchResultEntry'/2, + 'enc_PartialAttributeList'/2, + 'enc_SearchResultReference'/2, + 'enc_SearchResultDone'/2, + 'enc_ModifyRequest'/2, + 'enc_AttributeTypeAndValues'/2, + 'enc_ModifyResponse'/2, + 'enc_AddRequest'/2, + 'enc_AttributeList'/2, + 'enc_AddResponse'/2, + 'enc_DelRequest'/2, + 'enc_DelResponse'/2, + 'enc_ModifyDNRequest'/2, + 'enc_ModifyDNResponse'/2, + 'enc_CompareRequest'/2, + 'enc_CompareResponse'/2, + 'enc_AbandonRequest'/2, + 'enc_ExtendedRequest'/2, + 'enc_ExtendedResponse'/2, + 'enc_PasswdModifyRequestValue'/2, + 'enc_PasswdModifyResponseValue'/2]). --export([ -'dec_LDAPMessage'/2, -'dec_MessageID'/2, -'dec_LDAPString'/2, -'dec_LDAPOID'/2, -'dec_LDAPDN'/2, -'dec_RelativeLDAPDN'/2, -'dec_AttributeType'/2, -'dec_AttributeDescription'/2, -'dec_AttributeDescriptionList'/2, -'dec_AttributeValue'/2, -'dec_AttributeValueAssertion'/2, -'dec_AssertionValue'/2, -'dec_Attribute'/2, -'dec_MatchingRuleId'/2, -'dec_LDAPResult'/2, -'dec_Referral'/2, -'dec_LDAPURL'/2, -'dec_Controls'/2, -'dec_Control'/2, -'dec_BindRequest'/2, -'dec_AuthenticationChoice'/2, -'dec_SaslCredentials'/2, -'dec_BindResponse'/2, -'dec_UnbindRequest'/2, -'dec_SearchRequest'/2, -'dec_Filter'/2, -'dec_SubstringFilter'/2, -'dec_MatchingRuleAssertion'/2, -'dec_SearchResultEntry'/2, -'dec_PartialAttributeList'/2, -'dec_SearchResultReference'/2, -'dec_SearchResultDone'/2, -'dec_ModifyRequest'/2, -'dec_AttributeTypeAndValues'/2, -'dec_ModifyResponse'/2, -'dec_AddRequest'/2, -'dec_AttributeList'/2, -'dec_AddResponse'/2, -'dec_DelRequest'/2, -'dec_DelResponse'/2, -'dec_ModifyDNRequest'/2, -'dec_ModifyDNResponse'/2, -'dec_CompareRequest'/2, -'dec_CompareResponse'/2, -'dec_AbandonRequest'/2, -'dec_ExtendedRequest'/2, -'dec_ExtendedResponse'/2, -'dec_PasswdModifyRequestValue'/2, -'dec_PasswdModifyResponseValue'/2 -]). +-export(['dec_LDAPMessage'/2, + 'dec_MessageID'/2, + 'dec_LDAPString'/2, + 'dec_LDAPOID'/2, + 'dec_LDAPDN'/2, + 'dec_RelativeLDAPDN'/2, + 'dec_AttributeType'/2, + 'dec_AttributeDescription'/2, + 'dec_AttributeDescriptionList'/2, + 'dec_AttributeValue'/2, + 'dec_AttributeValueAssertion'/2, + 'dec_AssertionValue'/2, + 'dec_Attribute'/2, + 'dec_MatchingRuleId'/2, + 'dec_LDAPResult'/2, + 'dec_Referral'/2, + 'dec_LDAPURL'/2, + 'dec_Controls'/2, + 'dec_Control'/2, + 'dec_BindRequest'/2, + 'dec_AuthenticationChoice'/2, + 'dec_SaslCredentials'/2, + 'dec_BindResponse'/2, + 'dec_UnbindRequest'/2, + 'dec_SearchRequest'/2, + 'dec_Filter'/2, + 'dec_SubstringFilter'/2, + 'dec_MatchingRuleAssertion'/2, + 'dec_SearchResultEntry'/2, + 'dec_PartialAttributeList'/2, + 'dec_SearchResultReference'/2, + 'dec_SearchResultDone'/2, + 'dec_ModifyRequest'/2, + 'dec_AttributeTypeAndValues'/2, + 'dec_ModifyResponse'/2, + 'dec_AddRequest'/2, + 'dec_AttributeList'/2, + 'dec_AddResponse'/2, + 'dec_DelRequest'/2, + 'dec_DelResponse'/2, + 'dec_ModifyDNRequest'/2, + 'dec_ModifyDNResponse'/2, + 'dec_CompareRequest'/2, + 'dec_CompareResponse'/2, + 'dec_AbandonRequest'/2, + 'dec_ExtendedRequest'/2, + 'dec_ExtendedResponse'/2, + 'dec_PasswdModifyRequestValue'/2, + 'dec_PasswdModifyResponseValue'/2]). --export([ -'maxInt'/0, -'passwdModifyOID'/0 -]). +-export(['maxInt'/0, + 'passwdModifyOID'/0]). -export([info/0]). +-export([encode/2, decode/2]). --export([encode/2,decode/2]). encoding_rule() -> ber. + bit_string_format() -> bitstring. -encode(Type,Data) -> -case catch encode_disp(Type,Data) of - {'EXIT',{error,Reason}} -> - {error,Reason}; - {'EXIT',Reason} -> - {error,{asn1,Reason}}; - {Bytes,_Len} -> - {ok,iolist_to_binary(Bytes)}; - Bytes -> - {ok,iolist_to_binary(Bytes)} -end. -decode(Type,Data) -> -case catch decode_disp(Type,element(1, ber_decode_nif(Data))) of - {'EXIT',{error,Reason}} -> - {error,Reason}; - {'EXIT',Reason} -> - {error,{asn1,Reason}}; - Result -> - {ok,Result} -end. - -encode_disp('LDAPMessage',Data) -> 'enc_LDAPMessage'(Data); -encode_disp('MessageID',Data) -> 'enc_MessageID'(Data); -encode_disp('LDAPString',Data) -> 'enc_LDAPString'(Data); -encode_disp('LDAPOID',Data) -> 'enc_LDAPOID'(Data); -encode_disp('LDAPDN',Data) -> 'enc_LDAPDN'(Data); -encode_disp('RelativeLDAPDN',Data) -> 'enc_RelativeLDAPDN'(Data); -encode_disp('AttributeType',Data) -> 'enc_AttributeType'(Data); -encode_disp('AttributeDescription',Data) -> 'enc_AttributeDescription'(Data); -encode_disp('AttributeDescriptionList',Data) -> 'enc_AttributeDescriptionList'(Data); -encode_disp('AttributeValue',Data) -> 'enc_AttributeValue'(Data); -encode_disp('AttributeValueAssertion',Data) -> 'enc_AttributeValueAssertion'(Data); -encode_disp('AssertionValue',Data) -> 'enc_AssertionValue'(Data); -encode_disp('Attribute',Data) -> 'enc_Attribute'(Data); -encode_disp('MatchingRuleId',Data) -> 'enc_MatchingRuleId'(Data); -encode_disp('LDAPResult',Data) -> 'enc_LDAPResult'(Data); -encode_disp('Referral',Data) -> 'enc_Referral'(Data); -encode_disp('LDAPURL',Data) -> 'enc_LDAPURL'(Data); -encode_disp('Controls',Data) -> 'enc_Controls'(Data); -encode_disp('Control',Data) -> 'enc_Control'(Data); -encode_disp('BindRequest',Data) -> 'enc_BindRequest'(Data); -encode_disp('AuthenticationChoice',Data) -> 'enc_AuthenticationChoice'(Data); -encode_disp('SaslCredentials',Data) -> 'enc_SaslCredentials'(Data); -encode_disp('BindResponse',Data) -> 'enc_BindResponse'(Data); -encode_disp('UnbindRequest',Data) -> 'enc_UnbindRequest'(Data); -encode_disp('SearchRequest',Data) -> 'enc_SearchRequest'(Data); -encode_disp('Filter',Data) -> 'enc_Filter'(Data); -encode_disp('SubstringFilter',Data) -> 'enc_SubstringFilter'(Data); -encode_disp('MatchingRuleAssertion',Data) -> 'enc_MatchingRuleAssertion'(Data); -encode_disp('SearchResultEntry',Data) -> 'enc_SearchResultEntry'(Data); -encode_disp('PartialAttributeList',Data) -> 'enc_PartialAttributeList'(Data); -encode_disp('SearchResultReference',Data) -> 'enc_SearchResultReference'(Data); -encode_disp('SearchResultDone',Data) -> 'enc_SearchResultDone'(Data); -encode_disp('ModifyRequest',Data) -> 'enc_ModifyRequest'(Data); -encode_disp('AttributeTypeAndValues',Data) -> 'enc_AttributeTypeAndValues'(Data); -encode_disp('ModifyResponse',Data) -> 'enc_ModifyResponse'(Data); -encode_disp('AddRequest',Data) -> 'enc_AddRequest'(Data); -encode_disp('AttributeList',Data) -> 'enc_AttributeList'(Data); -encode_disp('AddResponse',Data) -> 'enc_AddResponse'(Data); -encode_disp('DelRequest',Data) -> 'enc_DelRequest'(Data); -encode_disp('DelResponse',Data) -> 'enc_DelResponse'(Data); -encode_disp('ModifyDNRequest',Data) -> 'enc_ModifyDNRequest'(Data); -encode_disp('ModifyDNResponse',Data) -> 'enc_ModifyDNResponse'(Data); -encode_disp('CompareRequest',Data) -> 'enc_CompareRequest'(Data); -encode_disp('CompareResponse',Data) -> 'enc_CompareResponse'(Data); -encode_disp('AbandonRequest',Data) -> 'enc_AbandonRequest'(Data); -encode_disp('ExtendedRequest',Data) -> 'enc_ExtendedRequest'(Data); -encode_disp('ExtendedResponse',Data) -> 'enc_ExtendedResponse'(Data); -encode_disp('PasswdModifyRequestValue',Data) -> 'enc_PasswdModifyRequestValue'(Data); -encode_disp('PasswdModifyResponseValue',Data) -> 'enc_PasswdModifyResponseValue'(Data); -encode_disp(Type,_Data) -> exit({error,{asn1,{undefined_type,Type}}}). +encode(Type, Data) -> + case catch encode_disp(Type, Data) of + {'EXIT', {error, Reason}} -> + {error, Reason}; + {'EXIT', Reason} -> + {error, {asn1, Reason}}; + {Bytes, _Len} -> + {ok, iolist_to_binary(Bytes)}; + Bytes -> + {ok, iolist_to_binary(Bytes)} + end. -decode_disp('LDAPMessage',Data) -> 'dec_LDAPMessage'(Data); -decode_disp('MessageID',Data) -> 'dec_MessageID'(Data); -decode_disp('LDAPString',Data) -> 'dec_LDAPString'(Data); -decode_disp('LDAPOID',Data) -> 'dec_LDAPOID'(Data); -decode_disp('LDAPDN',Data) -> 'dec_LDAPDN'(Data); -decode_disp('RelativeLDAPDN',Data) -> 'dec_RelativeLDAPDN'(Data); -decode_disp('AttributeType',Data) -> 'dec_AttributeType'(Data); -decode_disp('AttributeDescription',Data) -> 'dec_AttributeDescription'(Data); -decode_disp('AttributeDescriptionList',Data) -> 'dec_AttributeDescriptionList'(Data); -decode_disp('AttributeValue',Data) -> 'dec_AttributeValue'(Data); -decode_disp('AttributeValueAssertion',Data) -> 'dec_AttributeValueAssertion'(Data); -decode_disp('AssertionValue',Data) -> 'dec_AssertionValue'(Data); -decode_disp('Attribute',Data) -> 'dec_Attribute'(Data); -decode_disp('MatchingRuleId',Data) -> 'dec_MatchingRuleId'(Data); -decode_disp('LDAPResult',Data) -> 'dec_LDAPResult'(Data); -decode_disp('Referral',Data) -> 'dec_Referral'(Data); -decode_disp('LDAPURL',Data) -> 'dec_LDAPURL'(Data); -decode_disp('Controls',Data) -> 'dec_Controls'(Data); -decode_disp('Control',Data) -> 'dec_Control'(Data); -decode_disp('BindRequest',Data) -> 'dec_BindRequest'(Data); -decode_disp('AuthenticationChoice',Data) -> 'dec_AuthenticationChoice'(Data); -decode_disp('SaslCredentials',Data) -> 'dec_SaslCredentials'(Data); -decode_disp('BindResponse',Data) -> 'dec_BindResponse'(Data); -decode_disp('UnbindRequest',Data) -> 'dec_UnbindRequest'(Data); -decode_disp('SearchRequest',Data) -> 'dec_SearchRequest'(Data); -decode_disp('Filter',Data) -> 'dec_Filter'(Data); -decode_disp('SubstringFilter',Data) -> 'dec_SubstringFilter'(Data); -decode_disp('MatchingRuleAssertion',Data) -> 'dec_MatchingRuleAssertion'(Data); -decode_disp('SearchResultEntry',Data) -> 'dec_SearchResultEntry'(Data); -decode_disp('PartialAttributeList',Data) -> 'dec_PartialAttributeList'(Data); -decode_disp('SearchResultReference',Data) -> 'dec_SearchResultReference'(Data); -decode_disp('SearchResultDone',Data) -> 'dec_SearchResultDone'(Data); -decode_disp('ModifyRequest',Data) -> 'dec_ModifyRequest'(Data); -decode_disp('AttributeTypeAndValues',Data) -> 'dec_AttributeTypeAndValues'(Data); -decode_disp('ModifyResponse',Data) -> 'dec_ModifyResponse'(Data); -decode_disp('AddRequest',Data) -> 'dec_AddRequest'(Data); -decode_disp('AttributeList',Data) -> 'dec_AttributeList'(Data); -decode_disp('AddResponse',Data) -> 'dec_AddResponse'(Data); -decode_disp('DelRequest',Data) -> 'dec_DelRequest'(Data); -decode_disp('DelResponse',Data) -> 'dec_DelResponse'(Data); -decode_disp('ModifyDNRequest',Data) -> 'dec_ModifyDNRequest'(Data); -decode_disp('ModifyDNResponse',Data) -> 'dec_ModifyDNResponse'(Data); -decode_disp('CompareRequest',Data) -> 'dec_CompareRequest'(Data); -decode_disp('CompareResponse',Data) -> 'dec_CompareResponse'(Data); -decode_disp('AbandonRequest',Data) -> 'dec_AbandonRequest'(Data); -decode_disp('ExtendedRequest',Data) -> 'dec_ExtendedRequest'(Data); -decode_disp('ExtendedResponse',Data) -> 'dec_ExtendedResponse'(Data); -decode_disp('PasswdModifyRequestValue',Data) -> 'dec_PasswdModifyRequestValue'(Data); -decode_disp('PasswdModifyResponseValue',Data) -> 'dec_PasswdModifyResponseValue'(Data); -decode_disp(Type,_Data) -> exit({error,{asn1,{undefined_type,Type}}}). +decode(Type, Data) -> + case catch decode_disp(Type, element(1, ber_decode_nif(Data))) of + {'EXIT', {error, Reason}} -> + {error, Reason}; + {'EXIT', Reason} -> + {error, {asn1, Reason}}; + Result -> + {ok, Result} + end. +encode_disp('LDAPMessage', Data) -> 'enc_LDAPMessage'(Data); +encode_disp('MessageID', Data) -> 'enc_MessageID'(Data); +encode_disp('LDAPString', Data) -> 'enc_LDAPString'(Data); +encode_disp('LDAPOID', Data) -> 'enc_LDAPOID'(Data); +encode_disp('LDAPDN', Data) -> 'enc_LDAPDN'(Data); +encode_disp('RelativeLDAPDN', Data) -> 'enc_RelativeLDAPDN'(Data); +encode_disp('AttributeType', Data) -> 'enc_AttributeType'(Data); +encode_disp('AttributeDescription', Data) -> 'enc_AttributeDescription'(Data); +encode_disp('AttributeDescriptionList', Data) -> 'enc_AttributeDescriptionList'(Data); +encode_disp('AttributeValue', Data) -> 'enc_AttributeValue'(Data); +encode_disp('AttributeValueAssertion', Data) -> 'enc_AttributeValueAssertion'(Data); +encode_disp('AssertionValue', Data) -> 'enc_AssertionValue'(Data); +encode_disp('Attribute', Data) -> 'enc_Attribute'(Data); +encode_disp('MatchingRuleId', Data) -> 'enc_MatchingRuleId'(Data); +encode_disp('LDAPResult', Data) -> 'enc_LDAPResult'(Data); +encode_disp('Referral', Data) -> 'enc_Referral'(Data); +encode_disp('LDAPURL', Data) -> 'enc_LDAPURL'(Data); +encode_disp('Controls', Data) -> 'enc_Controls'(Data); +encode_disp('Control', Data) -> 'enc_Control'(Data); +encode_disp('BindRequest', Data) -> 'enc_BindRequest'(Data); +encode_disp('AuthenticationChoice', Data) -> 'enc_AuthenticationChoice'(Data); +encode_disp('SaslCredentials', Data) -> 'enc_SaslCredentials'(Data); +encode_disp('BindResponse', Data) -> 'enc_BindResponse'(Data); +encode_disp('UnbindRequest', Data) -> 'enc_UnbindRequest'(Data); +encode_disp('SearchRequest', Data) -> 'enc_SearchRequest'(Data); +encode_disp('Filter', Data) -> 'enc_Filter'(Data); +encode_disp('SubstringFilter', Data) -> 'enc_SubstringFilter'(Data); +encode_disp('MatchingRuleAssertion', Data) -> 'enc_MatchingRuleAssertion'(Data); +encode_disp('SearchResultEntry', Data) -> 'enc_SearchResultEntry'(Data); +encode_disp('PartialAttributeList', Data) -> 'enc_PartialAttributeList'(Data); +encode_disp('SearchResultReference', Data) -> 'enc_SearchResultReference'(Data); +encode_disp('SearchResultDone', Data) -> 'enc_SearchResultDone'(Data); +encode_disp('ModifyRequest', Data) -> 'enc_ModifyRequest'(Data); +encode_disp('AttributeTypeAndValues', Data) -> 'enc_AttributeTypeAndValues'(Data); +encode_disp('ModifyResponse', Data) -> 'enc_ModifyResponse'(Data); +encode_disp('AddRequest', Data) -> 'enc_AddRequest'(Data); +encode_disp('AttributeList', Data) -> 'enc_AttributeList'(Data); +encode_disp('AddResponse', Data) -> 'enc_AddResponse'(Data); +encode_disp('DelRequest', Data) -> 'enc_DelRequest'(Data); +encode_disp('DelResponse', Data) -> 'enc_DelResponse'(Data); +encode_disp('ModifyDNRequest', Data) -> 'enc_ModifyDNRequest'(Data); +encode_disp('ModifyDNResponse', Data) -> 'enc_ModifyDNResponse'(Data); +encode_disp('CompareRequest', Data) -> 'enc_CompareRequest'(Data); +encode_disp('CompareResponse', Data) -> 'enc_CompareResponse'(Data); +encode_disp('AbandonRequest', Data) -> 'enc_AbandonRequest'(Data); +encode_disp('ExtendedRequest', Data) -> 'enc_ExtendedRequest'(Data); +encode_disp('ExtendedResponse', Data) -> 'enc_ExtendedResponse'(Data); +encode_disp('PasswdModifyRequestValue', Data) -> 'enc_PasswdModifyRequestValue'(Data); +encode_disp('PasswdModifyResponseValue', Data) -> 'enc_PasswdModifyResponseValue'(Data); +encode_disp(Type, _Data) -> exit({error, {asn1, {undefined_type, Type}}}). +decode_disp('LDAPMessage', Data) -> 'dec_LDAPMessage'(Data); +decode_disp('MessageID', Data) -> 'dec_MessageID'(Data); +decode_disp('LDAPString', Data) -> 'dec_LDAPString'(Data); +decode_disp('LDAPOID', Data) -> 'dec_LDAPOID'(Data); +decode_disp('LDAPDN', Data) -> 'dec_LDAPDN'(Data); +decode_disp('RelativeLDAPDN', Data) -> 'dec_RelativeLDAPDN'(Data); +decode_disp('AttributeType', Data) -> 'dec_AttributeType'(Data); +decode_disp('AttributeDescription', Data) -> 'dec_AttributeDescription'(Data); +decode_disp('AttributeDescriptionList', Data) -> 'dec_AttributeDescriptionList'(Data); +decode_disp('AttributeValue', Data) -> 'dec_AttributeValue'(Data); +decode_disp('AttributeValueAssertion', Data) -> 'dec_AttributeValueAssertion'(Data); +decode_disp('AssertionValue', Data) -> 'dec_AssertionValue'(Data); +decode_disp('Attribute', Data) -> 'dec_Attribute'(Data); +decode_disp('MatchingRuleId', Data) -> 'dec_MatchingRuleId'(Data); +decode_disp('LDAPResult', Data) -> 'dec_LDAPResult'(Data); +decode_disp('Referral', Data) -> 'dec_Referral'(Data); +decode_disp('LDAPURL', Data) -> 'dec_LDAPURL'(Data); +decode_disp('Controls', Data) -> 'dec_Controls'(Data); +decode_disp('Control', Data) -> 'dec_Control'(Data); +decode_disp('BindRequest', Data) -> 'dec_BindRequest'(Data); +decode_disp('AuthenticationChoice', Data) -> 'dec_AuthenticationChoice'(Data); +decode_disp('SaslCredentials', Data) -> 'dec_SaslCredentials'(Data); +decode_disp('BindResponse', Data) -> 'dec_BindResponse'(Data); +decode_disp('UnbindRequest', Data) -> 'dec_UnbindRequest'(Data); +decode_disp('SearchRequest', Data) -> 'dec_SearchRequest'(Data); +decode_disp('Filter', Data) -> 'dec_Filter'(Data); +decode_disp('SubstringFilter', Data) -> 'dec_SubstringFilter'(Data); +decode_disp('MatchingRuleAssertion', Data) -> 'dec_MatchingRuleAssertion'(Data); +decode_disp('SearchResultEntry', Data) -> 'dec_SearchResultEntry'(Data); +decode_disp('PartialAttributeList', Data) -> 'dec_PartialAttributeList'(Data); +decode_disp('SearchResultReference', Data) -> 'dec_SearchResultReference'(Data); +decode_disp('SearchResultDone', Data) -> 'dec_SearchResultDone'(Data); +decode_disp('ModifyRequest', Data) -> 'dec_ModifyRequest'(Data); +decode_disp('AttributeTypeAndValues', Data) -> 'dec_AttributeTypeAndValues'(Data); +decode_disp('ModifyResponse', Data) -> 'dec_ModifyResponse'(Data); +decode_disp('AddRequest', Data) -> 'dec_AddRequest'(Data); +decode_disp('AttributeList', Data) -> 'dec_AttributeList'(Data); +decode_disp('AddResponse', Data) -> 'dec_AddResponse'(Data); +decode_disp('DelRequest', Data) -> 'dec_DelRequest'(Data); +decode_disp('DelResponse', Data) -> 'dec_DelResponse'(Data); +decode_disp('ModifyDNRequest', Data) -> 'dec_ModifyDNRequest'(Data); +decode_disp('ModifyDNResponse', Data) -> 'dec_ModifyDNResponse'(Data); +decode_disp('CompareRequest', Data) -> 'dec_CompareRequest'(Data); +decode_disp('CompareResponse', Data) -> 'dec_CompareResponse'(Data); +decode_disp('AbandonRequest', Data) -> 'dec_AbandonRequest'(Data); +decode_disp('ExtendedRequest', Data) -> 'dec_ExtendedRequest'(Data); +decode_disp('ExtendedResponse', Data) -> 'dec_ExtendedResponse'(Data); +decode_disp('PasswdModifyRequestValue', Data) -> 'dec_PasswdModifyRequestValue'(Data); +decode_disp('PasswdModifyResponseValue', Data) -> 'dec_PasswdModifyResponseValue'(Data); +decode_disp(Type, _Data) -> exit({error, {asn1, {undefined_type, Type}}}). + info() -> - case ?MODULE:module_info(attributes) of - Attributes when is_list(Attributes) -> - case lists:keyfind(asn1_info, 1, Attributes) of - {_,Info} when is_list(Info) -> - Info; - _ -> - [] - end; - _ -> - [] - end. + case ?MODULE:module_info(attributes) of + Attributes when is_list(Attributes) -> + case lists:keyfind(asn1_info, 1, Attributes) of + {_, Info} when is_list(Info) -> + Info; + _ -> + [] + end; + _ -> + [] + end. %%================================ @@ -277,231 +272,211 @@ info() -> 'enc_LDAPMessage'(Val) -> 'enc_LDAPMessage'(Val, [<<48>>]). + 'enc_LDAPMessage'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3} = Val, + {_, Cindex1, Cindex2, Cindex3} = Val, -%%------------------------------------------------- -%% attribute messageID(1) with type INTEGER -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_integer(Cindex1, [<<2>>]), + %%------------------------------------------------- + %% attribute messageID(1) with type INTEGER + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_integer(Cindex1, [<<2>>]), -%%------------------------------------------------- -%% attribute protocolOp(2) with type CHOICE -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_LDAPMessage_protocolOp'(Cindex2, []), + %%------------------------------------------------- + %% attribute protocolOp(2) with type CHOICE + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_LDAPMessage_protocolOp'(Cindex2, []), -%%------------------------------------------------- -%% attribute controls(3) External ELDAPv3:Controls OPTIONAL -%%------------------------------------------------- - {EncBytes3,EncLen3} = case Cindex3 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - 'enc_Controls'(Cindex3, [<<160>>]) - end, - - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], -LenSoFar = EncLen1 + EncLen2 + EncLen3, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute controls(3) External ELDAPv3:Controls OPTIONAL + %%------------------------------------------------- + {EncBytes3, EncLen3} = case Cindex3 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + 'enc_Controls'(Cindex3, [<<160>>]) + end, + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], + LenSoFar = EncLen1 + EncLen2 + EncLen3, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% LDAPMessage_protocolOp %%================================ 'enc_LDAPMessage_protocolOp'(Val, TagIn) -> - {EncBytes,EncLen} = case element(1,Val) of - bindRequest -> - 'enc_BindRequest'(element(2,Val), [<<96>>]); - bindResponse -> - 'enc_BindResponse'(element(2,Val), [<<97>>]); - unbindRequest -> - encode_null(element(2,Val), [<<66>>]); - searchRequest -> - 'enc_SearchRequest'(element(2,Val), [<<99>>]); - searchResEntry -> - 'enc_SearchResultEntry'(element(2,Val), [<<100>>]); - searchResDone -> - 'enc_SearchResultDone'(element(2,Val), [<<101>>]); - searchResRef -> - 'enc_SearchResultReference'(element(2,Val), [<<115>>]); - modifyRequest -> - 'enc_ModifyRequest'(element(2,Val), [<<102>>]); - modifyResponse -> - 'enc_ModifyResponse'(element(2,Val), [<<103>>]); - addRequest -> - 'enc_AddRequest'(element(2,Val), [<<104>>]); - addResponse -> - 'enc_AddResponse'(element(2,Val), [<<105>>]); - delRequest -> - encode_restricted_string(element(2,Val), [<<74>>]); - delResponse -> - 'enc_DelResponse'(element(2,Val), [<<107>>]); - modDNRequest -> - 'enc_ModifyDNRequest'(element(2,Val), [<<108>>]); - modDNResponse -> - 'enc_ModifyDNResponse'(element(2,Val), [<<109>>]); - compareRequest -> - 'enc_CompareRequest'(element(2,Val), [<<110>>]); - compareResponse -> - 'enc_CompareResponse'(element(2,Val), [<<111>>]); - abandonRequest -> - encode_integer(element(2,Val), [<<80>>]); - extendedReq -> - 'enc_ExtendedRequest'(element(2,Val), [<<119>>]); - extendedResp -> - 'enc_ExtendedResponse'(element(2,Val), [<<120>>]); - Else -> - exit({error,{asn1,{invalid_choice_type,Else}}}) - end, + {EncBytes, EncLen} = case element(1, Val) of + bindRequest -> + 'enc_BindRequest'(element(2, Val), [<<96>>]); + bindResponse -> + 'enc_BindResponse'(element(2, Val), [<<97>>]); + unbindRequest -> + encode_null(element(2, Val), [<<66>>]); + searchRequest -> + 'enc_SearchRequest'(element(2, Val), [<<99>>]); + searchResEntry -> + 'enc_SearchResultEntry'(element(2, Val), [<<100>>]); + searchResDone -> + 'enc_SearchResultDone'(element(2, Val), [<<101>>]); + searchResRef -> + 'enc_SearchResultReference'(element(2, Val), [<<115>>]); + modifyRequest -> + 'enc_ModifyRequest'(element(2, Val), [<<102>>]); + modifyResponse -> + 'enc_ModifyResponse'(element(2, Val), [<<103>>]); + addRequest -> + 'enc_AddRequest'(element(2, Val), [<<104>>]); + addResponse -> + 'enc_AddResponse'(element(2, Val), [<<105>>]); + delRequest -> + encode_restricted_string(element(2, Val), [<<74>>]); + delResponse -> + 'enc_DelResponse'(element(2, Val), [<<107>>]); + modDNRequest -> + 'enc_ModifyDNRequest'(element(2, Val), [<<108>>]); + modDNResponse -> + 'enc_ModifyDNResponse'(element(2, Val), [<<109>>]); + compareRequest -> + 'enc_CompareRequest'(element(2, Val), [<<110>>]); + compareResponse -> + 'enc_CompareResponse'(element(2, Val), [<<111>>]); + abandonRequest -> + encode_integer(element(2, Val), [<<80>>]); + extendedReq -> + 'enc_ExtendedRequest'(element(2, Val), [<<119>>]); + extendedResp -> + 'enc_ExtendedResponse'(element(2, Val), [<<120>>]); + Else -> + exit({error, {asn1, {invalid_choice_type, Else}}}) + end, -encode_tags(TagIn, EncBytes, EncLen). + encode_tags(TagIn, EncBytes, EncLen). 'dec_LDAPMessage_protocolOp'(Tlv, TagIn) -> -Tlv1 = match_tags(Tlv, TagIn), -case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of + Tlv1 = match_tags(Tlv, TagIn), + case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of -%% 'bindRequest' - {65536, V1} -> - {bindRequest, 'dec_BindRequest'(V1, [])}; + %% 'bindRequest' + {65536, V1} -> + {bindRequest, 'dec_BindRequest'(V1, [])}; + %% 'bindResponse' + {65537, V1} -> + {bindResponse, 'dec_BindResponse'(V1, [])}; -%% 'bindResponse' - {65537, V1} -> - {bindResponse, 'dec_BindResponse'(V1, [])}; + %% 'unbindRequest' + {65538, V1} -> + {unbindRequest, decode_null(V1, [])}; + %% 'searchRequest' + {65539, V1} -> + {searchRequest, 'dec_SearchRequest'(V1, [])}; -%% 'unbindRequest' - {65538, V1} -> - {unbindRequest, decode_null(V1,[])}; + %% 'searchResEntry' + {65540, V1} -> + {searchResEntry, 'dec_SearchResultEntry'(V1, [])}; + %% 'searchResDone' + {65541, V1} -> + {searchResDone, 'dec_SearchResultDone'(V1, [])}; -%% 'searchRequest' - {65539, V1} -> - {searchRequest, 'dec_SearchRequest'(V1, [])}; + %% 'searchResRef' + {65555, V1} -> + {searchResRef, 'dec_SearchResultReference'(V1, [])}; + %% 'modifyRequest' + {65542, V1} -> + {modifyRequest, 'dec_ModifyRequest'(V1, [])}; -%% 'searchResEntry' - {65540, V1} -> - {searchResEntry, 'dec_SearchResultEntry'(V1, [])}; + %% 'modifyResponse' + {65543, V1} -> + {modifyResponse, 'dec_ModifyResponse'(V1, [])}; + %% 'addRequest' + {65544, V1} -> + {addRequest, 'dec_AddRequest'(V1, [])}; -%% 'searchResDone' - {65541, V1} -> - {searchResDone, 'dec_SearchResultDone'(V1, [])}; + %% 'addResponse' + {65545, V1} -> + {addResponse, 'dec_AddResponse'(V1, [])}; + %% 'delRequest' + {65546, V1} -> + {delRequest, decode_restricted_string(V1, [])}; -%% 'searchResRef' - {65555, V1} -> - {searchResRef, 'dec_SearchResultReference'(V1, [])}; + %% 'delResponse' + {65547, V1} -> + {delResponse, 'dec_DelResponse'(V1, [])}; + %% 'modDNRequest' + {65548, V1} -> + {modDNRequest, 'dec_ModifyDNRequest'(V1, [])}; -%% 'modifyRequest' - {65542, V1} -> - {modifyRequest, 'dec_ModifyRequest'(V1, [])}; + %% 'modDNResponse' + {65549, V1} -> + {modDNResponse, 'dec_ModifyDNResponse'(V1, [])}; + %% 'compareRequest' + {65550, V1} -> + {compareRequest, 'dec_CompareRequest'(V1, [])}; -%% 'modifyResponse' - {65543, V1} -> - {modifyResponse, 'dec_ModifyResponse'(V1, [])}; + %% 'compareResponse' + {65551, V1} -> + {compareResponse, 'dec_CompareResponse'(V1, [])}; + %% 'abandonRequest' + {65552, V1} -> + {abandonRequest, decode_integer(V1, {0, 2147483647}, [])}; -%% 'addRequest' - {65544, V1} -> - {addRequest, 'dec_AddRequest'(V1, [])}; + %% 'extendedReq' + {65559, V1} -> + {extendedReq, 'dec_ExtendedRequest'(V1, [])}; + %% 'extendedResp' + {65560, V1} -> + {extendedResp, 'dec_ExtendedResponse'(V1, [])}; -%% 'addResponse' - {65545, V1} -> - {addResponse, 'dec_AddResponse'(V1, [])}; - - -%% 'delRequest' - {65546, V1} -> - {delRequest, decode_restricted_string(V1,[])}; - - -%% 'delResponse' - {65547, V1} -> - {delResponse, 'dec_DelResponse'(V1, [])}; - - -%% 'modDNRequest' - {65548, V1} -> - {modDNRequest, 'dec_ModifyDNRequest'(V1, [])}; - - -%% 'modDNResponse' - {65549, V1} -> - {modDNResponse, 'dec_ModifyDNResponse'(V1, [])}; - - -%% 'compareRequest' - {65550, V1} -> - {compareRequest, 'dec_CompareRequest'(V1, [])}; - - -%% 'compareResponse' - {65551, V1} -> - {compareResponse, 'dec_CompareResponse'(V1, [])}; - - -%% 'abandonRequest' - {65552, V1} -> - {abandonRequest, decode_integer(V1,{0,2147483647},[])}; - - -%% 'extendedReq' - {65559, V1} -> - {extendedReq, 'dec_ExtendedRequest'(V1, [])}; - - -%% 'extendedResp' - {65560, V1} -> - {extendedResp, 'dec_ExtendedResponse'(V1, [])}; - - Else -> - exit({error,{asn1,{invalid_choice_tag,Else}}}) - end -. + Else -> + exit({error, {asn1, {invalid_choice_tag, Else}}}) + end. 'dec_LDAPMessage'(Tlv) -> - 'dec_LDAPMessage'(Tlv, [16]). + 'dec_LDAPMessage'(Tlv, [16]). + 'dec_LDAPMessage'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute messageID(1) with type INTEGER -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_integer(V1,{0,2147483647},[2]), + %%------------------------------------------------- + %% attribute messageID(1) with type INTEGER + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_integer(V1, {0, 2147483647}, [2]), -%%------------------------------------------------- -%% attribute protocolOp(2) with type CHOICE -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_LDAPMessage_protocolOp'(V2, []), + %%------------------------------------------------- + %% attribute protocolOp(2) with type CHOICE + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_LDAPMessage_protocolOp'(V2, []), -%%------------------------------------------------- -%% attribute controls(3) External ELDAPv3:Controls OPTIONAL -%%------------------------------------------------- -{Term3,Tlv4} = case Tlv3 of -[{131072,V3}|TempTlv4] -> - {'dec_Controls'(V3, []), TempTlv4}; - _ -> - { asn1_NOVALUE, Tlv3} -end, - -case Tlv4 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed -end, - {'LDAPMessage', Term1, Term2, Term3}. + %%------------------------------------------------- + %% attribute controls(3) External ELDAPv3:Controls OPTIONAL + %%------------------------------------------------- + {Term3, Tlv4} = case Tlv3 of + [{131072, V3} | TempTlv4] -> + {'dec_Controls'(V3, []), TempTlv4}; + _ -> + {asn1_NOVALUE, Tlv3} + end, + case Tlv4 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv4}}}) % extra fields not allowed + end, + {'LDAPMessage', Term1, Term2, Term3}. %%================================ @@ -510,16 +485,17 @@ end, 'enc_MessageID'(Val) -> 'enc_MessageID'(Val, [<<2>>]). + 'enc_MessageID'(Val, TagIn) -> -encode_integer(Val, TagIn). + encode_integer(Val, TagIn). 'dec_MessageID'(Tlv) -> - 'dec_MessageID'(Tlv, [2]). + 'dec_MessageID'(Tlv, [2]). + 'dec_MessageID'(Tlv, TagIn) -> -decode_integer(Tlv,{0,2147483647},TagIn). - + decode_integer(Tlv, {0, 2147483647}, TagIn). %%================================ @@ -528,16 +504,17 @@ decode_integer(Tlv,{0,2147483647},TagIn). 'enc_LDAPString'(Val) -> 'enc_LDAPString'(Val, [<<4>>]). + 'enc_LDAPString'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_LDAPString'(Tlv) -> - 'dec_LDAPString'(Tlv, [4]). + 'dec_LDAPString'(Tlv, [4]). + 'dec_LDAPString'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -546,16 +523,17 @@ decode_restricted_string(Tlv,TagIn). 'enc_LDAPOID'(Val) -> 'enc_LDAPOID'(Val, [<<4>>]). + 'enc_LDAPOID'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_LDAPOID'(Tlv) -> - 'dec_LDAPOID'(Tlv, [4]). + 'dec_LDAPOID'(Tlv, [4]). + 'dec_LDAPOID'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -564,16 +542,17 @@ decode_restricted_string(Tlv,TagIn). 'enc_LDAPDN'(Val) -> 'enc_LDAPDN'(Val, [<<4>>]). + 'enc_LDAPDN'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_LDAPDN'(Tlv) -> - 'dec_LDAPDN'(Tlv, [4]). + 'dec_LDAPDN'(Tlv, [4]). + 'dec_LDAPDN'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -582,16 +561,17 @@ decode_restricted_string(Tlv,TagIn). 'enc_RelativeLDAPDN'(Val) -> 'enc_RelativeLDAPDN'(Val, [<<4>>]). + 'enc_RelativeLDAPDN'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_RelativeLDAPDN'(Tlv) -> - 'dec_RelativeLDAPDN'(Tlv, [4]). + 'dec_RelativeLDAPDN'(Tlv, [4]). + 'dec_RelativeLDAPDN'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -600,16 +580,17 @@ decode_restricted_string(Tlv,TagIn). 'enc_AttributeType'(Val) -> 'enc_AttributeType'(Val, [<<4>>]). + 'enc_AttributeType'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_AttributeType'(Tlv) -> - 'dec_AttributeType'(Tlv, [4]). + 'dec_AttributeType'(Tlv, [4]). + 'dec_AttributeType'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -618,16 +599,17 @@ decode_restricted_string(Tlv,TagIn). 'enc_AttributeDescription'(Val) -> 'enc_AttributeDescription'(Val, [<<4>>]). + 'enc_AttributeDescription'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_AttributeDescription'(Tlv) -> - 'dec_AttributeDescription'(Tlv, [4]). + 'dec_AttributeDescription'(Tlv, [4]). + 'dec_AttributeDescription'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -636,30 +618,30 @@ decode_restricted_string(Tlv,TagIn). 'enc_AttributeDescriptionList'(Val) -> 'enc_AttributeDescriptionList'(Val, [<<48>>]). + 'enc_AttributeDescriptionList'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_AttributeDescriptionList_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_AttributeDescriptionList_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_AttributeDescriptionList_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_AttributeDescriptionList_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_AttributeDescriptionList_components'(T,[EncBytes|AccBytes], AccLen + EncLen). + {lists:reverse(AccBytes), AccLen}; +'enc_AttributeDescriptionList_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_AttributeDescriptionList_components'(T, [EncBytes | AccBytes], AccLen + EncLen). 'dec_AttributeDescriptionList'(Tlv) -> - 'dec_AttributeDescriptionList'(Tlv, [16]). + 'dec_AttributeDescriptionList'(Tlv, [16]). + 'dec_AttributeDescriptionList'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. %%================================ @@ -668,16 +650,17 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_AttributeValue'(Val) -> 'enc_AttributeValue'(Val, [<<4>>]). + 'enc_AttributeValue'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_AttributeValue'(Tlv) -> - 'dec_AttributeValue'(Tlv, [4]). + 'dec_AttributeValue'(Tlv, [4]). + 'dec_AttributeValue'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -686,50 +669,51 @@ decode_restricted_string(Tlv,TagIn). 'enc_AttributeValueAssertion'(Val) -> 'enc_AttributeValueAssertion'(Val, [<<48>>]). + 'enc_AttributeValueAssertion'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute attributeDesc(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute attributeDesc(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute assertionValue(2) with type OCTET STRING -%%------------------------------------------------- - {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), + %%------------------------------------------------- + %% attribute assertionValue(2) with type OCTET STRING + %%------------------------------------------------- + {EncBytes2, EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AttributeValueAssertion'(Tlv) -> - 'dec_AttributeValueAssertion'(Tlv, [16]). + 'dec_AttributeValueAssertion'(Tlv, [16]). + 'dec_AttributeValueAssertion'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute attributeDesc(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute attributeDesc(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute assertionValue(2) with type OCTET STRING -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_restricted_string(V2,[4]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'AttributeValueAssertion', Term1, Term2}. + %%------------------------------------------------- + %% attribute assertionValue(2) with type OCTET STRING + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_restricted_string(V2, [4]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'AttributeValueAssertion', Term1, Term2}. %%================================ @@ -738,16 +722,17 @@ end, 'enc_AssertionValue'(Val) -> 'enc_AssertionValue'(Val, [<<4>>]). + 'enc_AssertionValue'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_AssertionValue'(Tlv) -> - 'dec_AssertionValue'(Tlv, [4]). + 'dec_AssertionValue'(Tlv, [4]). + 'dec_AssertionValue'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -756,75 +741,75 @@ decode_restricted_string(Tlv,TagIn). 'enc_Attribute'(Val) -> 'enc_Attribute'(Val, [<<48>>]). + 'enc_Attribute'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_Attribute_vals'(Cindex2, [<<49>>]), - - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_Attribute_vals'(Cindex2, [<<49>>]), + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% Attribute_vals %%================================ 'enc_Attribute_vals'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_Attribute_vals_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_Attribute_vals_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_Attribute_vals_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; + {lists:reverse(AccBytes), AccLen}; + +'enc_Attribute_vals_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_Attribute_vals_components'(T, [EncBytes | AccBytes], AccLen + EncLen). -'enc_Attribute_vals_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_Attribute_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Attribute_vals'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. 'dec_Attribute'(Tlv) -> - 'dec_Attribute'(Tlv, [16]). + 'dec_Attribute'(Tlv, [16]). + 'dec_Attribute'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_Attribute_vals'(V2, [17]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'Attribute', Term1, Term2}. + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_Attribute_vals'(V2, [17]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'Attribute', Term1, Term2}. %%================================ @@ -833,16 +818,17 @@ end, 'enc_MatchingRuleId'(Val) -> 'enc_MatchingRuleId'(Val, [<<4>>]). + 'enc_MatchingRuleId'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_MatchingRuleId'(Tlv) -> - 'dec_MatchingRuleId'(Tlv, [4]). + 'dec_MatchingRuleId'(Tlv, [4]). + 'dec_MatchingRuleId'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -851,121 +837,122 @@ decode_restricted_string(Tlv,TagIn). 'enc_LDAPResult'(Val) -> 'enc_LDAPResult'(Val, [<<48>>]). + 'enc_LDAPResult'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3, Cindex4} = Val, + {_, Cindex1, Cindex2, Cindex3, Cindex4} = Val, -%%------------------------------------------------- -%% attribute resultCode(1) with type ENUMERATED -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of -success -> encode_enumerated(0, [<<10>>]); -operationsError -> encode_enumerated(1, [<<10>>]); -protocolError -> encode_enumerated(2, [<<10>>]); -timeLimitExceeded -> encode_enumerated(3, [<<10>>]); -sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); -compareFalse -> encode_enumerated(5, [<<10>>]); -compareTrue -> encode_enumerated(6, [<<10>>]); -authMethodNotSupported -> encode_enumerated(7, [<<10>>]); -strongAuthRequired -> encode_enumerated(8, [<<10>>]); -referral -> encode_enumerated(10, [<<10>>]); -adminLimitExceeded -> encode_enumerated(11, [<<10>>]); -unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); -confidentialityRequired -> encode_enumerated(13, [<<10>>]); -saslBindInProgress -> encode_enumerated(14, [<<10>>]); -noSuchAttribute -> encode_enumerated(16, [<<10>>]); -undefinedAttributeType -> encode_enumerated(17, [<<10>>]); -inappropriateMatching -> encode_enumerated(18, [<<10>>]); -constraintViolation -> encode_enumerated(19, [<<10>>]); -attributeOrValueExists -> encode_enumerated(20, [<<10>>]); -invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); -noSuchObject -> encode_enumerated(32, [<<10>>]); -aliasProblem -> encode_enumerated(33, [<<10>>]); -invalidDNSyntax -> encode_enumerated(34, [<<10>>]); -aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); -inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); -invalidCredentials -> encode_enumerated(49, [<<10>>]); -insufficientAccessRights -> encode_enumerated(50, [<<10>>]); -busy -> encode_enumerated(51, [<<10>>]); -unavailable -> encode_enumerated(52, [<<10>>]); -unwillingToPerform -> encode_enumerated(53, [<<10>>]); -loopDetect -> encode_enumerated(54, [<<10>>]); -namingViolation -> encode_enumerated(64, [<<10>>]); -objectClassViolation -> encode_enumerated(65, [<<10>>]); -notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); -notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); -entryAlreadyExists -> encode_enumerated(68, [<<10>>]); -objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); -affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); -other -> encode_enumerated(80, [<<10>>]); -Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) -end, + %%------------------------------------------------- + %% attribute resultCode(1) with type ENUMERATED + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + success -> encode_enumerated(0, [<<10>>]); + operationsError -> encode_enumerated(1, [<<10>>]); + protocolError -> encode_enumerated(2, [<<10>>]); + timeLimitExceeded -> encode_enumerated(3, [<<10>>]); + sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); + compareFalse -> encode_enumerated(5, [<<10>>]); + compareTrue -> encode_enumerated(6, [<<10>>]); + authMethodNotSupported -> encode_enumerated(7, [<<10>>]); + strongAuthRequired -> encode_enumerated(8, [<<10>>]); + referral -> encode_enumerated(10, [<<10>>]); + adminLimitExceeded -> encode_enumerated(11, [<<10>>]); + unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); + confidentialityRequired -> encode_enumerated(13, [<<10>>]); + saslBindInProgress -> encode_enumerated(14, [<<10>>]); + noSuchAttribute -> encode_enumerated(16, [<<10>>]); + undefinedAttributeType -> encode_enumerated(17, [<<10>>]); + inappropriateMatching -> encode_enumerated(18, [<<10>>]); + constraintViolation -> encode_enumerated(19, [<<10>>]); + attributeOrValueExists -> encode_enumerated(20, [<<10>>]); + invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); + noSuchObject -> encode_enumerated(32, [<<10>>]); + aliasProblem -> encode_enumerated(33, [<<10>>]); + invalidDNSyntax -> encode_enumerated(34, [<<10>>]); + aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); + inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); + invalidCredentials -> encode_enumerated(49, [<<10>>]); + insufficientAccessRights -> encode_enumerated(50, [<<10>>]); + busy -> encode_enumerated(51, [<<10>>]); + unavailable -> encode_enumerated(52, [<<10>>]); + unwillingToPerform -> encode_enumerated(53, [<<10>>]); + loopDetect -> encode_enumerated(54, [<<10>>]); + namingViolation -> encode_enumerated(64, [<<10>>]); + objectClassViolation -> encode_enumerated(65, [<<10>>]); + notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); + notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); + entryAlreadyExists -> encode_enumerated(68, [<<10>>]); + objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); + affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); + other -> encode_enumerated(80, [<<10>>]); + Enumval1 -> exit({error, {asn1, {enumerated_not_in_range, Enumval1}}}) + end, -%%------------------------------------------------- -%% attribute matchedDN(2) with type OCTET STRING -%%------------------------------------------------- - {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), + %%------------------------------------------------- + %% attribute matchedDN(2) with type OCTET STRING + %%------------------------------------------------- + {EncBytes2, EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), -%%------------------------------------------------- -%% attribute errorMessage(3) with type OCTET STRING -%%------------------------------------------------- - {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), + %%------------------------------------------------- + %% attribute errorMessage(3) with type OCTET STRING + %%------------------------------------------------- + {EncBytes3, EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), -%%------------------------------------------------- -%% attribute referral(4) External ELDAPv3:Referral OPTIONAL -%%------------------------------------------------- - {EncBytes4,EncLen4} = case Cindex4 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - 'enc_Referral'(Cindex4, [<<163>>]) - end, + %%------------------------------------------------- + %% attribute referral(4) External ELDAPv3:Referral OPTIONAL + %%------------------------------------------------- + {EncBytes4, EncLen4} = case Cindex4 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + 'enc_Referral'(Cindex4, [<<163>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], -LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], + LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_LDAPResult'(Tlv) -> - 'dec_LDAPResult'(Tlv, [16]). + 'dec_LDAPResult'(Tlv, [16]). + 'dec_LDAPResult'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute resultCode(1) with type ENUMERATED -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), + %%------------------------------------------------- + %% attribute resultCode(1) with type ENUMERATED + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_enumerated(V1, [{success, 0}, {operationsError, 1}, {protocolError, 2}, {timeLimitExceeded, 3}, {sizeLimitExceeded, 4}, {compareFalse, 5}, {compareTrue, 6}, {authMethodNotSupported, 7}, {strongAuthRequired, 8}, {referral, 10}, {adminLimitExceeded, 11}, {unavailableCriticalExtension, 12}, {confidentialityRequired, 13}, {saslBindInProgress, 14}, {noSuchAttribute, 16}, {undefinedAttributeType, 17}, {inappropriateMatching, 18}, {constraintViolation, 19}, {attributeOrValueExists, 20}, {invalidAttributeSyntax, 21}, {noSuchObject, 32}, {aliasProblem, 33}, {invalidDNSyntax, 34}, {aliasDereferencingProblem, 36}, {inappropriateAuthentication, 48}, {invalidCredentials, 49}, {insufficientAccessRights, 50}, {busy, 51}, {unavailable, 52}, {unwillingToPerform, 53}, {loopDetect, 54}, {namingViolation, 64}, {objectClassViolation, 65}, {notAllowedOnNonLeaf, 66}, {notAllowedOnRDN, 67}, {entryAlreadyExists, 68}, {objectClassModsProhibited, 69}, {affectsMultipleDSAs, 71}, {other, 80}], [10]), -%%------------------------------------------------- -%% attribute matchedDN(2) with type OCTET STRING -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_restricted_string(V2,[4]), + %%------------------------------------------------- + %% attribute matchedDN(2) with type OCTET STRING + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_restricted_string(V2, [4]), -%%------------------------------------------------- -%% attribute errorMessage(3) with type OCTET STRING -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = decode_restricted_string(V3,[4]), + %%------------------------------------------------- + %% attribute errorMessage(3) with type OCTET STRING + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = decode_restricted_string(V3, [4]), -%%------------------------------------------------- -%% attribute referral(4) External ELDAPv3:Referral OPTIONAL -%%------------------------------------------------- -{Term4,Tlv5} = case Tlv4 of -[{131075,V4}|TempTlv5] -> - {'dec_Referral'(V4, []), TempTlv5}; - _ -> - { asn1_NOVALUE, Tlv4} -end, - -case Tlv5 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv5}}}) % extra fields not allowed -end, - {'LDAPResult', Term1, Term2, Term3, Term4}. + %%------------------------------------------------- + %% attribute referral(4) External ELDAPv3:Referral OPTIONAL + %%------------------------------------------------- + {Term4, Tlv5} = case Tlv4 of + [{131075, V4} | TempTlv5] -> + {'dec_Referral'(V4, []), TempTlv5}; + _ -> + {asn1_NOVALUE, Tlv4} + end, + case Tlv5 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv5}}}) % extra fields not allowed + end, + {'LDAPResult', Term1, Term2, Term3, Term4}. %%================================ @@ -974,30 +961,30 @@ end, 'enc_Referral'(Val) -> 'enc_Referral'(Val, [<<48>>]). + 'enc_Referral'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_Referral_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_Referral_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_Referral_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_Referral_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_Referral_components'(T,[EncBytes|AccBytes], AccLen + EncLen). + {lists:reverse(AccBytes), AccLen}; +'enc_Referral_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_Referral_components'(T, [EncBytes | AccBytes], AccLen + EncLen). 'dec_Referral'(Tlv) -> - 'dec_Referral'(Tlv, [16]). + 'dec_Referral'(Tlv, [16]). + 'dec_Referral'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. %%================================ @@ -1006,16 +993,17 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_LDAPURL'(Val) -> 'enc_LDAPURL'(Val, [<<4>>]). + 'enc_LDAPURL'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_LDAPURL'(Tlv) -> - 'dec_LDAPURL'(Tlv, [4]). + 'dec_LDAPURL'(Tlv, [4]). + 'dec_LDAPURL'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -1024,30 +1012,30 @@ decode_restricted_string(Tlv,TagIn). 'enc_Controls'(Val) -> 'enc_Controls'(Val, [<<48>>]). + 'enc_Controls'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_Controls_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_Controls_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_Controls_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_Controls_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_Control'(H, [<<48>>]), - 'enc_Controls_components'(T,[EncBytes|AccBytes], AccLen + EncLen). + {lists:reverse(AccBytes), AccLen}; +'enc_Controls_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_Control'(H, [<<48>>]), + 'enc_Controls_components'(T, [EncBytes | AccBytes], AccLen + EncLen). 'dec_Controls'(Tlv) -> - 'dec_Controls'(Tlv, [16]). + 'dec_Controls'(Tlv, [16]). + 'dec_Controls'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_Control'(V1, [16]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_Control'(V1, [16]) || V1 <- Tlv1 ]. %%================================ @@ -1056,78 +1044,79 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_Control'(Val) -> 'enc_Control'(Val, [<<48>>]). + 'enc_Control'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3} = Val, + {_, Cindex1, Cindex2, Cindex3} = Val, -%%------------------------------------------------- -%% attribute controlType(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute controlType(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute criticality(2) with type BOOLEAN DEFAULT = false -%%------------------------------------------------- - {EncBytes2,EncLen2} = case Cindex2 of - asn1_DEFAULT -> {<<>>,0}; - false -> {<<>>,0}; - _ -> - encode_boolean(Cindex2, [<<1>>]) - end, + %%------------------------------------------------- + %% attribute criticality(2) with type BOOLEAN DEFAULT = false + %%------------------------------------------------- + {EncBytes2, EncLen2} = case Cindex2 of + asn1_DEFAULT -> {<<>>, 0}; + false -> {<<>>, 0}; + _ -> + encode_boolean(Cindex2, [<<1>>]) + end, -%%------------------------------------------------- -%% attribute controlValue(3) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes3,EncLen3} = case Cindex3 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex3, [<<4>>]) - end, + %%------------------------------------------------- + %% attribute controlValue(3) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes3, EncLen3} = case Cindex3 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex3, [<<4>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], -LenSoFar = EncLen1 + EncLen2 + EncLen3, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], + LenSoFar = EncLen1 + EncLen2 + EncLen3, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_Control'(Tlv) -> - 'dec_Control'(Tlv, [16]). + 'dec_Control'(Tlv, [16]). + 'dec_Control'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute controlType(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute controlType(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute criticality(2) with type BOOLEAN DEFAULT = false -%%------------------------------------------------- -{Term2,Tlv3} = case Tlv2 of -[{1,V2}|TempTlv3] -> - {decode_boolean(V2,[]), TempTlv3}; - _ -> - {false,Tlv2} -end, + %%------------------------------------------------- + %% attribute criticality(2) with type BOOLEAN DEFAULT = false + %%------------------------------------------------- + {Term2, Tlv3} = case Tlv2 of + [{1, V2} | TempTlv3] -> + {decode_boolean(V2, []), TempTlv3}; + _ -> + {false, Tlv2} + end, -%%------------------------------------------------- -%% attribute controlValue(3) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term3,Tlv4} = case Tlv3 of -[{4,V3}|TempTlv4] -> - {decode_restricted_string(V3,[]), TempTlv4}; - _ -> - { asn1_NOVALUE, Tlv3} -end, - -case Tlv4 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed -end, - {'Control', Term1, Term2, Term3}. + %%------------------------------------------------- + %% attribute controlValue(3) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term3, Tlv4} = case Tlv3 of + [{4, V3} | TempTlv4] -> + {decode_restricted_string(V3, []), TempTlv4}; + _ -> + {asn1_NOVALUE, Tlv3} + end, + case Tlv4 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv4}}}) % extra fields not allowed + end, + {'Control', Term1, Term2, Term3}. %%================================ @@ -1136,61 +1125,62 @@ end, 'enc_BindRequest'(Val) -> 'enc_BindRequest'(Val, [<<96>>]). + 'enc_BindRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3} = Val, + {_, Cindex1, Cindex2, Cindex3} = Val, -%%------------------------------------------------- -%% attribute version(1) with type INTEGER -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_integer(Cindex1, [<<2>>]), + %%------------------------------------------------- + %% attribute version(1) with type INTEGER + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_integer(Cindex1, [<<2>>]), -%%------------------------------------------------- -%% attribute name(2) with type OCTET STRING -%%------------------------------------------------- - {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), + %%------------------------------------------------- + %% attribute name(2) with type OCTET STRING + %%------------------------------------------------- + {EncBytes2, EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), -%%------------------------------------------------- -%% attribute authentication(3) External ELDAPv3:AuthenticationChoice -%%------------------------------------------------- - {EncBytes3,EncLen3} = 'enc_AuthenticationChoice'(Cindex3, []), + %%------------------------------------------------- + %% attribute authentication(3) External ELDAPv3:AuthenticationChoice + %%------------------------------------------------- + {EncBytes3, EncLen3} = 'enc_AuthenticationChoice'(Cindex3, []), - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], -LenSoFar = EncLen1 + EncLen2 + EncLen3, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], + LenSoFar = EncLen1 + EncLen2 + EncLen3, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_BindRequest'(Tlv) -> - 'dec_BindRequest'(Tlv, [65536]). + 'dec_BindRequest'(Tlv, [65536]). + 'dec_BindRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute version(1) with type INTEGER -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_integer(V1,{1,127},[2]), + %%------------------------------------------------- + %% attribute version(1) with type INTEGER + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_integer(V1, {1, 127}, [2]), -%%------------------------------------------------- -%% attribute name(2) with type OCTET STRING -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_restricted_string(V2,[4]), + %%------------------------------------------------- + %% attribute name(2) with type OCTET STRING + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_restricted_string(V2, [4]), -%%------------------------------------------------- -%% attribute authentication(3) External ELDAPv3:AuthenticationChoice -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = 'dec_AuthenticationChoice'(V3, []), - -case Tlv4 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed -end, - {'BindRequest', Term1, Term2, Term3}. + %%------------------------------------------------- + %% attribute authentication(3) External ELDAPv3:AuthenticationChoice + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = 'dec_AuthenticationChoice'(V3, []), + case Tlv4 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv4}}}) % extra fields not allowed + end, + {'BindRequest', Term1, Term2, Term3}. %%================================ @@ -1199,41 +1189,39 @@ end, 'enc_AuthenticationChoice'(Val) -> 'enc_AuthenticationChoice'(Val, []). + 'enc_AuthenticationChoice'(Val, TagIn) -> - {EncBytes,EncLen} = case element(1,Val) of - simple -> - encode_restricted_string(element(2,Val), [<<128>>]); - sasl -> - 'enc_SaslCredentials'(element(2,Val), [<<163>>]); - Else -> - exit({error,{asn1,{invalid_choice_type,Else}}}) - end, - -encode_tags(TagIn, EncBytes, EncLen). - + {EncBytes, EncLen} = case element(1, Val) of + simple -> + encode_restricted_string(element(2, Val), [<<128>>]); + sasl -> + 'enc_SaslCredentials'(element(2, Val), [<<163>>]); + Else -> + exit({error, {asn1, {invalid_choice_type, Else}}}) + end, + encode_tags(TagIn, EncBytes, EncLen). 'dec_AuthenticationChoice'(Tlv) -> - 'dec_AuthenticationChoice'(Tlv, []). + 'dec_AuthenticationChoice'(Tlv, []). + 'dec_AuthenticationChoice'(Tlv, TagIn) -> -Tlv1 = match_tags(Tlv, TagIn), -case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of + Tlv1 = match_tags(Tlv, TagIn), + case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of -%% 'simple' - {131072, V1} -> - {simple, decode_restricted_string(V1,[])}; + %% 'simple' + {131072, V1} -> + {simple, decode_restricted_string(V1, [])}; + %% 'sasl' + {131075, V1} -> + {sasl, 'dec_SaslCredentials'(V1, [])}; -%% 'sasl' - {131075, V1} -> - {sasl, 'dec_SaslCredentials'(V1, [])}; - - Else -> - exit({error,{asn1,{invalid_choice_tag,Else}}}) - end -. + Else -> + exit({error, {asn1, {invalid_choice_tag, Else}}}) + end. %%================================ @@ -1242,58 +1230,59 @@ case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of 'enc_SaslCredentials'(Val) -> 'enc_SaslCredentials'(Val, [<<48>>]). + 'enc_SaslCredentials'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute mechanism(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute mechanism(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute credentials(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes2,EncLen2} = case Cindex2 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex2, [<<4>>]) - end, + %%------------------------------------------------- + %% attribute credentials(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes2, EncLen2} = case Cindex2 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex2, [<<4>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SaslCredentials'(Tlv) -> - 'dec_SaslCredentials'(Tlv, [16]). + 'dec_SaslCredentials'(Tlv, [16]). + 'dec_SaslCredentials'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute mechanism(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute mechanism(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute credentials(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term2,Tlv3} = case Tlv2 of -[{4,V2}|TempTlv3] -> - {decode_restricted_string(V2,[]), TempTlv3}; - _ -> - { asn1_NOVALUE, Tlv2} -end, - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'SaslCredentials', Term1, Term2}. + %%------------------------------------------------- + %% attribute credentials(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term2, Tlv3} = case Tlv2 of + [{4, V2} | TempTlv3] -> + {decode_restricted_string(V2, []), TempTlv3}; + _ -> + {asn1_NOVALUE, Tlv2} + end, + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'SaslCredentials', Term1, Term2}. %%================================ @@ -1302,140 +1291,141 @@ end, 'enc_BindResponse'(Val) -> 'enc_BindResponse'(Val, [<<97>>]). + 'enc_BindResponse'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3, Cindex4, Cindex5} = Val, + {_, Cindex1, Cindex2, Cindex3, Cindex4, Cindex5} = Val, -%%------------------------------------------------- -%% attribute resultCode(1) with type ENUMERATED -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of -success -> encode_enumerated(0, [<<10>>]); -operationsError -> encode_enumerated(1, [<<10>>]); -protocolError -> encode_enumerated(2, [<<10>>]); -timeLimitExceeded -> encode_enumerated(3, [<<10>>]); -sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); -compareFalse -> encode_enumerated(5, [<<10>>]); -compareTrue -> encode_enumerated(6, [<<10>>]); -authMethodNotSupported -> encode_enumerated(7, [<<10>>]); -strongAuthRequired -> encode_enumerated(8, [<<10>>]); -referral -> encode_enumerated(10, [<<10>>]); -adminLimitExceeded -> encode_enumerated(11, [<<10>>]); -unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); -confidentialityRequired -> encode_enumerated(13, [<<10>>]); -saslBindInProgress -> encode_enumerated(14, [<<10>>]); -noSuchAttribute -> encode_enumerated(16, [<<10>>]); -undefinedAttributeType -> encode_enumerated(17, [<<10>>]); -inappropriateMatching -> encode_enumerated(18, [<<10>>]); -constraintViolation -> encode_enumerated(19, [<<10>>]); -attributeOrValueExists -> encode_enumerated(20, [<<10>>]); -invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); -noSuchObject -> encode_enumerated(32, [<<10>>]); -aliasProblem -> encode_enumerated(33, [<<10>>]); -invalidDNSyntax -> encode_enumerated(34, [<<10>>]); -aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); -inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); -invalidCredentials -> encode_enumerated(49, [<<10>>]); -insufficientAccessRights -> encode_enumerated(50, [<<10>>]); -busy -> encode_enumerated(51, [<<10>>]); -unavailable -> encode_enumerated(52, [<<10>>]); -unwillingToPerform -> encode_enumerated(53, [<<10>>]); -loopDetect -> encode_enumerated(54, [<<10>>]); -namingViolation -> encode_enumerated(64, [<<10>>]); -objectClassViolation -> encode_enumerated(65, [<<10>>]); -notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); -notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); -entryAlreadyExists -> encode_enumerated(68, [<<10>>]); -objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); -affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); -other -> encode_enumerated(80, [<<10>>]); -Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) -end, + %%------------------------------------------------- + %% attribute resultCode(1) with type ENUMERATED + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + success -> encode_enumerated(0, [<<10>>]); + operationsError -> encode_enumerated(1, [<<10>>]); + protocolError -> encode_enumerated(2, [<<10>>]); + timeLimitExceeded -> encode_enumerated(3, [<<10>>]); + sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); + compareFalse -> encode_enumerated(5, [<<10>>]); + compareTrue -> encode_enumerated(6, [<<10>>]); + authMethodNotSupported -> encode_enumerated(7, [<<10>>]); + strongAuthRequired -> encode_enumerated(8, [<<10>>]); + referral -> encode_enumerated(10, [<<10>>]); + adminLimitExceeded -> encode_enumerated(11, [<<10>>]); + unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); + confidentialityRequired -> encode_enumerated(13, [<<10>>]); + saslBindInProgress -> encode_enumerated(14, [<<10>>]); + noSuchAttribute -> encode_enumerated(16, [<<10>>]); + undefinedAttributeType -> encode_enumerated(17, [<<10>>]); + inappropriateMatching -> encode_enumerated(18, [<<10>>]); + constraintViolation -> encode_enumerated(19, [<<10>>]); + attributeOrValueExists -> encode_enumerated(20, [<<10>>]); + invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); + noSuchObject -> encode_enumerated(32, [<<10>>]); + aliasProblem -> encode_enumerated(33, [<<10>>]); + invalidDNSyntax -> encode_enumerated(34, [<<10>>]); + aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); + inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); + invalidCredentials -> encode_enumerated(49, [<<10>>]); + insufficientAccessRights -> encode_enumerated(50, [<<10>>]); + busy -> encode_enumerated(51, [<<10>>]); + unavailable -> encode_enumerated(52, [<<10>>]); + unwillingToPerform -> encode_enumerated(53, [<<10>>]); + loopDetect -> encode_enumerated(54, [<<10>>]); + namingViolation -> encode_enumerated(64, [<<10>>]); + objectClassViolation -> encode_enumerated(65, [<<10>>]); + notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); + notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); + entryAlreadyExists -> encode_enumerated(68, [<<10>>]); + objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); + affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); + other -> encode_enumerated(80, [<<10>>]); + Enumval1 -> exit({error, {asn1, {enumerated_not_in_range, Enumval1}}}) + end, -%%------------------------------------------------- -%% attribute matchedDN(2) with type OCTET STRING -%%------------------------------------------------- - {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), + %%------------------------------------------------- + %% attribute matchedDN(2) with type OCTET STRING + %%------------------------------------------------- + {EncBytes2, EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), -%%------------------------------------------------- -%% attribute errorMessage(3) with type OCTET STRING -%%------------------------------------------------- - {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), + %%------------------------------------------------- + %% attribute errorMessage(3) with type OCTET STRING + %%------------------------------------------------- + {EncBytes3, EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), -%%------------------------------------------------- -%% attribute referral(4) External ELDAPv3:Referral OPTIONAL -%%------------------------------------------------- - {EncBytes4,EncLen4} = case Cindex4 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - 'enc_Referral'(Cindex4, [<<163>>]) - end, + %%------------------------------------------------- + %% attribute referral(4) External ELDAPv3:Referral OPTIONAL + %%------------------------------------------------- + {EncBytes4, EncLen4} = case Cindex4 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + 'enc_Referral'(Cindex4, [<<163>>]) + end, -%%------------------------------------------------- -%% attribute serverSaslCreds(5) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes5,EncLen5} = case Cindex5 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex5, [<<135>>]) - end, + %%------------------------------------------------- + %% attribute serverSaslCreds(5) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes5, EncLen5} = case Cindex5 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex5, [<<135>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5], -LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5], + LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_BindResponse'(Tlv) -> - 'dec_BindResponse'(Tlv, [65537]). + 'dec_BindResponse'(Tlv, [65537]). + 'dec_BindResponse'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute resultCode(1) with type ENUMERATED -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), + %%------------------------------------------------- + %% attribute resultCode(1) with type ENUMERATED + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_enumerated(V1, [{success, 0}, {operationsError, 1}, {protocolError, 2}, {timeLimitExceeded, 3}, {sizeLimitExceeded, 4}, {compareFalse, 5}, {compareTrue, 6}, {authMethodNotSupported, 7}, {strongAuthRequired, 8}, {referral, 10}, {adminLimitExceeded, 11}, {unavailableCriticalExtension, 12}, {confidentialityRequired, 13}, {saslBindInProgress, 14}, {noSuchAttribute, 16}, {undefinedAttributeType, 17}, {inappropriateMatching, 18}, {constraintViolation, 19}, {attributeOrValueExists, 20}, {invalidAttributeSyntax, 21}, {noSuchObject, 32}, {aliasProblem, 33}, {invalidDNSyntax, 34}, {aliasDereferencingProblem, 36}, {inappropriateAuthentication, 48}, {invalidCredentials, 49}, {insufficientAccessRights, 50}, {busy, 51}, {unavailable, 52}, {unwillingToPerform, 53}, {loopDetect, 54}, {namingViolation, 64}, {objectClassViolation, 65}, {notAllowedOnNonLeaf, 66}, {notAllowedOnRDN, 67}, {entryAlreadyExists, 68}, {objectClassModsProhibited, 69}, {affectsMultipleDSAs, 71}, {other, 80}], [10]), -%%------------------------------------------------- -%% attribute matchedDN(2) with type OCTET STRING -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_restricted_string(V2,[4]), + %%------------------------------------------------- + %% attribute matchedDN(2) with type OCTET STRING + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_restricted_string(V2, [4]), -%%------------------------------------------------- -%% attribute errorMessage(3) with type OCTET STRING -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = decode_restricted_string(V3,[4]), + %%------------------------------------------------- + %% attribute errorMessage(3) with type OCTET STRING + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = decode_restricted_string(V3, [4]), -%%------------------------------------------------- -%% attribute referral(4) External ELDAPv3:Referral OPTIONAL -%%------------------------------------------------- -{Term4,Tlv5} = case Tlv4 of -[{131075,V4}|TempTlv5] -> - {'dec_Referral'(V4, []), TempTlv5}; - _ -> - { asn1_NOVALUE, Tlv4} -end, + %%------------------------------------------------- + %% attribute referral(4) External ELDAPv3:Referral OPTIONAL + %%------------------------------------------------- + {Term4, Tlv5} = case Tlv4 of + [{131075, V4} | TempTlv5] -> + {'dec_Referral'(V4, []), TempTlv5}; + _ -> + {asn1_NOVALUE, Tlv4} + end, -%%------------------------------------------------- -%% attribute serverSaslCreds(5) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term5,Tlv6} = case Tlv5 of -[{131079,V5}|TempTlv6] -> - {decode_restricted_string(V5,[]), TempTlv6}; - _ -> - { asn1_NOVALUE, Tlv5} -end, - -case Tlv6 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv6}}}) % extra fields not allowed -end, - {'BindResponse', Term1, Term2, Term3, Term4, Term5}. + %%------------------------------------------------- + %% attribute serverSaslCreds(5) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term5, Tlv6} = case Tlv5 of + [{131079, V5} | TempTlv6] -> + {decode_restricted_string(V5, []), TempTlv6}; + _ -> + {asn1_NOVALUE, Tlv5} + end, + case Tlv6 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv6}}}) % extra fields not allowed + end, + {'BindResponse', Term1, Term2, Term3, Term4, Term5}. %%================================ @@ -1444,16 +1434,17 @@ end, 'enc_UnbindRequest'(Val) -> 'enc_UnbindRequest'(Val, [<<66>>]). + 'enc_UnbindRequest'(Val, TagIn) -> -encode_null(Val, TagIn). + encode_null(Val, TagIn). 'dec_UnbindRequest'(Tlv) -> - 'dec_UnbindRequest'(Tlv, [65538]). + 'dec_UnbindRequest'(Tlv, [65538]). + 'dec_UnbindRequest'(Tlv, TagIn) -> -decode_null(Tlv,TagIn). - + decode_null(Tlv, TagIn). %%================================ @@ -1462,127 +1453,128 @@ decode_null(Tlv,TagIn). 'enc_SearchRequest'(Val) -> 'enc_SearchRequest'(Val, [<<99>>]). + 'enc_SearchRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3, Cindex4, Cindex5, Cindex6, Cindex7, Cindex8} = Val, + {_, Cindex1, Cindex2, Cindex3, Cindex4, Cindex5, Cindex6, Cindex7, Cindex8} = Val, -%%------------------------------------------------- -%% attribute baseObject(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute baseObject(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute scope(2) with type ENUMERATED -%%------------------------------------------------- - {EncBytes2,EncLen2} = case Cindex2 of -baseObject -> encode_enumerated(0, [<<10>>]); -singleLevel -> encode_enumerated(1, [<<10>>]); -wholeSubtree -> encode_enumerated(2, [<<10>>]); -Enumval2 -> exit({error,{asn1, {enumerated_not_in_range,Enumval2}}}) -end, + %%------------------------------------------------- + %% attribute scope(2) with type ENUMERATED + %%------------------------------------------------- + {EncBytes2, EncLen2} = case Cindex2 of + baseObject -> encode_enumerated(0, [<<10>>]); + singleLevel -> encode_enumerated(1, [<<10>>]); + wholeSubtree -> encode_enumerated(2, [<<10>>]); + Enumval2 -> exit({error, {asn1, {enumerated_not_in_range, Enumval2}}}) + end, -%%------------------------------------------------- -%% attribute derefAliases(3) with type ENUMERATED -%%------------------------------------------------- - {EncBytes3,EncLen3} = case Cindex3 of -neverDerefAliases -> encode_enumerated(0, [<<10>>]); -derefInSearching -> encode_enumerated(1, [<<10>>]); -derefFindingBaseObj -> encode_enumerated(2, [<<10>>]); -derefAlways -> encode_enumerated(3, [<<10>>]); -Enumval3 -> exit({error,{asn1, {enumerated_not_in_range,Enumval3}}}) -end, + %%------------------------------------------------- + %% attribute derefAliases(3) with type ENUMERATED + %%------------------------------------------------- + {EncBytes3, EncLen3} = case Cindex3 of + neverDerefAliases -> encode_enumerated(0, [<<10>>]); + derefInSearching -> encode_enumerated(1, [<<10>>]); + derefFindingBaseObj -> encode_enumerated(2, [<<10>>]); + derefAlways -> encode_enumerated(3, [<<10>>]); + Enumval3 -> exit({error, {asn1, {enumerated_not_in_range, Enumval3}}}) + end, -%%------------------------------------------------- -%% attribute sizeLimit(4) with type INTEGER -%%------------------------------------------------- - {EncBytes4,EncLen4} = encode_integer(Cindex4, [<<2>>]), + %%------------------------------------------------- + %% attribute sizeLimit(4) with type INTEGER + %%------------------------------------------------- + {EncBytes4, EncLen4} = encode_integer(Cindex4, [<<2>>]), -%%------------------------------------------------- -%% attribute timeLimit(5) with type INTEGER -%%------------------------------------------------- - {EncBytes5,EncLen5} = encode_integer(Cindex5, [<<2>>]), + %%------------------------------------------------- + %% attribute timeLimit(5) with type INTEGER + %%------------------------------------------------- + {EncBytes5, EncLen5} = encode_integer(Cindex5, [<<2>>]), -%%------------------------------------------------- -%% attribute typesOnly(6) with type BOOLEAN -%%------------------------------------------------- - {EncBytes6,EncLen6} = encode_boolean(Cindex6, [<<1>>]), + %%------------------------------------------------- + %% attribute typesOnly(6) with type BOOLEAN + %%------------------------------------------------- + {EncBytes6, EncLen6} = encode_boolean(Cindex6, [<<1>>]), -%%------------------------------------------------- -%% attribute filter(7) External ELDAPv3:Filter -%%------------------------------------------------- - {EncBytes7,EncLen7} = 'enc_Filter'(Cindex7, []), + %%------------------------------------------------- + %% attribute filter(7) External ELDAPv3:Filter + %%------------------------------------------------- + {EncBytes7, EncLen7} = 'enc_Filter'(Cindex7, []), -%%------------------------------------------------- -%% attribute attributes(8) External ELDAPv3:AttributeDescriptionList -%%------------------------------------------------- - {EncBytes8,EncLen8} = 'enc_AttributeDescriptionList'(Cindex8, [<<48>>]), + %%------------------------------------------------- + %% attribute attributes(8) External ELDAPv3:AttributeDescriptionList + %%------------------------------------------------- + {EncBytes8, EncLen8} = 'enc_AttributeDescriptionList'(Cindex8, [<<48>>]), - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5, EncBytes6, EncBytes7, EncBytes8], -LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5 + EncLen6 + EncLen7 + EncLen8, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5, EncBytes6, EncBytes7, EncBytes8], + LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5 + EncLen6 + EncLen7 + EncLen8, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SearchRequest'(Tlv) -> - 'dec_SearchRequest'(Tlv, [65539]). + 'dec_SearchRequest'(Tlv, [65539]). + 'dec_SearchRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute baseObject(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute baseObject(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute scope(2) with type ENUMERATED -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_enumerated(V2,[{baseObject,0},{singleLevel,1},{wholeSubtree,2}],[10]), + %%------------------------------------------------- + %% attribute scope(2) with type ENUMERATED + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_enumerated(V2, [{baseObject, 0}, {singleLevel, 1}, {wholeSubtree, 2}], [10]), -%%------------------------------------------------- -%% attribute derefAliases(3) with type ENUMERATED -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = decode_enumerated(V3,[{neverDerefAliases,0},{derefInSearching,1},{derefFindingBaseObj,2},{derefAlways,3}],[10]), + %%------------------------------------------------- + %% attribute derefAliases(3) with type ENUMERATED + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = decode_enumerated(V3, [{neverDerefAliases, 0}, {derefInSearching, 1}, {derefFindingBaseObj, 2}, {derefAlways, 3}], [10]), -%%------------------------------------------------- -%% attribute sizeLimit(4) with type INTEGER -%%------------------------------------------------- -[V4|Tlv5] = Tlv4, -Term4 = decode_integer(V4,{0,2147483647},[2]), + %%------------------------------------------------- + %% attribute sizeLimit(4) with type INTEGER + %%------------------------------------------------- + [V4 | Tlv5] = Tlv4, + Term4 = decode_integer(V4, {0, 2147483647}, [2]), -%%------------------------------------------------- -%% attribute timeLimit(5) with type INTEGER -%%------------------------------------------------- -[V5|Tlv6] = Tlv5, -Term5 = decode_integer(V5,{0,2147483647},[2]), + %%------------------------------------------------- + %% attribute timeLimit(5) with type INTEGER + %%------------------------------------------------- + [V5 | Tlv6] = Tlv5, + Term5 = decode_integer(V5, {0, 2147483647}, [2]), -%%------------------------------------------------- -%% attribute typesOnly(6) with type BOOLEAN -%%------------------------------------------------- -[V6|Tlv7] = Tlv6, -Term6 = decode_boolean(V6,[1]), + %%------------------------------------------------- + %% attribute typesOnly(6) with type BOOLEAN + %%------------------------------------------------- + [V6 | Tlv7] = Tlv6, + Term6 = decode_boolean(V6, [1]), -%%------------------------------------------------- -%% attribute filter(7) External ELDAPv3:Filter -%%------------------------------------------------- -[V7|Tlv8] = Tlv7, -Term7 = 'dec_Filter'(V7, []), + %%------------------------------------------------- + %% attribute filter(7) External ELDAPv3:Filter + %%------------------------------------------------- + [V7 | Tlv8] = Tlv7, + Term7 = 'dec_Filter'(V7, []), -%%------------------------------------------------- -%% attribute attributes(8) External ELDAPv3:AttributeDescriptionList -%%------------------------------------------------- -[V8|Tlv9] = Tlv8, -Term8 = 'dec_AttributeDescriptionList'(V8, [16]), - -case Tlv9 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv9}}}) % extra fields not allowed -end, - {'SearchRequest', Term1, Term2, Term3, Term4, Term5, Term6, Term7, Term8}. + %%------------------------------------------------- + %% attribute attributes(8) External ELDAPv3:AttributeDescriptionList + %%------------------------------------------------- + [V8 | Tlv9] = Tlv8, + Term8 = 'dec_AttributeDescriptionList'(V8, [16]), + case Tlv9 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv9}}}) % extra fields not allowed + end, + {'SearchRequest', Term1, Term2, Term3, Term4, Term5, Term6, Term7, Term8}. %%================================ @@ -1591,147 +1583,135 @@ end, 'enc_Filter'(Val) -> 'enc_Filter'(Val, []). + 'enc_Filter'(Val, TagIn) -> - {EncBytes,EncLen} = case element(1,Val) of - 'and' -> - 'enc_Filter_and'(element(2,Val), [<<160>>]); - 'or' -> - 'enc_Filter_or'(element(2,Val), [<<161>>]); - 'not' -> - 'enc_Filter'(element(2,Val), [<<162>>]); - equalityMatch -> - 'enc_AttributeValueAssertion'(element(2,Val), [<<163>>]); - substrings -> - 'enc_SubstringFilter'(element(2,Val), [<<164>>]); - greaterOrEqual -> - 'enc_AttributeValueAssertion'(element(2,Val), [<<165>>]); - lessOrEqual -> - 'enc_AttributeValueAssertion'(element(2,Val), [<<166>>]); - present -> - encode_restricted_string(element(2,Val), [<<135>>]); - approxMatch -> - 'enc_AttributeValueAssertion'(element(2,Val), [<<168>>]); - extensibleMatch -> - 'enc_MatchingRuleAssertion'(element(2,Val), [<<169>>]); - Else -> - exit({error,{asn1,{invalid_choice_type,Else}}}) - end, - -encode_tags(TagIn, EncBytes, EncLen). - - + {EncBytes, EncLen} = case element(1, Val) of + 'and' -> + 'enc_Filter_and'(element(2, Val), [<<160>>]); + 'or' -> + 'enc_Filter_or'(element(2, Val), [<<161>>]); + 'not' -> + 'enc_Filter'(element(2, Val), [<<162>>]); + equalityMatch -> + 'enc_AttributeValueAssertion'(element(2, Val), [<<163>>]); + substrings -> + 'enc_SubstringFilter'(element(2, Val), [<<164>>]); + greaterOrEqual -> + 'enc_AttributeValueAssertion'(element(2, Val), [<<165>>]); + lessOrEqual -> + 'enc_AttributeValueAssertion'(element(2, Val), [<<166>>]); + present -> + encode_restricted_string(element(2, Val), [<<135>>]); + approxMatch -> + 'enc_AttributeValueAssertion'(element(2, Val), [<<168>>]); + extensibleMatch -> + 'enc_MatchingRuleAssertion'(element(2, Val), [<<169>>]); + Else -> + exit({error, {asn1, {invalid_choice_type, Else}}}) + end, + encode_tags(TagIn, EncBytes, EncLen). %%================================ %% Filter_and %%================================ 'enc_Filter_and'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_Filter_and_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_Filter_and_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_Filter_and_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; + {lists:reverse(AccBytes), AccLen}; + +'enc_Filter_and_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_Filter'(H, []), + 'enc_Filter_and_components'(T, [EncBytes | AccBytes], AccLen + EncLen). -'enc_Filter_and_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_Filter'(H, []), - 'enc_Filter_and_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Filter_and'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_Filter'(V1, []) || V1 <- Tlv1]. - - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_Filter'(V1, []) || V1 <- Tlv1 ]. %%================================ %% Filter_or %%================================ 'enc_Filter_or'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_Filter_or_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_Filter_or_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_Filter_or_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; + {lists:reverse(AccBytes), AccLen}; + +'enc_Filter_or_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_Filter'(H, []), + 'enc_Filter_or_components'(T, [EncBytes | AccBytes], AccLen + EncLen). -'enc_Filter_or_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_Filter'(H, []), - 'enc_Filter_or_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Filter_or'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_Filter'(V1, []) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_Filter'(V1, []) || V1 <- Tlv1 ]. 'dec_Filter'(Tlv) -> - 'dec_Filter'(Tlv, []). + 'dec_Filter'(Tlv, []). + 'dec_Filter'(Tlv, TagIn) -> -Tlv1 = match_tags(Tlv, TagIn), -case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of + Tlv1 = match_tags(Tlv, TagIn), + case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of -%% 'and' - {131072, V1} -> - {'and', 'dec_Filter_and'(V1, [])}; + %% 'and' + {131072, V1} -> + {'and', 'dec_Filter_and'(V1, [])}; + %% 'or' + {131073, V1} -> + {'or', 'dec_Filter_or'(V1, [])}; -%% 'or' - {131073, V1} -> - {'or', 'dec_Filter_or'(V1, [])}; + %% 'not' + {131074, V1} -> + {'not', 'dec_Filter'(V1, [])}; + %% 'equalityMatch' + {131075, V1} -> + {equalityMatch, 'dec_AttributeValueAssertion'(V1, [])}; -%% 'not' - {131074, V1} -> - {'not', 'dec_Filter'(V1, [])}; + %% 'substrings' + {131076, V1} -> + {substrings, 'dec_SubstringFilter'(V1, [])}; + %% 'greaterOrEqual' + {131077, V1} -> + {greaterOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; -%% 'equalityMatch' - {131075, V1} -> - {equalityMatch, 'dec_AttributeValueAssertion'(V1, [])}; + %% 'lessOrEqual' + {131078, V1} -> + {lessOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; + %% 'present' + {131079, V1} -> + {present, decode_restricted_string(V1, [])}; -%% 'substrings' - {131076, V1} -> - {substrings, 'dec_SubstringFilter'(V1, [])}; + %% 'approxMatch' + {131080, V1} -> + {approxMatch, 'dec_AttributeValueAssertion'(V1, [])}; + %% 'extensibleMatch' + {131081, V1} -> + {extensibleMatch, 'dec_MatchingRuleAssertion'(V1, [])}; -%% 'greaterOrEqual' - {131077, V1} -> - {greaterOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; - - -%% 'lessOrEqual' - {131078, V1} -> - {lessOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; - - -%% 'present' - {131079, V1} -> - {present, decode_restricted_string(V1,[])}; - - -%% 'approxMatch' - {131080, V1} -> - {approxMatch, 'dec_AttributeValueAssertion'(V1, [])}; - - -%% 'extensibleMatch' - {131081, V1} -> - {extensibleMatch, 'dec_MatchingRuleAssertion'(V1, [])}; - - Else -> - exit({error,{asn1,{invalid_choice_tag,Else}}}) - end -. + Else -> + exit({error, {asn1, {invalid_choice_tag, Else}}}) + end. %%================================ @@ -1740,118 +1720,114 @@ case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of 'enc_SubstringFilter'(Val) -> 'enc_SubstringFilter'(Val, [<<48>>]). + 'enc_SubstringFilter'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute substrings(2) with type SEQUENCE OF -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_SubstringFilter_substrings'(Cindex2, [<<48>>]), - - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute substrings(2) with type SEQUENCE OF + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_SubstringFilter_substrings'(Cindex2, [<<48>>]), + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% SubstringFilter_substrings %%================================ 'enc_SubstringFilter_substrings'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_SubstringFilter_substrings_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_SubstringFilter_substrings_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_SubstringFilter_substrings_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_SubstringFilter_substrings_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_SubstringFilter_substrings_SEQOF'(H, []), - 'enc_SubstringFilter_substrings_components'(T,[EncBytes|AccBytes], AccLen + EncLen). - + {lists:reverse(AccBytes), AccLen}; +'enc_SubstringFilter_substrings_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_SubstringFilter_substrings_SEQOF'(H, []), + 'enc_SubstringFilter_substrings_components'(T, [EncBytes | AccBytes], AccLen + EncLen). %%================================ %% SubstringFilter_substrings_SEQOF %%================================ 'enc_SubstringFilter_substrings_SEQOF'(Val, TagIn) -> - {EncBytes,EncLen} = case element(1,Val) of - initial -> - encode_restricted_string(element(2,Val), [<<128>>]); - any -> - encode_restricted_string(element(2,Val), [<<129>>]); - final -> - encode_restricted_string(element(2,Val), [<<130>>]); - Else -> - exit({error,{asn1,{invalid_choice_type,Else}}}) - end, + {EncBytes, EncLen} = case element(1, Val) of + initial -> + encode_restricted_string(element(2, Val), [<<128>>]); + any -> + encode_restricted_string(element(2, Val), [<<129>>]); + final -> + encode_restricted_string(element(2, Val), [<<130>>]); + Else -> + exit({error, {asn1, {invalid_choice_type, Else}}}) + end, -encode_tags(TagIn, EncBytes, EncLen). + encode_tags(TagIn, EncBytes, EncLen). 'dec_SubstringFilter_substrings_SEQOF'(Tlv, TagIn) -> -Tlv1 = match_tags(Tlv, TagIn), -case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of + Tlv1 = match_tags(Tlv, TagIn), + case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of -%% 'initial' - {131072, V1} -> - {initial, decode_restricted_string(V1,[])}; + %% 'initial' + {131072, V1} -> + {initial, decode_restricted_string(V1, [])}; + + %% 'any' + {131073, V1} -> + {any, decode_restricted_string(V1, [])}; + + %% 'final' + {131074, V1} -> + {final, decode_restricted_string(V1, [])}; + + Else -> + exit({error, {asn1, {invalid_choice_tag, Else}}}) + end. -%% 'any' - {131073, V1} -> - {any, decode_restricted_string(V1,[])}; - - -%% 'final' - {131074, V1} -> - {final, decode_restricted_string(V1,[])}; - - Else -> - exit({error,{asn1,{invalid_choice_tag,Else}}}) - end -. 'dec_SubstringFilter_substrings'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_SubstringFilter_substrings_SEQOF'(V1, []) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_SubstringFilter_substrings_SEQOF'(V1, []) || V1 <- Tlv1 ]. 'dec_SubstringFilter'(Tlv) -> - 'dec_SubstringFilter'(Tlv, [16]). + 'dec_SubstringFilter'(Tlv, [16]). + 'dec_SubstringFilter'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute substrings(2) with type SEQUENCE OF -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_SubstringFilter_substrings'(V2, [16]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'SubstringFilter', Term1, Term2}. + %%------------------------------------------------- + %% attribute substrings(2) with type SEQUENCE OF + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_SubstringFilter_substrings'(V2, [16]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'SubstringFilter', Term1, Term2}. %%================================ @@ -1860,97 +1836,98 @@ end, 'enc_MatchingRuleAssertion'(Val) -> 'enc_MatchingRuleAssertion'(Val, [<<48>>]). + 'enc_MatchingRuleAssertion'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3, Cindex4} = Val, + {_, Cindex1, Cindex2, Cindex3, Cindex4} = Val, -%%------------------------------------------------- -%% attribute matchingRule(1) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex1, [<<129>>]) - end, + %%------------------------------------------------- + %% attribute matchingRule(1) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex1, [<<129>>]) + end, -%%------------------------------------------------- -%% attribute type(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes2,EncLen2} = case Cindex2 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex2, [<<130>>]) - end, + %%------------------------------------------------- + %% attribute type(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes2, EncLen2} = case Cindex2 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex2, [<<130>>]) + end, -%%------------------------------------------------- -%% attribute matchValue(3) with type OCTET STRING -%%------------------------------------------------- - {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<131>>]), + %%------------------------------------------------- + %% attribute matchValue(3) with type OCTET STRING + %%------------------------------------------------- + {EncBytes3, EncLen3} = encode_restricted_string(Cindex3, [<<131>>]), -%%------------------------------------------------- -%% attribute dnAttributes(4) with type BOOLEAN DEFAULT = false -%%------------------------------------------------- - {EncBytes4,EncLen4} = case Cindex4 of - asn1_DEFAULT -> {<<>>,0}; - false -> {<<>>,0}; - _ -> - encode_boolean(Cindex4, [<<132>>]) - end, + %%------------------------------------------------- + %% attribute dnAttributes(4) with type BOOLEAN DEFAULT = false + %%------------------------------------------------- + {EncBytes4, EncLen4} = case Cindex4 of + asn1_DEFAULT -> {<<>>, 0}; + false -> {<<>>, 0}; + _ -> + encode_boolean(Cindex4, [<<132>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], -LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], + LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_MatchingRuleAssertion'(Tlv) -> - 'dec_MatchingRuleAssertion'(Tlv, [16]). + 'dec_MatchingRuleAssertion'(Tlv, [16]). + 'dec_MatchingRuleAssertion'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute matchingRule(1) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term1,Tlv2} = case Tlv1 of -[{131073,V1}|TempTlv2] -> - {decode_restricted_string(V1,[]), TempTlv2}; - _ -> - { asn1_NOVALUE, Tlv1} -end, + %%------------------------------------------------- + %% attribute matchingRule(1) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term1, Tlv2} = case Tlv1 of + [{131073, V1} | TempTlv2] -> + {decode_restricted_string(V1, []), TempTlv2}; + _ -> + {asn1_NOVALUE, Tlv1} + end, -%%------------------------------------------------- -%% attribute type(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term2,Tlv3} = case Tlv2 of -[{131074,V2}|TempTlv3] -> - {decode_restricted_string(V2,[]), TempTlv3}; - _ -> - { asn1_NOVALUE, Tlv2} -end, + %%------------------------------------------------- + %% attribute type(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term2, Tlv3} = case Tlv2 of + [{131074, V2} | TempTlv3] -> + {decode_restricted_string(V2, []), TempTlv3}; + _ -> + {asn1_NOVALUE, Tlv2} + end, -%%------------------------------------------------- -%% attribute matchValue(3) with type OCTET STRING -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = decode_restricted_string(V3,[131075]), + %%------------------------------------------------- + %% attribute matchValue(3) with type OCTET STRING + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = decode_restricted_string(V3, [131075]), -%%------------------------------------------------- -%% attribute dnAttributes(4) with type BOOLEAN DEFAULT = false -%%------------------------------------------------- -{Term4,Tlv5} = case Tlv4 of -[{131076,V4}|TempTlv5] -> - {decode_boolean(V4,[]), TempTlv5}; - _ -> - {false,Tlv4} -end, - -case Tlv5 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv5}}}) % extra fields not allowed -end, - {'MatchingRuleAssertion', Term1, Term2, Term3, Term4}. + %%------------------------------------------------- + %% attribute dnAttributes(4) with type BOOLEAN DEFAULT = false + %%------------------------------------------------- + {Term4, Tlv5} = case Tlv4 of + [{131076, V4} | TempTlv5] -> + {decode_boolean(V4, []), TempTlv5}; + _ -> + {false, Tlv4} + end, + case Tlv5 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv5}}}) % extra fields not allowed + end, + {'MatchingRuleAssertion', Term1, Term2, Term3, Term4}. %%================================ @@ -1959,50 +1936,51 @@ end, 'enc_SearchResultEntry'(Val) -> 'enc_SearchResultEntry'(Val, [<<100>>]). + 'enc_SearchResultEntry'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute objectName(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute objectName(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute attributes(2) External ELDAPv3:PartialAttributeList -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_PartialAttributeList'(Cindex2, [<<48>>]), + %%------------------------------------------------- + %% attribute attributes(2) External ELDAPv3:PartialAttributeList + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_PartialAttributeList'(Cindex2, [<<48>>]), - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SearchResultEntry'(Tlv) -> - 'dec_SearchResultEntry'(Tlv, [65540]). + 'dec_SearchResultEntry'(Tlv, [65540]). + 'dec_SearchResultEntry'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute objectName(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute objectName(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute attributes(2) External ELDAPv3:PartialAttributeList -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_PartialAttributeList'(V2, [16]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'SearchResultEntry', Term1, Term2}. + %%------------------------------------------------- + %% attribute attributes(2) External ELDAPv3:PartialAttributeList + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_PartialAttributeList'(V2, [16]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'SearchResultEntry', Term1, Term2}. %%================================ @@ -2011,100 +1989,99 @@ end, 'enc_PartialAttributeList'(Val) -> 'enc_PartialAttributeList'(Val, [<<48>>]). + 'enc_PartialAttributeList'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_PartialAttributeList_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_PartialAttributeList_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_PartialAttributeList_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_PartialAttributeList_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_PartialAttributeList_SEQOF'(H, [<<48>>]), - 'enc_PartialAttributeList_components'(T,[EncBytes|AccBytes], AccLen + EncLen). - + {lists:reverse(AccBytes), AccLen}; +'enc_PartialAttributeList_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_PartialAttributeList_SEQOF'(H, [<<48>>]), + 'enc_PartialAttributeList_components'(T, [EncBytes | AccBytes], AccLen + EncLen). %%================================ %% PartialAttributeList_SEQOF %%================================ 'enc_PartialAttributeList_SEQOF'(Val, TagIn) -> - {_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_PartialAttributeList_SEQOF_vals'(Cindex2, [<<49>>]), - - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_PartialAttributeList_SEQOF_vals'(Cindex2, [<<49>>]), + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% PartialAttributeList_SEQOF_vals %%================================ 'enc_PartialAttributeList_SEQOF_vals'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_PartialAttributeList_SEQOF_vals_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_PartialAttributeList_SEQOF_vals_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_PartialAttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; + {lists:reverse(AccBytes), AccLen}; + +'enc_PartialAttributeList_SEQOF_vals_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_PartialAttributeList_SEQOF_vals_components'(T, [EncBytes | AccBytes], AccLen + EncLen). -'enc_PartialAttributeList_SEQOF_vals_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_PartialAttributeList_SEQOF_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_PartialAttributeList_SEQOF_vals'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. 'dec_PartialAttributeList_SEQOF'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_PartialAttributeList_SEQOF_vals'(V2, [17]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'PartialAttributeList_SEQOF', Term1, Term2}. + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_PartialAttributeList_SEQOF_vals'(V2, [17]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'PartialAttributeList_SEQOF', Term1, Term2}. 'dec_PartialAttributeList'(Tlv) -> - 'dec_PartialAttributeList'(Tlv, [16]). + 'dec_PartialAttributeList'(Tlv, [16]). + 'dec_PartialAttributeList'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_PartialAttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_PartialAttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1 ]. %%================================ @@ -2113,30 +2090,30 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_SearchResultReference'(Val) -> 'enc_SearchResultReference'(Val, [<<115>>]). + 'enc_SearchResultReference'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_SearchResultReference_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_SearchResultReference_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_SearchResultReference_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_SearchResultReference_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_SearchResultReference_components'(T,[EncBytes|AccBytes], AccLen + EncLen). + {lists:reverse(AccBytes), AccLen}; +'enc_SearchResultReference_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_SearchResultReference_components'(T, [EncBytes | AccBytes], AccLen + EncLen). 'dec_SearchResultReference'(Tlv) -> - 'dec_SearchResultReference'(Tlv, [65555]). + 'dec_SearchResultReference'(Tlv, [65555]). + 'dec_SearchResultReference'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. %%================================ @@ -2145,16 +2122,17 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_SearchResultDone'(Val) -> 'enc_SearchResultDone'(Val, [<<101>>]). + 'enc_SearchResultDone'(Val, TagIn) -> - 'enc_LDAPResult'(Val, TagIn). + 'enc_LDAPResult'(Val, TagIn). 'dec_SearchResultDone'(Tlv) -> - 'dec_SearchResultDone'(Tlv, [65541]). + 'dec_SearchResultDone'(Tlv, [65541]). + 'dec_SearchResultDone'(Tlv, TagIn) -> -'dec_LDAPResult'(Tlv, TagIn). - + 'dec_LDAPResult'(Tlv, TagIn). %%================================ @@ -2163,125 +2141,125 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_ModifyRequest'(Val) -> 'enc_ModifyRequest'(Val, [<<102>>]). + 'enc_ModifyRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute object(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute object(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute modification(2) with type SEQUENCE OF -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_ModifyRequest_modification'(Cindex2, [<<48>>]), - - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute modification(2) with type SEQUENCE OF + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_ModifyRequest_modification'(Cindex2, [<<48>>]), + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% ModifyRequest_modification %%================================ 'enc_ModifyRequest_modification'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_ModifyRequest_modification_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_ModifyRequest_modification_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_ModifyRequest_modification_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_ModifyRequest_modification_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_ModifyRequest_modification_SEQOF'(H, [<<48>>]), - 'enc_ModifyRequest_modification_components'(T,[EncBytes|AccBytes], AccLen + EncLen). - + {lists:reverse(AccBytes), AccLen}; +'enc_ModifyRequest_modification_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_ModifyRequest_modification_SEQOF'(H, [<<48>>]), + 'enc_ModifyRequest_modification_components'(T, [EncBytes | AccBytes], AccLen + EncLen). %%================================ %% ModifyRequest_modification_SEQOF %%================================ 'enc_ModifyRequest_modification_SEQOF'(Val, TagIn) -> - {_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute operation(1) with type ENUMERATED -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of -add -> encode_enumerated(0, [<<10>>]); -delete -> encode_enumerated(1, [<<10>>]); -replace -> encode_enumerated(2, [<<10>>]); -Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) -end, + %%------------------------------------------------- + %% attribute operation(1) with type ENUMERATED + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + add -> encode_enumerated(0, [<<10>>]); + delete -> encode_enumerated(1, [<<10>>]); + replace -> encode_enumerated(2, [<<10>>]); + Enumval1 -> exit({error, {asn1, {enumerated_not_in_range, Enumval1}}}) + end, + + %%------------------------------------------------- + %% attribute modification(2) External ELDAPv3:AttributeTypeAndValues + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_AttributeTypeAndValues'(Cindex2, [<<48>>]), + + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). -%%------------------------------------------------- -%% attribute modification(2) External ELDAPv3:AttributeTypeAndValues -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_AttributeTypeAndValues'(Cindex2, [<<48>>]), - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ModifyRequest_modification_SEQOF'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute operation(1) with type ENUMERATED -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_enumerated(V1,[{add,0},{delete,1},{replace,2}],[10]), + %%------------------------------------------------- + %% attribute operation(1) with type ENUMERATED + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_enumerated(V1, [{add, 0}, {delete, 1}, {replace, 2}], [10]), -%%------------------------------------------------- -%% attribute modification(2) External ELDAPv3:AttributeTypeAndValues -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_AttributeTypeAndValues'(V2, [16]), + %%------------------------------------------------- + %% attribute modification(2) External ELDAPv3:AttributeTypeAndValues + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_AttributeTypeAndValues'(V2, [16]), + + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'ModifyRequest_modification_SEQOF', Term1, Term2}. -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'ModifyRequest_modification_SEQOF', Term1, Term2}. 'dec_ModifyRequest_modification'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_ModifyRequest_modification_SEQOF'(V1, [16]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_ModifyRequest_modification_SEQOF'(V1, [16]) || V1 <- Tlv1 ]. 'dec_ModifyRequest'(Tlv) -> - 'dec_ModifyRequest'(Tlv, [65542]). + 'dec_ModifyRequest'(Tlv, [65542]). + 'dec_ModifyRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute object(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute object(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute modification(2) with type SEQUENCE OF -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_ModifyRequest_modification'(V2, [16]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'ModifyRequest', Term1, Term2}. + %%------------------------------------------------- + %% attribute modification(2) with type SEQUENCE OF + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_ModifyRequest_modification'(V2, [16]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'ModifyRequest', Term1, Term2}. %%================================ @@ -2290,75 +2268,75 @@ end, 'enc_AttributeTypeAndValues'(Val) -> 'enc_AttributeTypeAndValues'(Val, [<<48>>]). + 'enc_AttributeTypeAndValues'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_AttributeTypeAndValues_vals'(Cindex2, [<<49>>]), - - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_AttributeTypeAndValues_vals'(Cindex2, [<<49>>]), + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% AttributeTypeAndValues_vals %%================================ 'enc_AttributeTypeAndValues_vals'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_AttributeTypeAndValues_vals_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_AttributeTypeAndValues_vals_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_AttributeTypeAndValues_vals_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; + {lists:reverse(AccBytes), AccLen}; + +'enc_AttributeTypeAndValues_vals_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_AttributeTypeAndValues_vals_components'(T, [EncBytes | AccBytes], AccLen + EncLen). -'enc_AttributeTypeAndValues_vals_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_AttributeTypeAndValues_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_AttributeTypeAndValues_vals'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. 'dec_AttributeTypeAndValues'(Tlv) -> - 'dec_AttributeTypeAndValues'(Tlv, [16]). + 'dec_AttributeTypeAndValues'(Tlv, [16]). + 'dec_AttributeTypeAndValues'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_AttributeTypeAndValues_vals'(V2, [17]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'AttributeTypeAndValues', Term1, Term2}. + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_AttributeTypeAndValues_vals'(V2, [17]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'AttributeTypeAndValues', Term1, Term2}. %%================================ @@ -2367,16 +2345,17 @@ end, 'enc_ModifyResponse'(Val) -> 'enc_ModifyResponse'(Val, [<<103>>]). + 'enc_ModifyResponse'(Val, TagIn) -> - 'enc_LDAPResult'(Val, TagIn). + 'enc_LDAPResult'(Val, TagIn). 'dec_ModifyResponse'(Tlv) -> - 'dec_ModifyResponse'(Tlv, [65543]). + 'dec_ModifyResponse'(Tlv, [65543]). + 'dec_ModifyResponse'(Tlv, TagIn) -> -'dec_LDAPResult'(Tlv, TagIn). - + 'dec_LDAPResult'(Tlv, TagIn). %%================================ @@ -2385,50 +2364,51 @@ end, 'enc_AddRequest'(Val) -> 'enc_AddRequest'(Val, [<<104>>]). + 'enc_AddRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute entry(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute entry(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute attributes(2) External ELDAPv3:AttributeList -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_AttributeList'(Cindex2, [<<48>>]), + %%------------------------------------------------- + %% attribute attributes(2) External ELDAPv3:AttributeList + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_AttributeList'(Cindex2, [<<48>>]), - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AddRequest'(Tlv) -> - 'dec_AddRequest'(Tlv, [65544]). + 'dec_AddRequest'(Tlv, [65544]). + 'dec_AddRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute entry(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute entry(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute attributes(2) External ELDAPv3:AttributeList -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_AttributeList'(V2, [16]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'AddRequest', Term1, Term2}. + %%------------------------------------------------- + %% attribute attributes(2) External ELDAPv3:AttributeList + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_AttributeList'(V2, [16]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'AddRequest', Term1, Term2}. %%================================ @@ -2437,100 +2417,99 @@ end, 'enc_AttributeList'(Val) -> 'enc_AttributeList'(Val, [<<48>>]). + 'enc_AttributeList'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_AttributeList_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_AttributeList_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_AttributeList_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; - -'enc_AttributeList_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = 'enc_AttributeList_SEQOF'(H, [<<48>>]), - 'enc_AttributeList_components'(T,[EncBytes|AccBytes], AccLen + EncLen). - + {lists:reverse(AccBytes), AccLen}; +'enc_AttributeList_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = 'enc_AttributeList_SEQOF'(H, [<<48>>]), + 'enc_AttributeList_components'(T, [EncBytes | AccBytes], AccLen + EncLen). %%================================ %% AttributeList_SEQOF %%================================ 'enc_AttributeList_SEQOF'(Val, TagIn) -> - {_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_AttributeList_SEQOF_vals'(Cindex2, [<<49>>]), - - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_AttributeList_SEQOF_vals'(Cindex2, [<<49>>]), + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% AttributeList_SEQOF_vals %%================================ 'enc_AttributeList_SEQOF_vals'(Val, TagIn) -> - {EncBytes,EncLen} = 'enc_AttributeList_SEQOF_vals_components'(Val,[],0), - encode_tags(TagIn, EncBytes, EncLen). + {EncBytes, EncLen} = 'enc_AttributeList_SEQOF_vals_components'(Val, [], 0), + encode_tags(TagIn, EncBytes, EncLen). + 'enc_AttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> - {lists:reverse(AccBytes),AccLen}; + {lists:reverse(AccBytes), AccLen}; + +'enc_AttributeList_SEQOF_vals_components'([H | T], AccBytes, AccLen) -> + {EncBytes, EncLen} = encode_restricted_string(H, [<<4>>]), + 'enc_AttributeList_SEQOF_vals_components'(T, [EncBytes | AccBytes], AccLen + EncLen). -'enc_AttributeList_SEQOF_vals_components'([H|T],AccBytes, AccLen) -> - {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), - 'enc_AttributeList_SEQOF_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_AttributeList_SEQOF_vals'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -[decode_restricted_string(V1,[4]) || V1 <- Tlv1]. + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ decode_restricted_string(V1, [4]) || V1 <- Tlv1 ]. 'dec_AttributeList_SEQOF'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute type(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute type(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute vals(2) with type SET OF -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_AttributeList_SEQOF_vals'(V2, [17]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'AttributeList_SEQOF', Term1, Term2}. + %%------------------------------------------------- + %% attribute vals(2) with type SET OF + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_AttributeList_SEQOF_vals'(V2, [17]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'AttributeList_SEQOF', Term1, Term2}. 'dec_AttributeList'(Tlv) -> - 'dec_AttributeList'(Tlv, [16]). + 'dec_AttributeList'(Tlv, [16]). + 'dec_AttributeList'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), -['dec_AttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1]. - - + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), + [ 'dec_AttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1 ]. %%================================ @@ -2539,16 +2518,17 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_AddResponse'(Val) -> 'enc_AddResponse'(Val, [<<105>>]). + 'enc_AddResponse'(Val, TagIn) -> - 'enc_LDAPResult'(Val, TagIn). + 'enc_LDAPResult'(Val, TagIn). 'dec_AddResponse'(Tlv) -> - 'dec_AddResponse'(Tlv, [65545]). + 'dec_AddResponse'(Tlv, [65545]). + 'dec_AddResponse'(Tlv, TagIn) -> -'dec_LDAPResult'(Tlv, TagIn). - + 'dec_LDAPResult'(Tlv, TagIn). %%================================ @@ -2557,16 +2537,17 @@ Tlv1 = match_tags(Tlv, TagIn), 'enc_DelRequest'(Val) -> 'enc_DelRequest'(Val, [<<74>>]). + 'enc_DelRequest'(Val, TagIn) -> -encode_restricted_string(Val, TagIn). + encode_restricted_string(Val, TagIn). 'dec_DelRequest'(Tlv) -> - 'dec_DelRequest'(Tlv, [65546]). + 'dec_DelRequest'(Tlv, [65546]). + 'dec_DelRequest'(Tlv, TagIn) -> -decode_restricted_string(Tlv,TagIn). - + decode_restricted_string(Tlv, TagIn). %%================================ @@ -2575,16 +2556,17 @@ decode_restricted_string(Tlv,TagIn). 'enc_DelResponse'(Val) -> 'enc_DelResponse'(Val, [<<107>>]). + 'enc_DelResponse'(Val, TagIn) -> - 'enc_LDAPResult'(Val, TagIn). + 'enc_LDAPResult'(Val, TagIn). 'dec_DelResponse'(Tlv) -> - 'dec_DelResponse'(Tlv, [65547]). + 'dec_DelResponse'(Tlv, [65547]). + 'dec_DelResponse'(Tlv, TagIn) -> -'dec_LDAPResult'(Tlv, TagIn). - + 'dec_LDAPResult'(Tlv, TagIn). %%================================ @@ -2593,80 +2575,81 @@ decode_restricted_string(Tlv,TagIn). 'enc_ModifyDNRequest'(Val) -> 'enc_ModifyDNRequest'(Val, [<<108>>]). + 'enc_ModifyDNRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3, Cindex4} = Val, + {_, Cindex1, Cindex2, Cindex3, Cindex4} = Val, -%%------------------------------------------------- -%% attribute entry(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute entry(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute newrdn(2) with type OCTET STRING -%%------------------------------------------------- - {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), + %%------------------------------------------------- + %% attribute newrdn(2) with type OCTET STRING + %%------------------------------------------------- + {EncBytes2, EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), -%%------------------------------------------------- -%% attribute deleteoldrdn(3) with type BOOLEAN -%%------------------------------------------------- - {EncBytes3,EncLen3} = encode_boolean(Cindex3, [<<1>>]), + %%------------------------------------------------- + %% attribute deleteoldrdn(3) with type BOOLEAN + %%------------------------------------------------- + {EncBytes3, EncLen3} = encode_boolean(Cindex3, [<<1>>]), -%%------------------------------------------------- -%% attribute newSuperior(4) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes4,EncLen4} = case Cindex4 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex4, [<<128>>]) - end, + %%------------------------------------------------- + %% attribute newSuperior(4) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes4, EncLen4} = case Cindex4 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex4, [<<128>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], -LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], + LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ModifyDNRequest'(Tlv) -> - 'dec_ModifyDNRequest'(Tlv, [65548]). + 'dec_ModifyDNRequest'(Tlv, [65548]). + 'dec_ModifyDNRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute entry(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute entry(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute newrdn(2) with type OCTET STRING -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_restricted_string(V2,[4]), + %%------------------------------------------------- + %% attribute newrdn(2) with type OCTET STRING + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_restricted_string(V2, [4]), -%%------------------------------------------------- -%% attribute deleteoldrdn(3) with type BOOLEAN -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = decode_boolean(V3,[1]), + %%------------------------------------------------- + %% attribute deleteoldrdn(3) with type BOOLEAN + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = decode_boolean(V3, [1]), -%%------------------------------------------------- -%% attribute newSuperior(4) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term4,Tlv5} = case Tlv4 of -[{131072,V4}|TempTlv5] -> - {decode_restricted_string(V4,[]), TempTlv5}; - _ -> - { asn1_NOVALUE, Tlv4} -end, - -case Tlv5 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv5}}}) % extra fields not allowed -end, - {'ModifyDNRequest', Term1, Term2, Term3, Term4}. + %%------------------------------------------------- + %% attribute newSuperior(4) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term4, Tlv5} = case Tlv4 of + [{131072, V4} | TempTlv5] -> + {decode_restricted_string(V4, []), TempTlv5}; + _ -> + {asn1_NOVALUE, Tlv4} + end, + case Tlv5 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv5}}}) % extra fields not allowed + end, + {'ModifyDNRequest', Term1, Term2, Term3, Term4}. %%================================ @@ -2675,16 +2658,17 @@ end, 'enc_ModifyDNResponse'(Val) -> 'enc_ModifyDNResponse'(Val, [<<109>>]). + 'enc_ModifyDNResponse'(Val, TagIn) -> - 'enc_LDAPResult'(Val, TagIn). + 'enc_LDAPResult'(Val, TagIn). 'dec_ModifyDNResponse'(Tlv) -> - 'dec_ModifyDNResponse'(Tlv, [65549]). + 'dec_ModifyDNResponse'(Tlv, [65549]). + 'dec_ModifyDNResponse'(Tlv, TagIn) -> -'dec_LDAPResult'(Tlv, TagIn). - + 'dec_LDAPResult'(Tlv, TagIn). %%================================ @@ -2693,50 +2677,51 @@ end, 'enc_CompareRequest'(Val) -> 'enc_CompareRequest'(Val, [<<110>>]). + 'enc_CompareRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute entry(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), + %%------------------------------------------------- + %% attribute entry(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), -%%------------------------------------------------- -%% attribute ava(2) External ELDAPv3:AttributeValueAssertion -%%------------------------------------------------- - {EncBytes2,EncLen2} = 'enc_AttributeValueAssertion'(Cindex2, [<<48>>]), + %%------------------------------------------------- + %% attribute ava(2) External ELDAPv3:AttributeValueAssertion + %%------------------------------------------------- + {EncBytes2, EncLen2} = 'enc_AttributeValueAssertion'(Cindex2, [<<48>>]), - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_CompareRequest'(Tlv) -> - 'dec_CompareRequest'(Tlv, [65550]). + 'dec_CompareRequest'(Tlv, [65550]). + 'dec_CompareRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute entry(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[4]), + %%------------------------------------------------- + %% attribute entry(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [4]), -%%------------------------------------------------- -%% attribute ava(2) External ELDAPv3:AttributeValueAssertion -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = 'dec_AttributeValueAssertion'(V2, [16]), - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'CompareRequest', Term1, Term2}. + %%------------------------------------------------- + %% attribute ava(2) External ELDAPv3:AttributeValueAssertion + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = 'dec_AttributeValueAssertion'(V2, [16]), + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'CompareRequest', Term1, Term2}. %%================================ @@ -2745,16 +2730,17 @@ end, 'enc_CompareResponse'(Val) -> 'enc_CompareResponse'(Val, [<<111>>]). + 'enc_CompareResponse'(Val, TagIn) -> - 'enc_LDAPResult'(Val, TagIn). + 'enc_LDAPResult'(Val, TagIn). 'dec_CompareResponse'(Tlv) -> - 'dec_CompareResponse'(Tlv, [65551]). + 'dec_CompareResponse'(Tlv, [65551]). + 'dec_CompareResponse'(Tlv, TagIn) -> -'dec_LDAPResult'(Tlv, TagIn). - + 'dec_LDAPResult'(Tlv, TagIn). %%================================ @@ -2763,16 +2749,17 @@ end, 'enc_AbandonRequest'(Val) -> 'enc_AbandonRequest'(Val, [<<80>>]). + 'enc_AbandonRequest'(Val, TagIn) -> -encode_integer(Val, TagIn). + encode_integer(Val, TagIn). 'dec_AbandonRequest'(Tlv) -> - 'dec_AbandonRequest'(Tlv, [65552]). + 'dec_AbandonRequest'(Tlv, [65552]). + 'dec_AbandonRequest'(Tlv, TagIn) -> -decode_integer(Tlv,{0,2147483647},TagIn). - + decode_integer(Tlv, {0, 2147483647}, TagIn). %%================================ @@ -2781,58 +2768,59 @@ decode_integer(Tlv,{0,2147483647},TagIn). 'enc_ExtendedRequest'(Val) -> 'enc_ExtendedRequest'(Val, [<<119>>]). + 'enc_ExtendedRequest'(Val, TagIn) -> -{_,Cindex1, Cindex2} = Val, + {_, Cindex1, Cindex2} = Val, -%%------------------------------------------------- -%% attribute requestName(1) with type OCTET STRING -%%------------------------------------------------- - {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<128>>]), + %%------------------------------------------------- + %% attribute requestName(1) with type OCTET STRING + %%------------------------------------------------- + {EncBytes1, EncLen1} = encode_restricted_string(Cindex1, [<<128>>]), -%%------------------------------------------------- -%% attribute requestValue(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes2,EncLen2} = case Cindex2 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex2, [<<129>>]) - end, + %%------------------------------------------------- + %% attribute requestValue(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes2, EncLen2} = case Cindex2 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex2, [<<129>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2], -LenSoFar = EncLen1 + EncLen2, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2], + LenSoFar = EncLen1 + EncLen2, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ExtendedRequest'(Tlv) -> - 'dec_ExtendedRequest'(Tlv, [65559]). + 'dec_ExtendedRequest'(Tlv, [65559]). + 'dec_ExtendedRequest'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute requestName(1) with type OCTET STRING -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_restricted_string(V1,[131072]), + %%------------------------------------------------- + %% attribute requestName(1) with type OCTET STRING + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_restricted_string(V1, [131072]), -%%------------------------------------------------- -%% attribute requestValue(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term2,Tlv3} = case Tlv2 of -[{131073,V2}|TempTlv3] -> - {decode_restricted_string(V2,[]), TempTlv3}; - _ -> - { asn1_NOVALUE, Tlv2} -end, - -case Tlv3 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed -end, - {'ExtendedRequest', Term1, Term2}. + %%------------------------------------------------- + %% attribute requestValue(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term2, Tlv3} = case Tlv2 of + [{131073, V2} | TempTlv3] -> + {decode_restricted_string(V2, []), TempTlv3}; + _ -> + {asn1_NOVALUE, Tlv2} + end, + case Tlv3 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv3}}}) % extra fields not allowed + end, + {'ExtendedRequest', Term1, Term2}. %%================================ @@ -2841,159 +2829,160 @@ end, 'enc_ExtendedResponse'(Val) -> 'enc_ExtendedResponse'(Val, [<<120>>]). + 'enc_ExtendedResponse'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3, Cindex4, Cindex5, Cindex6} = Val, + {_, Cindex1, Cindex2, Cindex3, Cindex4, Cindex5, Cindex6} = Val, -%%------------------------------------------------- -%% attribute resultCode(1) with type ENUMERATED -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of -success -> encode_enumerated(0, [<<10>>]); -operationsError -> encode_enumerated(1, [<<10>>]); -protocolError -> encode_enumerated(2, [<<10>>]); -timeLimitExceeded -> encode_enumerated(3, [<<10>>]); -sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); -compareFalse -> encode_enumerated(5, [<<10>>]); -compareTrue -> encode_enumerated(6, [<<10>>]); -authMethodNotSupported -> encode_enumerated(7, [<<10>>]); -strongAuthRequired -> encode_enumerated(8, [<<10>>]); -referral -> encode_enumerated(10, [<<10>>]); -adminLimitExceeded -> encode_enumerated(11, [<<10>>]); -unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); -confidentialityRequired -> encode_enumerated(13, [<<10>>]); -saslBindInProgress -> encode_enumerated(14, [<<10>>]); -noSuchAttribute -> encode_enumerated(16, [<<10>>]); -undefinedAttributeType -> encode_enumerated(17, [<<10>>]); -inappropriateMatching -> encode_enumerated(18, [<<10>>]); -constraintViolation -> encode_enumerated(19, [<<10>>]); -attributeOrValueExists -> encode_enumerated(20, [<<10>>]); -invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); -noSuchObject -> encode_enumerated(32, [<<10>>]); -aliasProblem -> encode_enumerated(33, [<<10>>]); -invalidDNSyntax -> encode_enumerated(34, [<<10>>]); -aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); -inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); -invalidCredentials -> encode_enumerated(49, [<<10>>]); -insufficientAccessRights -> encode_enumerated(50, [<<10>>]); -busy -> encode_enumerated(51, [<<10>>]); -unavailable -> encode_enumerated(52, [<<10>>]); -unwillingToPerform -> encode_enumerated(53, [<<10>>]); -loopDetect -> encode_enumerated(54, [<<10>>]); -namingViolation -> encode_enumerated(64, [<<10>>]); -objectClassViolation -> encode_enumerated(65, [<<10>>]); -notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); -notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); -entryAlreadyExists -> encode_enumerated(68, [<<10>>]); -objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); -affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); -other -> encode_enumerated(80, [<<10>>]); -Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) -end, + %%------------------------------------------------- + %% attribute resultCode(1) with type ENUMERATED + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + success -> encode_enumerated(0, [<<10>>]); + operationsError -> encode_enumerated(1, [<<10>>]); + protocolError -> encode_enumerated(2, [<<10>>]); + timeLimitExceeded -> encode_enumerated(3, [<<10>>]); + sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); + compareFalse -> encode_enumerated(5, [<<10>>]); + compareTrue -> encode_enumerated(6, [<<10>>]); + authMethodNotSupported -> encode_enumerated(7, [<<10>>]); + strongAuthRequired -> encode_enumerated(8, [<<10>>]); + referral -> encode_enumerated(10, [<<10>>]); + adminLimitExceeded -> encode_enumerated(11, [<<10>>]); + unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); + confidentialityRequired -> encode_enumerated(13, [<<10>>]); + saslBindInProgress -> encode_enumerated(14, [<<10>>]); + noSuchAttribute -> encode_enumerated(16, [<<10>>]); + undefinedAttributeType -> encode_enumerated(17, [<<10>>]); + inappropriateMatching -> encode_enumerated(18, [<<10>>]); + constraintViolation -> encode_enumerated(19, [<<10>>]); + attributeOrValueExists -> encode_enumerated(20, [<<10>>]); + invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); + noSuchObject -> encode_enumerated(32, [<<10>>]); + aliasProblem -> encode_enumerated(33, [<<10>>]); + invalidDNSyntax -> encode_enumerated(34, [<<10>>]); + aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); + inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); + invalidCredentials -> encode_enumerated(49, [<<10>>]); + insufficientAccessRights -> encode_enumerated(50, [<<10>>]); + busy -> encode_enumerated(51, [<<10>>]); + unavailable -> encode_enumerated(52, [<<10>>]); + unwillingToPerform -> encode_enumerated(53, [<<10>>]); + loopDetect -> encode_enumerated(54, [<<10>>]); + namingViolation -> encode_enumerated(64, [<<10>>]); + objectClassViolation -> encode_enumerated(65, [<<10>>]); + notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); + notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); + entryAlreadyExists -> encode_enumerated(68, [<<10>>]); + objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); + affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); + other -> encode_enumerated(80, [<<10>>]); + Enumval1 -> exit({error, {asn1, {enumerated_not_in_range, Enumval1}}}) + end, -%%------------------------------------------------- -%% attribute matchedDN(2) with type OCTET STRING -%%------------------------------------------------- - {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), + %%------------------------------------------------- + %% attribute matchedDN(2) with type OCTET STRING + %%------------------------------------------------- + {EncBytes2, EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), -%%------------------------------------------------- -%% attribute errorMessage(3) with type OCTET STRING -%%------------------------------------------------- - {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), + %%------------------------------------------------- + %% attribute errorMessage(3) with type OCTET STRING + %%------------------------------------------------- + {EncBytes3, EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), -%%------------------------------------------------- -%% attribute referral(4) External ELDAPv3:Referral OPTIONAL -%%------------------------------------------------- - {EncBytes4,EncLen4} = case Cindex4 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - 'enc_Referral'(Cindex4, [<<163>>]) - end, + %%------------------------------------------------- + %% attribute referral(4) External ELDAPv3:Referral OPTIONAL + %%------------------------------------------------- + {EncBytes4, EncLen4} = case Cindex4 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + 'enc_Referral'(Cindex4, [<<163>>]) + end, -%%------------------------------------------------- -%% attribute responseName(5) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes5,EncLen5} = case Cindex5 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex5, [<<138>>]) - end, + %%------------------------------------------------- + %% attribute responseName(5) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes5, EncLen5} = case Cindex5 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex5, [<<138>>]) + end, -%%------------------------------------------------- -%% attribute response(6) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes6,EncLen6} = case Cindex6 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex6, [<<139>>]) - end, + %%------------------------------------------------- + %% attribute response(6) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes6, EncLen6} = case Cindex6 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex6, [<<139>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5, EncBytes6], -LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5 + EncLen6, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5, EncBytes6], + LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5 + EncLen6, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ExtendedResponse'(Tlv) -> - 'dec_ExtendedResponse'(Tlv, [65560]). + 'dec_ExtendedResponse'(Tlv, [65560]). + 'dec_ExtendedResponse'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute resultCode(1) with type ENUMERATED -%%------------------------------------------------- -[V1|Tlv2] = Tlv1, -Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), + %%------------------------------------------------- + %% attribute resultCode(1) with type ENUMERATED + %%------------------------------------------------- + [V1 | Tlv2] = Tlv1, + Term1 = decode_enumerated(V1, [{success, 0}, {operationsError, 1}, {protocolError, 2}, {timeLimitExceeded, 3}, {sizeLimitExceeded, 4}, {compareFalse, 5}, {compareTrue, 6}, {authMethodNotSupported, 7}, {strongAuthRequired, 8}, {referral, 10}, {adminLimitExceeded, 11}, {unavailableCriticalExtension, 12}, {confidentialityRequired, 13}, {saslBindInProgress, 14}, {noSuchAttribute, 16}, {undefinedAttributeType, 17}, {inappropriateMatching, 18}, {constraintViolation, 19}, {attributeOrValueExists, 20}, {invalidAttributeSyntax, 21}, {noSuchObject, 32}, {aliasProblem, 33}, {invalidDNSyntax, 34}, {aliasDereferencingProblem, 36}, {inappropriateAuthentication, 48}, {invalidCredentials, 49}, {insufficientAccessRights, 50}, {busy, 51}, {unavailable, 52}, {unwillingToPerform, 53}, {loopDetect, 54}, {namingViolation, 64}, {objectClassViolation, 65}, {notAllowedOnNonLeaf, 66}, {notAllowedOnRDN, 67}, {entryAlreadyExists, 68}, {objectClassModsProhibited, 69}, {affectsMultipleDSAs, 71}, {other, 80}], [10]), -%%------------------------------------------------- -%% attribute matchedDN(2) with type OCTET STRING -%%------------------------------------------------- -[V2|Tlv3] = Tlv2, -Term2 = decode_restricted_string(V2,[4]), + %%------------------------------------------------- + %% attribute matchedDN(2) with type OCTET STRING + %%------------------------------------------------- + [V2 | Tlv3] = Tlv2, + Term2 = decode_restricted_string(V2, [4]), -%%------------------------------------------------- -%% attribute errorMessage(3) with type OCTET STRING -%%------------------------------------------------- -[V3|Tlv4] = Tlv3, -Term3 = decode_restricted_string(V3,[4]), + %%------------------------------------------------- + %% attribute errorMessage(3) with type OCTET STRING + %%------------------------------------------------- + [V3 | Tlv4] = Tlv3, + Term3 = decode_restricted_string(V3, [4]), -%%------------------------------------------------- -%% attribute referral(4) External ELDAPv3:Referral OPTIONAL -%%------------------------------------------------- -{Term4,Tlv5} = case Tlv4 of -[{131075,V4}|TempTlv5] -> - {'dec_Referral'(V4, []), TempTlv5}; - _ -> - { asn1_NOVALUE, Tlv4} -end, + %%------------------------------------------------- + %% attribute referral(4) External ELDAPv3:Referral OPTIONAL + %%------------------------------------------------- + {Term4, Tlv5} = case Tlv4 of + [{131075, V4} | TempTlv5] -> + {'dec_Referral'(V4, []), TempTlv5}; + _ -> + {asn1_NOVALUE, Tlv4} + end, -%%------------------------------------------------- -%% attribute responseName(5) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term5,Tlv6} = case Tlv5 of -[{131082,V5}|TempTlv6] -> - {decode_restricted_string(V5,[]), TempTlv6}; - _ -> - { asn1_NOVALUE, Tlv5} -end, + %%------------------------------------------------- + %% attribute responseName(5) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term5, Tlv6} = case Tlv5 of + [{131082, V5} | TempTlv6] -> + {decode_restricted_string(V5, []), TempTlv6}; + _ -> + {asn1_NOVALUE, Tlv5} + end, -%%------------------------------------------------- -%% attribute response(6) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term6,Tlv7} = case Tlv6 of -[{131083,V6}|TempTlv7] -> - {decode_restricted_string(V6,[]), TempTlv7}; - _ -> - { asn1_NOVALUE, Tlv6} -end, - -case Tlv7 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv7}}}) % extra fields not allowed -end, - {'ExtendedResponse', Term1, Term2, Term3, Term4, Term5, Term6}. + %%------------------------------------------------- + %% attribute response(6) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term6, Tlv7} = case Tlv6 of + [{131083, V6} | TempTlv7] -> + {decode_restricted_string(V6, []), TempTlv7}; + _ -> + {asn1_NOVALUE, Tlv6} + end, + case Tlv7 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv7}}}) % extra fields not allowed + end, + {'ExtendedResponse', Term1, Term2, Term3, Term4, Term5, Term6}. %%================================ @@ -3002,85 +2991,86 @@ end, 'enc_PasswdModifyRequestValue'(Val) -> 'enc_PasswdModifyRequestValue'(Val, [<<48>>]). + 'enc_PasswdModifyRequestValue'(Val, TagIn) -> -{_,Cindex1, Cindex2, Cindex3} = Val, + {_, Cindex1, Cindex2, Cindex3} = Val, -%%------------------------------------------------- -%% attribute userIdentity(1) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex1, [<<128>>]) - end, + %%------------------------------------------------- + %% attribute userIdentity(1) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex1, [<<128>>]) + end, -%%------------------------------------------------- -%% attribute oldPasswd(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes2,EncLen2} = case Cindex2 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex2, [<<129>>]) - end, + %%------------------------------------------------- + %% attribute oldPasswd(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes2, EncLen2} = case Cindex2 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex2, [<<129>>]) + end, -%%------------------------------------------------- -%% attribute newPasswd(3) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes3,EncLen3} = case Cindex3 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex3, [<<130>>]) - end, + %%------------------------------------------------- + %% attribute newPasswd(3) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes3, EncLen3} = case Cindex3 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex3, [<<130>>]) + end, - BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], -LenSoFar = EncLen1 + EncLen2 + EncLen3, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], + LenSoFar = EncLen1 + EncLen2 + EncLen3, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PasswdModifyRequestValue'(Tlv) -> - 'dec_PasswdModifyRequestValue'(Tlv, [16]). + 'dec_PasswdModifyRequestValue'(Tlv, [16]). + 'dec_PasswdModifyRequestValue'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute userIdentity(1) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term1,Tlv2} = case Tlv1 of -[{131072,V1}|TempTlv2] -> - {decode_restricted_string(V1,[]), TempTlv2}; - _ -> - { asn1_NOVALUE, Tlv1} -end, + %%------------------------------------------------- + %% attribute userIdentity(1) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term1, Tlv2} = case Tlv1 of + [{131072, V1} | TempTlv2] -> + {decode_restricted_string(V1, []), TempTlv2}; + _ -> + {asn1_NOVALUE, Tlv1} + end, -%%------------------------------------------------- -%% attribute oldPasswd(2) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term2,Tlv3} = case Tlv2 of -[{131073,V2}|TempTlv3] -> - {decode_restricted_string(V2,[]), TempTlv3}; - _ -> - { asn1_NOVALUE, Tlv2} -end, + %%------------------------------------------------- + %% attribute oldPasswd(2) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term2, Tlv3} = case Tlv2 of + [{131073, V2} | TempTlv3] -> + {decode_restricted_string(V2, []), TempTlv3}; + _ -> + {asn1_NOVALUE, Tlv2} + end, -%%------------------------------------------------- -%% attribute newPasswd(3) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term3,Tlv4} = case Tlv3 of -[{131074,V3}|TempTlv4] -> - {decode_restricted_string(V3,[]), TempTlv4}; - _ -> - { asn1_NOVALUE, Tlv3} -end, - -case Tlv4 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed -end, - {'PasswdModifyRequestValue', Term1, Term2, Term3}. + %%------------------------------------------------- + %% attribute newPasswd(3) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term3, Tlv4} = case Tlv3 of + [{131074, V3} | TempTlv4] -> + {decode_restricted_string(V3, []), TempTlv4}; + _ -> + {asn1_NOVALUE, Tlv3} + end, + case Tlv4 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv4}}}) % extra fields not allowed + end, + {'PasswdModifyRequestValue', Term1, Term2, Term3}. %%================================ @@ -3089,77 +3079,86 @@ end, 'enc_PasswdModifyResponseValue'(Val) -> 'enc_PasswdModifyResponseValue'(Val, [<<48>>]). + 'enc_PasswdModifyResponseValue'(Val, TagIn) -> -{_,Cindex1} = Val, + {_, Cindex1} = Val, -%%------------------------------------------------- -%% attribute genPasswd(1) with type OCTET STRING OPTIONAL -%%------------------------------------------------- - {EncBytes1,EncLen1} = case Cindex1 of - asn1_NOVALUE -> {<<>>,0}; - _ -> - encode_restricted_string(Cindex1, [<<128>>]) - end, + %%------------------------------------------------- + %% attribute genPasswd(1) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {EncBytes1, EncLen1} = case Cindex1 of + asn1_NOVALUE -> {<<>>, 0}; + _ -> + encode_restricted_string(Cindex1, [<<128>>]) + end, - BytesSoFar = [EncBytes1], -LenSoFar = EncLen1, -encode_tags(TagIn, BytesSoFar, LenSoFar). + BytesSoFar = [EncBytes1], + LenSoFar = EncLen1, + encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PasswdModifyResponseValue'(Tlv) -> - 'dec_PasswdModifyResponseValue'(Tlv, [16]). + 'dec_PasswdModifyResponseValue'(Tlv, [16]). + 'dec_PasswdModifyResponseValue'(Tlv, TagIn) -> - %%------------------------------------------------- - %% decode tag and length - %%------------------------------------------------- -Tlv1 = match_tags(Tlv, TagIn), + %%------------------------------------------------- + %% decode tag and length + %%------------------------------------------------- + Tlv1 = match_tags(Tlv, TagIn), -%%------------------------------------------------- -%% attribute genPasswd(1) with type OCTET STRING OPTIONAL -%%------------------------------------------------- -{Term1,Tlv2} = case Tlv1 of -[{131072,V1}|TempTlv2] -> - {decode_restricted_string(V1,[]), TempTlv2}; - _ -> - { asn1_NOVALUE, Tlv1} -end, + %%------------------------------------------------- + %% attribute genPasswd(1) with type OCTET STRING OPTIONAL + %%------------------------------------------------- + {Term1, Tlv2} = case Tlv1 of + [{131072, V1} | TempTlv2] -> + {decode_restricted_string(V1, []), TempTlv2}; + _ -> + {asn1_NOVALUE, Tlv1} + end, + + case Tlv2 of + [] -> true; _ -> exit({error, {asn1, {unexpected, Tlv2}}}) % extra fields not allowed + end, + {'PasswdModifyResponseValue', Term1}. -case Tlv2 of -[] -> true;_ -> exit({error,{asn1, {unexpected,Tlv2}}}) % extra fields not allowed -end, - {'PasswdModifyResponseValue', Term1}. 'maxInt'() -> -2147483647. + 2147483647. + 'passwdModifyOID'() -> -[49,46,51,46,54,46,49,46,52,46,49,46,52,50,48,51,46,49,46,49,49,46,49]. + [49, 46, 51, 46, 54, 46, 49, 46, 52, 46, 49, 46, 52, 50, 48, 51, 46, 49, 46, 49, 49, 46, 49]. %%% %%% Run-time functions. %%% + ber_decode_nif(B) -> asn1rt_nif:decode_ber_tlv(B). + collect_parts(TlvList) -> collect_parts(TlvList, []). -collect_parts([{_,L}|Rest], Acc) when is_list(L) -> - collect_parts(Rest, [collect_parts(L)|Acc]); -collect_parts([{3,<>}|Rest], _Acc) -> + +collect_parts([{_, L} | Rest], Acc) when is_list(L) -> + collect_parts(Rest, [collect_parts(L) | Acc]); +collect_parts([{3, <>} | Rest], _Acc) -> collect_parts_bit(Rest, [Bits], Unused); -collect_parts([{_T,V}|Rest], Acc) -> - collect_parts(Rest, [V|Acc]); +collect_parts([{_T, V} | Rest], Acc) -> + collect_parts(Rest, [V | Acc]); collect_parts([], Acc) -> list_to_binary(lists:reverse(Acc)). -collect_parts_bit([{3,<>}|Rest], Acc, Uacc) -> - collect_parts_bit(Rest, [Bits|Acc], Unused + Uacc); + +collect_parts_bit([{3, <>} | Rest], Acc, Uacc) -> + collect_parts_bit(Rest, [Bits | Acc], Unused + Uacc); collect_parts_bit([], Acc, Uacc) -> - list_to_binary([Uacc|lists:reverse(Acc)]). + list_to_binary([Uacc | lists:reverse(Acc)]). + decode_boolean(Tlv, TagIn) -> Val = match_tags(Tlv, TagIn), @@ -3169,25 +3168,28 @@ decode_boolean(Tlv, TagIn) -> <<_:8>> -> true; _ -> - exit({error,{asn1,{decode_boolean,Val}}}) + exit({error, {asn1, {decode_boolean, Val}}}) end. + decode_enumerated(Tlv, NamedNumberList, Tags) -> Buffer = match_tags(Tlv, Tags), decode_enumerated_notag(Buffer, NamedNumberList, Tags). + decode_enumerated1(Val, NamedNumberList) -> case lists:keyfind(Val, 2, NamedNumberList) of - {NamedVal,_} -> + {NamedVal, _} -> NamedVal; _ -> - {asn1_enum,Val} + {asn1_enum, Val} end. -decode_enumerated_notag(Buffer, {NamedNumberList,ExtList}, _Tags) -> + +decode_enumerated_notag(Buffer, {NamedNumberList, ExtList}, _Tags) -> IVal = decode_integer(Buffer), case decode_enumerated1(IVal, NamedNumberList) of - {asn1_enum,IVal} -> + {asn1_enum, IVal} -> decode_enumerated1(IVal, ExtList); EVal -> EVal @@ -3195,45 +3197,52 @@ decode_enumerated_notag(Buffer, {NamedNumberList,ExtList}, _Tags) -> decode_enumerated_notag(Buffer, NNList, _Tags) -> IVal = decode_integer(Buffer), case decode_enumerated1(IVal, NNList) of - {asn1_enum,_} -> - exit({error,{asn1,{illegal_enumerated,IVal}}}); + {asn1_enum, _} -> + exit({error, {asn1, {illegal_enumerated, IVal}}}); EVal -> EVal end. + decode_integer(Bin) -> Len = byte_size(Bin), <> = Bin, Int. + decode_integer(Tlv, Range, TagIn) -> V = match_tags(Tlv, TagIn), Int = decode_integer(V), range_check_integer(Int, Range). + decode_null(Tlv, Tags) -> Val = match_tags(Tlv, Tags), case Val of <<>> -> 'NULL'; _ -> - exit({error,{asn1,{decode_null,Val}}}) + exit({error, {asn1, {decode_null, Val}}}) end. + decode_restricted_string(Tlv, TagsIn) -> Bin = match_and_collect(Tlv, TagsIn), Bin. + encode_boolean(true, TagIn) -> encode_tags(TagIn, [255], 1); encode_boolean(false, TagIn) -> encode_tags(TagIn, [0], 1); encode_boolean(X, _) -> - exit({error,{asn1,{encode_boolean,X}}}). + exit({error, {asn1, {encode_boolean, X}}}). + encode_enumerated(Val, TagIn) when is_integer(Val) -> encode_tags(TagIn, encode_integer(Val)). + encode_integer(Val) -> Bytes = if @@ -3242,96 +3251,109 @@ encode_integer(Val) -> true -> encode_integer_neg(Val, []) end, - {Bytes,length(Bytes)}. + {Bytes, length(Bytes)}. + encode_integer(Val, Tag) when is_integer(Val) -> encode_tags(Tag, encode_integer(Val)); encode_integer(Val, _Tag) -> - exit({error,{asn1,{encode_integer,Val}}}). + exit({error, {asn1, {encode_integer, Val}}}). -encode_integer_neg(- 1, [B1|_T] = L) when B1 > 127 -> + +encode_integer_neg(-1, [B1 | _T] = L) when B1 > 127 -> L; encode_integer_neg(N, Acc) -> - encode_integer_neg(N bsr 8, [N band 255|Acc]). + encode_integer_neg(N bsr 8, [N band 255 | Acc]). -encode_integer_pos(0, [B|_Acc] = L) when B < 128 -> + +encode_integer_pos(0, [B | _Acc] = L) when B < 128 -> L; encode_integer_pos(N, Acc) -> - encode_integer_pos(N bsr 8, [N band 255|Acc]). + encode_integer_pos(N bsr 8, [N band 255 | Acc]). + encode_length(L) when L =< 127 -> - {[L],1}; + {[L], 1}; encode_length(L) -> Oct = minimum_octets(L), Len = length(Oct), if Len =< 126 -> - {[128 bor Len|Oct],Len + 1}; + {[128 bor Len | Oct], Len + 1}; true -> - exit({error,{asn1,too_long_length_oct,Len}}) + exit({error, {asn1, too_long_length_oct, Len}}) end. + encode_null(_Val, TagIn) -> encode_tags(TagIn, [], 0). + encode_restricted_string(OctetList, TagIn) when is_binary(OctetList) -> encode_tags(TagIn, OctetList, byte_size(OctetList)); encode_restricted_string(OctetList, TagIn) when is_list(OctetList) -> encode_tags(TagIn, OctetList, length(OctetList)). -encode_tags(TagIn, {BytesSoFar,LenSoFar}) -> + +encode_tags(TagIn, {BytesSoFar, LenSoFar}) -> encode_tags(TagIn, BytesSoFar, LenSoFar). -encode_tags([Tag|Trest], BytesSoFar, LenSoFar) -> - {Bytes2,L2} = encode_length(LenSoFar), + +encode_tags([Tag | Trest], BytesSoFar, LenSoFar) -> + {Bytes2, L2} = encode_length(LenSoFar), encode_tags(Trest, - [Tag,Bytes2|BytesSoFar], + [Tag, Bytes2 | BytesSoFar], LenSoFar + byte_size(Tag) + L2); encode_tags([], BytesSoFar, LenSoFar) -> - {BytesSoFar,LenSoFar}. + {BytesSoFar, LenSoFar}. + match_and_collect(Tlv, TagsIn) -> Val = match_tags(Tlv, TagsIn), case Val of - [_|_] = PartList -> + [_ | _] = PartList -> collect_parts(PartList); Bin when is_binary(Bin) -> Bin end. -match_tags({T,V}, [T]) -> + +match_tags({T, V}, [T]) -> V; -match_tags({T,V}, [T|Tt]) -> +match_tags({T, V}, [T | Tt]) -> match_tags(V, Tt); -match_tags([{T,V}], [T|Tt]) -> +match_tags([{T, V}], [T | Tt]) -> match_tags(V, Tt); -match_tags([{T,_V}|_] = Vlist, [T]) -> +match_tags([{T, _V} | _] = Vlist, [T]) -> Vlist; match_tags(Tlv, []) -> Tlv; -match_tags({Tag,_V} = Tlv, [T|_Tt]) -> - exit({error,{asn1,{wrong_tag,{{expected,T},{got,Tag,Tlv}}}}}). +match_tags({Tag, _V} = Tlv, [T | _Tt]) -> + exit({error, {asn1, {wrong_tag, {{expected, T}, {got, Tag, Tlv}}}}}). + minimum_octets(0, Acc) -> Acc; minimum_octets(Val, Acc) -> - minimum_octets(Val bsr 8, [Val band 255|Acc]). + minimum_octets(Val bsr 8, [Val band 255 | Acc]). + minimum_octets(Val) -> minimum_octets(Val, []). + range_check_integer(Int, Range) -> case Range of [] -> Int; - {Lb,Ub} when Int >= Lb, Ub >= Int -> + {Lb, Ub} when Int >= Lb, Ub >= Int -> Int; - {_,_} -> - exit({error,{asn1,{integer_range,Range,Int}}}); + {_, _} -> + exit({error, {asn1, {integer_range, Range, Int}}}); Int -> Int; SingleValue when is_integer(SingleValue) -> - exit({error,{asn1,{integer_range,Range,Int}}}); + exit({error, {asn1, {integer_range, Range, Int}}}); _ -> Int end. diff --git a/src/acl.erl b/src/acl.erl index eaa0aa50f..537f45761 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -27,8 +27,12 @@ -export([validator/1, validators/0]). -export([loaded_shared_roster_module/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). @@ -37,48 +41,55 @@ -type ip_mask() :: {inet:ip4_address(), 0..32} | {inet:ip6_address(), 0..128}. -type access_rule() :: {acl, atom()} | acl_rule(). -type acl_rule() :: {user, {binary(), binary()} | binary()} | - {server, binary()} | - {resource, binary()} | - {user_regexp, {misc:re_mp(), binary()} | misc:re_mp()} | - {server_regexp, misc:re_mp()} | - {resource_regexp, misc:re_mp()} | - {node_regexp, {misc:re_mp(), misc:re_mp()}} | - {user_glob, {misc:re_mp(), binary()} | misc:re_mp()} | - {server_glob, misc:re_mp()} | - {resource_glob, misc:re_mp()} | - {node_glob, {misc:re_mp(), misc:re_mp()}} | - {shared_group, {binary(), binary()} | binary()} | - {ip, ip_mask()}. + {server, binary()} | + {resource, binary()} | + {user_regexp, {misc:re_mp(), binary()} | misc:re_mp()} | + {server_regexp, misc:re_mp()} | + {resource_regexp, misc:re_mp()} | + {node_regexp, {misc:re_mp(), misc:re_mp()}} | + {user_glob, {misc:re_mp(), binary()} | misc:re_mp()} | + {server_glob, misc:re_mp()} | + {resource_glob, misc:re_mp()} | + {node_glob, {misc:re_mp(), misc:re_mp()}} | + {shared_group, {binary(), binary()} | binary()} | + {ip, ip_mask()}. -type access() :: [{action(), [access_rule()]}]. -type acl() :: atom() | access(). --type match() :: #{ip => inet:ip_address(), - usr => jid:ljid(), - atom() => term()}. +-type match() :: #{ + ip => inet:ip_address(), + usr => jid:ljid(), + atom() => term() + }. -export_type([acl/0, acl_rule/0, access/0, access_rule/0, match/0]). + %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --spec match_rule(global | binary(), atom() | access(), + +-spec match_rule(global | binary(), + atom() | access(), jid:jid() | jid:ljid() | inet:ip_address() | match()) -> action(). match_rule(_, all, _) -> allow; match_rule(_, none, _) -> deny; match_rule(Host, Access, Match) when is_map(Match) -> - Rules = if is_atom(Access) -> read_access(Access, Host); - true -> Access - end, + Rules = if + is_atom(Access) -> read_access(Access, Host); + true -> Access + end, match_rules(Host, Rules, Match, deny); match_rule(Host, Access, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 -> match_rule(Host, Access, #{ip => IP}); match_rule(Host, Access, JID) -> match_rule(Host, Access, #{usr => jid:tolower(JID)}). + -spec match_acl(global | binary(), access_rule(), match()) -> boolean(). match_acl(_Host, {acl, all}, _) -> true; @@ -87,8 +98,9 @@ match_acl(_Host, {acl, none}, _) -> match_acl(Host, {acl, ACLName}, Match) -> lists:any( fun(ACL) -> - match_acl(Host, ACL, Match) - end, read_acl(ACLName, Host)); + match_acl(Host, ACL, Match) + end, + read_acl(ACLName, Host)); match_acl(_Host, {ip, {Net, Mask}}, #{ip := {IP, _Port}}) -> misc:match_ip_mask(IP, Net, Mask); match_acl(_Host, {ip, {Net, Mask}}, #{ip := IP}) -> @@ -103,8 +115,8 @@ match_acl(_Host, {resource, R}, #{usr := {_, _, R}}) -> true; match_acl(_Host, {shared_group, {G, H}}, #{usr := {U, S, _}}) -> case loaded_shared_roster_module(H) of - undefined -> false; - Mod -> Mod:is_user_in_group({U, S}, G, H) + undefined -> false; + Mod -> Mod:is_user_in_group({U, S}, G, H) end; match_acl(Host, {shared_group, G}, #{usr := {_, S, _}} = Map) -> match_acl(Host, {shared_group, {G, S}}, Map); @@ -131,30 +143,35 @@ match_acl(_Host, {node_glob, {UR, SR}}, #{usr := {U, S, _}}) -> match_acl(_, _, _) -> false. + -spec match_rules(global | binary(), [{T, [access_rule()]}], match(), T) -> T. match_rules(Host, [{Return, Rules} | Rest], Match, Default) -> case match_acls(Host, Rules, Match) of - false -> - match_rules(Host, Rest, Match, Default); - true -> - Return + false -> + match_rules(Host, Rest, Match, Default); + true -> + Return end; match_rules(_Host, [], _Match, Default) -> Default. + -spec match_acls(global | binary(), [access_rule()], match()) -> boolean(). match_acls(_Host, [], _Match) -> false; match_acls(Host, Rules, Match) -> lists:all( fun(Rule) -> - match_acl(Host, Rule, Match) - end, Rules). + match_acl(Host, Rule, Match) + end, + Rules). + -spec reload_from_config() -> ok. reload_from_config() -> gen_server:call(?MODULE, reload_from_config, timer:minutes(1)). + -spec validator(access_rules | acl) -> econf:validator(). validator(access_rules) -> econf:options( @@ -165,6 +182,7 @@ validator(acl) -> #{'_' => acl_validator()}, [{disallowed, [all, none]}, unique]). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== @@ -177,6 +195,7 @@ init([]) -> ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20), {ok, #{hosts => Hosts}}. + -spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}. handle_call(reload_from_config, _, State) -> NewHosts = ejabberd_option:hosts(), @@ -186,24 +205,29 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -spec terminate(any(), state()) -> ok. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20). + -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -217,18 +241,21 @@ load_from_config(NewHosts) -> load_tab(access, NewHosts, fun ejabberd_option:access_rules/1), ?DEBUG("Access rules loaded successfully", []). + -spec create_tab(atom()) -> atom(). create_tab(Tab) -> _ = mnesia:delete_table(Tab), ets:new(Tab, [named_table, set, {read_concurrency, true}]). + -spec load_tab(atom(), [binary()], fun((global | binary()) -> {atom(), list()})) -> ok. load_tab(Tab, Hosts, Fun) -> Old = ets:tab2list(Tab), New = lists:flatmap( fun(Host) -> - [{{Name, Host}, List} || {Name, List} <- Fun(Host)] - end, [global|Hosts]), + [ {{Name, Host}, List} || {Name, List} <- Fun(Host) ] + end, + [global | Hosts]), ets:insert(Tab, New), lists:foreach( fun({Key, _}) -> @@ -236,15 +263,18 @@ load_tab(Tab, Hosts, Fun) -> false -> ets:delete(Tab, Key); true -> ok end - end, Old). + end, + Old). + -spec read_access(atom(), global | binary()) -> access(). read_access(Name, Host) -> case ets:lookup(access, {Name, Host}) of - [{_, Access}] -> Access; - [] -> [] + [{_, Access}] -> Access; + [] -> [] end. + -spec read_acl(atom(), global | binary()) -> [acl_rule()]. read_acl(Name, Host) -> case ets:lookup(acl, {Name, Host}) of @@ -252,11 +282,13 @@ read_acl(Name, Host) -> [] -> [] end. + %%%=================================================================== %%% Validators %%%=================================================================== validators() -> - #{ip => econf:list_or_single(econf:ip_mask()), + #{ + ip => econf:list_or_single(econf:ip_mask()), user => user_validator(econf:user(), econf:domain()), user_regexp => user_validator(econf:re([unicode]), econf:domain()), user_glob => user_validator(econf:glob([unicode]), econf:domain()), @@ -269,82 +301,96 @@ validators() -> node_regexp => node_validator(econf:re([unicode]), econf:re([unicode])), node_glob => node_validator(econf:glob([unicode]), econf:glob([unicode])), shared_group => user_validator(econf:binary(), econf:domain()), - acl => econf:atom()}. + acl => econf:atom() + }. + rule_validator() -> rule_validator(validators()). + rule_validator(RVs) -> econf:and_then( econf:non_empty(econf:options(RVs, [])), fun(Rules) -> - lists:flatmap( - fun({Type, Rs}) when is_list(Rs) -> - [{Type, R} || R <- Rs]; - (Other) -> - [Other] - end, Rules) + lists:flatmap( + fun({Type, Rs}) when is_list(Rs) -> + [ {Type, R} || R <- Rs ]; + (Other) -> + [Other] + end, + Rules) end). + access_validator() -> econf:and_then( fun(L) when is_list(L) -> - lists:map( - fun({K, V}) -> {(econf:atom())(K), V}; - (A) -> {acl, (econf:atom())(A)} - end, lists:flatten(L)); - (A) -> - [{acl, (econf:atom())(A)}] + lists:map( + fun({K, V}) -> {(econf:atom())(K), V}; + (A) -> {acl, (econf:atom())(A)} + end, + lists:flatten(L)); + (A) -> + [{acl, (econf:atom())(A)}] end, rule_validator()). + access_rules_validator() -> econf:and_then( fun(L) when is_list(L) -> - lists:map( - fun({K, V}) -> {(econf:atom())(K), V}; - (A) -> {(econf:atom())(A), [{acl, all}]} - end, lists:flatten(L)); - (Bad) -> - Bad + lists:map( + fun({K, V}) -> {(econf:atom())(K), V}; + (A) -> {(econf:atom())(A), [{acl, all}]} + end, + lists:flatten(L)); + (Bad) -> + Bad end, econf:non_empty( - econf:options( - #{allow => access_validator(), - deny => access_validator()}, - []))). + econf:options( + #{ + allow => access_validator(), + deny => access_validator() + }, + []))). + acl_validator() -> econf:and_then( fun(L) when is_list(L) -> lists:flatten(L); - (Bad) -> Bad + (Bad) -> Bad end, rule_validator(maps:remove(acl, validators()))). + user_validator(UV, SV) -> econf:and_then( econf:list_or_single( - fun({U, S}) -> - {UV(U), SV(S)}; - (M) when is_list(M) -> - (econf:map(UV, SV))(M); - (Val) -> - US = (econf:binary())(Val), - case binary:split(US, <<"@">>, [global]) of - [U, S] -> {UV(U), SV(S)}; - [U] -> UV(U); - _ -> econf:fail({bad_user, Val}) - end - end), + fun({U, S}) -> + {UV(U), SV(S)}; + (M) when is_list(M) -> + (econf:map(UV, SV))(M); + (Val) -> + US = (econf:binary())(Val), + case binary:split(US, <<"@">>, [global]) of + [U, S] -> {UV(U), SV(S)}; + [U] -> UV(U); + _ -> econf:fail({bad_user, Val}) + end + end), fun lists:flatten/1). + node_validator(UV, SV) -> econf:and_then( econf:and_then( - econf:list(econf:any()), - fun lists:flatten/1), + econf:list(econf:any()), + fun lists:flatten/1), econf:map(UV, SV)). + %%%=================================================================== %%% Aux %%%=================================================================== @@ -352,15 +398,16 @@ node_validator(UV, SV) -> match_regexp(Data, RegExp) -> re:run(Data, RegExp) /= nomatch. + -spec loaded_shared_roster_module(global | binary()) -> atom(). loaded_shared_roster_module(global) -> loaded_shared_roster_module(ejabberd_config:get_myname()); loaded_shared_roster_module(Host) -> case gen_mod:is_loaded(Host, mod_shared_roster_ldap) of - true -> mod_shared_roster_ldap; - false -> - case gen_mod:is_loaded(Host, mod_shared_roster) of - true -> mod_shared_roster; - false -> undefined - end + true -> mod_shared_roster_ldap; + false -> + case gen_mod:is_loaded(Host, mod_shared_roster) of + true -> mod_shared_roster; + false -> undefined + end end. diff --git a/src/econf.erl b/src/econf.erl index 5bc4f4599..ddb25d083 100644 --- a/src/econf.erl +++ b/src/econf.erl @@ -65,52 +65,62 @@ -export_type([validator/0, validator/1, validators/0]). -export_type([error_reason/0, error_return/0]). + %%%=================================================================== %%% API %%%=================================================================== parse(File, Validators, Options) -> - try yconf:parse(File, Validators, Options) - catch _:{?MODULE, Reason, Ctx} -> - {error, Reason, Ctx} + try + yconf:parse(File, Validators, Options) + catch + _:{?MODULE, Reason, Ctx} -> + {error, Reason, Ctx} end. + validate(Validator, Y) -> - try yconf:validate(Validator, Y) - catch _:{?MODULE, Reason, Ctx} -> - {error, Reason, Ctx} + try + yconf:validate(Validator, Y) + catch + _:{?MODULE, Reason, Ctx} -> + {error, Reason, Ctx} end. + replace_macros(Y) -> yconf:replace_macros(Y). + -spec fail(error_reason()) -> no_return(). fail(Reason) -> yconf:fail(?MODULE, Reason). + format_error({bad_module, Mod}, Ctx) when Ctx == [listen, module]; Ctx == [listen, request_handlers] -> Mods = ejabberd_config:beams(all), format("~ts: unknown ~ts: ~ts. Did you mean ~ts?", - [yconf:format_ctx(Ctx), - format_module_type(Ctx), - format_module(Mod), - format_module(misc:best_match(Mod, Mods))]); + [yconf:format_ctx(Ctx), + format_module_type(Ctx), + format_module(Mod), + format_module(misc:best_match(Mod, Mods))]); format_error({bad_module, Mod}, Ctx) when Ctx == [modules] -> Mods = lists:filter( - fun(M) -> - case atom_to_list(M) of - "mod_" ++ _ -> true; - "Elixir.Mod" ++ _ -> true; - _ -> false - end - end, ejabberd_config:beams(all)), + fun(M) -> + case atom_to_list(M) of + "mod_" ++ _ -> true; + "Elixir.Mod" ++ _ -> true; + _ -> false + end + end, + ejabberd_config:beams(all)), format("~ts: unknown ~ts: ~ts. Did you mean ~ts?", - [yconf:format_ctx(Ctx), - format_module_type(Ctx), - format_module(Mod), - format_module(misc:best_match(Mod, Mods))]); + [yconf:format_ctx(Ctx), + format_module_type(Ctx), + format_module(Mod), + format_module(misc:best_match(Mod, Mods))]); format_error({bad_export, {F, A}, Mod}, Ctx) when Ctx == [listen, module]; Ctx == [listen, request_handlers]; @@ -118,46 +128,47 @@ format_error({bad_export, {F, A}, Mod}, Ctx) Type = format_module_type(Ctx), Slogan = yconf:format_ctx(Ctx), case lists:member(Mod, ejabberd_config:beams(local)) of - true -> - format("~ts: '~ts' is not a ~ts", - [Slogan, format_module(Mod), Type]); - false -> - case lists:member(Mod, ejabberd_config:beams(external)) of - true -> - format("~ts: third-party ~ts '~ts' doesn't export " - "function ~ts/~B. If it's really a ~ts, " - "consider to upgrade it", - [Slogan, Type, format_module(Mod),F, A, Type]); - false -> - format("~ts: '~ts' doesn't match any known ~ts", - [Slogan, format_module(Mod), Type]) - end + true -> + format("~ts: '~ts' is not a ~ts", + [Slogan, format_module(Mod), Type]); + false -> + case lists:member(Mod, ejabberd_config:beams(external)) of + true -> + format("~ts: third-party ~ts '~ts' doesn't export " + "function ~ts/~B. If it's really a ~ts, " + "consider to upgrade it", + [Slogan, Type, format_module(Mod), F, A, Type]); + false -> + format("~ts: '~ts' doesn't match any known ~ts", + [Slogan, format_module(Mod), Type]) + end end; format_error({unknown_option, [], _} = Why, Ctx) -> format("~ts. There are no available options", - [yconf:format_error(Why, Ctx)]); + [yconf:format_error(Why, Ctx)]); format_error({unknown_option, Known, Opt} = Why, Ctx) -> format("~ts. Did you mean ~ts? ~ts", - [yconf:format_error(Why, Ctx), - misc:best_match(Opt, Known), - format_known("Available options", Known)]); + [yconf:format_error(Why, Ctx), + misc:best_match(Opt, Known), + format_known("Available options", Known)]); format_error({bad_enum, Known, Bad} = Why, Ctx) -> format("~ts. Did you mean ~ts? ~ts", - [yconf:format_error(Why, Ctx), - misc:best_match(Bad, Known), - format_known("Possible values", Known)]); + [yconf:format_error(Why, Ctx), + misc:best_match(Bad, Known), + format_known("Possible values", Known)]); format_error({bad_yaml, _, _} = Why, _) -> format_error(Why); format_error(Reason, Ctx) -> yconf:format_ctx(Ctx) ++ ": " ++ format_error(Reason). + format_error({bad_db_type, _, Atom}) -> format("unsupported database: ~ts", [Atom]); format_error({bad_lang, Lang}) -> format("Invalid language tag: ~ts", [Lang]); format_error({bad_pem, Why, Path}) -> format("Failed to read PEM file '~ts': ~ts", - [Path, pkix:format_error(Why)]); + [Path, pkix:format_error(Why)]); format_error({bad_cert, Why, Path}) -> format_error({bad_pem, Why, Path}); format_error({bad_jwt_key, Path}) -> @@ -178,42 +189,46 @@ format_error({bad_sip_uri, Bad}) -> format("Invalid SIP URI: ~ts", [Bad]); format_error({route_conflict, R}) -> format("Failed to reuse route '~ts' because it's " - "already registered on a virtual host", - [R]); + "already registered on a virtual host", + [R]); format_error({listener_dup, AddrPort}) -> format("Overlapping listeners found at ~ts", - [format_addr_port(AddrPort)]); + [format_addr_port(AddrPort)]); format_error({listener_conflict, AddrPort1, AddrPort2}) -> format("Overlapping listeners found at ~ts and ~ts", - [format_addr_port(AddrPort1), - format_addr_port(AddrPort2)]); + [format_addr_port(AddrPort1), + format_addr_port(AddrPort2)]); format_error({invalid_syntax, Reason}) -> format("~ts", [Reason]); format_error({missing_module_dep, Mod, DepMod}) -> format("module ~ts depends on module ~ts, " - "which is not found in the config", - [Mod, DepMod]); + "which is not found in the config", + [Mod, DepMod]); format_error(eimp_error) -> format("ejabberd is built without image converter support", []); format_error({mqtt_codec, Reason}) -> mqtt_codec:format_error(Reason); format_error({external_module_error, Module, Error}) -> - try Module:format_error(Error) - catch _:_ -> - format("Invalid value", []) + try + Module:format_error(Error) + catch + _:_ -> + format("Invalid value", []) end; format_error(Reason) -> yconf:format_error(Reason). + -spec format_module(atom() | string()) -> string(). format_module(Mod) when is_atom(Mod) -> format_module(atom_to_list(Mod)); format_module(Mod) -> case Mod of - "Elixir." ++ M -> M; - M -> M + "Elixir." ++ M -> M; + M -> M end. + format_module_type([listen, module]) -> "listening module"; format_module_type([listen, request_handlers]) -> @@ -221,18 +236,21 @@ format_module_type([listen, request_handlers]) -> format_module_type([modules]) -> "ejabberd module". + format_known(_, Known) when length(Known) > 20 -> ""; format_known(Prefix, Known) -> [Prefix, " are: ", format_join(Known)]. + format_join([]) -> "(empty)"; -format_join([H|_] = L) when is_atom(H) -> - format_join([atom_to_binary(A, utf8) || A <- L]); +format_join([H | _] = L) when is_atom(H) -> + format_join([ atom_to_binary(A, utf8) || A <- L ]); format_join(L) -> str:join(lists:sort(L), <<", ">>). + %% All duplicated options having list-values are grouped %% into a single option with all list-values being concatenated -spec group_dups(list(T)) -> list(T). @@ -244,11 +262,14 @@ group_dups(Y1) -> {Option, Vals} when is_list(Vals) -> lists:keyreplace(Option, 1, Acc, {Option, Vals ++ Values}); _ -> - [{Option, Values}|Acc] + [{Option, Values} | Acc] end; (Other, Acc) -> - [Other|Acc] - end, [], Y1)). + [Other | Acc] + end, + [], + Y1)). + %%%=================================================================== %%% Validators from yconf @@ -256,371 +277,452 @@ group_dups(Y1) -> pos_int() -> yconf:pos_int(). + pos_int(Inf) -> yconf:pos_int(Inf). + non_neg_int() -> yconf:non_neg_int(). + non_neg_int(Inf) -> yconf:non_neg_int(Inf). + int() -> yconf:int(). + int(Min, Max) -> yconf:int(Min, Max). + number(Min) -> yconf:number(Min). + octal() -> yconf:octal(). + binary() -> yconf:binary(). + binary(Re) -> yconf:binary(Re). + binary(Re, Opts) -> yconf:binary(Re, Opts). + enum(L) -> yconf:enum(L). + bool() -> yconf:bool(). + atom() -> yconf:atom(). + string() -> yconf:string(). + string(Re) -> yconf:string(Re). + string(Re, Opts) -> yconf:string(Re, Opts). + any() -> yconf:any(). + url() -> yconf:url(). + url(Schemes) -> yconf:url(Schemes). + file() -> yconf:file(). + file(Type) -> yconf:file(Type). + directory() -> yconf:directory(). + directory(Type) -> yconf:directory(Type). + ip() -> yconf:ip(). + ipv4() -> yconf:ipv4(). + ipv6() -> yconf:ipv6(). + ip_mask() -> yconf:ip_mask(). + port() -> yconf:port(). + re() -> yconf:re(). + re(Opts) -> yconf:re(Opts). + glob() -> yconf:glob(). + glob(Opts) -> yconf:glob(Opts). + path() -> yconf:path(). + binary_sep(Sep) -> yconf:binary_sep(Sep). + timeout(Units) -> yconf:timeout(Units). + timeout(Units, Inf) -> yconf:timeout(Units, Inf). + base64() -> yconf:base64(). + non_empty(F) -> yconf:non_empty(F). + list(F) -> yconf:list(F). + list(F, Opts) -> yconf:list(F, Opts). + list_or_single(F) -> yconf:list_or_single(F). + list_or_single(F, Opts) -> yconf:list_or_single(F, Opts). + map(F1, F2) -> yconf:map(F1, F2). + map(F1, F2, Opts) -> yconf:map(F1, F2, Opts). + either(F1, F2) -> yconf:either(F1, F2). + and_then(F1, F2) -> yconf:and_then(F1, F2). + options(V) -> yconf:options(V). + options(V, O) -> yconf:options(V, O). + %%%=================================================================== %%% Custom validators %%%=================================================================== beam() -> beam([]). + beam(Exports) -> and_then( non_empty(binary()), fun(<<"Elixir.", _/binary>> = Val) -> - (yconf:beam(Exports))(Val); - (<> = Val) when C >= $A, C =< $Z -> - (yconf:beam(Exports))(<<"Elixir.", Val/binary>>); - (Val) -> - (yconf:beam(Exports))(Val) + (yconf:beam(Exports))(Val); + (<> = Val) when C >= $A, C =< $Z -> + (yconf:beam(Exports))(<<"Elixir.", Val/binary>>); + (Val) -> + (yconf:beam(Exports))(Val) end). + acl() -> either( atom(), acl:access_rules_validator()). + shaper() -> either( atom(), ejabberd_shaper:shaper_rules_validator()). + -spec url_or_file() -> yconf:validator({file | url, binary()}). url_or_file() -> either( and_then(url(), fun(URL) -> {url, URL} end), and_then(file(), fun(File) -> {file, File} end)). + -spec lang() -> yconf:validator(binary()). lang() -> and_then( binary(), fun(Lang) -> - try xmpp_lang:check(Lang) - catch _:_ -> fail({bad_lang, Lang}) - end + try + xmpp_lang:check(Lang) + catch + _:_ -> fail({bad_lang, Lang}) + end end). + -spec pem() -> yconf:validator(binary()). pem() -> and_then( path(), fun(Path) -> - case pkix:is_pem_file(Path) of - true -> Path; - {false, Reason} -> - fail({bad_pem, Reason, Path}) - end + case pkix:is_pem_file(Path) of + true -> Path; + {false, Reason} -> + fail({bad_pem, Reason, Path}) + end end). + -spec jid() -> yconf:validator(jid:jid()). jid() -> and_then( binary(), fun(Val) -> - try jid:decode(Val) - catch _:{bad_jid, _} = Reason -> fail(Reason) - end + try + jid:decode(Val) + catch + _:{bad_jid, _} = Reason -> fail(Reason) + end end). + -spec user() -> yconf:validator(binary()). user() -> and_then( binary(), fun(Val) -> - case jid:nodeprep(Val) of - error -> fail({bad_user, Val}); - U -> U - end + case jid:nodeprep(Val) of + error -> fail({bad_user, Val}); + U -> U + end end). + -spec domain() -> yconf:validator(binary()). domain() -> and_then( non_empty(binary()), fun(Val) -> - try jid:tolower(jid:decode(Val)) of - {<<"">>, <<"xn--", _/binary>> = Domain, <<"">>} -> - unicode:characters_to_binary(idna:decode(binary_to_list(Domain)), utf8); - {<<"">>, Domain, <<"">>} -> Domain; - _ -> fail({bad_domain, Val}) - catch _:{bad_jid, _} -> - fail({bad_domain, Val}) - end + try jid:tolower(jid:decode(Val)) of + {<<"">>, <<"xn--", _/binary>> = Domain, <<"">>} -> + unicode:characters_to_binary(idna:decode(binary_to_list(Domain)), utf8); + {<<"">>, Domain, <<"">>} -> Domain; + _ -> fail({bad_domain, Val}) + catch + _:{bad_jid, _} -> + fail({bad_domain, Val}) + end end). + -spec resource() -> yconf:validator(binary()). resource() -> and_then( binary(), fun(Val) -> - case jid:resourceprep(Val) of - error -> fail({bad_resource, Val}); - R -> R - end + case jid:resourceprep(Val) of + error -> fail({bad_resource, Val}); + R -> R + end end). + -spec db_type(module()) -> yconf:validator(atom()). db_type(M) -> and_then( atom(), fun(T) -> - case code:ensure_loaded(db_module(M, T)) of - {module, _} -> T; - {error, _} -> - ElixirModule = "Elixir." ++ atom_to_list(T), - case code:ensure_loaded(list_to_atom(ElixirModule)) of - {module, _} -> list_to_atom(ElixirModule); - {error, _} -> fail({bad_db_type, M, T}) - end - end + case code:ensure_loaded(db_module(M, T)) of + {module, _} -> T; + {error, _} -> + ElixirModule = "Elixir." ++ atom_to_list(T), + case code:ensure_loaded(list_to_atom(ElixirModule)) of + {module, _} -> list_to_atom(ElixirModule); + {error, _} -> fail({bad_db_type, M, T}) + end + end end). + -spec queue_type() -> yconf:validator(ram | file). queue_type() -> enum([ram, file]). + -spec ldap_filter() -> yconf:validator(binary()). ldap_filter() -> and_then( binary(), fun(Val) -> - case eldap_filter:parse(Val) of - {ok, _} -> Val; - _ -> fail({bad_ldap_filter, Val}) - end + case eldap_filter:parse(Val) of + {ok, _} -> Val; + _ -> fail({bad_ldap_filter, Val}) + end end). + -ifdef(SIP). + + sip_uri() -> and_then( binary(), fun(Val) -> - case esip:decode_uri(Val) of - error -> fail({bad_sip_uri, Val}); - URI -> URI - end + case esip:decode_uri(Val) of + error -> fail({bad_sip_uri, Val}); + URI -> URI + end end). + + -endif. + -spec host() -> yconf:validator(binary()). host() -> fun(Domain) -> - Hosts = ejabberd_config:get_option(hosts), - Domain3 = (domain())(Domain), - case lists:member(Domain3, Hosts) of - true -> fail({route_conflict, Domain}); - false -> Domain3 - end + Hosts = ejabberd_config:get_option(hosts), + Domain3 = (domain())(Domain), + case lists:member(Domain3, Hosts) of + true -> fail({route_conflict, Domain}); + false -> Domain3 + end end. + -spec hosts() -> yconf:validator([binary()]). hosts() -> list(host(), [unique]). + -spec vcard_temp() -> yconf:validator(). vcard_temp() -> and_then( - vcard_validator( - vcard_temp, undefined, - [{version, undefined, binary()}, - {fn, undefined, binary()}, - {n, undefined, vcard_name()}, - {nickname, undefined, binary()}, - {photo, undefined, vcard_photo()}, - {bday, undefined, binary()}, - {adr, [], list(vcard_adr())}, - {label, [], list(vcard_label())}, - {tel, [], list(vcard_tel())}, - {email, [], list(vcard_email())}, - {jabberid, undefined, binary()}, - {mailer, undefined, binary()}, - {tz, undefined, binary()}, - {geo, undefined, vcard_geo()}, - {title, undefined, binary()}, - {role, undefined, binary()}, - {logo, undefined, vcard_logo()}, - {org, undefined, vcard_org()}, - {categories, [], list(binary())}, - {note, undefined, binary()}, - {prodid, undefined, binary()}, - {rev, undefined, binary()}, - {sort_string, undefined, binary()}, - {sound, undefined, vcard_sound()}, - {uid, undefined, binary()}, - {url, undefined, binary()}, - {class, undefined, enum([confidential, private, public])}, - {key, undefined, vcard_key()}, - {desc, undefined, binary()}]), - fun(Tuple) -> - list_to_tuple(tuple_to_list(Tuple) ++ [[]]) - end). + vcard_validator( + vcard_temp, + undefined, + [{version, undefined, binary()}, + {fn, undefined, binary()}, + {n, undefined, vcard_name()}, + {nickname, undefined, binary()}, + {photo, undefined, vcard_photo()}, + {bday, undefined, binary()}, + {adr, [], list(vcard_adr())}, + {label, [], list(vcard_label())}, + {tel, [], list(vcard_tel())}, + {email, [], list(vcard_email())}, + {jabberid, undefined, binary()}, + {mailer, undefined, binary()}, + {tz, undefined, binary()}, + {geo, undefined, vcard_geo()}, + {title, undefined, binary()}, + {role, undefined, binary()}, + {logo, undefined, vcard_logo()}, + {org, undefined, vcard_org()}, + {categories, [], list(binary())}, + {note, undefined, binary()}, + {prodid, undefined, binary()}, + {rev, undefined, binary()}, + {sort_string, undefined, binary()}, + {sound, undefined, vcard_sound()}, + {uid, undefined, binary()}, + {url, undefined, binary()}, + {class, undefined, enum([confidential, private, public])}, + {key, undefined, vcard_key()}, + {desc, undefined, binary()}]), + fun(Tuple) -> + list_to_tuple(tuple_to_list(Tuple) ++ [[]]) + end). -spec vcard_name() -> yconf:validator(). vcard_name() -> vcard_validator( - vcard_name, undefined, + vcard_name, + undefined, [{family, undefined, binary()}, {given, undefined, binary()}, {middle, undefined, binary()}, {prefix, undefined, binary()}, {suffix, undefined, binary()}]). + -spec vcard_photo() -> yconf:validator(). vcard_photo() -> vcard_validator( - vcard_photo, undefined, + vcard_photo, + undefined, [{type, undefined, binary()}, {binval, undefined, base64()}, {extval, undefined, binary()}]). + -spec vcard_adr() -> yconf:validator(). vcard_adr() -> vcard_validator( - vcard_adr, [], + vcard_adr, + [], [{home, false, bool()}, {work, false, bool()}, {postal, false, bool()}, @@ -636,10 +738,12 @@ vcard_adr() -> {pcode, undefined, binary()}, {ctry, undefined, binary()}]). + -spec vcard_label() -> yconf:validator(). vcard_label() -> vcard_validator( - vcard_label, [], + vcard_label, + [], [{home, false, bool()}, {work, false, bool()}, {postal, false, bool()}, @@ -649,10 +753,12 @@ vcard_label() -> {pref, false, bool()}, {line, [], list(binary())}]). + -spec vcard_tel() -> yconf:validator(). vcard_tel() -> vcard_validator( - vcard_tel, [], + vcard_tel, + [], [{home, false, bool()}, {work, false, bool()}, {voice, false, bool()}, @@ -668,10 +774,12 @@ vcard_tel() -> {pref, false, bool()}, {number, undefined, binary()}]). + -spec vcard_email() -> yconf:validator(). vcard_email() -> vcard_validator( - vcard_email, [], + vcard_email, + [], [{home, false, bool()}, {work, false, bool()}, {internet, false, bool()}, @@ -679,77 +787,94 @@ vcard_email() -> {x400, false, bool()}, {userid, undefined, binary()}]). + -spec vcard_geo() -> yconf:validator(). vcard_geo() -> vcard_validator( - vcard_geo, undefined, + vcard_geo, + undefined, [{lat, undefined, binary()}, {lon, undefined, binary()}]). + -spec vcard_logo() -> yconf:validator(). vcard_logo() -> vcard_validator( - vcard_logo, undefined, + vcard_logo, + undefined, [{type, undefined, binary()}, {binval, undefined, base64()}, {extval, undefined, binary()}]). + -spec vcard_org() -> yconf:validator(). vcard_org() -> vcard_validator( - vcard_org, undefined, + vcard_org, + undefined, [{name, undefined, binary()}, {units, [], list(binary())}]). + -spec vcard_sound() -> yconf:validator(). vcard_sound() -> vcard_validator( - vcard_sound, undefined, + vcard_sound, + undefined, [{phonetic, undefined, binary()}, {binval, undefined, base64()}, {extval, undefined, binary()}]). + -spec vcard_key() -> yconf:validator(). vcard_key() -> vcard_validator( - vcard_key, undefined, + vcard_key, + undefined, [{type, undefined, binary()}, {cred, undefined, binary()}]). + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec db_module(module(), atom()) -> module(). db_module(M, Type) -> - try list_to_atom(atom_to_list(M) ++ "_" ++ atom_to_list(Type)) - catch _:system_limit -> - fail({bad_length, 255}) + try + list_to_atom(atom_to_list(M) ++ "_" ++ atom_to_list(Type)) + catch + _:system_limit -> + fail({bad_length, 255}) end. + format_addr_port({IP, Port}) -> IPStr = case tuple_size(IP) of - 4 -> inet:ntoa(IP); - 8 -> "[" ++ inet:ntoa(IP) ++ "]" - end, + 4 -> inet:ntoa(IP); + 8 -> "[" ++ inet:ntoa(IP) ++ "]" + end, IPStr ++ ":" ++ integer_to_list(Port). + -spec format(iolist(), list()) -> string(). format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). + -spec vcard_validator(atom(), term(), [{atom(), term(), validator()}]) -> validator(). vcard_validator(Name, Default, Schema) -> - Defaults = [{Key, Val} || {Key, Val, _} <- Schema], + Defaults = [ {Key, Val} || {Key, Val, _} <- Schema ], and_then( options( - maps:from_list([{Key, Fun} || {Key, _, Fun} <- Schema]), - [{return, map}, {unique, true}]), + maps:from_list([ {Key, Fun} || {Key, _, Fun} <- Schema ]), + [{return, map}, {unique, true}]), fun(Options) -> - merge(Defaults, Options, Name, Default) + merge(Defaults, Options, Name, Default) end). + -spec merge([{atom(), term()}], #{atom() => term()}, atom(), T) -> tuple() | T. merge(_, Options, _, Default) when Options == #{} -> Default; merge(Defaults, Options, Name, _) -> - list_to_tuple([Name|[maps:get(Key, Options, Val) || {Key, Val} <- Defaults]]). + list_to_tuple([Name | [ maps:get(Key, Options, Val) || {Key, Val} <- Defaults ]]). diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 844ef7ea2..254f25a98 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -43,56 +43,70 @@ -protocol({xep, 440, '0.4.0', '24.02', "complete", ""}). -protocol({xep, 474, '0.4.0', '24.02', "complete", "0.4.0 since 25.03"}). --export([start/0, stop/0, halt/0, start_app/1, start_app/2, - get_pid_file/0, check_apps/0, module_name/1, is_loaded/0]). +-export([start/0, + stop/0, + halt/0, + start_app/1, start_app/2, + get_pid_file/0, + check_apps/0, + module_name/1, + is_loaded/0]). -include("logger.hrl"). + start() -> case application:ensure_all_started(ejabberd) of - {error, Err} -> error_logger:error_msg("Failed to start ejabberd application: ~p", [Err]); - Ok -> Ok + {error, Err} -> error_logger:error_msg("Failed to start ejabberd application: ~p", [Err]); + Ok -> Ok end. + stop() -> application:stop(ejabberd). + halt() -> ejabberd_logger:flush(), erlang:halt(1, [{flush, true}]). + -spec get_pid_file() -> false | string(). get_pid_file() -> case os:getenv("EJABBERD_PID_PATH") of - false -> - false; - "" -> - false; - Path -> - Path + false -> + false; + "" -> + false; + Path -> + Path end. + start_app(App) -> start_app(App, temporary). + start_app(App, Type) -> StartFlag = not is_loaded(), start_app(App, Type, StartFlag). + is_loaded() -> Apps = application:which_applications(), lists:keymember(ejabberd, 1, Apps). + start_app(App, Type, StartFlag) when is_atom(App) -> start_app([App], Type, StartFlag); -start_app([App|Apps], Type, StartFlag) -> - case application:start(App,Type) of +start_app([App | Apps], Type, StartFlag) -> + case application:start(App, Type) of ok -> start_app(Apps, Type, StartFlag); {error, {already_started, _}} -> start_app(Apps, Type, StartFlag); {error, {not_started, DepApp}} -> - case lists:member(DepApp, [App|Apps]) of + case lists:member(DepApp, [App | Apps]) of true -> Reason = io_lib:format( "Failed to start Erlang application '~ts': " @@ -100,17 +114,18 @@ start_app([App|Apps], Type, StartFlag) -> [App, DepApp]), exit_or_halt(Reason, StartFlag); false -> - start_app([DepApp,App|Apps], Type, StartFlag) + start_app([DepApp, App | Apps], Type, StartFlag) end; {error, Why} -> Reason = io_lib:format( - "Failed to start Erlang application '~ts': ~ts. ~ts", - [App, format_error(Why), hint()]), + "Failed to start Erlang application '~ts': ~ts. ~ts", + [App, format_error(Why), hint()]), exit_or_halt(Reason, StartFlag) end; start_app([], _Type, _StartFlag) -> ok. + check_app_modules(App, StartFlag) -> case application:get_key(App, modules) of {ok, Mods} -> @@ -121,44 +136,50 @@ check_app_modules(App, StartFlag) -> File = get_module_file(App, Mod), Reason = io_lib:format( "Couldn't find file ~ts needed " - "for Erlang application '~ts'. ~ts", + "for Erlang application '~ts'. ~ts", [File, App, hint()]), exit_or_halt(Reason, StartFlag); _ -> - ok + ok end - end, Mods); + end, + Mods); _ -> %% No modules? This is strange ok end. + check_apps() -> spawn( fun() -> - Apps = [ejabberd | - [App || {App, _, _} <- application:which_applications(), - App /= ejabberd, App /= hex]], - ?DEBUG("Checking consistency of applications: ~ts", - [misc:join_atoms(Apps, <<", ">>)]), - misc:peach( - fun(App) -> - check_app_modules(App, true) - end, Apps), - ?DEBUG("All applications are intact", []), - lists:foreach(fun erlang:garbage_collect/1, processes()) + Apps = [ejabberd | [ App || {App, _, _} <- application:which_applications(), + App /= ejabberd, + App /= hex ]], + ?DEBUG("Checking consistency of applications: ~ts", + [misc:join_atoms(Apps, <<", ">>)]), + misc:peach( + fun(App) -> + check_app_modules(App, true) + end, + Apps), + ?DEBUG("All applications are intact", []), + lists:foreach(fun erlang:garbage_collect/1, processes()) end). + -spec exit_or_halt(iodata(), boolean()) -> no_return(). exit_or_halt(Reason, StartFlag) -> ?CRITICAL_MSG(Reason, []), - if StartFlag -> + if + StartFlag -> %% Wait for the critical message is written in the console/log halt(); - true -> + true -> erlang:error(application_start_failed) end. + get_module_file(App, Mod) -> BaseName = atom_to_list(Mod), case code:lib_dir(App) of @@ -168,46 +189,51 @@ get_module_file(App, Mod) -> filename:join([Dir, "ebin", BaseName ++ ".beam"]) end. -module_name([Dir, _, <> | _] = Mod) when H >= 65, H =< 90 -> - Module = str:join([elixir_name(M) || M<-tl(Mod)], <<>>), + +module_name([Dir, _, <> | _] = Mod) when H >= 65, H =< 90 -> + Module = str:join([ elixir_name(M) || M <- tl(Mod) ], <<>>), Prefix = case elixir_name(Dir) of - <<"Ejabberd">> -> <<"Elixir.Ejabberd.">>; - Lib -> <<"Elixir.Ejabberd.", Lib/binary, ".">> - end, + <<"Ejabberd">> -> <<"Elixir.Ejabberd.">>; + Lib -> <<"Elixir.Ejabberd.", Lib/binary, ".">> + end, misc:binary_to_atom(<>); module_name([<<"auth">> | T] = Mod) -> case hd(T) of %% T already starts with "Elixir" if an Elixir module is %% loaded with that name, as per `econf:db_type/1` - <<"Elixir", _/binary>> -> misc:binary_to_atom(hd(T)); + <<"Elixir", _/binary>> -> misc:binary_to_atom(hd(T)); _ -> module_name([<<"ejabberd">>] ++ Mod) end; module_name([<<"ejabberd">> | _] = Mod) -> - Module = str:join([erlang_name(M) || M<-Mod], $_), + Module = str:join([ erlang_name(M) || M <- Mod ], $_), misc:binary_to_atom(Module); module_name(Mod) when is_list(Mod) -> - Module = str:join([erlang_name(M) || M<-tl(Mod)], $_), + Module = str:join([ erlang_name(M) || M <- tl(Mod) ], $_), misc:binary_to_atom(Module). + elixir_name(Atom) when is_atom(Atom) -> elixir_name(misc:atom_to_binary(Atom)); -elixir_name(<>) when H >= 65, H =< 90 -> +elixir_name(<>) when H >= 65, H =< 90 -> <>; -elixir_name(<>) -> - <<(H-32), T/binary>>. +elixir_name(<>) -> + <<(H - 32), T/binary>>. + erlang_name(Atom) when is_atom(Atom) -> misc:atom_to_binary(Atom); erlang_name(Bin) when is_binary(Bin) -> Bin. + format_error({Reason, File}) when is_list(Reason), is_list(File) -> Reason ++ ": " ++ File; format_error(Term) -> io_lib:format("~p", [Term]). + hint() -> "This usually means that ejabberd or Erlang " "was compiled/installed incorrectly.". diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index 57b3637e3..264080ab2 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -32,41 +32,43 @@ %% API -export([start_link/0, - can_access/2, - invalidate/0, - validator/0, - show_current_definitions/0]). + can_access/2, + invalidate/0, + validator/0, + show_current_definitions/0]). %% gen_server callbacks -export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). --define(SERVER, ?MODULE). +-define(SERVER, ?MODULE). -define(CACHE_TAB, access_permissions_cache). --record(state, - {definitions = none :: none | [definition()]}). +-record(state, {definitions = none :: none | [definition()]}). -type state() :: #state{}. -type rule() :: {access, acl:access()} | - {acl, all | none | acl:acl_rule()}. + {acl, all | none | acl:acl_rule()}. -type what() :: all | none | [atom() | {tag, atom()}]. -type who() :: rule() | {oauth, {[binary()], [rule()]}}. -type from() :: atom(). -type permission() :: {binary(), {[from()], [who()], {what(), what()}}}. -type definition() :: {binary(), {[from()], [who()], [atom()] | all}}. --type caller_info() :: #{caller_module => module(), - caller_host => global | binary(), - tag => binary() | none, - extra_permissions => [definition()], - atom() => term()}. +-type caller_info() :: #{ + caller_module => module(), + caller_host => global | binary(), + tag => binary() | none, + extra_permissions => [definition()], + atom() => term() + }. -export_type([permission/0]). + %%%=================================================================== %%% API %%%=================================================================== @@ -78,41 +80,51 @@ can_access(Cmd, CallerInfo) -> Tag = maps:get(tag, CallerInfo, none), Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0, Res = lists:foldl( - fun({Name, _} = Def, none) -> - case matches_definition(Def, Cmd, CallerModule, Tag, Host, CallerInfo) of - true -> - ?DEBUG("Command '~p' execution allowed by rule " - "'~ts'~n (CallerInfo=~p)", [Cmd, Name, CallerInfo]), - allow; - _ -> - none - end; - (_, Val) -> - Val - end, none, Defs), + fun({Name, _} = Def, none) -> + case matches_definition(Def, Cmd, CallerModule, Tag, Host, CallerInfo) of + true -> + ?DEBUG("Command '~p' execution allowed by rule " + "'~ts'~n (CallerInfo=~p)", + [Cmd, Name, CallerInfo]), + allow; + _ -> + none + end; + (_, Val) -> + Val + end, + none, + Defs), case Res of - allow -> allow; - _ -> - ?DEBUG("Command '~p' execution denied~n " - "(CallerInfo=~p)", [Cmd, CallerInfo]), - deny + allow -> allow; + _ -> + ?DEBUG("Command '~p' execution denied~n " + "(CallerInfo=~p)", + [Cmd, CallerInfo]), + deny end. + -spec invalidate() -> ok. invalidate() -> gen_server:cast(?MODULE, invalidate), ets_cache:delete(?CACHE_TAB, definitions). + -spec show_current_definitions() -> [definition()]. show_current_definitions() -> - ets_cache:lookup(?CACHE_TAB, definitions, - fun() -> - {cache, gen_server:call(?MODULE, show_current_definitions)} - end). + ets_cache:lookup(?CACHE_TAB, + definitions, + fun() -> + {cache, gen_server:call(?MODULE, show_current_definitions)} + end). + + start_link() -> ets_cache:new(?CACHE_TAB, [{max_size, 2}]), gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== @@ -122,8 +134,10 @@ init([]) -> ets_cache:new(access_permissions), {ok, #state{}}. + -spec handle_call(show_current_definitions | term(), - term(), state()) -> {reply, term(), state()}. + term(), + state()) -> {reply, term(), state()}. handle_call(show_current_definitions, _From, State) -> {State2, Defs} = get_definitions(State), {reply, Defs, State2}; @@ -131,6 +145,7 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + -spec handle_cast(invalidate | term(), state()) -> {noreply, state()}. handle_cast(invalidate, State) -> {noreply, State#state{definitions = none}}; @@ -138,16 +153,20 @@ handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, invalidate, 90). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -158,81 +177,93 @@ get_definitions(#state{definitions = none} = State) -> ApiPerms = ejabberd_option:api_permissions(), AllCommands = ejabberd_commands:get_commands_definition(), NDefs0 = lists:map( - fun({Name, {From, Who, {Add, Del}}}) -> - Cmds = filter_commands_with_permissions(AllCommands, Add, Del), - {Name, {From, Who, Cmds}} - end, ApiPerms), + fun({Name, {From, Who, {Add, Del}}}) -> + Cmds = filter_commands_with_permissions(AllCommands, Add, Del), + {Name, {From, Who, Cmds}} + end, + ApiPerms), NDefs = case lists:keyfind(<<"console commands">>, 1, NDefs0) of - false -> - [{<<"console commands">>, - {[ejabberd_ctl], - [{acl, all}], - filter_commands_with_permissions(AllCommands, all, none)}} | NDefs0]; - _ -> - NDefs0 - end, + false -> + [{<<"console commands">>, + {[ejabberd_ctl], + [{acl, all}], + filter_commands_with_permissions(AllCommands, all, none)}} | NDefs0]; + _ -> + NDefs0 + end, {State#state{definitions = NDefs}, NDefs}. --spec matches_definition(definition(), atom(), module(), - atom(), global | binary(), caller_info()) -> boolean(). + +-spec matches_definition(definition(), + atom(), + module(), + atom(), + global | binary(), + caller_info()) -> boolean(). matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInfo) -> case What == all orelse lists:member(Cmd, What) of - true -> - {Tags, Modules} = lists:partition(fun({tag, _}) -> true; (_) -> false end, From), - case (Modules == [] orelse lists:member(Module, Modules)) andalso - (Tags == [] orelse lists:member({tag, Tag}, Tags)) of - true -> - Scope = maps:get(oauth_scope, CallerInfo, none), - lists:any( - fun({access, Access}) when Scope == none -> - acl:match_rule(Host, Access, CallerInfo) == allow; - ({acl, Name} = Acl) when Scope == none, is_atom(Name) -> - acl:match_acl(Host, Acl, CallerInfo); - ({acl, Acl}) when Scope == none -> - acl:match_acl(Host, Acl, CallerInfo); - ({oauth, {Scopes, List}}) when Scope /= none -> - case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of - true -> - lists:any( - fun({access, Access}) -> - acl:match_rule(Host, Access, CallerInfo) == allow; - ({acl, Name} = Acl) when is_atom(Name) -> - acl:match_acl(Host, Acl, CallerInfo); - ({acl, Acl}) -> - acl:match_acl(Host, Acl, CallerInfo) - end, List); - _ -> - false - end; - (_) -> - false - end, Who); - _ -> - false - end; - _ -> - false + true -> + {Tags, Modules} = lists:partition(fun({tag, _}) -> true; (_) -> false end, From), + case (Modules == [] orelse lists:member(Module, Modules)) andalso + (Tags == [] orelse lists:member({tag, Tag}, Tags)) of + true -> + Scope = maps:get(oauth_scope, CallerInfo, none), + lists:any( + fun({access, Access}) when Scope == none -> + acl:match_rule(Host, Access, CallerInfo) == allow; + ({acl, Name} = Acl) when Scope == none, is_atom(Name) -> + acl:match_acl(Host, Acl, CallerInfo); + ({acl, Acl}) when Scope == none -> + acl:match_acl(Host, Acl, CallerInfo); + ({oauth, {Scopes, List}}) when Scope /= none -> + case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of + true -> + lists:any( + fun({access, Access}) -> + acl:match_rule(Host, Access, CallerInfo) == allow; + ({acl, Name} = Acl) when is_atom(Name) -> + acl:match_acl(Host, Acl, CallerInfo); + ({acl, Acl}) -> + acl:match_acl(Host, Acl, CallerInfo) + end, + List); + _ -> + false + end; + (_) -> + false + end, + Who); + _ -> + false + end; + _ -> + false end. + -spec filter_commands_with_permissions([#ejabberd_commands{}], what(), what()) -> [atom()]. filter_commands_with_permissions(AllCommands, Add, Del) -> CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []), CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []), lists:map(fun(#ejabberd_commands{name = N}) -> N end, - CommandsAdd -- CommandsDel). + CommandsAdd -- CommandsDel). --spec filter_commands_with_patterns([#ejabberd_commands{}], what(), - [#ejabberd_commands{}]) -> [#ejabberd_commands{}]. + +-spec filter_commands_with_patterns([#ejabberd_commands{}], + what(), + [#ejabberd_commands{}]) -> [#ejabberd_commands{}]. filter_commands_with_patterns([], _Patterns, Acc) -> Acc; filter_commands_with_patterns([C | CRest], Patterns, Acc) -> case command_matches_patterns(C, Patterns) of - true -> - filter_commands_with_patterns(CRest, Patterns, [C | Acc]); - _ -> - filter_commands_with_patterns(CRest, Patterns, Acc) + true -> + filter_commands_with_patterns(CRest, Patterns, [C | Acc]); + _ -> + filter_commands_with_patterns(CRest, Patterns, Acc) end. + -spec command_matches_patterns(#ejabberd_commands{}, what()) -> boolean(). command_matches_patterns(_, all) -> true; @@ -242,46 +273,50 @@ command_matches_patterns(_, []) -> false; command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) -> case lists:member(Tag, Tags) of - true -> - true; - _ -> - command_matches_patterns(C, Tail) + true -> + true; + _ -> + command_matches_patterns(C, Tail) end; command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) -> true; command_matches_patterns(C, [_ | Tail]) -> command_matches_patterns(C, Tail). + %%%=================================================================== %%% Validators %%%=================================================================== -spec parse_what([binary()]) -> {what(), what()}. parse_what(Defs) -> {A, D} = - lists:foldl( - fun(Def, {Add, Del}) -> - case parse_single_what(Def) of - {error, Err} -> - econf:fail({invalid_syntax, [Err, ": ", Def]}); - all -> - {case Add of none -> none; _ -> all end, Del}; - {neg, all} -> - {none, all}; - {neg, Value} -> - {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end}; - Value -> - {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del} - end - end, {[], []}, Defs), + lists:foldl( + fun(Def, {Add, Del}) -> + case parse_single_what(Def) of + {error, Err} -> + econf:fail({invalid_syntax, [Err, ": ", Def]}); + all -> + {case Add of none -> none; _ -> all end, Del}; + {neg, all} -> + {none, all}; + {neg, Value} -> + {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end}; + Value -> + {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del} + end + end, + {[], []}, + Defs), case {A, D} of - {[], _} -> - {none, all}; - {A2, []} -> - {A2, none}; - V -> - V + {[], _} -> + {none, all}; + {A2, []} -> + {A2, none}; + V -> + V end. + -spec parse_single_what(binary()) -> atom() | {neg, atom()} | {tag, atom()} | {error, string()}. parse_single_what(<<"*">>) -> all; @@ -289,70 +324,76 @@ parse_single_what(<<"!*">>) -> {neg, all}; parse_single_what(<<"!", Rest/binary>>) -> case parse_single_what(Rest) of - {neg, _} -> - {error, "double negation"}; - {error, _} = Err -> - Err; - V -> - {neg, V} + {neg, _} -> + {error, "double negation"}; + {error, _} = Err -> + Err; + V -> + {neg, V} end; parse_single_what(<<"[tag:", Rest/binary>>) -> case binary:split(Rest, <<"]">>) of - [TagName, <<"">>] -> - case parse_single_what(TagName) of - {error, _} = Err -> - Err; - V when is_atom(V) -> - {tag, V}; - _ -> - {error, "invalid tag"} - end; - _ -> - {error, "invalid tag"} + [TagName, <<"">>] -> + case parse_single_what(TagName) of + {error, _} = Err -> + Err; + V when is_atom(V) -> + {tag, V}; + _ -> + {error, "invalid tag"} + end; + _ -> + {error, "invalid tag"} end; parse_single_what(B) -> case re:run(B, "^[a-z0-9_\\-]*$") of - nomatch -> {error, "invalid command"}; - _ -> binary_to_atom(B, latin1) + nomatch -> {error, "invalid command"}; + _ -> binary_to_atom(B, latin1) end. + validator(Map, Opts) -> econf:and_then( fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:atom())(K), V}; (A) -> {acl, (econf:atom())(A)} - end, lists:flatten(L)); + end, + lists:flatten(L)); (A) -> [{acl, (econf:atom())(A)}] end, econf:and_then( - econf:options(maps:merge(acl:validators(), Map), Opts), - fun(Rules) -> - lists:flatmap( - fun({Type, Rs}) when is_list(Rs) -> - case maps:is_key(Type, acl:validators()) of - true -> [{acl, {Type, R}} || R <- Rs]; - false -> [{Type, Rs}] - end; - (Other) -> - [Other] - end, Rules) - end)). + econf:options(maps:merge(acl:validators(), Map), Opts), + fun(Rules) -> + lists:flatmap( + fun({Type, Rs}) when is_list(Rs) -> + case maps:is_key(Type, acl:validators()) of + true -> [ {acl, {Type, R}} || R <- Rs ]; + false -> [{Type, Rs}] + end; + (Other) -> + [Other] + end, + Rules) + end)). + validator(from) -> fun(L) when is_list(L) -> - lists:map( - fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; - (A) -> (econf:enum([ejabberd_ctl, - ejabberd_web_admin, - ejabberd_xmlrpc, - mod_adhoc_api, - mod_cron, - mod_http_api]))(A) - end, lists:flatten(L)); + lists:map( + fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; + (A) -> + (econf:enum([ejabberd_ctl, + ejabberd_web_admin, + ejabberd_xmlrpc, + mod_adhoc_api, + mod_cron, + mod_http_api]))(A) + end, + lists:flatten(L)); (A) -> - [(econf:enum([ejabberd_ctl, + [(econf:enum([ejabberd_ctl, ejabberd_web_admin, ejabberd_xmlrpc, mod_adhoc_api, @@ -367,26 +408,31 @@ validator(who) -> validator(#{access => econf:acl(), oauth => validator(oauth)}, []); validator(oauth) -> econf:and_then( - validator(#{access => econf:acl(), - scope => econf:non_empty( - econf:list_or_single(econf:binary()))}, - [{required, [scope]}]), + validator(#{ + access => econf:acl(), + scope => econf:non_empty( + econf:list_or_single(econf:binary())) + }, + [{required, [scope]}]), fun(Os) -> - {[Scopes], Rest} = proplists:split(Os, [scope]), - {lists:flatten([S || {_, S} <- Scopes]), Rest} + {[Scopes], Rest} = proplists:split(Os, [scope]), + {lists:flatten([ S || {_, S} <- Scopes ]), Rest} end). + validator() -> econf:map( econf:binary(), econf:and_then( - econf:options( - #{from => validator(from), - what => validator(what), - who => validator(who)}), - fun(Os) -> - {proplists:get_value(from, Os, []), - proplists:get_value(who, Os, none), - proplists:get_value(what, Os, {none, none})} - end), + econf:options( + #{ + from => validator(from), + what => validator(what), + who => validator(who) + }), + fun(Os) -> + {proplists:get_value(from, Os, []), + proplists:get_value(who, Os, none), + proplists:get_value(what, Os, {none, none})} + end), [unique]). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 8b16fc727..e9601d614 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -27,11 +27,17 @@ %% Hooks -export([ejabberd_started/0, register_certfiles/0, cert_expired/2]). %% ejabberd commands --export([get_commands_spec/0, request_certificate/1, - revoke_certificate/1, list_certificates/0]). +-export([get_commands_spec/0, + request_certificate/1, + revoke_certificate/1, + list_certificates/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% WebAdmin -export([webadmin_menu_node/3, webadmin_page_node/3]). @@ -39,6 +45,7 @@ -include("ejabberd_commands.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). + -include_lib("public_key/include/public_key.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). @@ -52,9 +59,11 @@ -type cert() :: #'OTPCertificate'{}. -type cert_type() :: ec | rsa. -type io_error() :: file:posix(). --type issue_result() :: ok | p1_acme:issue_return() | - {error, {file, io_error()} | - {idna_failed, binary()}}. +-type issue_result() :: ok | + p1_acme:issue_return() | + {error, {file, io_error()} | + {idna_failed, binary()}}. + %%%=================================================================== %%% API @@ -62,45 +71,55 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + -spec register_certfiles() -> ok. register_certfiles() -> lists:foreach(fun ejabberd_pkix:add_certfile/1, - list_certfiles()). + list_certfiles()). + -spec process([binary()], _) -> {integer(), [{binary(), binary()}], binary()}. process([Token], _) -> ?DEBUG("Received ACME challenge request for token: ~ts", [Token]), try ets:lookup_element(acme_challenge, Token, 2) of - Key -> {200, [{<<"Content-Type">>, - <<"application/octet-stream">>}], - Key} - catch _:_ -> - {404, [], <<>>} + Key -> + {200, + [{<<"Content-Type">>, + <<"application/octet-stream">>}], + Key} + catch + _:_ -> + {404, [], <<>>} end; process(_, _) -> {404, [], <<>>}. + -spec cert_expired(_, pkix:cert_info()) -> ok | stop. cert_expired(_, #{domains := Domains, files := Files}) -> CertFiles = list_certfiles(), case lists:any( - fun({File, _}) -> - lists:member(File, CertFiles) - end, Files) of - true -> - gen_server:cast(?MODULE, {request, Domains}), - stop; - false -> - ok + fun({File, _}) -> + lists:member(File, CertFiles) + end, + Files) of + true -> + gen_server:cast(?MODULE, {request, Domains}), + stop; + false -> + ok end. + -spec ejabberd_started() -> ok. ejabberd_started() -> gen_server:cast(?MODULE, ejabberd_started). + default_directory_url() -> <<"https://acme-v02.api.letsencrypt.org/directory">>. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== @@ -119,9 +138,10 @@ init([]) -> register_certfiles(), {ok, #state{}}. -handle_call({request, [_|_] = Domains}, _From, State) -> + +handle_call({request, [_ | _] = Domains}, _From, State) -> ?INFO_MSG("Requesting new certificate for ~ts from ~ts", - [misc:format_hosts_list(Domains), directory_url()]), + [misc:format_hosts_list(Domains), directory_url()]), {Ret, State1} = issue_request(State, Domains), {reply, Ret, State1}; handle_call({revoke, Cert, Key, Path}, _From, State) -> @@ -132,29 +152,32 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(ejabberd_started, State) -> case request_on_start() of - {true, Domains} -> - ?INFO_MSG("Requesting new certificate for ~ts from ~ts", - [misc:format_hosts_list(Domains), directory_url()]), - {_, State1} = issue_request(State, Domains), - {noreply, State1}; - false -> - {noreply, State} + {true, Domains} -> + ?INFO_MSG("Requesting new certificate for ~ts from ~ts", + [misc:format_hosts_list(Domains), directory_url()]), + {_, State1} = issue_request(State, Domains), + {noreply, State1}; + false -> + {noreply, State} end; -handle_cast({request, [_|_] = Domains}, State) -> +handle_cast({request, [_ | _] = Domains}, State) -> ?INFO_MSG("Requesting renewal of certificate for ~ts from ~ts", - [misc:format_hosts_list(Domains), directory_url()]), + [misc:format_hosts_list(Domains), directory_url()]), {_, State1} = issue_request(State, Domains), {noreply, State1}; handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ejabberd_hooks:delete(cert_expired, ?MODULE, cert_expired, 60), ejabberd_hooks:delete(config_reloaded, ?MODULE, register_certfiles, 40), @@ -164,9 +187,11 @@ terminate(_Reason, _State) -> ejabberd_hooks:delete(webadmin_page_node, ?MODULE, webadmin_page_node, 110), ejabberd_commands:unregister_commands(get_commands_spec()). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -180,9 +205,11 @@ register_challenge(Auth, Ref) -> ets:insert( acme_challenge, lists:map( - fun(#{token := Token, key := Key}) -> - {Token, Key, Ref} - end, Auth)). + fun(#{token := Token, key := Key}) -> + {Token, Key, Ref} + end, + Auth)). + -spec unregister_challenge(reference()) -> non_neg_integer(). unregister_challenge(Ref) -> @@ -191,95 +218,111 @@ unregister_challenge(Ref) -> ets:select_delete( acme_challenge, ets:fun2ms( - fun({_, _, Ref1}) -> - Ref1 == Ref - end)). + fun({_, _, Ref1}) -> + Ref1 == Ref + end)). + %%%=================================================================== %%% Issuance %%%=================================================================== --spec issue_request(state(), [binary(),...]) -> {issue_result(), state()}. +-spec issue_request(state(), [binary(), ...]) -> {issue_result(), state()}. issue_request(State, Domains) -> case check_idna(Domains) of - {ok, AsciiDomains} -> - case read_account_key() of - {ok, AccKey} -> - Config = ejabberd_option:acme(), - DirURL = maps:get(ca_url, Config, default_directory_url()), - Contact = maps:get(contact, Config, []), - CertType = maps:get(cert_type, Config, rsa), - issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact); - {error, Reason} = Err -> - ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", - [misc:format_hosts_list(Domains), - format_error(Reason)]), - {Err, State} - end; - {error, Reason} = Err -> - ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", - [misc:format_hosts_list(Domains), - format_error(Reason)]), - {Err, State} + {ok, AsciiDomains} -> + case read_account_key() of + {ok, AccKey} -> + Config = ejabberd_option:acme(), + DirURL = maps:get(ca_url, Config, default_directory_url()), + Contact = maps:get(contact, Config, []), + CertType = maps:get(cert_type, Config, rsa), + issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact); + {error, Reason} = Err -> + ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} end. --spec issue_request(state(), binary(), [binary(),...], [string(), ...], priv_key(), - cert_type(), [binary()]) -> {issue_result(), state()}. + +-spec issue_request(state(), + binary(), + [binary(), ...], + [string(), ...], + priv_key(), + cert_type(), + [binary()]) -> {issue_result(), state()}. issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact) -> Ref = make_ref(), ChallengeFun = fun(Auth) -> register_challenge(Auth, Ref) end, - Ret = case p1_acme:issue(DirURL, AsciiDomains, AccKey, - [{cert_type, CertType}, - {contact, Contact}, - {debug_fun, debug_fun()}, - {challenge_fun, ChallengeFun}]) of - {ok, #{cert_key := CertKey, - cert_chain := Certs}} -> - case store_cert(CertKey, Certs, CertType, Domains) of - {ok, Path} -> - ejabberd_pkix:add_certfile(Path), - ejabberd_pkix:commit(), - ?INFO_MSG("Certificate for ~ts has been received, " - "stored and loaded successfully", - [misc:format_hosts_list(Domains)]), - {ok, State}; - {error, Reason} = Err -> - ?ERROR_MSG("Failed to store certificate for ~ts: ~ts", - [misc:format_hosts_list(Domains), - format_error(Reason)]), - {Err, State} - end; - {error, Reason} = Err -> - ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", - [misc:format_hosts_list(Domains), - format_error(Reason)]), - {Err, State} - end, + Ret = case p1_acme:issue(DirURL, + AsciiDomains, + AccKey, + [{cert_type, CertType}, + {contact, Contact}, + {debug_fun, debug_fun()}, + {challenge_fun, ChallengeFun}]) of + {ok, #{ + cert_key := CertKey, + cert_chain := Certs + }} -> + case store_cert(CertKey, Certs, CertType, Domains) of + {ok, Path} -> + ejabberd_pkix:add_certfile(Path), + ejabberd_pkix:commit(), + ?INFO_MSG("Certificate for ~ts has been received, " + "stored and loaded successfully", + [misc:format_hosts_list(Domains)]), + {ok, State}; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to store certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", + [misc:format_hosts_list(Domains), + format_error(Reason)]), + {Err, State} + end, unregister_challenge(Ref), Ret. + %%%=================================================================== %%% Revocation %%%=================================================================== revoke_request(State, Cert, Key, Path) -> - case p1_acme:revoke(directory_url(), Cert, Key, - [{debug_fun, debug_fun()}]) of - ok -> - ?INFO_MSG("Certificate from file ~ts has been " - "revoked successfully", [Path]), - case delete_file(Path) of - ok -> - ejabberd_pkix:del_certfile(Path), - ejabberd_pkix:commit(), - {ok, State}; - Err -> - {Err, State} - end; - {error, Reason} = Err -> - ?ERROR_MSG("Failed to revoke certificate from file ~ts: ~ts", - [Path, format_error(Reason)]), - {Err, State} + case p1_acme:revoke(directory_url(), + Cert, + Key, + [{debug_fun, debug_fun()}]) of + ok -> + ?INFO_MSG("Certificate from file ~ts has been " + "revoked successfully", + [Path]), + case delete_file(Path) of + ok -> + ejabberd_pkix:del_certfile(Path), + ejabberd_pkix:commit(), + {ok, State}; + Err -> + {Err, State} + end; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to revoke certificate from file ~ts: ~ts", + [Path, format_error(Reason)]), + {Err, State} end. + %%%=================================================================== %%% File management %%%=================================================================== @@ -288,57 +331,67 @@ acme_dir() -> MnesiaDir = mnesia:system_info(directory), filename:join(MnesiaDir, "acme"). + -spec acme_certs_dir(atom()) -> file:filename_all(). acme_certs_dir(Tag) -> filename:join(acme_dir(), Tag). + -spec account_file() -> file:filename_all(). account_file() -> filename:join(acme_dir(), "account.key"). + -spec cert_file(cert_type(), [binary()]) -> file:filename_all(). cert_file(CertType, Domains) -> - L = [erlang:atom_to_binary(CertType, latin1)|Domains], + L = [erlang:atom_to_binary(CertType, latin1) | Domains], Hash = str:sha(str:join(L, <<0>>)), filename:join(acme_certs_dir(live), Hash). + -spec prep_path(file:filename_all()) -> binary(). prep_path(Path) -> unicode:characters_to_binary(Path). + -spec list_certfiles() -> [binary()]. list_certfiles() -> filelib:fold_files( - acme_certs_dir(live), "^[0-9a-f]{40}$", false, - fun(F, Fs) -> [prep_path(F)|Fs] end, []). + acme_certs_dir(live), + "^[0-9a-f]{40}$", + false, + fun(F, Fs) -> [prep_path(F) | Fs] end, + []). + -spec read_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}. read_account_key() -> Path = account_file(), case pkix:read_file(Path) of - {ok, _, KeyMap} -> - case maps:keys(KeyMap) of - [#'ECPrivateKey'{} = Key|_] -> {ok, Key}; - _ -> - ?WARNING_MSG("File ~ts doesn't contain ACME account key. " - "Trying to create a new one...", - [Path]), - create_account_key() - end; - {error, enoent} -> - create_account_key(); - {error, {bad_cert, _, _} = Reason} -> - ?WARNING_MSG("ACME account key from '~ts' is corrupted: ~ts. " - "Trying to create a new one...", - [Path, pkix:format_error(Reason)]), - create_account_key(); - {error, Reason} -> - ?ERROR_MSG("Failed to read ACME account from ~ts: ~ts. " - "Try to fix permissions or delete the file completely", - [Path, pkix:format_error(Reason)]), - {error, {file, Reason}} + {ok, _, KeyMap} -> + case maps:keys(KeyMap) of + [#'ECPrivateKey'{} = Key | _] -> {ok, Key}; + _ -> + ?WARNING_MSG("File ~ts doesn't contain ACME account key. " + "Trying to create a new one...", + [Path]), + create_account_key() + end; + {error, enoent} -> + create_account_key(); + {error, {bad_cert, _, _} = Reason} -> + ?WARNING_MSG("ACME account key from '~ts' is corrupted: ~ts. " + "Trying to create a new one...", + [Path, pkix:format_error(Reason)]), + create_account_key(); + {error, Reason} -> + ?ERROR_MSG("Failed to read ACME account from ~ts: ~ts. " + "Try to fix permissions or delete the file completely", + [Path, pkix:format_error(Reason)]), + {error, {file, Reason}} end. + -spec create_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}. create_account_key() -> Path = account_file(), @@ -347,222 +400,257 @@ create_account_key() -> DER = public_key:der_encode(element(1, Key), Key), PEM = public_key:pem_encode([{element(1, Key), DER, not_encrypted}]), case write_file(Path, PEM) of - ok -> - ?DEBUG("ACME account key has been created successfully in ~ts", - [Path]), - {ok, Key}; - {error, Reason} -> - {error, {file, Reason}} + ok -> + ?DEBUG("ACME account key has been created successfully in ~ts", + [Path]), + {ok, Key}; + {error, Reason} -> + {error, {file, Reason}} end. + -spec store_cert(priv_key(), [cert()], cert_type(), [binary()]) -> {ok, file:filename_all()} | - {error, {file, io_error()}}. + {error, {file, io_error()}}. store_cert(Key, Chain, CertType, Domains) -> DerKey = public_key:der_encode(element(1, Key), Key), PemKey = [{element(1, Key), DerKey, not_encrypted}], PemChain = lists:map( - fun(Cert) -> - DerCert = public_key:pkix_encode( - element(1, Cert), Cert, otp), - {'Certificate', DerCert, not_encrypted} - end, Chain), + fun(Cert) -> + DerCert = public_key:pkix_encode( + element(1, Cert), Cert, otp), + {'Certificate', DerCert, not_encrypted} + end, + Chain), PEM = public_key:pem_encode(PemChain ++ PemKey), Path = cert_file(CertType, Domains), ?DEBUG("Storing certificate for ~ts in ~ts", - [misc:format_hosts_list(Domains), Path]), + [misc:format_hosts_list(Domains), Path]), case write_file(Path, PEM) of - ok -> - {ok, Path}; - {error, Reason} -> - {error, {file, Reason}} + ok -> + {ok, Path}; + {error, Reason} -> + {error, {file, Reason}} end. + -spec read_cert(file:filename_all()) -> {ok, [cert()], priv_key()} | - {error, {file, io_error()} | - {bad_cert, _, _} | - unexpected_certfile}. + {error, {file, io_error()} | + {bad_cert, _, _} | + unexpected_certfile}. read_cert(Path) -> ?DEBUG("Reading certificate from ~ts", [Path]), case pkix:read_file(Path) of - {ok, CertsMap, KeysMap} -> - case {maps:to_list(CertsMap), maps:keys(KeysMap)} of - {[_|_] = Certs, [CertKey]} -> - {ok, [Cert || {Cert, _} <- lists:keysort(2, Certs)], CertKey}; - _ -> - {error, unexpected_certfile} - end; - {error, Why} when is_atom(Why) -> - {error, {file, Why}}; - {error, _} = Err -> - Err + {ok, CertsMap, KeysMap} -> + case {maps:to_list(CertsMap), maps:keys(KeysMap)} of + {[_ | _] = Certs, [CertKey]} -> + {ok, [ Cert || {Cert, _} <- lists:keysort(2, Certs) ], CertKey}; + _ -> + {error, unexpected_certfile} + end; + {error, Why} when is_atom(Why) -> + {error, {file, Why}}; + {error, _} = Err -> + Err end. + -spec write_file(file:filename_all(), iodata()) -> ok | {error, io_error()}. write_file(Path, Data) -> case ensure_dir(Path) of - ok -> - case file:write_file(Path, Data) of - ok -> - case file:change_mode(Path, 8#600) of - ok -> ok; - {error, Why} -> - ?WARNING_MSG("Failed to change permissions of ~ts: ~ts", - [Path, file:format_error(Why)]) - end; - {error, Why} = Err -> - ?ERROR_MSG("Failed to write file ~ts: ~ts", - [Path, file:format_error(Why)]), - Err - end; - Err -> - Err + ok -> + case file:write_file(Path, Data) of + ok -> + case file:change_mode(Path, 8#600) of + ok -> ok; + {error, Why} -> + ?WARNING_MSG("Failed to change permissions of ~ts: ~ts", + [Path, file:format_error(Why)]) + end; + {error, Why} = Err -> + ?ERROR_MSG("Failed to write file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err + end; + Err -> + Err end. + -spec delete_file(file:filename_all()) -> ok | {error, io_error()}. delete_file(Path) -> case file:delete(Path) of - ok -> ok; - {error, Why} = Err -> - ?WARNING_MSG("Failed to delete file ~ts: ~ts", - [Path, file:format_error(Why)]), - Err + ok -> ok; + {error, Why} = Err -> + ?WARNING_MSG("Failed to delete file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err end. + -spec ensure_dir(file:filename_all()) -> ok | {error, io_error()}. ensure_dir(Path) -> case filelib:ensure_dir(Path) of - ok -> ok; - {error, Why} = Err -> - ?ERROR_MSG("Failed to create directory ~ts: ~ts", - [filename:dirname(Path), - file:format_error(Why)]), - Err + ok -> ok; + {error, Why} = Err -> + ?ERROR_MSG("Failed to create directory ~ts: ~ts", + [filename:dirname(Path), + file:format_error(Why)]), + Err end. + -spec delete_obsolete_data() -> ok. delete_obsolete_data() -> Path = filename:join(ejabberd_pkix:certs_dir(), "acme"), case filelib:is_dir(Path) of - true -> - ?INFO_MSG("Deleting obsolete directory ~ts", [Path]), - _ = misc:delete_dir(Path), - ok; - false -> - ok + true -> + ?INFO_MSG("Deleting obsolete directory ~ts", [Path]), + _ = misc:delete_dir(Path), + ok; + false -> + ok end. + %%%=================================================================== %%% ejabberd commands %%%=================================================================== get_commands_spec() -> - [#ejabberd_commands{name = request_certificate, tags = [acme], - desc = "Requests certificates for all or some domains", - longdesc = "Domains can be `all`, or a list of domains separared with comma characters", - module = ?MODULE, function = request_certificate, - args_desc = ["Domains for which to acquire a certificate"], - args_example = ["example.com,domain.tld,conference.domain.tld"], - args = [{domains, string}], - result = {res, restuple}}, - #ejabberd_commands{name = list_certificates, tags = [acme], - desc = "Lists all ACME certificates", - module = ?MODULE, function = list_certificates, - args = [], - result = {certificates, - {list, {certificate, - {tuple, [{domain, string}, - {file, string}, - {used, string}]}}}}}, - #ejabberd_commands{name = revoke_certificate, tags = [acme], - desc = "Revokes the selected ACME certificate", - module = ?MODULE, function = revoke_certificate, - args_desc = ["Filename of the certificate"], - args = [{file, string}], - result = {res, restuple}}]. + [#ejabberd_commands{ + name = request_certificate, + tags = [acme], + desc = "Requests certificates for all or some domains", + longdesc = "Domains can be `all`, or a list of domains separared with comma characters", + module = ?MODULE, + function = request_certificate, + args_desc = ["Domains for which to acquire a certificate"], + args_example = ["example.com,domain.tld,conference.domain.tld"], + args = [{domains, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = list_certificates, + tags = [acme], + desc = "Lists all ACME certificates", + module = ?MODULE, + function = list_certificates, + args = [], + result = {certificates, + {list, {certificate, + {tuple, [{domain, string}, + {file, string}, + {used, string}]}}}} + }, + #ejabberd_commands{ + name = revoke_certificate, + tags = [acme], + desc = "Revokes the selected ACME certificate", + module = ?MODULE, + function = revoke_certificate, + args_desc = ["Filename of the certificate"], + args = [{file, string}], + result = {res, restuple} + }]. + -spec request_certificate(iodata()) -> {ok | error, string()}. request_certificate(Arg) -> Ret = case lists:filter( - fun(S) -> S /= <<>> end, - re:split(Arg, "[\\h,;]+", [{return, binary}])) of - [<<"all">>] -> - case auto_domains() of - [] -> {error, no_auto_hosts}; - Domains -> - gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) - end; - [_|_] = Domains -> - case lists:dropwhile( - fun(D) -> - try ejabberd_router:is_my_route(D) of - true -> not is_ip_or_localhost(D); - false -> false - catch _:{invalid_domain, _} -> false - end - end, Domains) of - [Bad|_] -> - {error, {invalid_host, Bad}}; - [] -> - gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) - end; - [] -> - {error, invalid_argument} - end, + fun(S) -> S /= <<>> end, + re:split(Arg, "[\\h,;]+", [{return, binary}])) of + [<<"all">>] -> + case auto_domains() of + [] -> {error, no_auto_hosts}; + Domains -> + gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) + end; + [_ | _] = Domains -> + case lists:dropwhile( + fun(D) -> + try ejabberd_router:is_my_route(D) of + true -> not is_ip_or_localhost(D); + false -> false + catch + _:{invalid_domain, _} -> false + end + end, + Domains) of + [Bad | _] -> + {error, {invalid_host, Bad}}; + [] -> + gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) + end; + [] -> + {error, invalid_argument} + end, case Ret of - ok -> {ok, ""}; - {error, Why} -> {error, format_error(Why)} + ok -> {ok, ""}; + {error, Why} -> {error, format_error(Why)} end. + -spec revoke_certificate(iodata()) -> {ok | error, string()}. revoke_certificate(Path0) -> Path = prep_path(Path0), Ret = case read_cert(Path) of - {ok, [Cert|_], Key} -> - gen_server:call(?MODULE, {revoke, Cert, Key, Path}, ?CALL_TIMEOUT); - {error, _} = Err -> - Err - end, + {ok, [Cert | _], Key} -> + gen_server:call(?MODULE, {revoke, Cert, Key, Path}, ?CALL_TIMEOUT); + {error, _} = Err -> + Err + end, case Ret of - ok -> {ok, ""}; - {error, Reason} -> {error, format_error(Reason)} + ok -> {ok, ""}; + {error, Reason} -> {error, format_error(Reason)} end. + -spec list_certificates() -> [{binary(), binary(), boolean()}]. list_certificates() -> Known = lists:flatmap( - fun(Path) -> - try - {ok, [Cert|_], _} = read_cert(Path), - Domains = pkix:extract_domains(Cert), - [{Domain, Path} || Domain <- Domains] - catch _:{badmatch, _} -> - [] - end - end, list_certfiles()), + fun(Path) -> + try + {ok, [Cert | _], _} = read_cert(Path), + Domains = pkix:extract_domains(Cert), + [ {Domain, Path} || Domain <- Domains ] + catch + _:{badmatch, _} -> + [] + end + end, + list_certfiles()), Used = lists:foldl( - fun(Domain, S) -> - try - {ok, Path} = ejabberd_pkix:get_certfile_no_default(Domain), - {ok, [Cert|_], _} = read_cert(Path), - {ok, #{files := Files}} = pkix:get_cert_info(Cert), - lists:foldl(fun sets:add_element/2, - S, [{Domain, File} || {File, _} <- Files]) - catch _:{badmatch, _} -> - S - end - end, sets:new(), all_domains()), + fun(Domain, S) -> + try + {ok, Path} = ejabberd_pkix:get_certfile_no_default(Domain), + {ok, [Cert | _], _} = read_cert(Path), + {ok, #{files := Files}} = pkix:get_cert_info(Cert), + lists:foldl(fun sets:add_element/2, + S, + [ {Domain, File} || {File, _} <- Files ]) + catch + _:{badmatch, _} -> + S + end + end, + sets:new(), + all_domains()), lists:sort( lists:map( - fun({Domain, Path} = E) -> - {Domain, Path, sets:is_element(E, Used)} - end, Known)). + fun({Domain, Path} = E) -> + {Domain, Path, sets:is_element(E, Used)} + end, + Known)). + %%%=================================================================== %%% WebAdmin %%%=================================================================== + webadmin_menu_node(Acc, _Node, _Lang) -> Acc ++ [{<<"acme">>, <<"ACME">>}]. + webadmin_page_node(_, Node, #request{path = [<<"acme">>]} = R) -> Head = ?H1GLraw(<<"ACME Certificates">>, <<"admin/configuration/basic/#acme">>, <<"ACME">>), Set = [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [request_certificate, R]), @@ -571,97 +659,119 @@ webadmin_page_node(_, Node, #request{path = [<<"acme">>]} = R) -> {stop, Head ++ Get ++ Set}; webadmin_page_node(Acc, _, _) -> Acc. + %%%=================================================================== %%% Other stuff %%%=================================================================== --spec all_domains() -> [binary(),...]. +-spec all_domains() -> [binary(), ...]. all_domains() -> ejabberd_option:hosts() ++ ejabberd_router:get_all_routes(). + -spec auto_domains() -> [binary()]. auto_domains() -> lists:filter( fun(Host) -> - not is_ip_or_localhost(Host) - end, all_domains()). + not is_ip_or_localhost(Host) + end, + all_domains()). + -spec directory_url() -> binary(). directory_url() -> maps:get(ca_url, ejabberd_option:acme(), default_directory_url()). + -spec debug_fun() -> fun((string(), list()) -> ok). debug_fun() -> fun(Fmt, Args) -> ?DEBUG(Fmt, Args) end. + -spec request_on_start() -> false | {true, [binary()]}. request_on_start() -> Config = ejabberd_option:acme(), case maps:get(auto, Config, true) of - false -> false; - true -> - case ejabberd_listener:tls_listeners() of - [] -> false; - _ -> - case lists:filter( - fun(Host) -> - not (have_cert_for_domain(Host) - orelse is_ip_or_localhost(Host)) - end, auto_domains()) of - [] -> false; - Hosts -> - case have_acme_listener() of - true -> {true, Hosts}; - false -> - ?WARNING_MSG( - "No HTTP listeners for ACME challenges " - "are configured, automatic " - "certificate requests are aborted. Hint: " - "configure the listener and restart/reload " - "ejabberd. Or set acme->auto option to " - "`false` to suppress this warning.", - []), - false - end - end - end + false -> false; + true -> + case ejabberd_listener:tls_listeners() of + [] -> false; + _ -> + case lists:filter( + fun(Host) -> + not (have_cert_for_domain(Host) orelse + is_ip_or_localhost(Host)) + end, + auto_domains()) of + [] -> false; + Hosts -> + case have_acme_listener() of + true -> {true, Hosts}; + false -> + ?WARNING_MSG( + "No HTTP listeners for ACME challenges " + "are configured, automatic " + "certificate requests are aborted. Hint: " + "configure the listener and restart/reload " + "ejabberd. Or set acme->auto option to " + "`false` to suppress this warning.", + []), + false + end + end + end end. + well_known() -> [<<".well-known">>, <<"acme-challenge">>]. + -spec have_cert_for_domain(binary()) -> boolean(). have_cert_for_domain(Host) -> ejabberd_pkix:get_certfile_no_default(Host) /= error. + -spec is_ip_or_localhost(binary()) -> boolean(). is_ip_or_localhost(Host) -> Parts = binary:split(Host, <<".">>), TLD = binary_to_list(lists:last(Parts)), case inet:parse_address(TLD) of - {ok, _} -> true; - _ -> TLD == "localhost" + {ok, _} -> true; + _ -> TLD == "localhost" end. + -spec have_acme_listener() -> boolean(). have_acme_listener() -> lists:any( - fun({_, ejabberd_http, #{tls := false, - request_handlers := Handlers}}) -> - lists:keymember(well_known(), 1, Handlers); - (_) -> - false - end, ejabberd_option:listen()). + fun({_, + ejabberd_http, + #{ + tls := false, + request_handlers := Handlers + }}) -> + lists:keymember(well_known(), 1, Handlers); + (_) -> + false + end, + ejabberd_option:listen()). + -spec check_idna([binary()]) -> {ok, [string()]} | {error, {idna_failed, binary()}}. check_idna(Domains) -> lists:foldl( fun(D, {ok, Ds}) -> - try {ok, [idna:utf8_to_ascii(D)|Ds]} - catch _:_ -> {error, {idna_failed, D}} - end; - (_, Err) -> - Err - end, {ok, []}, Domains). + try + {ok, [idna:utf8_to_ascii(D) | Ds]} + catch + _:_ -> {error, {idna_failed, D}} + end; + (_, Err) -> + Err + end, + {ok, []}, + Domains). + -spec format_error(term()) -> string(). format_error({file, Reason}) -> diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 5ec0ac051..8028d8f09 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -29,69 +29,96 @@ -behaviour(gen_server). -export([start_link/0, - %% Server - status/0, stop/0, restart/0, - reopen_log/0, rotate_log/0, - set_loglevel/1, - evacuate_kindly/2, - stop_kindly/2, send_service_message_all_mucs/2, - registered_vhosts/0, - reload_config/0, - dump_config/1, - convert_to_yaml/2, - %% Cluster - join_cluster/1, leave_cluster/1, + %% Server + status/0, + stop/0, + restart/0, + reopen_log/0, + rotate_log/0, + set_loglevel/1, + evacuate_kindly/2, + stop_kindly/2, + send_service_message_all_mucs/2, + registered_vhosts/0, + reload_config/0, + dump_config/1, + convert_to_yaml/2, + %% Cluster + join_cluster/1, + leave_cluster/1, join_cluster_here/1, - list_cluster/0, list_cluster_detailed/0, - get_cluster_node_details3/0, - %% Erlang - update_list/0, update/1, update/0, - %% Accounts - register/3, unregister/2, - registered_users/1, - %% Migration jabberd1.4 - import_file/1, import_dir/1, - %% Purge DB - delete_expired_messages/0, delete_old_messages/1, - %% Mnesia - get_master/0, set_master/1, - backup_mnesia/1, restore_mnesia/1, - dump_mnesia/1, dump_table/2, load_mnesia/1, - mnesia_info/0, mnesia_table_info/1, - install_fallback_mnesia/1, - dump_to_textfile/1, dump_to_textfile/2, - mnesia_change_nodename/4, - restore/1, % Still used by some modules - clear_cache/0, - gc/0, - get_commands_spec/0, - delete_old_messages_batch/4, delete_old_messages_status/1, delete_old_messages_abort/1, - %% Internal - mnesia_list_tables/0, - mnesia_table_details/1, - mnesia_table_change_storage/2, - mnesia_table_clear/1, - mnesia_table_delete/1, - echo/1, echo3/3]). + list_cluster/0, + list_cluster_detailed/0, + get_cluster_node_details3/0, + %% Erlang + update_list/0, + update/1, update/0, + %% Accounts + register/3, + unregister/2, + registered_users/1, + %% Migration jabberd1.4 + import_file/1, + import_dir/1, + %% Purge DB + delete_expired_messages/0, + delete_old_messages/1, + %% Mnesia + get_master/0, + set_master/1, + backup_mnesia/1, + restore_mnesia/1, + dump_mnesia/1, + dump_table/2, + load_mnesia/1, + mnesia_info/0, + mnesia_table_info/1, + install_fallback_mnesia/1, + dump_to_textfile/1, dump_to_textfile/2, + mnesia_change_nodename/4, + restore/1, % Still used by some modules + clear_cache/0, + gc/0, + get_commands_spec/0, + delete_old_messages_batch/4, + delete_old_messages_status/1, + delete_old_messages_abort/1, + %% Internal + mnesia_list_tables/0, + mnesia_table_details/1, + mnesia_table_change_storage/2, + mnesia_table_clear/1, + mnesia_table_delete/1, + echo/1, + echo3/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). --export([web_menu_main/2, web_page_main/2, - web_menu_node/3, web_page_node/3]). +-export([web_menu_main/2, + web_page_main/2, + web_menu_node/3, + web_page_node/3]). -include_lib("xmpp/include/xmpp.hrl"). + -include("ejabberd_commands.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("logger.hrl"). --include("translate.hrl"). %+++ TODO +-include("translate.hrl"). %+++ TODO -record(state, {}). + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + init([]) -> process_flag(trap_exit, true), ejabberd_commands:register_commands(get_commands_spec()), @@ -101,18 +128,22 @@ init([]) -> ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50), {ok, #state{}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), @@ -120,155 +151,234 @@ terminate(_Reason, _State) -> ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50), ejabberd_commands:unregister_commands(get_commands_spec()). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%% %%% ejabberd commands %%% + get_commands_spec() -> [ %% The commands status, stop and restart are implemented also in ejabberd_ctl %% They are defined here so that other interfaces can use them too - #ejabberd_commands{name = status, tags = [server], - desc = "Get status of the ejabberd server", - module = ?MODULE, function = status, - result_desc = "Result tuple", - result_example = {ok, <<"The node ejabberd@localhost is started with status: started" - "ejabberd X.X is running in that node">>}, - args = [], result = {res, restuple}}, - #ejabberd_commands{name = stop, tags = [server], - desc = "Stop ejabberd gracefully", - module = ?MODULE, function = stop, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = halt, tags = [server], - desc = "Halt ejabberd abruptly with status code 1", - note = "added in 23.10", - module = ejabberd, function = halt, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = restart, tags = [server], - desc = "Restart ejabberd gracefully", - module = ?MODULE, function = restart, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = reopen_log, tags = [logs], - desc = "Reopen maybe the log files after being renamed", - longdesc = "Has no effect on ejabberd main log files, " - "only on log files generated by some modules.\n" - "This can be useful when an external tool is " - "used for log rotation. See " - "_`../../admin/guide/troubleshooting.md#log-files|Log Files`_.", - policy = admin, - module = ?MODULE, function = reopen_log, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = rotate_log, tags = [logs], - desc = "Rotate maybe log file of some module", - longdesc = "Has no effect on ejabberd main log files, " - "only on log files generated by some modules.", - module = ?MODULE, function = rotate_log, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = evacuate_kindly, tags = [server], - desc = "Evacuate kindly all users (kick and prevent login)", - longdesc = "Inform users and rooms, don't allow login, wait, " - "restart the server, and don't allow new logins.\n" - "Provide the delay in seconds, and the " - "announcement quoted, for example: \n" - "`ejabberdctl evacuate_kindly 60 " - "\\\"The server will stop in one minute.\\\"`", - note = "added in 24.12", - module = ?MODULE, function = evacuate_kindly, - args_desc = ["Seconds to wait", "Announcement to send, with quotes"], - args_example = [60, <<"Server will stop now.">>], - args = [{delay, integer}, {announcement, string}], - result = {res, rescode}}, - #ejabberd_commands{name = stop_kindly, tags = [server], - desc = "Stop kindly the server (informing users)", - longdesc = "Inform users and rooms, wait, and stop the server.\n" - "Provide the delay in seconds, and the " - "announcement quoted, for example: \n" - "`ejabberdctl stop_kindly 60 " - "\\\"The server will stop in one minute.\\\"`", - module = ?MODULE, function = stop_kindly, - args_desc = ["Seconds to wait", "Announcement to send, with quotes"], - args_example = [60, <<"Server will stop now.">>], - args = [{delay, integer}, {announcement, string}], - result = {res, rescode}}, - #ejabberd_commands{name = get_loglevel, tags = [logs], - desc = "Get the current loglevel", - module = ejabberd_logger, function = get, - result_desc = "Tuple with the log level number, its keyword and description", - result_example = warning, - args = [], - result = {levelatom, atom}}, - #ejabberd_commands{name = set_loglevel, tags = [logs], - desc = "Set the loglevel", - longdesc = "Possible loglevels: `none`, `emergency`, `alert`, `critical`, + #ejabberd_commands{ + name = status, + tags = [server], + desc = "Get status of the ejabberd server", + module = ?MODULE, + function = status, + result_desc = "Result tuple", + result_example = {ok, <<"The node ejabberd@localhost is started with status: started" + "ejabberd X.X is running in that node">>}, + args = [], + result = {res, restuple} + }, + #ejabberd_commands{ + name = stop, + tags = [server], + desc = "Stop ejabberd gracefully", + module = ?MODULE, + function = stop, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = halt, + tags = [server], + desc = "Halt ejabberd abruptly with status code 1", + note = "added in 23.10", + module = ejabberd, + function = halt, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = restart, + tags = [server], + desc = "Restart ejabberd gracefully", + module = ?MODULE, + function = restart, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = reopen_log, + tags = [logs], + desc = "Reopen maybe the log files after being renamed", + longdesc = "Has no effect on ejabberd main log files, " + "only on log files generated by some modules.\n" + "This can be useful when an external tool is " + "used for log rotation. See " + "_`../../admin/guide/troubleshooting.md#log-files|Log Files`_.", + policy = admin, + module = ?MODULE, + function = reopen_log, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = rotate_log, + tags = [logs], + desc = "Rotate maybe log file of some module", + longdesc = "Has no effect on ejabberd main log files, " + "only on log files generated by some modules.", + module = ?MODULE, + function = rotate_log, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = evacuate_kindly, + tags = [server], + desc = "Evacuate kindly all users (kick and prevent login)", + longdesc = "Inform users and rooms, don't allow login, wait, " + "restart the server, and don't allow new logins.\n" + "Provide the delay in seconds, and the " + "announcement quoted, for example: \n" + "`ejabberdctl evacuate_kindly 60 " + "\\\"The server will stop in one minute.\\\"`", + note = "added in 24.12", + module = ?MODULE, + function = evacuate_kindly, + args_desc = ["Seconds to wait", "Announcement to send, with quotes"], + args_example = [60, <<"Server will stop now.">>], + args = [{delay, integer}, {announcement, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = stop_kindly, + tags = [server], + desc = "Stop kindly the server (informing users)", + longdesc = "Inform users and rooms, wait, and stop the server.\n" + "Provide the delay in seconds, and the " + "announcement quoted, for example: \n" + "`ejabberdctl stop_kindly 60 " + "\\\"The server will stop in one minute.\\\"`", + module = ?MODULE, + function = stop_kindly, + args_desc = ["Seconds to wait", "Announcement to send, with quotes"], + args_example = [60, <<"Server will stop now.">>], + args = [{delay, integer}, {announcement, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = get_loglevel, + tags = [logs], + desc = "Get the current loglevel", + module = ejabberd_logger, + function = get, + result_desc = "Tuple with the log level number, its keyword and description", + result_example = warning, + args = [], + result = {levelatom, atom} + }, + #ejabberd_commands{ + name = set_loglevel, + tags = [logs], + desc = "Set the loglevel", + longdesc = "Possible loglevels: `none`, `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, `info`, `debug`.", - module = ?MODULE, function = set_loglevel, - args_desc = ["Desired logging level"], - args_example = ["debug"], - args = [{loglevel, string}], - result = {res, rescode}}, + module = ?MODULE, + function = set_loglevel, + args_desc = ["Desired logging level"], + args_example = ["debug"], + args = [{loglevel, string}], + result = {res, rescode} + }, - #ejabberd_commands{name = update_list, tags = [server], - desc = "List modified modules that can be updated", - module = ?MODULE, function = update_list, - args = [], - result_example = ["mod_configure", "mod_vcard"], - result = {modules, {list, {module, string}}}}, - #ejabberd_commands{name = update, tags = [server], - desc = "Update the given module", - longdesc = "To update all the possible modules, use `all`.", - note = "improved in 24.10", - module = ?MODULE, function = update, - args_example = ["all"], - args = [{module, string}], - result_example = {ok, <<"Updated modules: mod_configure, mod_vcard">>}, - result = {res, restuple}}, + #ejabberd_commands{ + name = update_list, + tags = [server], + desc = "List modified modules that can be updated", + module = ?MODULE, + function = update_list, + args = [], + result_example = ["mod_configure", "mod_vcard"], + result = {modules, {list, {module, string}}} + }, + #ejabberd_commands{ + name = update, + tags = [server], + desc = "Update the given module", + longdesc = "To update all the possible modules, use `all`.", + note = "improved in 24.10", + module = ?MODULE, + function = update, + args_example = ["all"], + args = [{module, string}], + result_example = {ok, <<"Updated modules: mod_configure, mod_vcard">>}, + result = {res, restuple} + }, - #ejabberd_commands{name = register, tags = [accounts], - desc = "Register a user", - policy = admin, - module = ?MODULE, function = register, - args_desc = ["Username", "Local vhost served by ejabberd", "Password"], - args_example = [<<"bob">>, <<"example.com">>, <<"SomEPass44">>], - args = [{user, binary}, {host, binary}, {password, binary}], - 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"], - args_example = [<<"bob">>, <<"example.com">>], - args = [{user, binary}, {host, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = registered_users, tags = [accounts], - desc = "List all registered users in HOST", - module = ?MODULE, function = registered_users, - args_desc = ["Local vhost"], - args_example = [<<"example.com">>], - result_desc = "List of registered accounts usernames", - result_example = [<<"user1">>, <<"user2">>], - args = [{host, binary}], - result = {users, {list, {username, string}}}}, - #ejabberd_commands{name = registered_vhosts, tags = [server], - desc = "List all registered vhosts in SERVER", - module = ?MODULE, function = registered_vhosts, - result_desc = "List of available vhosts", - result_example = [<<"example.com">>, <<"anon.example.com">>], - args = [], - result = {vhosts, {list, {vhost, string}}}}, - #ejabberd_commands{name = reload_config, tags = [config], - desc = "Reload config file in memory", - module = ?MODULE, function = reload_config, - args = [], - result = {res, rescode}}, + #ejabberd_commands{ + name = register, + tags = [accounts], + desc = "Register a user", + policy = admin, + module = ?MODULE, + function = register, + args_desc = ["Username", "Local vhost served by ejabberd", "Password"], + args_example = [<<"bob">>, <<"example.com">>, <<"SomEPass44">>], + args = [{user, binary}, {host, binary}, {password, binary}], + 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"], + args_example = [<<"bob">>, <<"example.com">>], + args = [{user, binary}, {host, binary}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = registered_users, + tags = [accounts], + desc = "List all registered users in HOST", + module = ?MODULE, + function = registered_users, + args_desc = ["Local vhost"], + args_example = [<<"example.com">>], + result_desc = "List of registered accounts usernames", + result_example = [<<"user1">>, <<"user2">>], + args = [{host, binary}], + result = {users, {list, {username, string}}} + }, + #ejabberd_commands{ + name = registered_vhosts, + tags = [server], + desc = "List all registered vhosts in SERVER", + module = ?MODULE, + function = registered_vhosts, + result_desc = "List of available vhosts", + result_example = [<<"example.com">>, <<"anon.example.com">>], + args = [], + result = {vhosts, {list, {vhost, string}}} + }, + #ejabberd_commands{ + name = reload_config, + tags = [config], + desc = "Reload config file in memory", + module = ?MODULE, + function = reload_config, + args = [], + result = {res, rescode} + }, - #ejabberd_commands{name = join_cluster, tags = [cluster], - desc = "Join our local node into the cluster handled by Node", - longdesc = "This command returns immediately, + #ejabberd_commands{ + name = join_cluster, + tags = [cluster], + desc = "Join our local node into the cluster handled by Node", + longdesc = "This command returns immediately, even before the joining process has completed. Consequently, if you are using `ejabberdctl` (or some `CTL_ON_` container @@ -280,435 +390,664 @@ get_commands_spec() -> clustering process has completed before proceeding. For example: `join_cluster ejabberd@main` > `started` > `list_cluster`.", - note = "improved in 24.06", - module = ?MODULE, function = join_cluster, - args_desc = ["Nodename of the node to join"], - args_example = [<<"ejabberd1@machine7">>], - args = [{node, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = join_cluster_here, tags = [cluster], - desc = "Join a remote Node here, into our cluster", - note = "added in 24.06", - module = ?MODULE, function = join_cluster_here, - args_desc = ["Nodename of the node to join here"], - args_example = [<<"ejabberd1@machine7">>], - args = [{node, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = leave_cluster, tags = [cluster], - desc = "Remove and shutdown Node from the running cluster", - longdesc = "This command can be run from any running " - "node of the cluster, even the node to be removed. " - "In the removed node, this command works only when " - "using ejabberdctl, not _`mod_http_api`_ or other code that " - "runs inside the same ejabberd node that will leave.", - module = ?MODULE, function = leave_cluster, - args_desc = ["Nodename of the node to kick from the cluster"], - args_example = [<<"ejabberd1@machine8">>], - args = [{node, binary}], - result = {res, rescode}}, + note = "improved in 24.06", + module = ?MODULE, + function = join_cluster, + args_desc = ["Nodename of the node to join"], + args_example = [<<"ejabberd1@machine7">>], + args = [{node, binary}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = join_cluster_here, + tags = [cluster], + desc = "Join a remote Node here, into our cluster", + note = "added in 24.06", + module = ?MODULE, + function = join_cluster_here, + args_desc = ["Nodename of the node to join here"], + args_example = [<<"ejabberd1@machine7">>], + args = [{node, binary}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = leave_cluster, + tags = [cluster], + desc = "Remove and shutdown Node from the running cluster", + longdesc = "This command can be run from any running " + "node of the cluster, even the node to be removed. " + "In the removed node, this command works only when " + "using ejabberdctl, not _`mod_http_api`_ or other code that " + "runs inside the same ejabberd node that will leave.", + module = ?MODULE, + function = leave_cluster, + args_desc = ["Nodename of the node to kick from the cluster"], + args_example = [<<"ejabberd1@machine8">>], + args = [{node, binary}], + result = {res, rescode} + }, - #ejabberd_commands{name = list_cluster, tags = [cluster], - desc = "List running nodes that are part of this cluster", - module = ?MODULE, function = list_cluster, - result_example = [ejabberd1@machine7, ejabberd1@machine8], - args = [], - result = {nodes, {list, {node, atom}}}}, - #ejabberd_commands{name = list_cluster_detailed, tags = [cluster], - desc = "List nodes (both running and known) and some stats", - note = "added in 24.06", - module = ?MODULE, function = list_cluster_detailed, - args = [], - result_example = [{'ejabberd@localhost', "true", - "The node ejabberd is started. Status...", - 7, 348, 60, none}], - result = {nodes, {list, {node, {tuple, [{name, atom}, - {running, string}, - {status, string}, - {online_users, integer}, - {processes, integer}, - {uptime_seconds, integer}, - {master_node, atom} - ]}}}}}, + #ejabberd_commands{ + name = list_cluster, + tags = [cluster], + desc = "List running nodes that are part of this cluster", + module = ?MODULE, + function = list_cluster, + result_example = [ejabberd1@machine7, ejabberd1@machine8], + args = [], + result = {nodes, {list, {node, atom}}} + }, + #ejabberd_commands{ + name = list_cluster_detailed, + tags = [cluster], + desc = "List nodes (both running and known) and some stats", + note = "added in 24.06", + module = ?MODULE, + function = list_cluster_detailed, + args = [], + result_example = [{'ejabberd@localhost', "true", + "The node ejabberd is started. Status...", + 7, 348, 60, none}], + result = {nodes, {list, {node, {tuple, [{name, atom}, + {running, string}, + {status, string}, + {online_users, integer}, + {processes, integer}, + {uptime_seconds, integer}, + {master_node, atom}]}}}} + }, - #ejabberd_commands{name = import_file, tags = [mnesia], - desc = "Import user data from jabberd14 spool file", - module = ?MODULE, function = import_file, - args_desc = ["Full path to the jabberd14 spool file"], - args_example = ["/var/lib/ejabberd/jabberd14.spool"], - args = [{file, string}], result = {res, restuple}}, - #ejabberd_commands{name = import_dir, tags = [mnesia], - desc = "Import users data from jabberd14 spool dir", - module = ?MODULE, function = import_dir, - args_desc = ["Full path to the jabberd14 spool directory"], - args_example = ["/var/lib/ejabberd/jabberd14/"], - args = [{file, string}], - result = {res, restuple}}, - #ejabberd_commands{name = import_piefxis, tags = [mnesia], - desc = "Import users data from a PIEFXIS file (XEP-0227)", - module = ejabberd_piefxis, function = import_file, - args_desc = ["Full path to the PIEFXIS file"], - args_example = ["/var/lib/ejabberd/example.com.xml"], - args = [{file, binary}], result = {res, rescode}}, - #ejabberd_commands{name = export_piefxis, tags = [mnesia], - desc = "Export data of all users in the server to PIEFXIS files (XEP-0227)", - module = ejabberd_piefxis, function = export_server, - args_desc = ["Full path to a directory"], - args_example = ["/var/lib/ejabberd/"], - args = [{dir, binary}], result = {res, rescode}}, - #ejabberd_commands{name = export_piefxis_host, tags = [mnesia], - desc = "Export data of users in a host to PIEFXIS files (XEP-0227)", - module = ejabberd_piefxis, function = export_host, - args_desc = ["Full path to a directory", "Vhost to export"], - args_example = ["/var/lib/ejabberd/", "example.com"], - args = [{dir, binary}, {host, binary}], result = {res, rescode}}, + #ejabberd_commands{ + name = import_file, + tags = [mnesia], + desc = "Import user data from jabberd14 spool file", + module = ?MODULE, + function = import_file, + args_desc = ["Full path to the jabberd14 spool file"], + args_example = ["/var/lib/ejabberd/jabberd14.spool"], + args = [{file, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = import_dir, + tags = [mnesia], + desc = "Import users data from jabberd14 spool dir", + module = ?MODULE, + function = import_dir, + args_desc = ["Full path to the jabberd14 spool directory"], + args_example = ["/var/lib/ejabberd/jabberd14/"], + args = [{file, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = import_piefxis, + tags = [mnesia], + desc = "Import users data from a PIEFXIS file (XEP-0227)", + module = ejabberd_piefxis, + function = import_file, + args_desc = ["Full path to the PIEFXIS file"], + args_example = ["/var/lib/ejabberd/example.com.xml"], + args = [{file, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = export_piefxis, + tags = [mnesia], + desc = "Export data of all users in the server to PIEFXIS files (XEP-0227)", + module = ejabberd_piefxis, + function = export_server, + args_desc = ["Full path to a directory"], + args_example = ["/var/lib/ejabberd/"], + args = [{dir, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = export_piefxis_host, + tags = [mnesia], + desc = "Export data of users in a host to PIEFXIS files (XEP-0227)", + module = ejabberd_piefxis, + function = export_host, + args_desc = ["Full path to a directory", "Vhost to export"], + args_example = ["/var/lib/ejabberd/", "example.com"], + args = [{dir, binary}, {host, binary}], + result = {res, rescode} + }, - #ejabberd_commands{name = delete_mnesia, tags = [mnesia], - desc = "Delete elements in Mnesia database for a given vhost", - module = ejd2sql, function = delete, - args_desc = ["Vhost which content will be deleted in Mnesia database"], - args_example = ["example.com"], - args = [{host, string}], result = {res, rescode}}, - #ejabberd_commands{name = convert_to_scram, tags = [sql], - desc = "Convert the passwords of users to SCRAM", - module = ejabberd_auth, function = convert_to_scram, - args_desc = ["Vhost which users' passwords will be scrammed"], - args_example = ["example.com"], - args = [{host, binary}], result = {res, rescode}}, - #ejabberd_commands{name = import_prosody, tags = [mnesia, sql], - desc = "Import data from Prosody", - longdesc = "Note: this requires ejabberd to be " - "compiled with `./configure --enable-lua` " - "(which installs the `luerl` library).", - module = prosody2ejabberd, function = from_dir, - args_desc = ["Full path to the Prosody data directory"], - args_example = ["/var/lib/prosody/datadump/"], - args = [{dir, string}], result = {res, rescode}}, + #ejabberd_commands{ + name = delete_mnesia, + tags = [mnesia], + desc = "Delete elements in Mnesia database for a given vhost", + module = ejd2sql, + function = delete, + args_desc = ["Vhost which content will be deleted in Mnesia database"], + args_example = ["example.com"], + args = [{host, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = convert_to_scram, + tags = [sql], + desc = "Convert the passwords of users to SCRAM", + module = ejabberd_auth, + function = convert_to_scram, + args_desc = ["Vhost which users' passwords will be scrammed"], + args_example = ["example.com"], + args = [{host, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = import_prosody, + tags = [mnesia, sql], + desc = "Import data from Prosody", + longdesc = "Note: this requires ejabberd to be " + "compiled with `./configure --enable-lua` " + "(which installs the `luerl` library).", + module = prosody2ejabberd, + function = from_dir, + args_desc = ["Full path to the Prosody data directory"], + args_example = ["/var/lib/prosody/datadump/"], + args = [{dir, string}], + result = {res, rescode} + }, - #ejabberd_commands{name = convert_to_yaml, tags = [config], - desc = "Convert the input file from Erlang to YAML format", - module = ?MODULE, function = convert_to_yaml, - args_desc = ["Full path to the original configuration file", "And full path to final file"], - args_example = ["/etc/ejabberd/ejabberd.cfg", "/etc/ejabberd/ejabberd.yml"], - args = [{in, string}, {out, string}], - result = {res, rescode}}, - #ejabberd_commands{name = dump_config, tags = [config], - desc = "Dump configuration in YAML format as seen by ejabberd", - module = ?MODULE, function = dump_config, - args_desc = ["Full path to output file"], - args_example = ["/tmp/ejabberd.yml"], - args = [{out, string}], - result = {res, rescode}}, + #ejabberd_commands{ + name = convert_to_yaml, + tags = [config], + desc = "Convert the input file from Erlang to YAML format", + module = ?MODULE, + function = convert_to_yaml, + args_desc = ["Full path to the original configuration file", "And full path to final file"], + args_example = ["/etc/ejabberd/ejabberd.cfg", "/etc/ejabberd/ejabberd.yml"], + args = [{in, string}, {out, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = dump_config, + tags = [config], + desc = "Dump configuration in YAML format as seen by ejabberd", + module = ?MODULE, + function = dump_config, + args_desc = ["Full path to output file"], + args_example = ["/tmp/ejabberd.yml"], + args = [{out, string}], + result = {res, rescode} + }, - #ejabberd_commands{name = delete_expired_messages, tags = [offline, purge], - desc = "Delete expired offline messages from database", - module = ?MODULE, function = delete_expired_messages, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = delete_old_messages, tags = [offline, purge], - desc = "Delete offline messages older than DAYS", - module = ?MODULE, function = delete_old_messages, - args_desc = ["Number of days"], - args_example = [31], - args = [{days, integer}], result = {res, rescode}}, - #ejabberd_commands{name = delete_old_messages_batch, tags = [offline, purge], - desc = "Delete offline messages older than DAYS", - note = "added in 22.05", - module = ?MODULE, function = delete_old_messages_batch, - args_desc = ["Name of host where messages should be deleted", - "Days to keep messages", - "Number of messages to delete per batch", - "Desired rate of messages to delete per minute"], - args_example = [<<"localhost">>, 31, 1000, 10000], - args = [{host, binary}, {days, integer}, {batch_size, integer}, {rate, integer}], - result = {res, restuple}, - result_desc = "Result tuple", - result_example = {ok, <<"Removal of 5000 messages in progress">>}}, - #ejabberd_commands{name = delete_old_messages_status, tags = [offline, purge], - desc = "Status of delete old offline messages operation", - note = "added in 22.05", - module = ?MODULE, function = delete_old_messages_status, - args_desc = ["Name of host where messages should be deleted"], - args_example = [<<"localhost">>], - args = [{host, binary}], - result = {status, string}, - result_desc = "Status test", - result_example = "Operation in progress, delete 5000 messages"}, - #ejabberd_commands{name = abort_delete_old_messages, tags = [offline, purge], - desc = "Abort currently running delete old offline messages operation", - note = "added in 22.05", - module = ?MODULE, function = delete_old_messages_abort, - args_desc = ["Name of host where operation should be aborted"], - args_example = [<<"localhost">>], - args = [{host, binary}], - result = {status, string}, - result_desc = "Status text", - result_example = "Operation aborted"}, + #ejabberd_commands{ + name = delete_expired_messages, + tags = [offline, purge], + desc = "Delete expired offline messages from database", + module = ?MODULE, + function = delete_expired_messages, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = delete_old_messages, + tags = [offline, purge], + desc = "Delete offline messages older than DAYS", + module = ?MODULE, + function = delete_old_messages, + args_desc = ["Number of days"], + args_example = [31], + args = [{days, integer}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = delete_old_messages_batch, + tags = [offline, purge], + desc = "Delete offline messages older than DAYS", + note = "added in 22.05", + module = ?MODULE, + function = delete_old_messages_batch, + args_desc = ["Name of host where messages should be deleted", + "Days to keep messages", + "Number of messages to delete per batch", + "Desired rate of messages to delete per minute"], + args_example = [<<"localhost">>, 31, 1000, 10000], + args = [{host, binary}, {days, integer}, {batch_size, integer}, {rate, integer}], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"Removal of 5000 messages in progress">>} + }, + #ejabberd_commands{ + name = delete_old_messages_status, + tags = [offline, purge], + desc = "Status of delete old offline messages operation", + note = "added in 22.05", + module = ?MODULE, + function = delete_old_messages_status, + args_desc = ["Name of host where messages should be deleted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status test", + result_example = "Operation in progress, delete 5000 messages" + }, + #ejabberd_commands{ + name = abort_delete_old_messages, + tags = [offline, purge], + desc = "Abort currently running delete old offline messages operation", + note = "added in 22.05", + module = ?MODULE, + function = delete_old_messages_abort, + args_desc = ["Name of host where operation should be aborted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status text", + result_example = "Operation aborted" + }, - #ejabberd_commands{name = export2sql, tags = [mnesia], - desc = "Export virtual host information from Mnesia tables to SQL file", - longdesc = "Configure the modules to use SQL, then call this command. " - "After correctly exported the database of a vhost, " - "you may want to delete from mnesia with " - "the _`delete_mnesia`_ API.", - module = ejd2sql, function = export, - args_desc = ["Vhost", "Full path to the destination SQL file"], - args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"], - args = [{host, string}, {file, string}], - result = {res, rescode}}, - #ejabberd_commands{name = get_master, tags = [cluster], - desc = "Get master node of the clustered Mnesia tables", - note = "added in 24.06", - longdesc = "If there is no master, returns `none`.", - module = ?MODULE, function = get_master, - result = {nodename, atom}}, - #ejabberd_commands{name = set_master, tags = [cluster], - desc = "Set master node of the clustered Mnesia tables", - longdesc = "If `nodename` is set to `self`, then this " - "node will be set as its own master.", - module = ?MODULE, function = set_master, - args_desc = ["Name of the erlang node that will be considered master of this node"], - args_example = ["ejabberd@machine7"], - args = [{nodename, string}], result = {res, restuple}}, - #ejabberd_commands{name = mnesia_change_nodename, tags = [mnesia], - desc = "Change the erlang node name in a backup file", - module = ?MODULE, function = mnesia_change_nodename, - args_desc = ["Name of the old erlang node", "Name of the new node", - "Path to old backup file", "Path to the new backup file"], - args_example = ["ejabberd@machine1", "ejabberd@machine2", - "/var/lib/ejabberd/old.backup", "/var/lib/ejabberd/new.backup"], - args = [{oldnodename, string}, {newnodename, string}, - {oldbackup, string}, {newbackup, string}], - result = {res, restuple}}, - #ejabberd_commands{name = backup, tags = [mnesia], - 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 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`_ API.", - 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 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 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 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`_ API and " - "_`install_fallback`_ API.", - module = ?MODULE, function = load_mnesia, - args_desc = ["Full path to the text file"], - args_example = ["/var/lib/ejabberd/database.txt"], - args = [{file, string}], result = {res, restuple}}, - #ejabberd_commands{name = mnesia_info, tags = [mnesia], - desc = "Dump info on global Mnesia state", - module = ?MODULE, function = mnesia_info, - args = [], result = {res, string}}, - #ejabberd_commands{name = mnesia_table_info, tags = [mnesia], - desc = "Dump info on Mnesia table state", - module = ?MODULE, function = mnesia_table_info, - args_desc = ["Mnesia table name"], - args_example = ["roster"], - args = [{table, string}], result = {res, string}}, - #ejabberd_commands{name = install_fallback, tags = [mnesia], - 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`_ API.", - module = ?MODULE, function = install_fallback_mnesia, - args_desc = ["Full path to the fallback file"], - args_example = ["/var/lib/ejabberd/database.fallback"], - args = [{file, string}], result = {res, restuple}}, - #ejabberd_commands{name = clear_cache, tags = [server], - desc = "Clear database cache on all nodes", - module = ?MODULE, function = clear_cache, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = gc, tags = [server], - desc = "Force full garbage collection", - note = "added in 20.01", - module = ?MODULE, function = gc, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = man, tags = [documentation], - desc = "Generate Unix manpage for current ejabberd version", - note = "added in 20.01", - module = ejabberd_doc, function = man, - args = [], result = {res, restuple}}, + #ejabberd_commands{ + name = export2sql, + tags = [mnesia], + desc = "Export virtual host information from Mnesia tables to SQL file", + longdesc = "Configure the modules to use SQL, then call this command. " + "After correctly exported the database of a vhost, " + "you may want to delete from mnesia with " + "the _`delete_mnesia`_ API.", + module = ejd2sql, + function = export, + args_desc = ["Vhost", "Full path to the destination SQL file"], + args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"], + args = [{host, string}, {file, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = get_master, + tags = [cluster], + desc = "Get master node of the clustered Mnesia tables", + note = "added in 24.06", + longdesc = "If there is no master, returns `none`.", + module = ?MODULE, + function = get_master, + result = {nodename, atom} + }, + #ejabberd_commands{ + name = set_master, + tags = [cluster], + desc = "Set master node of the clustered Mnesia tables", + longdesc = "If `nodename` is set to `self`, then this " + "node will be set as its own master.", + module = ?MODULE, + function = set_master, + args_desc = ["Name of the erlang node that will be considered master of this node"], + args_example = ["ejabberd@machine7"], + args = [{nodename, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = mnesia_change_nodename, + tags = [mnesia], + desc = "Change the erlang node name in a backup file", + module = ?MODULE, + function = mnesia_change_nodename, + args_desc = ["Name of the old erlang node", "Name of the new node", + "Path to old backup file", "Path to the new backup file"], + args_example = ["ejabberd@machine1", "ejabberd@machine2", + "/var/lib/ejabberd/old.backup", "/var/lib/ejabberd/new.backup"], + args = [{oldnodename, string}, + {newnodename, string}, + {oldbackup, string}, + {newbackup, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = backup, + tags = [mnesia], + 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 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`_ API.", + 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 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 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 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`_ API and " + "_`install_fallback`_ API.", + module = ?MODULE, + function = load_mnesia, + args_desc = ["Full path to the text file"], + args_example = ["/var/lib/ejabberd/database.txt"], + args = [{file, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = mnesia_info, + tags = [mnesia], + desc = "Dump info on global Mnesia state", + module = ?MODULE, + function = mnesia_info, + args = [], + result = {res, string} + }, + #ejabberd_commands{ + name = mnesia_table_info, + tags = [mnesia], + desc = "Dump info on Mnesia table state", + module = ?MODULE, + function = mnesia_table_info, + args_desc = ["Mnesia table name"], + args_example = ["roster"], + args = [{table, string}], + result = {res, string} + }, + #ejabberd_commands{ + name = install_fallback, + tags = [mnesia], + 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`_ API.", + module = ?MODULE, + function = install_fallback_mnesia, + args_desc = ["Full path to the fallback file"], + args_example = ["/var/lib/ejabberd/database.fallback"], + args = [{file, string}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = clear_cache, + tags = [server], + desc = "Clear database cache on all nodes", + module = ?MODULE, + function = clear_cache, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = gc, + tags = [server], + desc = "Force full garbage collection", + note = "added in 20.01", + module = ?MODULE, + function = gc, + args = [], + result = {res, rescode} + }, + #ejabberd_commands{ + name = man, + tags = [documentation], + desc = "Generate Unix manpage for current ejabberd version", + note = "added in 20.01", + module = ejabberd_doc, + function = man, + args = [], + result = {res, restuple} + }, - #ejabberd_commands{name = webadmin_host_user_queue, tags = [offline, internal], - desc = "Generate WebAdmin offline queue HTML", - module = mod_offline, function = webadmin_host_user_queue, - args = [{user, binary}, {host, binary}, {query, any}, {lang, binary}], - result = {res, any}}, + #ejabberd_commands{ + name = webadmin_host_user_queue, + tags = [offline, internal], + desc = "Generate WebAdmin offline queue HTML", + module = mod_offline, + function = webadmin_host_user_queue, + args = [{user, binary}, {host, binary}, {query, any}, {lang, binary}], + result = {res, any} + }, - #ejabberd_commands{name = webadmin_host_last_activity, tags = [internal], - desc = "Generate WebAdmin Last Activity HTML", - module = ejabberd_web_admin, function = webadmin_host_last_activity, - args = [{host, binary}, {query, any}, {lang, binary}], - result = {res, any}}, - #ejabberd_commands{name = webadmin_host_srg, tags = [internal], - desc = "Generate WebAdmin Shared Roster Group HTML", - module = mod_shared_roster, function = webadmin_host_srg, - args = [{host, binary}, {query, any}, {lang, binary}], - result = {res, any}}, - #ejabberd_commands{name = webadmin_host_srg_group, tags = [internal], - desc = "Generate WebAdmin Shared Roster Group HTML for a group", - module = mod_shared_roster, function = webadmin_host_srg_group, - args = [{host, binary}, {group, binary}, {query, any}, {lang, binary}], - result = {res, any}}, + #ejabberd_commands{ + name = webadmin_host_last_activity, + tags = [internal], + desc = "Generate WebAdmin Last Activity HTML", + module = ejabberd_web_admin, + function = webadmin_host_last_activity, + args = [{host, binary}, {query, any}, {lang, binary}], + result = {res, any} + }, + #ejabberd_commands{ + name = webadmin_host_srg, + tags = [internal], + desc = "Generate WebAdmin Shared Roster Group HTML", + module = mod_shared_roster, + function = webadmin_host_srg, + args = [{host, binary}, {query, any}, {lang, binary}], + result = {res, any} + }, + #ejabberd_commands{ + name = webadmin_host_srg_group, + tags = [internal], + desc = "Generate WebAdmin Shared Roster Group HTML for a group", + module = mod_shared_roster, + function = webadmin_host_srg_group, + args = [{host, binary}, {group, binary}, {query, any}, {lang, binary}], + result = {res, any} + }, - #ejabberd_commands{name = webadmin_node_contrib, tags = [internal], - desc = "Generate WebAdmin ejabberd-contrib HTML", - module = ext_mod, function = webadmin_node_contrib, - args = [{node, atom}, {query, any}, {lang, binary}], - result = {res, any}}, - #ejabberd_commands{name = webadmin_node_db, tags = [internal], - desc = "Generate WebAdmin Mnesia database HTML", - module = ejabberd_web_admin, function = webadmin_node_db, - args = [{node, atom}, {query, any}, {lang, binary}], - result = {res, any}}, - #ejabberd_commands{name = webadmin_node_db_table, tags = [internal], - desc = "Generate WebAdmin Mnesia database HTML for a table", - module = ejabberd_web_admin, function = webadmin_node_db_table, - args = [{node, atom}, {table, binary}, {lang, binary}], - result = {res, any}}, - #ejabberd_commands{name = webadmin_node_db_table_page, tags = [internal], - desc = "Generate WebAdmin Mnesia database HTML for a table content", - module = ejabberd_web_admin, function = webadmin_node_db_table_page, - args = [{node, atom}, {table, binary}, {page, integer}], - result = {res, any}}, + #ejabberd_commands{ + name = webadmin_node_contrib, + tags = [internal], + desc = "Generate WebAdmin ejabberd-contrib HTML", + module = ext_mod, + function = webadmin_node_contrib, + args = [{node, atom}, {query, any}, {lang, binary}], + result = {res, any} + }, + #ejabberd_commands{ + name = webadmin_node_db, + tags = [internal], + desc = "Generate WebAdmin Mnesia database HTML", + module = ejabberd_web_admin, + function = webadmin_node_db, + args = [{node, atom}, {query, any}, {lang, binary}], + result = {res, any} + }, + #ejabberd_commands{ + name = webadmin_node_db_table, + tags = [internal], + desc = "Generate WebAdmin Mnesia database HTML for a table", + module = ejabberd_web_admin, + function = webadmin_node_db_table, + args = [{node, atom}, {table, binary}, {lang, binary}], + result = {res, any} + }, + #ejabberd_commands{ + name = webadmin_node_db_table_page, + tags = [internal], + desc = "Generate WebAdmin Mnesia database HTML for a table content", + module = ejabberd_web_admin, + function = webadmin_node_db_table_page, + args = [{node, atom}, {table, binary}, {page, integer}], + result = {res, any} + }, - #ejabberd_commands{name = mnesia_list_tables, tags = [mnesia], - desc = "List of Mnesia tables", - note = "added in 25.03", - module = ?MODULE, function = mnesia_list_tables, - result = {tables, {list, {table, {tuple, [{name, atom}, - {storage_type, binary}, - {elements, integer}, - {memory_kb, integer}, - {memory_mb, integer} - ]}}}}}, - #ejabberd_commands{name = mnesia_table_details, tags = [internal, mnesia], - desc = "Get details of a Mnesia table", - module = ?MODULE, function = mnesia_table_details, - args = [{table, binary}], - result = {details, {list, {detail, {tuple, [{name, atom}, - {value, binary} - ]}}}}}, + #ejabberd_commands{ + name = mnesia_list_tables, + tags = [mnesia], + desc = "List of Mnesia tables", + note = "added in 25.03", + module = ?MODULE, + function = mnesia_list_tables, + result = {tables, {list, {table, {tuple, [{name, atom}, + {storage_type, binary}, + {elements, integer}, + {memory_kb, integer}, + {memory_mb, integer}]}}}} + }, + #ejabberd_commands{ + name = mnesia_table_details, + tags = [internal, mnesia], + desc = "Get details of a Mnesia table", + module = ?MODULE, + function = mnesia_table_details, + args = [{table, binary}], + result = {details, {list, {detail, {tuple, [{name, atom}, + {value, binary}]}}}} + }, + + #ejabberd_commands{ + name = mnesia_table_change_storage, + tags = [mnesia], + desc = "Change storage type of a Mnesia table", + note = "added in 25.03", + longdesc = "Storage type can be: `ram_copies`, `disc_copies`, `disc_only_copies`, `remote_copy`.", + module = ?MODULE, + function = mnesia_table_change_storage, + args = [{table, binary}, {storage_type, binary}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = mnesia_table_clear, + tags = [internal, mnesia], + desc = "Delete all content in a Mnesia table", + module = ?MODULE, + function = mnesia_table_clear, + args = [{table, binary}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = mnesia_table_destroy, + tags = [internal, mnesia], + desc = "Destroy a Mnesia table", + module = ?MODULE, + function = mnesia_table_destroy, + args = [{table, binary}], + result = {res, restuple} + }, + #ejabberd_commands{ + name = echo, + tags = [internal], + desc = "Return the same sentence that was provided", + module = ?MODULE, + function = echo, + args_desc = ["Sentence to echoe"], + args_example = [<<"Test Sentence">>], + args = [{sentence, binary}], + result = {sentence, string}, + result_example = "Test Sentence" + }, + #ejabberd_commands{ + name = echo3, + tags = [internal], + desc = "Return the same sentence that was provided", + module = ?MODULE, + function = echo3, + args_desc = ["First argument", "Second argument", "Sentence to echoe"], + args_example = [<<"example.com">>, <<"Group1">>, <<"Test Sentence">>], + args = [{first, binary}, {second, binary}, {sentence, binary}], + result = {sentence, string}, + result_example = "Test Sentence" + }]. - #ejabberd_commands{name = mnesia_table_change_storage, tags = [mnesia], - desc = "Change storage type of a Mnesia table", - note = "added in 25.03", - longdesc = "Storage type can be: `ram_copies`, `disc_copies`, `disc_only_copies`, `remote_copy`.", - module = ?MODULE, function = mnesia_table_change_storage, - args = [{table, binary}, {storage_type, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = mnesia_table_clear, tags = [internal, mnesia], - desc = "Delete all content in a Mnesia table", - module = ?MODULE, function = mnesia_table_clear, - args = [{table, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = mnesia_table_destroy, tags = [internal, mnesia], - desc = "Destroy a Mnesia table", - module = ?MODULE, function = mnesia_table_destroy, - args = [{table, binary}], - result = {res, restuple}}, - #ejabberd_commands{name = echo, tags = [internal], - desc = "Return the same sentence that was provided", - module = ?MODULE, function = echo, - args_desc = ["Sentence to echoe"], - args_example = [<<"Test Sentence">>], - args = [{sentence, binary}], - result = {sentence, string}, - result_example = "Test Sentence"}, - #ejabberd_commands{name = echo3, tags = [internal], - desc = "Return the same sentence that was provided", - module = ?MODULE, function = echo3, - args_desc = ["First argument", "Second argument", "Sentence to echoe"], - args_example = [<<"example.com">>, <<"Group1">>, <<"Test Sentence">>], - args = [{first, binary}, {second, binary}, {sentence, binary}], - result = {sentence, string}, - result_example = "Test Sentence"} - ]. %%% %%% Server management %%% + status() -> {InternalStatus, ProvidedStatus} = init:get_status(), String1 = io_lib:format("The node ~p is ~p. Status: ~p", - [node(), InternalStatus, ProvidedStatus]), + [node(), InternalStatus, ProvidedStatus]), {Is_running, String2} = - case lists:keysearch(ejabberd, 1, application:which_applications()) of - false -> - {ejabberd_not_running, "ejabberd is not running in that node."}; - {value, {_, _, Version}} -> - {ok, io_lib:format("ejabberd ~s is running in that node", [Version])} - end, - {Is_running, String1 ++ "\n" ++String2}. + case lists:keysearch(ejabberd, 1, application:which_applications()) of + false -> + {ejabberd_not_running, "ejabberd is not running in that node."}; + {value, {_, _, Version}} -> + {ok, io_lib:format("ejabberd ~s is running in that node", [Version])} + end, + {Is_running, String1 ++ "\n" ++ String2}. + stop() -> _ = supervisor:terminate_child(ejabberd_sup, ejabberd_sm), timer:sleep(1000), init:stop(). + restart() -> _ = supervisor:terminate_child(ejabberd_sup, ejabberd_sm), timer:sleep(1000), init:restart(). + reopen_log() -> ejabberd_hooks:run(reopen_log_hook, []). + rotate_log() -> ejabberd_hooks:run(rotate_log_hook, []). + set_loglevel(LogLevel) -> try binary_to_existing_atom(iolist_to_binary(LogLevel), latin1) of - Level -> - case lists:member(Level, ejabberd_logger:loglevels()) of - true -> - ejabberd_logger:set(Level); - false -> - {error, "Invalid log level"} - end - catch _:_ -> - {error, "Invalid log level"} + Level -> + case lists:member(Level, ejabberd_logger:loglevels()) of + true -> + ejabberd_logger:set(Level); + false -> + {error, "Invalid log level"} + end + catch + _:_ -> + {error, "Invalid log level"} end. + %%% %%% Stop Kindly %%% + evacuate_kindly(DelaySeconds, AnnouncementTextString) -> perform_kindly(DelaySeconds, AnnouncementTextString, evacuate). + stop_kindly(DelaySeconds, AnnouncementTextString) -> perform_kindly(DelaySeconds, AnnouncementTextString, stop). + perform_kindly(DelaySeconds, AnnouncementTextString, Action) -> Subject = str:format("Server stop in ~p seconds!", [DelaySeconds]), WaitingDesc = str:format("Waiting ~p seconds", [DelaySeconds]), @@ -737,41 +1076,46 @@ perform_kindly(DelaySeconds, AnnouncementTextString, Action) -> NumberLast = length(Steps), TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}), lists:foldl(fun({Desc, Mod, Func, Args}, NumberThis) -> - SecondsDiff = - calendar:datetime_to_gregorian_seconds({date(), time()}) - TimestampStart, - io:format("[~p/~p ~ps] ~ts... ", [NumberThis, NumberLast, SecondsDiff, Desc]), - Result = (catch apply(Mod, Func, Args)), - io:format("~p~n", [Result]), - NumberThis + 1 + SecondsDiff = + calendar:datetime_to_gregorian_seconds({date(), time()}) - TimestampStart, + io:format("[~p/~p ~ps] ~ts... ", [NumberThis, NumberLast, SecondsDiff, Desc]), + Result = (catch apply(Mod, Func, Args)), + io:format("~p~n", [Result]), + NumberThis + 1 end, 1, Steps), ok. + send_service_message_all_mucs(Subject, AnnouncementText) -> Message = str:format("~s~n~s", [Subject, AnnouncementText]), lists:foreach( fun(ServerHost) -> - MUCHosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), - lists:foreach( - fun(MUCHost) -> - mod_muc:broadcast_service_message(ServerHost, MUCHost, Message) - end, MUCHosts) + MUCHosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), + lists:foreach( + fun(MUCHost) -> + mod_muc:broadcast_service_message(ServerHost, MUCHost, Message) + end, + MUCHosts) end, ejabberd_option:hosts()). + %%% %%% ejabberd_update %%% + update_list() -> {ok, _Dir, UpdatedBeams, _Script, _LowLevelScript, _Check} = - ejabberd_update:update_info(), - [atom_to_list(Beam) || Beam <- UpdatedBeams]. + ejabberd_update:update_info(), + [ atom_to_list(Beam) || Beam <- UpdatedBeams ]. + update("all") -> - ResList = [{ModStr, update_module(ModStr)} || ModStr <- update_list()], - String = case string:join([Mod || {Mod, {ok, _}} <- ResList], ", ") of + ResList = [ {ModStr, update_module(ModStr)} || ModStr <- update_list() ], + String = case string:join([ Mod || {Mod, {ok, _}} <- ResList ], ", ") of [] -> "No modules updated"; ModulesString -> @@ -781,18 +1125,20 @@ update("all") -> update(ModStr) -> update_module(ModStr). + update_module(ModuleNameBin) when is_binary(ModuleNameBin) -> update_module(binary_to_list(ModuleNameBin)); update_module(ModuleNameString) -> ModuleName = list_to_atom(ModuleNameString), case ejabberd_update:update([ModuleName]) of - {ok, []} -> - {ok, "Not updated: "++ModuleNameString}; - {ok, [ModuleName]} -> - {ok, "Updated: "++ModuleNameString}; - {error, Reason} -> {error, Reason} + {ok, []} -> + {ok, "Not updated: " ++ ModuleNameString}; + {ok, [ModuleName]} -> + {ok, "Updated: " ++ ModuleNameString}; + {error, Reason} -> {error, Reason} end. + update() -> io:format("Compiling ejabberd...~n", []), os:cmd("make"), @@ -803,79 +1149,91 @@ update() -> io:format("Updated modules: ~p~n", [Mods2]), ok. + %%% %%% Account management %%% + register(User, Host, Password) -> case is_my_host(Host) of - true -> - case ejabberd_auth:try_register(User, Host, Password) of - ok -> - {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; - {error, exists} -> - Msg = io_lib:format("User ~s@~s already registered", [User, Host]), - {error, conflict, 10090, Msg}; - {error, Reason} -> - String = io_lib:format("Can't register user ~s@~s at node ~p: ~s", - [User, Host, node(), - mod_register:format_error(Reason)]), - {error, cannot_register, 10001, String} - end; - false -> - {error, cannot_register, 10001, "Unknown virtual host"} + true -> + case ejabberd_auth:try_register(User, Host, Password) of + ok -> + {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; + {error, exists} -> + Msg = io_lib:format("User ~s@~s already registered", [User, Host]), + {error, conflict, 10090, Msg}; + {error, Reason} -> + String = io_lib:format("Can't register user ~s@~s at node ~p: ~s", + [User, + Host, + node(), + mod_register:format_error(Reason)]), + {error, cannot_register, 10001, String} + end; + false -> + {error, cannot_register, 10001, "Unknown virtual host"} end. + unregister(User, Host) -> case is_my_host(Host) of - true -> - ejabberd_auth:remove_user(User, Host), - {ok, ""}; - false -> - {error, "Unknown virtual host"} + true -> + ejabberd_auth:remove_user(User, Host), + {ok, ""}; + false -> + {error, "Unknown virtual host"} end. + registered_users(Host) -> case is_my_host(Host) of - true -> - Users = ejabberd_auth:get_users(Host), - SUsers = lists:sort(Users), - lists:map(fun({U, _S}) -> U end, SUsers); - false -> - {error, "Unknown virtual host"} + true -> + Users = ejabberd_auth:get_users(Host), + SUsers = lists:sort(Users), + lists:map(fun({U, _S}) -> U end, SUsers); + false -> + {error, "Unknown virtual host"} end. + registered_vhosts() -> ejabberd_option:hosts(). + reload_config() -> case ejabberd_config:reload() of - ok -> ok; - Err -> - Reason = ejabberd_config:format_error(Err), - {error, Reason} + ok -> ok; + Err -> + Reason = ejabberd_config:format_error(Err), + {error, Reason} end. + dump_config(Path) -> case ejabberd_config:dump(Path) of - ok -> ok; - Err -> - Reason = ejabberd_config:format_error(Err), - {error, Reason} + ok -> ok; + Err -> + Reason = ejabberd_config:format_error(Err), + {error, Reason} end. + convert_to_yaml(In, Out) -> case ejabberd_config:convert_to_yaml(In, Out) of - ok -> {ok, ""}; - Err -> - Reason = ejabberd_config:format_error(Err), - {error, Reason} + ok -> {ok, ""}; + Err -> + Reason = ejabberd_config:format_error(Err), + {error, Reason} end. + %%% %%% Cluster management %%% + join_cluster(NodeBin) when is_binary(NodeBin) -> join_cluster(list_to_atom(binary_to_list(NodeBin))); join_cluster(Node) when is_atom(Node) -> @@ -884,6 +1242,7 @@ join_cluster(Node) when is_atom(Node) -> Ping = net_adm:ping(Node), join_cluster(Node, IsNodes, IsKnownNodes, Ping). + join_cluster(_Node, true, _IsKnownNodes, _Ping) -> {error, "This node already joined that running node."}; join_cluster(_Node, _IsNodes, true, _Ping) -> @@ -898,6 +1257,7 @@ join_cluster(Node, false, false, pong) -> {error, io_lib:format("Can't join that cluster: ~p", [Error])} end. + join_cluster_here(NodeBin) -> Node = list_to_atom(binary_to_list(NodeBin)), IsNodes = lists:member(Node, ejabberd_cluster:get_nodes()), @@ -905,6 +1265,7 @@ join_cluster_here(NodeBin) -> Ping = net_adm:ping(Node), join_cluster_here(Node, IsNodes, IsKnownNodes, Ping). + join_cluster_here(_Node, true, _IsKnownNodes, _Ping) -> {error, "This node already joined that running node."}; join_cluster_here(_Node, _IsNodes, true, _Ping) -> @@ -919,22 +1280,27 @@ join_cluster_here(Node, false, false, pong) -> {error, io_lib:format("Can't join node to this cluster: ~p", [Error])} end. + leave_cluster(NodeBin) when is_binary(NodeBin) -> leave_cluster(list_to_atom(binary_to_list(NodeBin))); leave_cluster(Node) -> ejabberd_cluster:leave(Node). + list_cluster() -> ejabberd_cluster:get_nodes(). + list_cluster_detailed() -> KnownNodes = ejabberd_cluster:get_known_nodes(), RunningNodes = ejabberd_cluster:get_nodes(), - [get_cluster_node_details(Node, RunningNodes) || Node <- KnownNodes]. + [ get_cluster_node_details(Node, RunningNodes) || Node <- KnownNodes ]. + get_cluster_node_details(Node, RunningNodes) -> get_cluster_node_details2(Node, lists:member(Node, RunningNodes)). + get_cluster_node_details2(Node, false) -> {Node, "false", "", -1, -1, -1, unknown}; get_cluster_node_details2(Node, true) -> @@ -946,6 +1312,7 @@ get_cluster_node_details2(Node, true) -> {Node, "true", Status, -1, -1, -1, unknown} end. + get_cluster_node_details3() -> {ok, StatusString} = status(), UptimeSeconds = mod_admin_extra:stats(<<"uptimeseconds">>), @@ -954,114 +1321,131 @@ get_cluster_node_details3() -> GetMaster = get_master(), {node(), "true", StatusString, OnlineUsers, Processes, UptimeSeconds, GetMaster}. + %%% %%% Migration management %%% + import_file(Path) -> case jd2ejd:import_file(Path) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't import jabberd14 spool file ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_import_file, String} + [filename:absname(Path), node(), Reason]), + {cannot_import_file, String} end. + import_dir(Path) -> case jd2ejd:import_dir(Path) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't import jabberd14 spool dir ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_import_dir, String} + [filename:absname(Path), node(), Reason]), + {cannot_import_dir, String} end. + %%% %%% Purge DB %%% + delete_expired_messages() -> lists:foreach( fun(Host) -> {atomic, ok} = mod_offline:remove_expired_messages(Host) - end, ejabberd_option:hosts()). + end, + ejabberd_option:hosts()). + delete_old_messages(Days) -> lists:foreach( fun(Host) -> {atomic, _} = mod_offline:remove_old_messages(Days, Host) - end, ejabberd_option:hosts()). + end, + ejabberd_option:hosts()). + delete_old_messages_batch(Server, Days, BatchSize, Rate) -> LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, mod_offline), - case ejabberd_batch:register_task({spool, LServer}, 0, Rate, {LServer, Days, BatchSize, none}, - fun({L, Da, B, IS} = S) -> - case {erlang:function_exported(Mod, remove_old_messages_batch, 3), - erlang:function_exported(Mod, remove_old_messages_batch, 4)} of - {true, _} -> - case Mod:remove_old_messages_batch(L, Da, B) of - {ok, Count} -> - {ok, S, Count}; - {error, _} = E -> - E - end; - {_, true} -> - case Mod:remove_old_messages_batch(L, Da, B, IS) of - {ok, IS2, Count} -> - {ok, {L, Da, B, IS2}, Count}; - {error, _} = E -> - E - end; - _ -> - {error, not_implemented_for_backend} - end - end) of - ok -> - {ok, ""}; - {error, in_progress} -> - {error, "Operation in progress"} + case ejabberd_batch:register_task({spool, LServer}, + 0, + Rate, + {LServer, Days, BatchSize, none}, + fun({L, Da, B, IS} = S) -> + case {erlang:function_exported(Mod, remove_old_messages_batch, 3), + erlang:function_exported(Mod, remove_old_messages_batch, 4)} of + {true, _} -> + case Mod:remove_old_messages_batch(L, Da, B) of + {ok, Count} -> + {ok, S, Count}; + {error, _} = E -> + E + end; + {_, true} -> + case Mod:remove_old_messages_batch(L, Da, B, IS) of + {ok, IS2, Count} -> + {ok, {L, Da, B, IS2}, Count}; + {error, _} = E -> + E + end; + _ -> + {error, not_implemented_for_backend} + end + end) of + ok -> + {ok, ""}; + {error, in_progress} -> + {error, "Operation in progress"} end. + delete_old_messages_status(Server) -> LServer = jid:nameprep(Server), Msg = case ejabberd_batch:task_status({spool, LServer}) of - not_started -> - "Operation not started"; - {failed, Steps, Error} -> - io_lib:format("Operation failed after deleting ~p messages with error ~p", - [Steps, misc:format_val(Error)]); - {aborted, Steps} -> - io_lib:format("Operation was aborted after deleting ~p messages", - [Steps]); - {working, Steps} -> - io_lib:format("Operation in progress, deleted ~p messages", - [Steps]); - {completed, Steps} -> - io_lib:format("Operation was completed after deleting ~p messages", - [Steps]) - end, + not_started -> + "Operation not started"; + {failed, Steps, Error} -> + io_lib:format("Operation failed after deleting ~p messages with error ~p", + [Steps, misc:format_val(Error)]); + {aborted, Steps} -> + io_lib:format("Operation was aborted after deleting ~p messages", + [Steps]); + {working, Steps} -> + io_lib:format("Operation in progress, deleted ~p messages", + [Steps]); + {completed, Steps} -> + io_lib:format("Operation was completed after deleting ~p messages", + [Steps]) + end, lists:flatten(Msg). + delete_old_messages_abort(Server) -> LServer = jid:nameprep(Server), case ejabberd_batch:abort_task({spool, LServer}) of - aborted -> "Operation aborted"; - not_started -> "No task running" + aborted -> "Operation aborted"; + not_started -> "No task running" end. + %%% %%% Mnesia management %%% + get_master() -> case mnesia:table_info(session, master_nodes) of - [] -> none; - [Node] -> Node + [] -> none; + [Node] -> Node end. + set_master("self") -> set_master(node()); set_master(NodeString) when is_list(NodeString) -> @@ -1069,57 +1453,66 @@ set_master(NodeString) when is_list(NodeString) -> set_master(Node) when is_atom(Node) -> case mnesia:set_master_nodes([Node]) of ok -> - {ok, "ok"}; - {error, Reason} -> - String = io_lib:format("Can't set master node ~p at node ~p:~n~p", - [Node, node(), Reason]), - {error, String} + {ok, "ok"}; + {error, Reason} -> + String = io_lib:format("Can't set master node ~p at node ~p:~n~p", + [Node, node(), Reason]), + {error, String} end. + backup_mnesia(Path) -> case mnesia:backup(Path) of ok -> - {ok, ""}; - {error, Reason} -> - String = io_lib:format("Can't store backup in ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_backup, String} + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't store backup in ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_backup, String} end. + restore_mnesia(Path) -> case ejabberd_admin:restore(Path) of - {atomic, _} -> - {ok, ""}; - {aborted,{no_exists,Table}} -> - String = io_lib:format("Can't restore backup from ~p at node ~p: Table ~p does not exist.", - [filename:absname(Path), node(), Table]), - {table_not_exists, String}; - {aborted,enoent} -> - String = io_lib:format("Can't restore backup from ~p at node ~p: File not found.", - [filename:absname(Path), node()]), - {file_not_found, String} + {atomic, _} -> + {ok, ""}; + {aborted, {no_exists, Table}} -> + String = io_lib:format("Can't restore backup from ~p at node ~p: Table ~p does not exist.", + [filename:absname(Path), node(), Table]), + {table_not_exists, String}; + {aborted, enoent} -> + String = io_lib:format("Can't restore backup from ~p at node ~p: File not found.", + [filename:absname(Path), node()]), + {file_not_found, String} end. + %% Mnesia database restore %% This function is called from ejabberd_ctl, ejabberd_web_admin and %% mod_configure/adhoc restore(Path) -> - mnesia:restore(Path, [{keep_tables,keep_tables()}, - {default_op, skip_tables}]). + mnesia:restore(Path, + [{keep_tables, keep_tables()}, + {default_op, skip_tables}]). + %% This function return a list of tables that should be kept from a previous %% version backup. %% Obsolete tables or tables created by module who are no longer used are not %% restored and are ignored. keep_tables() -> - lists:flatten([acl, passwd, config, - keep_modules_tables()]). + lists:flatten([acl, + passwd, + config, + keep_modules_tables()]). + %% Returns the list of modules tables in use, according to the list of actually %% loaded modules keep_modules_tables() -> lists:map(fun(Module) -> module_tables(Module) end, - gen_mod:loaded_modules(ejabberd_config:get_myname())). + gen_mod:loaded_modules(ejabberd_config:get_myname())). + %% TODO: This mapping should probably be moved to a callback function in each %% module. @@ -1136,48 +1529,59 @@ module_tables(mod_shared_roster) -> [sr_group, sr_user]; module_tables(mod_vcard) -> [vcard, vcard_search]; module_tables(_Other) -> []. + get_local_tables() -> Tabs1 = lists:delete(schema, mnesia:system_info(local_tables)), Tabs = lists:filter( - fun(T) -> - case mnesia:table_info(T, storage_type) of - disc_copies -> true; - disc_only_copies -> true; - _ -> false - end - end, Tabs1), + fun(T) -> + case mnesia:table_info(T, storage_type) of + disc_copies -> true; + disc_only_copies -> true; + _ -> false + end + end, + Tabs1), Tabs. + dump_mnesia(Path) -> Tabs = get_local_tables(), dump_tables(Path, Tabs). + dump_table(Path, STable) -> Table = list_to_atom(STable), dump_tables(Path, [Table]). + dump_tables(Path, Tables) -> case dump_to_textfile(Path, Tables) of - ok -> - {ok, ""}; - {error, Reason} -> + ok -> + {ok, ""}; + {error, Reason} -> String = io_lib:format("Can't store dump in ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_dump, String} + [filename:absname(Path), node(), Reason]), + {cannot_dump, String} end. + dump_to_textfile(File) -> Tabs = get_local_tables(), dump_to_textfile(File, Tabs). + dump_to_textfile(File, Tabs) -> dump_to_textfile(mnesia:system_info(is_running), Tabs, file:open(File, [write])). + + dump_to_textfile(yes, Tabs, {ok, F}) -> Defs = lists:map( - fun(T) -> {T, [{record_name, mnesia:table_info(T, record_name)}, - {attributes, mnesia:table_info(T, attributes)}]} - end, - Tabs), + fun(T) -> + {T, + [{record_name, mnesia:table_info(T, record_name)}, + {attributes, mnesia:table_info(T, attributes)}]} + end, + Tabs), io:format(F, "~p.~n", [{tables, Defs}]), lists:foreach(fun(T) -> dump_tab(F, T) end, Tabs), file:close(F); @@ -1187,12 +1591,14 @@ dump_to_textfile(_, _, {ok, F}) -> dump_to_textfile(_, _, {error, Reason}) -> {error, Reason}. + dump_tab(F, T) -> W = mnesia:table_info(T, wild_pattern), - {atomic,All} = mnesia:transaction( - fun() -> mnesia:match_object(T, W, read) end), + {atomic, All} = mnesia:transaction( + fun() -> mnesia:match_object(T, W, read) end), lists:foreach( - fun(Term) -> io:format(F,"~p.~n", [setelement(1, Term, T)]) end, All). + fun(Term) -> io:format(F, "~p.~n", [setelement(1, Term, T)]) end, All). + load_mnesia(Path) -> case mnesia:load_textfile(Path) of @@ -1200,92 +1606,101 @@ load_mnesia(Path) -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't load dump in ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_load, String} + [filename:absname(Path), node(), Reason]), + {cannot_load, String} end. + mnesia_info() -> lists:flatten(io_lib:format("~p", [mnesia:system_info(all)])). + mnesia_table_info(Table) -> ATable = list_to_atom(Table), lists:flatten(io_lib:format("~p", [mnesia:table_info(ATable, all)])). + install_fallback_mnesia(Path) -> case mnesia:install_fallback(Path) of - ok -> - {ok, ""}; - {error, Reason} -> - String = io_lib:format("Can't install fallback from ~p at node ~p: ~p", - [filename:absname(Path), node(), Reason]), - {cannot_fallback, String} + ok -> + {ok, ""}; + {error, Reason} -> + String = io_lib:format("Can't install fallback from ~p at node ~p: ~p", + [filename:absname(Path), node(), Reason]), + {cannot_fallback, String} end. + mnesia_change_nodename(FromString, ToString, Source, Target) -> From = list_to_atom(FromString), To = list_to_atom(ToString), Switch = - fun - (Node) when Node == From -> - io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), - To; - (Node) when Node == To -> - %% throw({error, already_exists}); - io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), - Node; - (Node) -> - io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), - Node - end, + fun(Node) when Node == From -> + io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), + To; + (Node) when Node == To -> + %% throw({error, already_exists}); + io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), + Node; + (Node) -> + io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), + Node + end, Convert = - fun - ({schema, db_nodes, Nodes}, Acc) -> - io:format(" +++ db_nodes ~p~n", [Nodes]), - {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; - ({schema, version, Version}, Acc) -> - io:format(" +++ version: ~p~n", [Version]), - {[{schema, version, Version}], Acc}; - ({schema, cookie, Cookie}, Acc) -> - io:format(" +++ cookie: ~p~n", [Cookie]), - {[{schema, cookie, Cookie}], Acc}; - ({schema, Tab, CreateList}, Acc) -> - io:format("~n * Checking table: '~p'~n", [Tab]), - Keys = [ram_copies, disc_copies, disc_only_copies], - OptSwitch = - fun({Key, Val}) -> - case lists:member(Key, Keys) of - true -> - io:format(" + Checking key: '~p'~n", [Key]), - {Key, lists:map(Switch, Val)}; - false-> {Key, Val} - end - end, - Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, - Res; - (Other, Acc) -> - {[Other], Acc} - end, + fun({schema, db_nodes, Nodes}, Acc) -> + io:format(" +++ db_nodes ~p~n", [Nodes]), + {[{schema, db_nodes, lists:map(Switch, Nodes)}], Acc}; + ({schema, version, Version}, Acc) -> + io:format(" +++ version: ~p~n", [Version]), + {[{schema, version, Version}], Acc}; + ({schema, cookie, Cookie}, Acc) -> + io:format(" +++ cookie: ~p~n", [Cookie]), + {[{schema, cookie, Cookie}], Acc}; + ({schema, Tab, CreateList}, Acc) -> + io:format("~n * Checking table: '~p'~n", [Tab]), + Keys = [ram_copies, disc_copies, disc_only_copies], + OptSwitch = + fun({Key, Val}) -> + case lists:member(Key, Keys) of + true -> + io:format(" + Checking key: '~p'~n", [Key]), + {Key, lists:map(Switch, Val)}; + false -> {Key, Val} + end + end, + Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, + Res; + (Other, Acc) -> + {[Other], Acc} + end, mnesia:traverse_backup(Source, Target, Convert, switched). + clear_cache() -> Nodes = ejabberd_cluster:get_nodes(), lists:foreach(fun(T) -> ets_cache:clear(T, Nodes) end, ets_cache:all()). + gc() -> lists:foreach(fun erlang:garbage_collect/1, processes()). + -spec is_my_host(binary()) -> boolean(). is_my_host(Host) -> - try ejabberd_router:is_my_host(Host) - catch _:{invalid_domain, _} -> false + try + ejabberd_router:is_my_host(Host) + catch + _:{invalid_domain, _} -> false end. + %%% %%% Internal %%% %% @format-begin + mnesia_table_change_storage(STable, SType) -> Table = binary_to_existing_atom(STable, latin1), Type = @@ -1320,35 +1735,40 @@ mnesia_table_change_storage(STable, SType) -> end, {ok, Result}. + mnesia_table_clear(STable) -> Table = binary_to_existing_atom(STable, latin1), mnesia:clear_table(Table). + mnesia_table_delete(STable) -> Table = binary_to_existing_atom(STable, latin1), mnesia:delete_table(Table). + mnesia_table_details(STable) -> Table = binary_to_existing_atom(STable, latin1), - [{Name, iolist_to_binary(str:format("~p", [Value]))} - || {Name, Value} <- mnesia:table_info(Table, all)]. + [ {Name, iolist_to_binary(str:format("~p", [Value]))} + || {Name, Value} <- mnesia:table_info(Table, all) ]. + mnesia_list_tables() -> STables = lists:sort( - mnesia:system_info(tables)), + mnesia:system_info(tables)), lists:map(fun(Table) -> - TInfo = mnesia:table_info(Table, all), - {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), - {value, {size, Size}} = lists:keysearch(size, 1, TInfo), - {value, {memory, Memory}} = lists:keysearch(memory, 1, TInfo), - MemoryB = Memory * erlang:system_info(wordsize), - MemoryKB = MemoryB div 1024, - MemoryMB = MemoryKB div 1024, - {Table, storage_type_bin(Type), Size, MemoryKB, MemoryMB} + TInfo = mnesia:table_info(Table, all), + {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), + {value, {size, Size}} = lists:keysearch(size, 1, TInfo), + {value, {memory, Memory}} = lists:keysearch(memory, 1, TInfo), + MemoryB = Memory * erlang:system_info(wordsize), + MemoryKB = MemoryB div 1024, + MemoryMB = MemoryKB div 1024, + {Table, storage_type_bin(Type), Size, MemoryKB, MemoryMB} end, STables). + storage_type_bin(ram_copies) -> <<"RAM copy">>; storage_type_bin(disc_copies) -> @@ -1358,19 +1778,24 @@ storage_type_bin(disc_only_copies) -> storage_type_bin(unknown) -> <<"Remote copy">>. + echo(Sentence) -> Sentence. + echo3(_, _, Sentence) -> Sentence. + %%% %%% Web Admin: Main %%% + web_menu_main(Acc, _Lang) -> Acc ++ [{<<"purge">>, <<"Purge">>}, {<<"stanza">>, <<"Stanza">>}]. + web_page_main(_, #request{path = [<<"purge">>]} = R) -> Types = [{<<"#erlang">>, <<"Erlang">>}, @@ -1380,7 +1805,7 @@ web_page_main(_, #request{path = [<<"purge">>]} = R) -> {<<"#pubsub">>, <<"PubSub">>}, {<<"#push">>, <<"Push">>}], Head = [?XC(<<"h1">>, <<"Purge">>)], - Set = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types]), + Set = [?XE(<<"ul">>, [ ?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types ]), ?X(<<"hr">>), ?XAC(<<"h2">>, [{<<"id">>, <<"erlang">>}], <<"Erlang">>), ?XE(<<"blockquote">>, @@ -1420,21 +1845,24 @@ web_page_main(_, #request{path = [<<"stanza">>]} = R) -> web_page_main(Acc, _) -> Acc. + %%% %%% Web Admin: Node %%% + web_menu_node(Acc, _Node, _Lang) -> - Acc - ++ [{<<"cluster">>, <<"Clustering">>}, - {<<"update">>, <<"Code Update">>}, - {<<"config-file">>, <<"Configuration File">>}, - {<<"logs">>, <<"Logs">>}, - {<<"stop">>, <<"Stop Node">>}]. + Acc ++ + [{<<"cluster">>, <<"Clustering">>}, + {<<"update">>, <<"Code Update">>}, + {<<"config-file">>, <<"Configuration File">>}, + {<<"logs">>, <<"Logs">>}, + {<<"stop">>, <<"Stop Node">>}]. + web_page_node(_, Node, #request{path = [<<"cluster">>]} = R) -> {ok, Names} = net_adm:names(), - NodeNames = lists:join(", ", [Name || {Name, _Port} <- Names]), + NodeNames = lists:join(", ", [ Name || {Name, _Port} <- Names ]), Hint = list_to_binary(io_lib:format("Hint: Erlang nodes found in this machine that may be running ejabberd: ~s", [NodeNames])), @@ -1459,7 +1887,7 @@ web_page_node(_, Node, #request{path = [<<"cluster">>]} = R) -> ejabberd_web_admin, make_command, [set_master, R, [], [{style, danger}]])], - timer:sleep(100), % leaving a cluster takes a while, let's delay the get commands + timer:sleep(100), % leaving a cluster takes a while, let's delay the get commands Get1 = [ejabberd_cluster:call(Node, ejabberd_web_admin, @@ -1486,10 +1914,10 @@ web_page_node(_, Node, #request{path = [<<"update">>]} = R) -> web_page_node(_, Node, #request{path = [<<"config-file">>]} = R) -> Res = ?H1GLraw(<<"Configuration File">>, <<"admin/configuration/file-format/">>, - <<"File Format">>) - ++ [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [convert_to_yaml, R]), - ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [dump_config, R]), - ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reload_config, R])], + <<"File Format">>) ++ + [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [convert_to_yaml, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [dump_config, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reload_config, R])], {stop, Res}; web_page_node(_, Node, #request{path = [<<"stop">>]} = R) -> Res = [?XC(<<"h1">>, <<"Stop This Node">>), @@ -1511,11 +1939,11 @@ web_page_node(_, Node, #request{path = [<<"stop">>]} = R) -> [halt, R, [], [{style, danger}]])], {stop, Res}; web_page_node(_, Node, #request{path = [<<"logs">>]} = R) -> - Res = ?H1GLraw(<<"Logs">>, <<"admin/configuration/basic/#logging">>, <<"Logging">>) - ++ [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [set_loglevel, R]), - ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [get_loglevel, R]), - ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reopen_log, R]), - ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [rotate_log, R])], + Res = ?H1GLraw(<<"Logs">>, <<"admin/configuration/basic/#logging">>, <<"Logging">>) ++ + [ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [set_loglevel, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [get_loglevel, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [reopen_log, R]), + ejabberd_cluster:call(Node, ejabberd_web_admin, make_command, [rotate_log, R])], {stop, Res}; web_page_node(Acc, _, _) -> Acc. diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index a31d7b1a6..72096b29a 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -38,65 +38,71 @@ %%% Application API %%% + start(normal, _Args) -> try - {T1, _} = statistics(wall_clock), - ejabberd_logger:start(), - write_pid_file(), - start_included_apps(), - misc:warn_unset_home(), - start_elixir_application(), - setup_if_elixir_conf_used(), - case ejabberd_config:load() of - ok -> - ejabberd_mnesia:start(), - file_queue_init(), - maybe_add_nameservers(), - case ejabberd_sup:start_link() of - {ok, SupPid} -> - ejabberd_system_monitor:start(), - register_elixir_config_hooks(), - ejabberd_cluster:wait_for_sync(infinity), - ejabberd_hooks:run(ejabberd_started, []), - ejabberd:check_apps(), - ejabberd_systemd:ready(), - maybe_start_exsync(), - {T2, _} = statistics(wall_clock), - ?INFO_MSG("ejabberd ~ts is started in the node ~p in ~.2fs", - [ejabberd_option:version(), - node(), (T2-T1)/1000]), - maybe_print_elixir_version(), - ?INFO_MSG("~ts", - [erlang:system_info(system_version)]), - {ok, SupPid}; - Err -> - ?CRITICAL_MSG("Failed to start ejabberd application: ~p", [Err]), - ejabberd:halt() - end; - Err -> - ?CRITICAL_MSG("Failed to start ejabberd application: ~ts", - [ejabberd_config:format_error(Err)]), - ejabberd:halt() - end - catch throw:{?MODULE, Error} -> - ?DEBUG("Failed to start ejabberd application: ~p", [Error]), - ejabberd:halt() + {T1, _} = statistics(wall_clock), + ejabberd_logger:start(), + write_pid_file(), + start_included_apps(), + misc:warn_unset_home(), + start_elixir_application(), + setup_if_elixir_conf_used(), + case ejabberd_config:load() of + ok -> + ejabberd_mnesia:start(), + file_queue_init(), + maybe_add_nameservers(), + case ejabberd_sup:start_link() of + {ok, SupPid} -> + ejabberd_system_monitor:start(), + register_elixir_config_hooks(), + ejabberd_cluster:wait_for_sync(infinity), + ejabberd_hooks:run(ejabberd_started, []), + ejabberd:check_apps(), + ejabberd_systemd:ready(), + maybe_start_exsync(), + {T2, _} = statistics(wall_clock), + ?INFO_MSG("ejabberd ~ts is started in the node ~p in ~.2fs", + [ejabberd_option:version(), + node(), + (T2 - T1) / 1000]), + maybe_print_elixir_version(), + ?INFO_MSG("~ts", + [erlang:system_info(system_version)]), + {ok, SupPid}; + Err -> + ?CRITICAL_MSG("Failed to start ejabberd application: ~p", [Err]), + ejabberd:halt() + end; + Err -> + ?CRITICAL_MSG("Failed to start ejabberd application: ~ts", + [ejabberd_config:format_error(Err)]), + ejabberd:halt() + end + catch + throw:{?MODULE, Error} -> + ?DEBUG("Failed to start ejabberd application: ~p", [Error]), + ejabberd:halt() end; start(_, _) -> {error, badarg}. + start_included_apps() -> {ok, Apps} = application:get_key(ejabberd, included_applications), lists:foreach( - fun(mnesia) -> - ok; - (lager) -> - ok; - (os_mon)-> - ok; - (App) -> - application:ensure_all_started(App) - end, Apps). + fun(mnesia) -> + ok; + (lager) -> + ok; + (os_mon) -> + ok; + (App) -> + application:ensure_all_started(App) + end, + Apps). + %% Prepare the application for termination. %% This function is called when an application is about to be stopped, @@ -113,76 +119,88 @@ prep_stop(State) -> gen_mod:stop(), State. + %% All the processes were killed when this function is called stop(_State) -> ?INFO_MSG("ejabberd ~ts is stopped in the node ~p", - [ejabberd_option:version(), node()]), + [ejabberd_option:version(), node()]), delete_pid_file(). + %%% %%% Internal functions %%% + %% If ejabberd is running on some Windows machine, get nameservers and add to Erlang maybe_add_nameservers() -> case os:type() of - {win32, _} -> add_windows_nameservers(); - _ -> ok + {win32, _} -> add_windows_nameservers(); + _ -> ok end. + add_windows_nameservers() -> IPTs = win32_dns:get_nameservers(), ?INFO_MSG("Adding machine's DNS IPs to Erlang system:~n~p", [IPTs]), lists:foreach(fun(IPT) -> inet_db:add_ns(IPT) end, IPTs). + %%% %%% PID file %%% + write_pid_file() -> case ejabberd:get_pid_file() of - false -> - ok; - PidFilename -> - write_pid_file(os:getpid(), PidFilename) + false -> + ok; + PidFilename -> + write_pid_file(os:getpid(), PidFilename) end. + write_pid_file(Pid, PidFilename) -> case file:write_file(PidFilename, io_lib:format("~ts~n", [Pid])) of - ok -> - ok; - {error, Reason} = Err -> - ?CRITICAL_MSG("Cannot write PID file ~ts: ~ts", - [PidFilename, file:format_error(Reason)]), - throw({?MODULE, Err}) + ok -> + ok; + {error, Reason} = Err -> + ?CRITICAL_MSG("Cannot write PID file ~ts: ~ts", + [PidFilename, file:format_error(Reason)]), + throw({?MODULE, Err}) end. + delete_pid_file() -> case ejabberd:get_pid_file() of - false -> - ok; - PidFilename -> - file:delete(PidFilename) + false -> + ok; + PidFilename -> + file:delete(PidFilename) end. + file_queue_init() -> QueueDir = case ejabberd_option:queue_dir() of - undefined -> - MnesiaDir = mnesia:system_info(directory), - filename:join(MnesiaDir, "queue"); - Path -> - Path - end, + undefined -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, "queue"); + Path -> + Path + end, case p1_queue:start(QueueDir) of - ok -> ok; - Err -> throw({?MODULE, Err}) + ok -> ok; + Err -> throw({?MODULE, Err}) end. + %%% %%% Elixir %%% -ifdef(ELIXIR_ENABLED). + + is_using_elixir_config() -> Config = ejabberd_config:path(), try 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config) of @@ -191,36 +209,55 @@ is_using_elixir_config() -> _:_ -> false end. + setup_if_elixir_conf_used() -> - case is_using_elixir_config() of - true -> 'Elixir.Ejabberd.Config.Store':start_link(); - false -> ok - end. + case is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config.Store':start_link(); + false -> ok + end. + register_elixir_config_hooks() -> - case is_using_elixir_config() of - true -> 'Elixir.Ejabberd.Config':start_hooks(); - false -> ok - end. + case is_using_elixir_config() of + true -> 'Elixir.Ejabberd.Config':start_hooks(); + false -> ok + end. + start_elixir_application() -> case application:ensure_started(elixir) of - ok -> ok; - {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) + ok -> ok; + {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) end. + maybe_start_exsync() -> case os:getenv("RELIVE") of "true" -> rpc:call(node(), 'Elixir.ExSync.Application', start, []); _ -> ok end. + maybe_print_elixir_version() -> ?INFO_MSG("Elixir ~ts", [maps:get(build, 'Elixir.System':build_info())]). + + -else. + + setup_if_elixir_conf_used() -> ok. + + register_elixir_config_hooks() -> ok. + + start_elixir_application() -> ok. + + maybe_start_exsync() -> ok. + + maybe_print_elixir_version() -> ok. + + -endif. diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 0c5d2fc69..89bde3f04 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -31,26 +31,46 @@ -protocol({rfc, 5802}). %% External exports --export([start_link/0, host_up/1, host_down/1, config_reloaded/0, - set_password/3, check_password/4, - check_password/6, check_password_with_authmodule/4, - check_password_with_authmodule/6, try_register/3, - get_users/0, get_users/1, password_to_scram/2, - get_users/2, import_info/0, - count_users/1, import/5, import_start/2, - count_users/2, get_password/2, - get_password_s/2, get_password_with_authmodule/2, - user_exists/2, user_exists_in_other_modules/3, - remove_user/2, remove_user/3, plain_password_required/1, - store_type/1, entropy/1, backend_type/1, password_format/1, - which_users_exists/1]). +-export([start_link/0, + host_up/1, + host_down/1, + config_reloaded/0, + set_password/3, + check_password/4, check_password/6, + check_password_with_authmodule/4, check_password_with_authmodule/6, + try_register/3, + get_users/0, get_users/1, + password_to_scram/2, + get_users/2, + import_info/0, + count_users/1, + import/5, + import_start/2, + count_users/2, + get_password/2, + get_password_s/2, + get_password_with_authmodule/2, + user_exists/2, + user_exists_in_other_modules/3, + remove_user/2, remove_user/3, + plain_password_required/1, + store_type/1, + entropy/1, + backend_type/1, + password_format/1, + which_users_exists/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -export([auth_modules/1, convert_to_scram/1, drop_password_type/2, set_password_instance/3]). -include_lib("xmpp/include/scram.hrl"). + -include("logger.hrl"). -define(SALT_LENGTH, 16). @@ -65,76 +85,86 @@ %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- --type opts() :: [{prefix, binary()} | {from, integer()} | - {to, integer()} | {limit, integer()} | +-type opts() :: [{prefix, binary()} | + {from, integer()} | + {to, integer()} | + {limit, integer()} | {offset, integer()}]. + -callback start(binary()) -> any(). -callback stop(binary()) -> any(). -callback reload(binary()) -> any(). -callback plain_password_required(binary()) -> boolean(). -callback store_type(binary()) -> plain | external | scram. -callback set_password(binary(), binary(), password()) -> - {ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}. + {ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}. -callback set_password_multiple(binary(), binary(), [password()]) -> - {ets_cache:tag(), {ok, [password()]} | {error, db_failure | not_allowed}}. + {ets_cache:tag(), {ok, [password()]} | {error, db_failure | not_allowed}}. -callback set_password_instance(binary(), binary(), password()) -> - ok | {error, db_failure | not_allowed}. + ok | {error, db_failure | not_allowed}. -callback remove_user(binary(), binary()) -> ok | {error, db_failure | not_allowed}. -callback user_exists(binary(), binary()) -> {ets_cache:tag(), boolean() | {error, db_failure}}. -callback check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. -callback try_register(binary(), binary(), password()) -> - {ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}. + {ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}. -callback try_register_multiple(binary(), binary(), [password()]) -> - {ets_cache:tag(), {ok, [password()]} | {error, exists | db_failure | not_allowed}}. + {ets_cache:tag(), {ok, [password()]} | {error, exists | db_failure | not_allowed}}. -callback get_users(binary(), opts()) -> [{binary(), binary()}]. -callback count_users(binary(), opts()) -> number(). -callback get_password(binary(), binary()) -> {ets_cache:tag(), {ok, password() | [password()]} | error}. -callback drop_password_type(binary(), atom()) -> - ok | {error, db_failure | not_allowed}. + ok | {error, db_failure | not_allowed}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> boolean(). -optional_callbacks([reload/1, - set_password/3, - set_password_multiple/3, - set_password_instance/3, - remove_user/2, - user_exists/2, - check_password/4, - try_register/3, - try_register_multiple/3, - get_users/2, - count_users/2, - get_password/2, - drop_password_type/2, - use_cache/1, - cache_nodes/1]). + set_password/3, + set_password_multiple/3, + set_password_instance/3, + remove_user/2, + user_exists/2, + check_password/4, + try_register/3, + try_register_multiple/3, + get_users/2, + count_users/2, + get_password/2, + drop_password_type/2, + use_cache/1, + cache_nodes/1]). + -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + init([]) -> ejabberd_hooks:add(host_up, ?MODULE, host_up, 30), ejabberd_hooks:add(host_down, ?MODULE, host_down, 80), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 40), HostModules = lists:foldl( - fun(Host, Acc) -> - Modules = auth_modules(Host), - maps:put(Host, Modules, Acc) - end, #{}, ejabberd_option:hosts()), + fun(Host, Acc) -> + Modules = auth_modules(Host), + maps:put(Host, Modules, Acc) + end, + #{}, + ejabberd_option:hosts()), lists:foreach( fun({Host, Modules}) -> - start(Host, Modules) - end, maps:to_list(HostModules)), + start(Host, Modules) + end, + maps:to_list(HostModules)), init_cache(HostModules), {ok, #state{host_modules = HostModules}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast({host_up, Host}, #state{host_modules = HostModules} = State) -> Modules = auth_modules(Host), start(Host, Modules), @@ -149,113 +179,143 @@ handle_cast({host_down, Host}, #state{host_modules = HostModules} = State) -> {noreply, State#state{host_modules = NewHostModules}}; handle_cast(config_reloaded, #state{host_modules = HostModules} = State) -> NewHostModules = - lists:foldl( - fun(Host, Acc) -> - OldModules = maps:get(Host, HostModules, []), - NewModules = auth_modules(Host), - start(Host, NewModules -- OldModules), - stop(Host, OldModules -- NewModules), - reload(Host, misc:intersection(OldModules, NewModules)), - maps:put(Host, NewModules, Acc) - end, HostModules, ejabberd_option:hosts()), + lists:foldl( + fun(Host, Acc) -> + OldModules = maps:get(Host, HostModules, []), + NewModules = auth_modules(Host), + start(Host, NewModules -- OldModules), + stop(Host, OldModules -- NewModules), + reload(Host, misc:intersection(OldModules, NewModules)), + maps:put(Host, NewModules, Acc) + end, + HostModules, + ejabberd_option:hosts()), init_cache(NewHostModules), {noreply, State#state{host_modules = NewHostModules}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, State) -> ejabberd_hooks:delete(host_up, ?MODULE, host_up, 30), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 80), ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 40), lists:foreach( fun({Host, Modules}) -> - stop(Host, Modules) - end, maps:to_list(State#state.host_modules)). + stop(Host, Modules) + end, + maps:to_list(State#state.host_modules)). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + start(Host, Modules) -> lists:foreach(fun(M) -> M:start(Host) end, Modules). + stop(Host, Modules) -> lists:foreach(fun(M) -> M:stop(Host) end, Modules). + reload(Host, Modules) -> lists:foreach( fun(M) -> - case erlang:function_exported(M, reload, 1) of - true -> M:reload(Host); - false -> ok - end - end, Modules). + case erlang:function_exported(M, reload, 1) of + true -> M:reload(Host); + false -> ok + end + end, + Modules). + host_up(Host) -> gen_server:cast(?MODULE, {host_up, Host}). + host_down(Host) -> gen_server:cast(?MODULE, {host_down, Host}). + config_reloaded() -> gen_server:cast(?MODULE, config_reloaded). + -spec plain_password_required(binary()) -> boolean(). plain_password_required(Server) -> - lists:any(fun (M) -> M:plain_password_required(Server) end, - auth_modules(Server)). + lists:any(fun(M) -> M:plain_password_required(Server) end, + auth_modules(Server)). + -spec store_type(binary()) -> plain | scram | external. store_type(Server) -> case auth_modules(Server) of - [ejabberd_auth_anonymous] -> external; - Modules -> - lists:foldl( - fun(ejabberd_auth_anonymous, Type) -> Type; - (_, external) -> external; - (M, scram) -> - case M:store_type(Server) of - external -> external; - _ -> scram - end; - (M, plain) -> - M:store_type(Server) - end, plain, Modules) + [ejabberd_auth_anonymous] -> external; + Modules -> + lists:foldl( + fun(ejabberd_auth_anonymous, Type) -> Type; + (_, external) -> external; + (M, scram) -> + case M:store_type(Server) of + external -> external; + _ -> scram + end; + (M, plain) -> + M:store_type(Server) + end, + plain, + Modules) end. + -spec check_password(binary(), binary(), binary(), binary()) -> boolean(). check_password(User, AuthzId, Server, Password) -> check_password(User, AuthzId, Server, Password, <<"">>, undefined). --spec check_password(binary(), binary(), binary(), binary(), binary(), + +-spec check_password(binary(), + binary(), + binary(), + binary(), + binary(), digest_fun() | undefined) -> boolean(). check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> case check_password_with_authmodule( - User, AuthzId, Server, Password, Digest, DigestGen) of - {true, _AuthModule} -> true; - {false, _ErrorAtom, _Reason} -> false; - false -> false + User, AuthzId, Server, Password, Digest, DigestGen) of + {true, _AuthModule} -> true; + {false, _ErrorAtom, _Reason} -> false; + false -> false end. --spec check_password_with_authmodule(binary(), binary(), - binary(), binary()) -> false | {true, atom()}. + +-spec check_password_with_authmodule(binary(), + binary(), + binary(), + binary()) -> false | {true, atom()}. check_password_with_authmodule(User, AuthzId, Server, Password) -> check_password_with_authmodule( User, AuthzId, Server, Password, <<"">>, undefined). --spec check_password_with_authmodule( - binary(), binary(), binary(), binary(), binary(), - digest_fun() | undefined) -> false | {false, atom(), binary()} | {true, atom()}. + +-spec check_password_with_authmodule(binary(), + binary(), + binary(), + binary(), + binary(), + digest_fun() | undefined) -> false | {false, atom(), binary()} | {true, atom()}. check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGen) -> case validate_credentials(User, Server) of - {ok, LUser, LServer} -> - case {jid:nodeprep(AuthzId), get_is_banned(LUser, LServer)} of + {ok, LUser, LServer} -> + case {jid:nodeprep(AuthzId), get_is_banned(LUser, LServer)} of {error, _} -> - false; + false; {_, {is_banned, BanReason}} -> {false, 'account-disabled', BanReason}; {LAuthzId, _} -> @@ -263,8 +323,13 @@ check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGe lists:foldl( fun(Mod, false) -> case db_check_password( - LUser, LAuthzId, LServer, Password, - Digest, DigestGen, Mod) of + LUser, + LAuthzId, + LServer, + Password, + Digest, + DigestGen, + Mod) of true -> {true, Mod}; false -> false; {stop, true} -> {stop, {true, Mod}}; @@ -272,685 +337,792 @@ check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGe end; (_, Acc) -> Acc - end, false, auth_modules(LServer))) - end; - _ -> - false + end, + false, + auth_modules(LServer))) + end; + _ -> + false end. + convert_password_for_storage(_Server, #scram{} = Password) -> {Password, [Password]}; convert_password_for_storage(Server, Password) -> P = case ejabberd_option:auth_stored_password_types(Server) of - [] -> - case ejabberd_option:auth_password_format(Server) of - plain -> - [Password]; - _ -> - [password_to_scram(Server, Password)] - end; - M -> - lists:sort(lists:map( - fun(scram_sha1) -> - password_to_scram(Server, Password, sha, ?SCRAM_DEFAULT_ITERATION_COUNT); - (scram_sha256) -> - password_to_scram(Server, Password, sha256, ?SCRAM_DEFAULT_ITERATION_COUNT); - (scram_sha512) -> - password_to_scram(Server, Password, sha512, ?SCRAM_DEFAULT_ITERATION_COUNT); - (plain) -> - Password - end, M)) - end, + [] -> + case ejabberd_option:auth_password_format(Server) of + plain -> + [Password]; + _ -> + [password_to_scram(Server, Password)] + end; + M -> + lists:sort(lists:map( + fun(scram_sha1) -> + password_to_scram(Server, Password, sha, ?SCRAM_DEFAULT_ITERATION_COUNT); + (scram_sha256) -> + password_to_scram(Server, Password, sha256, ?SCRAM_DEFAULT_ITERATION_COUNT); + (scram_sha512) -> + password_to_scram(Server, Password, sha512, ?SCRAM_DEFAULT_ITERATION_COUNT); + (plain) -> + Password + end, + M)) + end, {Password, P}. --spec set_password(binary(), binary(), password()) -> ok | {error, - db_failure | not_allowed | - invalid_jid | invalid_password}. + +-spec set_password(binary(), binary(), password()) -> ok | + {error, + db_failure | + not_allowed | + invalid_jid | + invalid_password}. set_password(User, Server, Password) -> case validate_credentials(User, Server, Password) of - {ok, LUser, LServer} -> - {Plain, Passwords} = convert_password_for_storage(Server, Password), - lists:foldl( - fun(M, {error, _}) -> - db_set_password(LUser, LServer, Plain, Passwords, M); - (_, ok) -> - ok - end, {error, not_allowed}, auth_modules(LServer)); - Err -> - Err + {ok, LUser, LServer} -> + {Plain, Passwords} = convert_password_for_storage(Server, Password), + lists:foldl( + fun(M, {error, _}) -> + db_set_password(LUser, LServer, Plain, Passwords, M); + (_, ok) -> + ok + end, + {error, not_allowed}, + auth_modules(LServer)); + Err -> + Err end. + set_password_instance(User, Server, Password) -> case validate_credentials(User, Server, Password) of - {ok, LUser, LServer} -> - lists:foldl( - fun(Mod, {error, _} = Acc) -> - case erlang:function_exported(Mod, set_password_instance, 3) of - true -> - R = Mod:set_password_instance(LUser, LServer, Password), - case use_cache(Mod, LServer) of - true -> - ets_cache:delete(cache_tab(Mod), {LUser, LServer}, - cache_nodes(Mod, LServer)); - _ -> - ok - end, - R; - _ -> - Acc - end; - (_, ok) -> - ok - end, {error, not_allowed}, auth_modules(LServer)); - Err -> - Err + {ok, LUser, LServer} -> + lists:foldl( + fun(Mod, {error, _} = Acc) -> + case erlang:function_exported(Mod, set_password_instance, 3) of + true -> + R = Mod:set_password_instance(LUser, LServer, Password), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(cache_tab(Mod), + {LUser, LServer}, + cache_nodes(Mod, LServer)); + _ -> + ok + end, + R; + _ -> + Acc + end; + (_, ok) -> + ok + end, + {error, not_allowed}, + auth_modules(LServer)); + Err -> + Err end. --spec try_register(binary(), binary(), password()) -> ok | {error, - db_failure | not_allowed | exists | - invalid_jid | invalid_password}. + +-spec try_register(binary(), binary(), password()) -> ok | + {error, + db_failure | + not_allowed | + exists | + invalid_jid | + invalid_password}. try_register(User, Server, Password) -> case validate_credentials(User, Server, Password) of - {ok, LUser, LServer} -> - case user_exists(LUser, LServer) of - true -> - {error, exists}; - false -> - case ejabberd_router:is_my_host(LServer) of - true -> - case ejabberd_hooks:run_fold(check_register_user, LServer, true, - [User, Server, Password]) of - true -> - {Plain, Passwords} = convert_password_for_storage(Server, Password), - case lists:foldl( - fun(_, ok) -> - ok; - (Mod, _) -> - db_try_register( - LUser, LServer, Plain, Passwords, Mod) - end, {error, not_allowed}, auth_modules(LServer)) of - ok -> - ejabberd_hooks:run( - register_user, LServer, [LUser, LServer]); - {error, _} = Err -> - Err - end; - false -> - {error, not_allowed} - end; - false -> - {error, not_allowed} - end - end; - Err -> - Err + {ok, LUser, LServer} -> + case user_exists(LUser, LServer) of + true -> + {error, exists}; + false -> + case ejabberd_router:is_my_host(LServer) of + true -> + case ejabberd_hooks:run_fold(check_register_user, + LServer, + true, + [User, Server, Password]) of + true -> + {Plain, Passwords} = convert_password_for_storage(Server, Password), + case lists:foldl( + fun(_, ok) -> + ok; + (Mod, _) -> + db_try_register( + LUser, LServer, Plain, Passwords, Mod) + end, + {error, not_allowed}, + auth_modules(LServer)) of + ok -> + ejabberd_hooks:run( + register_user, LServer, [LUser, LServer]); + {error, _} = Err -> + Err + end; + false -> + {error, not_allowed} + end; + false -> + {error, not_allowed} + end + end; + Err -> + Err end. + -spec get_users() -> [{binary(), binary()}]. get_users() -> lists:flatmap( fun({Host, Mod}) -> - db_get_users(Host, [], Mod) - end, auth_modules()). + db_get_users(Host, [], Mod) + end, + auth_modules()). + -spec get_users(binary()) -> [{binary(), binary()}]. get_users(Server) -> get_users(Server, []). + -spec get_users(binary(), opts()) -> [{binary(), binary()}]. get_users(Server, Opts) -> case jid:nameprep(Server) of - error -> []; - LServer -> - lists:flatmap( - fun(M) -> db_get_users(LServer, Opts, M) end, - auth_modules(LServer)) + error -> []; + LServer -> + lists:flatmap( + fun(M) -> db_get_users(LServer, Opts, M) end, + auth_modules(LServer)) end. + -spec count_users(binary()) -> non_neg_integer(). count_users(Server) -> count_users(Server, []). + -spec count_users(binary(), opts()) -> non_neg_integer(). count_users(Server, Opts) -> case jid:nameprep(Server) of - error -> 0; - LServer -> - lists:sum( - lists:map( - fun(M) -> db_count_users(LServer, Opts, M) end, - auth_modules(LServer))) + error -> 0; + LServer -> + lists:sum( + lists:map( + fun(M) -> db_count_users(LServer, Opts, M) end, + auth_modules(LServer))) end. + -spec get_password(binary(), binary()) -> false | [password()]. get_password(User, Server) -> {Passwords, _} = get_password_with_authmodule(User, Server), Passwords. + -spec get_password_s(binary(), binary()) -> password(). get_password_s(User, Server) -> case get_password(User, Server) of - false -> <<"">>; - Passwords -> - {_, Pass} = lists:foldl( - fun(Plain, _) when is_binary(Plain) -> {true, Plain}; - (Pass, {false, _}) -> {true, Pass}; - (_, Acc) -> Acc - end, {false, <<"">>}, Passwords), - Pass + false -> <<"">>; + Passwords -> + {_, Pass} = lists:foldl( + fun(Plain, _) when is_binary(Plain) -> {true, Plain}; + (Pass, {false, _}) -> {true, Pass}; + (_, Acc) -> Acc + end, + {false, <<"">>}, + Passwords), + Pass end. + -spec get_password_with_authmodule(binary(), binary()) -> - {false | {false, atom(), binary()} | [password()], module()}. + {false | {false, atom(), binary()} | [password()], module()}. get_password_with_authmodule(User, Server) -> case validate_credentials(User, Server) of - {ok, LUser, LServer} -> - case get_is_banned(LUser, LServer) of + {ok, LUser, LServer} -> + case get_is_banned(LUser, LServer) of {is_banned, BanReason} -> {{false, 'account-disabled', BanReason}, module_not_consulted}; not_banned -> - case lists:foldl( - fun(M, {error, _}) -> - {db_get_password(LUser, LServer, M), M}; - (_M, Acc) -> - Acc - end, {error, undefined}, auth_modules(LServer)) of - {{ok, Password}, Module} when is_list(Password) -> - {Password, Module}; - {{ok, Password}, Module} -> - {[Password], Module}; - {error, Module} -> - {false, Module} - end - end; - _ -> - {false, undefined} + case lists:foldl( + fun(M, {error, _}) -> + {db_get_password(LUser, LServer, M), M}; + (_M, Acc) -> + Acc + end, + {error, undefined}, + auth_modules(LServer)) of + {{ok, Password}, Module} when is_list(Password) -> + {Password, Module}; + {{ok, Password}, Module} -> + {[Password], Module}; + {error, Module} -> + {false, Module} + end + end; + _ -> + {false, undefined} end. + -spec user_exists(binary(), binary()) -> boolean(). user_exists(_User, <<"">>) -> false; user_exists(User, Server) -> case validate_credentials(User, Server) of - {ok, LUser, LServer} -> - {Exists, PerformExternalUserCheck} = - lists:foldl( - fun(M, {Exists0, PerformExternalUserCheck0}) -> - case db_user_exists(LUser, LServer, M) of - {{error, _}, Check} -> - {Exists0, PerformExternalUserCheck0 orelse Check}; - {Else, Check2} -> - {Exists0 orelse Else, PerformExternalUserCheck0 orelse Check2} - end - end, {false, false}, auth_modules(LServer)), - case (not Exists) andalso PerformExternalUserCheck andalso - ejabberd_option:auth_external_user_exists_check(Server) andalso - gen_mod:is_loaded(Server, mod_last) of - true -> - case mod_last:get_last_info(User, Server) of - not_found -> - false; - _ -> - true - end; - _ -> - Exists - end; - _ -> - false + {ok, LUser, LServer} -> + {Exists, PerformExternalUserCheck} = + lists:foldl( + fun(M, {Exists0, PerformExternalUserCheck0}) -> + case db_user_exists(LUser, LServer, M) of + {{error, _}, Check} -> + {Exists0, PerformExternalUserCheck0 orelse Check}; + {Else, Check2} -> + {Exists0 orelse Else, PerformExternalUserCheck0 orelse Check2} + end + end, + {false, false}, + auth_modules(LServer)), + case (not Exists) andalso PerformExternalUserCheck andalso + ejabberd_option:auth_external_user_exists_check(Server) andalso + gen_mod:is_loaded(Server, mod_last) of + true -> + case mod_last:get_last_info(User, Server) of + not_found -> + false; + _ -> + true + end; + _ -> + Exists + end; + _ -> + false end. + -spec user_exists_in_other_modules(atom(), binary(), binary()) -> boolean() | maybe_exists. user_exists_in_other_modules(Module, User, Server) -> user_exists_in_other_modules_loop( auth_modules(Server) -- [Module], User, Server). + user_exists_in_other_modules_loop([], _User, _Server) -> false; user_exists_in_other_modules_loop([AuthModule | AuthModules], User, Server) -> case db_user_exists(User, Server, AuthModule) of - {true, _} -> - true; - {false, _} -> - user_exists_in_other_modules_loop(AuthModules, User, Server); - {{error, _}, _} -> - maybe_exists + {true, _} -> + true; + {false, _} -> + user_exists_in_other_modules_loop(AuthModules, User, Server); + {{error, _}, _} -> + maybe_exists end. + drop_password_type(LServer, Type) -> Hash = case Type of - plain -> plain; - scram_sha1 -> sha; - scram_sha256 -> sha256; - scram_sha512 -> sha512 - end, + plain -> plain; + scram_sha1 -> sha; + scram_sha256 -> sha256; + scram_sha512 -> sha512 + end, lists:foreach( - fun(M) -> - case erlang:function_exported(M, drop_password_type, 2) of - true -> - M:drop_password_type(LServer, Hash), - case use_cache(M, LServer) of - true -> - ets_cache:clear(cache_tab(M), - cache_nodes(M, LServer)); - false -> - ok - end; - _ -> - ok - end - end, auth_modules(LServer)). + fun(M) -> + case erlang:function_exported(M, drop_password_type, 2) of + true -> + M:drop_password_type(LServer, Hash), + case use_cache(M, LServer) of + true -> + ets_cache:clear(cache_tab(M), + cache_nodes(M, LServer)); + false -> + ok + end; + _ -> + ok + end + end, + auth_modules(LServer)). + -spec which_users_exists(list({binary(), binary()})) -> list({binary(), binary()}). which_users_exists(USPairs) -> ByServer = lists:foldl( - fun({User, Server}, Dict) -> - LServer = jid:nameprep(Server), - LUser = jid:nodeprep(User), - case gb_trees:lookup(LServer, Dict) of - none -> - gb_trees:insert(LServer, gb_sets:singleton(LUser), Dict); - {value, Set} -> - gb_trees:update(LServer, gb_sets:add(LUser, Set), Dict) - end - end, gb_trees:empty(), USPairs), + fun({User, Server}, Dict) -> + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), + case gb_trees:lookup(LServer, Dict) of + none -> + gb_trees:insert(LServer, gb_sets:singleton(LUser), Dict); + {value, Set} -> + gb_trees:update(LServer, gb_sets:add(LUser, Set), Dict) + end + end, + gb_trees:empty(), + USPairs), Set = lists:foldl( - fun({LServer, UsersSet}, Results) -> - UsersList = gb_sets:to_list(UsersSet), - lists:foldl( - fun(M, Results2) -> - try M:which_users_exists(LServer, UsersList) of - {error, _} -> - Results2; - Res -> - gb_sets:union( - gb_sets:from_list([{U, LServer} || U <- Res]), - Results2) - catch - _:undef -> - lists:foldl( - fun(U, R2) -> - case user_exists(U, LServer) of - true -> - gb_sets:add({U, LServer}, R2); - _ -> - R2 - end - end, Results2, UsersList) - end - end, Results, auth_modules(LServer)) - end, gb_sets:empty(), gb_trees:to_list(ByServer)), + fun({LServer, UsersSet}, Results) -> + UsersList = gb_sets:to_list(UsersSet), + lists:foldl( + fun(M, Results2) -> + try M:which_users_exists(LServer, UsersList) of + {error, _} -> + Results2; + Res -> + gb_sets:union( + gb_sets:from_list([ {U, LServer} || U <- Res ]), + Results2) + catch + _:undef -> + lists:foldl( + fun(U, R2) -> + case user_exists(U, LServer) of + true -> + gb_sets:add({U, LServer}, R2); + _ -> + R2 + end + end, + Results2, + UsersList) + end + end, + Results, + auth_modules(LServer)) + end, + gb_sets:empty(), + gb_trees:to_list(ByServer)), gb_sets:to_list(Set). + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> case validate_credentials(User, Server) of - {ok, LUser, LServer} -> - lists:foreach( - fun(Mod) -> db_remove_user(LUser, LServer, Mod) end, - auth_modules(LServer)), - ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); - _Err -> - ok + {ok, LUser, LServer} -> + lists:foreach( + fun(Mod) -> db_remove_user(LUser, LServer, Mod) end, + auth_modules(LServer)), + ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); + _Err -> + ok end. + -spec remove_user(binary(), binary(), password()) -> ok | {error, atom()}. remove_user(User, Server, Password) -> case validate_credentials(User, Server, Password) of - {ok, LUser, LServer} -> - case lists:foldl( - fun (_, ok) -> - ok; - (Mod, _) -> - case db_check_password( - LUser, <<"">>, LServer, Password, - <<"">>, undefined, Mod) of - true -> - db_remove_user(LUser, LServer, Mod); - {stop, true} -> - db_remove_user(LUser, LServer, Mod); - false -> - {error, not_allowed}; - {stop, false} -> - {error, not_allowed} - end - end, {error, not_allowed}, auth_modules(Server)) of - ok -> - ejabberd_hooks:run( - remove_user, LServer, [LUser, LServer]); - Err -> - Err - end; - Err -> - Err + {ok, LUser, LServer} -> + case lists:foldl( + fun(_, ok) -> + ok; + (Mod, _) -> + case db_check_password( + LUser, + <<"">>, + LServer, + Password, + <<"">>, + undefined, + Mod) of + true -> + db_remove_user(LUser, LServer, Mod); + {stop, true} -> + db_remove_user(LUser, LServer, Mod); + false -> + {error, not_allowed}; + {stop, false} -> + {error, not_allowed} + end + end, + {error, not_allowed}, + auth_modules(Server)) of + ok -> + ejabberd_hooks:run( + remove_user, LServer, [LUser, LServer]); + Err -> + Err + end; + Err -> + Err end. + %% @doc Calculate informational entropy. -spec entropy(iodata()) -> float(). entropy(B) -> case binary_to_list(B) of - "" -> 0.0; - S -> - Set = lists:foldl(fun (C, - [Digit, Printable, LowLetter, HiLetter, - Other]) -> - if C >= $a, C =< $z -> - [Digit, Printable, 26, HiLetter, - Other]; - C >= $0, C =< $9 -> - [9, Printable, LowLetter, HiLetter, - Other]; - C >= $A, C =< $Z -> - [Digit, Printable, LowLetter, 26, - Other]; - C >= 33, C =< 126 -> - [Digit, 33, LowLetter, HiLetter, - Other]; - true -> - [Digit, Printable, LowLetter, - HiLetter, 128] - end - end, - [0, 0, 0, 0, 0], S), - length(S) * math:log(lists:sum(Set)) / math:log(2) + "" -> 0.0; + S -> + Set = lists:foldl(fun(C, + [Digit, Printable, LowLetter, HiLetter, + Other]) -> + if + C >= $a, C =< $z -> + [Digit, Printable, 26, HiLetter, + Other]; + C >= $0, C =< $9 -> + [9, Printable, LowLetter, HiLetter, + Other]; + C >= $A, C =< $Z -> + [Digit, Printable, LowLetter, 26, + Other]; + C >= 33, C =< 126 -> + [Digit, 33, LowLetter, HiLetter, + Other]; + true -> + [Digit, Printable, LowLetter, + HiLetter, 128] + end + end, + [0, 0, 0, 0, 0], + S), + length(S) * math:log(lists:sum(Set)) / math:log(2) end. + -spec backend_type(atom()) -> atom(). backend_type(Mod) -> case atom_to_list(Mod) of - "ejabberd_auth_" ++ T -> list_to_atom(T); - _ -> Mod + "ejabberd_auth_" ++ T -> list_to_atom(T); + _ -> Mod end. + -spec password_format(binary() | global) -> plain | scram. password_format(LServer) -> ejabberd_option:auth_password_format(LServer). + get_is_banned(User, Server) -> case mod_admin_extra:get_ban_details(User, Server) of - [] -> - not_banned; - BanDetails -> - {_, ReasonText} = lists:keyfind("reason", 1, BanDetails), - {is_banned, <<"Account is banned: ", ReasonText/binary>>} + [] -> + not_banned; + BanDetails -> + {_, ReasonText} = lists:keyfind("reason", 1, BanDetails), + {is_banned, <<"Account is banned: ", ReasonText/binary>>} end. + %%%---------------------------------------------------------------------- %%% Backend calls %%%---------------------------------------------------------------------- -spec db_try_register(binary(), binary(), binary(), [password()], module()) -> ok | {error, exists | db_failure | not_allowed}. db_try_register(User, Server, PlainPassword, Passwords, Mod) -> Ret = case erlang:function_exported(Mod, try_register_multiple, 3) of - true -> - case use_cache(Mod, Server) of - true -> - ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, Passwords}, - fun() -> Mod:try_register_multiple(User, Server, Passwords) end, - cache_nodes(Mod, Server)); - false -> - ets_cache:untag(Mod:try_register_multiple(User, Server, Passwords)) - end; - _ -> - case erlang:function_exported(Mod, try_register, 3) of - true -> - case use_cache(Mod, Server) of - true -> - ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, [PlainPassword]}, - fun() -> - case Mod:try_register(User, Server, PlainPassword) of - {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; - Other -> Other - end - end, cache_nodes(Mod, Server)); - false -> - case Mod:try_register(User, Server, PlainPassword) of - {_, {ok, Pass}} -> {ok, [Pass]}; - V -> ets_cache:untag(V) - end - end; - false -> - {error, not_allowed} - end - end, + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), + {User, Server}, + {ok, Passwords}, + fun() -> Mod:try_register_multiple(User, Server, Passwords) end, + cache_nodes(Mod, Server)); + false -> + ets_cache:untag(Mod:try_register_multiple(User, Server, Passwords)) + end; + _ -> + case erlang:function_exported(Mod, try_register, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), + {User, Server}, + {ok, [PlainPassword]}, + fun() -> + case Mod:try_register(User, Server, PlainPassword) of + {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; + Other -> Other + end + end, + cache_nodes(Mod, Server)); + false -> + case Mod:try_register(User, Server, PlainPassword) of + {_, {ok, Pass}} -> {ok, [Pass]}; + V -> ets_cache:untag(V) + end + end; + false -> + {error, not_allowed} + end + end, case Ret of - {ok, _} -> ok; - {error, _} = Err -> Err + {ok, _} -> ok; + {error, _} = Err -> Err end. + -spec db_set_password(binary(), binary(), binary(), [password()], module()) -> ok | {error, db_failure | not_allowed}. db_set_password(User, Server, PlainPassword, Passwords, Mod) -> Ret = case erlang:function_exported(Mod, set_password_multiple, 3) of - true -> - case use_cache(Mod, Server) of - true -> - ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, Passwords}, - fun() -> Mod:set_password_multiple(User, Server, Passwords) end, - cache_nodes(Mod, Server)); - false -> - ets_cache:untag(Mod:set_password_multiple(User, Server, Passwords)) - end; - _ -> - case erlang:function_exported(Mod, set_password, 3) of - true -> - case use_cache(Mod, Server) of - true -> - ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, [PlainPassword]}, - fun() -> - case Mod:set_password(User, Server, PlainPassword) of - {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; - Other -> Other - end - end, cache_nodes(Mod, Server)); - false -> - case Mod:set_password(User, Server, PlainPassword) of - {_, {ok, Pass}} -> {ok, [Pass]}; - V -> ets_cache:untag(V) - end - end; - false -> - {error, not_allowed} - end - end, + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), + {User, Server}, + {ok, Passwords}, + fun() -> Mod:set_password_multiple(User, Server, Passwords) end, + cache_nodes(Mod, Server)); + false -> + ets_cache:untag(Mod:set_password_multiple(User, Server, Passwords)) + end; + _ -> + case erlang:function_exported(Mod, set_password, 3) of + true -> + case use_cache(Mod, Server) of + true -> + ets_cache:update( + cache_tab(Mod), + {User, Server}, + {ok, [PlainPassword]}, + fun() -> + case Mod:set_password(User, Server, PlainPassword) of + {Tag, {ok, Pass}} -> {Tag, {ok, [Pass]}}; + Other -> Other + end + end, + cache_nodes(Mod, Server)); + false -> + case Mod:set_password(User, Server, PlainPassword) of + {_, {ok, Pass}} -> {ok, [Pass]}; + V -> ets_cache:untag(V) + end + end; + false -> + {error, not_allowed} + end + end, case Ret of - {ok, _} -> ejabberd_hooks:run(set_password, Server, [User, Server]); - {error, _} = Err -> Err + {ok, _} -> ejabberd_hooks:run(set_password, Server, [User, Server]); + {error, _} = Err -> Err end. + db_get_password(User, Server, Mod) -> UseCache = use_cache(Mod, Server), case erlang:function_exported(Mod, get_password, 2) of - false when UseCache -> - case ets_cache:lookup(cache_tab(Mod), {User, Server}) of - {ok, exists} -> error; - not_found -> error; - {ok, List} = V when is_list(List) -> V; - {ok, Single} -> {ok, [Single]}; - Other -> Other - end; - false -> - error; - true when UseCache -> - ets_cache:lookup( - cache_tab(Mod), {User, Server}, - fun() -> - case Mod:get_password(User, Server) of - {_, {ok, List}} = V when is_list(List) -> V; - {Tag, {ok, Single}} -> {Tag, {ok, [Single]}}; - Other -> Other - end - end); - true -> - case Mod:get_password(User, Server) of - {_, {ok, List}} when is_list(List) -> {ok, List}; - {_, {ok, Single}} -> {ok, [Single]}; - Other -> ets_cache:untag(Other) - end + false when UseCache -> + case ets_cache:lookup(cache_tab(Mod), {User, Server}) of + {ok, exists} -> error; + not_found -> error; + {ok, List} = V when is_list(List) -> V; + {ok, Single} -> {ok, [Single]}; + Other -> Other + end; + false -> + error; + true when UseCache -> + ets_cache:lookup( + cache_tab(Mod), + {User, Server}, + fun() -> + case Mod:get_password(User, Server) of + {_, {ok, List}} = V when is_list(List) -> V; + {Tag, {ok, Single}} -> {Tag, {ok, [Single]}}; + Other -> Other + end + end); + true -> + case Mod:get_password(User, Server) of + {_, {ok, List}} when is_list(List) -> {ok, List}; + {_, {ok, Single}} -> {ok, [Single]}; + Other -> ets_cache:untag(Other) + end end. + db_user_exists(User, Server, Mod) -> case db_get_password(User, Server, Mod) of - {ok, _} -> - {true, false}; - not_found -> - {false, false}; - error -> - case {Mod:store_type(Server), use_cache(Mod, Server)} of - {external, true} -> - Val = case ets_cache:lookup(cache_tab(Mod), {User, Server}, error) of - error -> - ets_cache:update(cache_tab(Mod), {User, Server}, {ok, exists}, - fun() -> - case Mod:user_exists(User, Server) of - {CacheTag, true} -> {CacheTag, {ok, exists}}; - {CacheTag, false} -> {CacheTag, not_found}; - {_, {error, _}} = Err -> Err - end - end); - Other -> - Other - end, - case Val of - {ok, _} -> - {true, Mod /= ejabberd_auth_anonymous}; - not_found -> - {false, Mod /= ejabberd_auth_anonymous}; - error -> - {false, Mod /= ejabberd_auth_anonymous}; - {error, _} = Err -> - {Err, Mod /= ejabberd_auth_anonymous} - end; - {external, false} -> - {ets_cache:untag(Mod:user_exists(User, Server)), Mod /= ejabberd_auth_anonymous}; - _ -> - {false, false} - end + {ok, _} -> + {true, false}; + not_found -> + {false, false}; + error -> + case {Mod:store_type(Server), use_cache(Mod, Server)} of + {external, true} -> + Val = case ets_cache:lookup(cache_tab(Mod), {User, Server}, error) of + error -> + ets_cache:update(cache_tab(Mod), + {User, Server}, + {ok, exists}, + fun() -> + case Mod:user_exists(User, Server) of + {CacheTag, true} -> {CacheTag, {ok, exists}}; + {CacheTag, false} -> {CacheTag, not_found}; + {_, {error, _}} = Err -> Err + end + end); + Other -> + Other + end, + case Val of + {ok, _} -> + {true, Mod /= ejabberd_auth_anonymous}; + not_found -> + {false, Mod /= ejabberd_auth_anonymous}; + error -> + {false, Mod /= ejabberd_auth_anonymous}; + {error, _} = Err -> + {Err, Mod /= ejabberd_auth_anonymous} + end; + {external, false} -> + {ets_cache:untag(Mod:user_exists(User, Server)), Mod /= ejabberd_auth_anonymous}; + _ -> + {false, false} + end end. -db_check_password(User, AuthzId, Server, ProvidedPassword, - Digest, DigestFun, Mod) -> + +db_check_password(User, + AuthzId, + Server, + ProvidedPassword, + Digest, + DigestFun, + Mod) -> case db_get_password(User, Server, Mod) of - {ok, ValidPasswords} -> - match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun); - error -> - case {Mod:store_type(Server), use_cache(Mod, Server)} of - {external, true} -> - case ets_cache:update( - cache_tab(Mod), {User, Server}, {ok, ProvidedPassword}, - fun() -> - case Mod:check_password( - User, AuthzId, Server, ProvidedPassword) of - {CacheTag, true} -> {CacheTag, {ok, ProvidedPassword}}; - {CacheTag, {stop, true}} -> {CacheTag, {ok, ProvidedPassword}}; - {CacheTag, false} -> {CacheTag, error}; - {CacheTag, {stop, false}} -> {CacheTag, error} - end - end) of - {ok, _} -> - true; - error -> - false - end; - {external, false} -> - ets_cache:untag( - Mod:check_password(User, AuthzId, Server, ProvidedPassword)); - _ -> - false - end + {ok, ValidPasswords} -> + match_passwords(ProvidedPassword, ValidPasswords, Digest, DigestFun); + error -> + case {Mod:store_type(Server), use_cache(Mod, Server)} of + {external, true} -> + case ets_cache:update( + cache_tab(Mod), + {User, Server}, + {ok, ProvidedPassword}, + fun() -> + case Mod:check_password( + User, AuthzId, Server, ProvidedPassword) of + {CacheTag, true} -> {CacheTag, {ok, ProvidedPassword}}; + {CacheTag, {stop, true}} -> {CacheTag, {ok, ProvidedPassword}}; + {CacheTag, false} -> {CacheTag, error}; + {CacheTag, {stop, false}} -> {CacheTag, error} + end + end) of + {ok, _} -> + true; + error -> + false + end; + {external, false} -> + ets_cache:untag( + Mod:check_password(User, AuthzId, Server, ProvidedPassword)); + _ -> + false + end end. + db_remove_user(User, Server, Mod) -> case erlang:function_exported(Mod, remove_user, 2) of - true -> - case Mod:remove_user(User, Server) of - ok -> - case use_cache(Mod, Server) of - true -> - ets_cache:delete(cache_tab(Mod), {User, Server}, - cache_nodes(Mod, Server)); - false -> - ok - end; - {error, _} = Err -> - Err - end; - false -> - {error, not_allowed} + true -> + case Mod:remove_user(User, Server) of + ok -> + case use_cache(Mod, Server) of + true -> + ets_cache:delete(cache_tab(Mod), + {User, Server}, + cache_nodes(Mod, Server)); + false -> + ok + end; + {error, _} = Err -> + Err + end; + false -> + {error, not_allowed} end. + db_get_users(Server, Opts, Mod) -> case erlang:function_exported(Mod, get_users, 2) of - true -> - Mod:get_users(Server, Opts); - false -> - case use_cache(Mod, Server) of - true -> - ets_cache:fold( - fun({User, S}, {ok, _}, Users) when S == Server -> - [{User, Server}|Users]; - (_, _, Users) -> - Users - end, [], cache_tab(Mod)); - false -> - [] - end + true -> + Mod:get_users(Server, Opts); + false -> + case use_cache(Mod, Server) of + true -> + ets_cache:fold( + fun({User, S}, {ok, _}, Users) when S == Server -> + [{User, Server} | Users]; + (_, _, Users) -> + Users + end, + [], + cache_tab(Mod)); + false -> + [] + end end. + db_count_users(Server, Opts, Mod) -> case erlang:function_exported(Mod, count_users, 2) of - true -> - Mod:count_users(Server, Opts); - false -> - case use_cache(Mod, Server) of - true -> - ets_cache:fold( - fun({_, S}, {ok, _}, Num) when S == Server -> - Num + 1; - (_, _, Num) -> - Num - end, 0, cache_tab(Mod)); - false -> - 0 - end + true -> + Mod:count_users(Server, Opts); + false -> + case use_cache(Mod, Server) of + true -> + ets_cache:fold( + fun({_, S}, {ok, _}, Num) when S == Server -> + Num + 1; + (_, _, Num) -> + Num + end, + 0, + cache_tab(Mod)); + false -> + 0 + end end. + %%%---------------------------------------------------------------------- %%% SCRAM stuff %%%---------------------------------------------------------------------- is_password_scram_valid(Password, Scram) -> case jid:resourceprep(Password) of - error -> - false; - _ -> - IterationCount = Scram#scram.iterationcount, - Hash = Scram#scram.hash, - Salt = base64:decode(Scram#scram.salt), - SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), - StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), - base64:decode(Scram#scram.storedkey) == StoredKey + error -> + false; + _ -> + IterationCount = Scram#scram.iterationcount, + Hash = Scram#scram.hash, + Salt = base64:decode(Scram#scram.salt), + SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), + StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), + base64:decode(Scram#scram.storedkey) == StoredKey end. + password_to_scram(Host, Password) -> password_to_scram(Host, Password, ?SCRAM_DEFAULT_ITERATION_COUNT). + password_to_scram(_Host, #scram{} = Password, _IterationCount) -> Password; password_to_scram(Host, Password, IterationCount) -> password_to_scram(Host, Password, ejabberd_option:auth_scram_hash(Host), IterationCount). + password_to_scram(_Host, Password, Hash, IterationCount) -> Salt = p1_rand:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), ServerKey = scram:server_key(Hash, SaltedPassword), - #scram{storedkey = base64:encode(StoredKey), - serverkey = base64:encode(ServerKey), - salt = base64:encode(Salt), - hash = Hash, - iterationcount = IterationCount}. + #scram{ + storedkey = base64:encode(StoredKey), + serverkey = base64:encode(ServerKey), + salt = base64:encode(Salt), + hash = Hash, + iterationcount = IterationCount + }. + %%%---------------------------------------------------------------------- %%% Cache stuff @@ -961,12 +1133,15 @@ init_cache(HostModules) -> {True, False} = use_cache(HostModules), lists:foreach( fun(Module) -> - ets_cache:new(cache_tab(Module), CacheOpts) - end, True), + ets_cache:new(cache_tab(Module), CacheOpts) + end, + True), lists:foreach( fun(Module) -> - ets_cache:delete(cache_tab(Module)) - end, False). + ets_cache:delete(cache_tab(Module)) + end, + False). + -spec cache_opts() -> [proplists:property()]. cache_opts() -> @@ -975,42 +1150,51 @@ cache_opts() -> LifeTime = ejabberd_option:auth_cache_life_time(), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(host_modules()) -> {True :: [module()], False :: [module()]}. use_cache(HostModules) -> {Enabled, Disabled} = - maps:fold( - fun(Host, Modules, Acc) -> - lists:foldl( - fun(Module, {True, False}) -> - case use_cache(Module, Host) of - true -> - {sets:add_element(Module, True), False}; - false -> - {True, sets:add_element(Module, False)} - end - end, Acc, Modules) - end, {sets:new(), sets:new()}, HostModules), + maps:fold( + fun(Host, Modules, Acc) -> + lists:foldl( + fun(Module, {True, False}) -> + case use_cache(Module, Host) of + true -> + {sets:add_element(Module, True), False}; + false -> + {True, sets:add_element(Module, False)} + end + end, + Acc, + Modules) + end, + {sets:new(), sets:new()}, + HostModules), {sets:to_list(Enabled), sets:to_list(sets:subtract(Disabled, Enabled))}. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, LServer) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(LServer); - false -> - ejabberd_option:auth_use_cache(LServer) + true -> Mod:use_cache(LServer); + false -> + ejabberd_option:auth_use_cache(LServer) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, LServer) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(LServer); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(LServer); + false -> ejabberd_cluster:get_nodes() end. + -spec cache_tab(module()) -> atom(). cache_tab(Mod) -> list_to_atom(atom_to_list(Mod) ++ "_cache"). + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- @@ -1018,117 +1202,139 @@ cache_tab(Mod) -> auth_modules() -> lists:flatmap( fun(Host) -> - [{Host, Mod} || Mod <- auth_modules(Host)] - end, ejabberd_option:hosts()). + [ {Host, Mod} || Mod <- auth_modules(Host) ] + end, + ejabberd_option:hosts()). + -spec auth_modules(binary()) -> [module()]. auth_modules(Server) -> LServer = jid:nameprep(Server), Methods = ejabberd_option:auth_method(LServer), - [ejabberd:module_name([<<"auth">>, - misc:atom_to_binary(M)]) - || M <- Methods]. + [ ejabberd:module_name([<<"auth">>, + misc:atom_to_binary(M)]) + || M <- Methods ]. --spec match_passwords(password(), [password()], - binary(), digest_fun() | undefined) -> boolean(). + +-spec match_passwords(password(), + [password()], + binary(), + digest_fun() | undefined) -> boolean(). match_passwords(Provided, Passwords, Digest, DigestFun) -> lists:any( - fun(Pass) -> - match_password(Provided, Pass, Digest, DigestFun) - end, Passwords). + fun(Pass) -> + match_password(Provided, Pass, Digest, DigestFun) + end, + Passwords). --spec match_password(password(), password(), - binary(), digest_fun() | undefined) -> boolean(). + +-spec match_password(password(), + password(), + binary(), + digest_fun() | undefined) -> boolean(). match_password(Password, #scram{} = Scram, <<"">>, undefined) -> is_password_scram_valid(Password, Scram); match_password(Password, #scram{} = Scram, Digest, DigestFun) -> StoredKey = base64:decode(Scram#scram.storedkey), - DigRes = if Digest /= <<"">> -> - Digest == DigestFun(StoredKey); - true -> false - end, - if DigRes -> - true; - true -> - StoredKey == Password andalso Password /= <<"">> + DigRes = if + Digest /= <<"">> -> + Digest == DigestFun(StoredKey); + true -> false + end, + if + DigRes -> + true; + true -> + StoredKey == Password andalso Password /= <<"">> end; match_password(ProvidedPassword, ValidPassword, <<"">>, undefined) -> ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>; match_password(ProvidedPassword, ValidPassword, Digest, DigestFun) -> - DigRes = if Digest /= <<"">> -> - Digest == DigestFun(ValidPassword); - true -> false - end, - if DigRes -> - true; - true -> - ValidPassword == ProvidedPassword andalso ProvidedPassword /= <<"">> + DigRes = if + Digest /= <<"">> -> + Digest == DigestFun(ValidPassword); + true -> false + end, + if + DigRes -> + true; + true -> + ValidPassword == ProvidedPassword andalso ProvidedPassword /= <<"">> end. + -spec validate_credentials(binary(), binary()) -> - {ok, binary(), binary()} | {error, invalid_jid}. + {ok, binary(), binary()} | {error, invalid_jid}. validate_credentials(User, Server) -> validate_credentials(User, Server, #scram{}). + -spec validate_credentials(binary(), binary(), password()) -> - {ok, binary(), binary()} | {error, invalid_jid | invalid_password}. + {ok, binary(), binary()} | {error, invalid_jid | invalid_password}. validate_credentials(_User, _Server, <<"">>) -> {error, invalid_password}; validate_credentials(User, Server, Password) -> case jid:nodeprep(User) of - error -> - {error, invalid_jid}; - LUser -> - case jid:nameprep(Server) of - error -> - {error, invalid_jid}; - LServer -> - if is_record(Password, scram) -> - {ok, LUser, LServer}; - true -> - case jid:resourceprep(Password) of - error -> - {error, invalid_password}; - _ -> - {ok, LUser, LServer} - end - end - end + error -> + {error, invalid_jid}; + LUser -> + case jid:nameprep(Server) of + error -> + {error, invalid_jid}; + LServer -> + if + is_record(Password, scram) -> + {ok, LUser, LServer}; + true -> + case jid:resourceprep(Password) of + error -> + {error, invalid_password}; + _ -> + {ok, LUser, LServer} + end + end + end end. + untag_stop({stop, Val}) -> Val; untag_stop(Val) -> Val. + import_info() -> [{<<"users">>, 3}]. + import_start(_LServer, mnesia) -> ejabberd_auth_mnesia:init_db(); import_start(_LServer, _) -> ok. + import(Server, {sql, _}, mnesia, <<"users">>, Fields) -> ejabberd_auth_mnesia:import(Server, Fields); import(_LServer, {sql, _}, sql, <<"users">>, _) -> ok. + -spec convert_to_scram(binary()) -> {error, any()} | ok. convert_to_scram(Server) -> LServer = jid:nameprep(Server), if - LServer == error; - LServer == <<>> -> - {error, {incorrect_server_name, Server}}; - true -> - lists:foreach( - fun({U, S}) -> - case get_password(U, S) of - [Pass] when is_binary(Pass) -> - SPass = password_to_scram(Server, Pass), - set_password(U, S, SPass); - _ -> - ok - end - end, get_users(LServer)), - ok + LServer == error; + LServer == <<>> -> + {error, {incorrect_server_name, Server}}; + true -> + lists:foreach( + fun({U, S}) -> + case get_password(U, S) of + [Pass] when is_binary(Pass) -> + SPass = password_to_scram(Server, Pass), + set_password(U, S, SPass); + _ -> + ok + end + end, + get_users(LServer)), + ok end. diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index 8f67b695b..9381e5fbf 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -31,159 +31,196 @@ -protocol({xep, 175, '1.2', '1.1.0', "complete", ""}). -export([start/1, - stop/1, + stop/1, use_cache/1, - allow_anonymous/1, - is_sasl_anonymous_enabled/1, - is_login_anonymous_enabled/1, - anonymous_user_exist/2, - allow_multiple_connections/1, - register_connection/3, - unregister_connection/3 - ]). + allow_anonymous/1, + is_sasl_anonymous_enabled/1, + is_login_anonymous_enabled/1, + anonymous_user_exist/2, + allow_multiple_connections/1, + register_connection/3, + unregister_connection/3]). --export([login/2, check_password/4, user_exists/2, - get_users/2, count_users/2, store_type/1, - plain_password_required/1]). +-export([login/2, + check_password/4, + user_exists/2, + get_users/2, + count_users/2, + store_type/1, + plain_password_required/1]). -include("logger.hrl"). + -include_lib("xmpp/include/jid.hrl"). + start(Host) -> - ejabberd_hooks:add(sm_register_connection_hook, Host, - ?MODULE, register_connection, 100), - ejabberd_hooks:add(sm_remove_connection_hook, Host, - ?MODULE, unregister_connection, 100), + ejabberd_hooks:add(sm_register_connection_hook, + Host, + ?MODULE, + register_connection, + 100), + ejabberd_hooks:add(sm_remove_connection_hook, + Host, + ?MODULE, + unregister_connection, + 100), ok. + stop(Host) -> - ejabberd_hooks:delete(sm_register_connection_hook, Host, - ?MODULE, register_connection, 100), - ejabberd_hooks:delete(sm_remove_connection_hook, Host, - ?MODULE, unregister_connection, 100). + ejabberd_hooks:delete(sm_register_connection_hook, + Host, + ?MODULE, + register_connection, + 100), + ejabberd_hooks:delete(sm_remove_connection_hook, + Host, + ?MODULE, + unregister_connection, + 100). + use_cache(_) -> false. + %% Return true if anonymous is allowed for host or false otherwise allow_anonymous(Host) -> lists:member(?MODULE, ejabberd_auth:auth_modules(Host)). + %% Return true if anonymous mode is enabled and if anonymous protocol is SASL %% anonymous protocol can be: sasl_anon|login_anon|both is_sasl_anonymous_enabled(Host) -> case allow_anonymous(Host) of - false -> false; - true -> - case anonymous_protocol(Host) of - sasl_anon -> true; - both -> true; - _Other -> false - end + false -> false; + true -> + case anonymous_protocol(Host) of + sasl_anon -> true; + both -> true; + _Other -> false + end end. + %% Return true if anonymous login is enabled on the server %% anonymous login can be use using standard authentication method (i.e. with %% clients that do not support anonymous login) is_login_anonymous_enabled(Host) -> case allow_anonymous(Host) of - false -> false; - true -> - case anonymous_protocol(Host) of - login_anon -> true; - both -> true; - _Other -> false - end + false -> false; + true -> + case anonymous_protocol(Host) of + login_anon -> true; + both -> true; + _Other -> false + end end. + %% Return the anonymous protocol to use: sasl_anon|login_anon|both %% defaults to login_anon anonymous_protocol(Host) -> ejabberd_option:anonymous_protocol(Host). + %% Return true if multiple connections have been allowed in the config file %% defaults to false allow_multiple_connections(Host) -> ejabberd_option:allow_multiple_connections(Host). + anonymous_user_exist(User, Server) -> lists:any( fun({_LResource, Info}) -> - proplists:get_value(auth_module, Info) == ?MODULE - end, ejabberd_sm:get_user_info(User, Server)). + proplists:get_value(auth_module, Info) == ?MODULE + end, + ejabberd_sm:get_user_info(User, Server)). + %% Register connection -spec register_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. register_connection(_SID, - #jid{luser = LUser, lserver = LServer, lresource = LResource}, Info) -> + #jid{luser = LUser, lserver = LServer, lresource = LResource}, + Info) -> case proplists:get_value(auth_module, Info) of - ?MODULE -> - % Register user only if we are first resource - case ejabberd_sm:get_user_resources(LUser, LServer) of - [LResource] -> - ejabberd_hooks:run(register_user, LServer, [LUser, LServer]); - _ -> - ok - end; - _ -> - ok + ?MODULE -> + % Register user only if we are first resource + case ejabberd_sm:get_user_resources(LUser, LServer) of + [LResource] -> + ejabberd_hooks:run(register_user, LServer, [LUser, LServer]); + _ -> + ok + end; + _ -> + ok end. + %% Remove an anonymous user from the anonymous users table -spec unregister_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). unregister_connection(_SID, - #jid{luser = LUser, lserver = LServer}, Info) -> + #jid{luser = LUser, lserver = LServer}, + Info) -> case proplists:get_value(auth_module, Info) of - ?MODULE -> - % Remove user data only if there is no more resources around - case ejabberd_sm:get_user_resources(LUser, LServer) of - [] -> - ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); - _ -> - ok - end; - _ -> - ok + ?MODULE -> + % Remove user data only if there is no more resources around + case ejabberd_sm:get_user_resources(LUser, LServer) of + [] -> + ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); + _ -> + ok + end; + _ -> + ok end. + %% --------------------------------- %% Specific anonymous auth functions %% --------------------------------- check_password(User, _AuthzId, Server, _Password) -> {nocache, case ejabberd_auth:user_exists_in_other_modules(?MODULE, User, Server) of - %% If user exists in other module, reject anonnymous authentication - true -> false; - %% If we are not sure whether the user exists in other module, reject anon auth - maybe_exists -> false; - false -> login(User, Server) + %% If user exists in other module, reject anonnymous authentication + true -> false; + %% If we are not sure whether the user exists in other module, reject anon auth + maybe_exists -> false; + false -> login(User, Server) end}. + login(User, Server) -> case is_login_anonymous_enabled(Server) of - false -> false; - true -> - case anonymous_user_exist(User, Server) of - %% Reject the login if an anonymous user with the same login - %% is already logged and if multiple login has not been enable - %% in the config file. - true -> allow_multiple_connections(Server); - %% Accept login and add user to the anonymous table - false -> true - end + false -> false; + true -> + case anonymous_user_exist(User, Server) of + %% Reject the login if an anonymous user with the same login + %% is already logged and if multiple login has not been enable + %% in the config file. + true -> allow_multiple_connections(Server); + %% Accept login and add user to the anonymous table + false -> true + end end. + get_users(Server, _) -> - [{U, S} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Server)]. + [ {U, S} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Server) ]. + count_users(Server, Opts) -> length(get_users(Server, Opts)). + user_exists(User, Server) -> {nocache, anonymous_user_exist(User, Server)}. + plain_password_required(_) -> false. + store_type(_) -> external. diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index 1b69a9a10..6c469a4e8 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -29,77 +29,99 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, reload/1, set_password/3, check_password/4, - try_register/3, user_exists/2, remove_user/2, - store_type/1, plain_password_required/1]). +-export([start/1, + stop/1, + reload/1, + set_password/3, + check_password/4, + try_register/3, + user_exists/2, + remove_user/2, + store_type/1, + plain_password_required/1]). -include("logger.hrl"). + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> extauth:start(Host). + stop(Host) -> extauth:stop(Host). + reload(Host) -> extauth:reload(Host). + plain_password_required(_) -> true. + store_type(_) -> external. + check_password(User, AuthzId, Server, Password) -> - if AuthzId /= <<>> andalso AuthzId /= User -> - {nocache, false}; - true -> - check_password_extauth(User, AuthzId, Server, Password) + if + AuthzId /= <<>> andalso AuthzId /= User -> + {nocache, false}; + true -> + check_password_extauth(User, AuthzId, Server, Password) end. + set_password(User, Server, Password) -> case extauth:set_password(User, Server, Password) of - Res when is_boolean(Res) -> {cache, {ok, Password}}; - {error, Reason} -> failure(User, Server, set_password, Reason) + Res when is_boolean(Res) -> {cache, {ok, Password}}; + {error, Reason} -> failure(User, Server, set_password, Reason) end. + try_register(User, Server, Password) -> case extauth:try_register(User, Server, Password) of - true -> {cache, {ok, Password}}; - false -> {cache, {error, not_allowed}}; - {error, Reason} -> failure(User, Server, try_register, Reason) + true -> {cache, {ok, Password}}; + false -> {cache, {error, not_allowed}}; + {error, Reason} -> failure(User, Server, try_register, Reason) end. + user_exists(User, Server) -> case extauth:user_exists(User, Server) of - Res when is_boolean(Res) -> {cache, Res}; - {error, Reason} -> failure(User, Server, user_exists, Reason) + Res when is_boolean(Res) -> {cache, Res}; + {error, Reason} -> failure(User, Server, user_exists, Reason) end. + remove_user(User, Server) -> case extauth:remove_user(User, Server) of - false -> {error, not_allowed}; - true -> ok; - {error, Reason} -> - {_, Err} = failure(User, Server, remove_user, Reason), - Err + false -> {error, not_allowed}; + true -> ok; + {error, Reason} -> + {_, Err} = failure(User, Server, remove_user, Reason), + Err end. + check_password_extauth(User, _AuthzId, Server, Password) -> - if Password /= <<"">> -> - case extauth:check_password(User, Server, Password) of - Res when is_boolean(Res) -> {cache, Res}; - {error, Reason} -> - {Tag, _} = failure(User, Server, check_password, Reason), - {Tag, false} - end; - true -> - {nocache, false} + if + Password /= <<"">> -> + case extauth:check_password(User, Server, Password) of + Res when is_boolean(Res) -> {cache, Res}; + {error, Reason} -> + {Tag, _} = failure(User, Server, check_password, Reason), + {Tag, false} + end; + true -> + {nocache, false} end. + -spec failure(binary(), binary(), atom(), any()) -> {nocache, {error, db_failure}}. failure(User, Server, Fun, Reason) -> ?ERROR_MSG("External authentication program failed when calling " - "'~ts' for ~ts@~ts: ~p", [Fun, User, Server, Reason]), + "'~ts' for ~ts@~ts: ~p", + [Fun, User, Server, Reason]), {nocache, {error, db_failure}}. diff --git a/src/ejabberd_auth_jwt.erl b/src/ejabberd_auth_jwt.erl index 7fac3e4f7..633245572 100644 --- a/src/ejabberd_auth_jwt.erl +++ b/src/ejabberd_auth_jwt.erl @@ -29,16 +29,21 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, check_password/4, - store_type/1, plain_password_required/1, - user_exists/2, use_cache/1 - ]). +-export([start/1, + stop/1, + check_password/4, + store_type/1, + plain_password_required/1, + user_exists/2, + use_cache/1]). %% 'ejabberd_hooks' callback: -export([check_decoded_jwt/5]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- @@ -50,32 +55,40 @@ start(Host) -> %% callback function. ejabberd_hooks:add(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100), case ejabberd_option:jwt_key(Host) of - undefined -> - ?ERROR_MSG("Option jwt_key is not configured for ~ts: " - "JWT authentication won't work", [Host]); - _ -> - ok + undefined -> + ?ERROR_MSG("Option jwt_key is not configured for ~ts: " + "JWT authentication won't work", + [Host]); + _ -> + ok end. + stop(Host) -> ejabberd_hooks:delete(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100). + plain_password_required(_Host) -> true. + store_type(_Host) -> external. + -spec check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. check_password(User, AuthzId, Server, Token) -> %% MREMOND: Should we move the AuthzId check at a higher level in %% the call stack? - if AuthzId /= <<>> andalso AuthzId /= User -> + if + AuthzId /= <<>> andalso AuthzId /= User -> {nocache, false}; - true -> - if Token == <<"">> -> {nocache, false}; - true -> + true -> + if + Token == <<"">> -> {nocache, false}; + true -> Res = check_jwt_token(User, Server, Token), Rule = ejabberd_option:jwt_auth_only_rule(Server), - case acl:match_rule(Server, Rule, + case acl:match_rule(Server, + Rule, jid:make(User, Server, <<"">>)) of deny -> {nocache, Res}; @@ -85,18 +98,21 @@ check_password(User, AuthzId, Server, Token) -> end end. + user_exists(User, Host) -> - %% Checking that the user has an active session - %% If the session was negociated by the JWT auth method then we define that the user exists - %% Any other cases will return that the user doesn't exist - {nocache, case ejabberd_sm:get_user_info(User, Host) of - [{_, Info}] -> proplists:get_value(auth_module, Info) == ejabberd_auth_jwt; - _ -> false - end}. + %% Checking that the user has an active session + %% If the session was negociated by the JWT auth method then we define that the user exists + %% Any other cases will return that the user doesn't exist + {nocache, case ejabberd_sm:get_user_info(User, Host) of + [{_, Info}] -> proplists:get_value(auth_module, Info) == ejabberd_auth_jwt; + _ -> false + end}. + use_cache(_) -> false. + %%%---------------------------------------------------------------------- %%% 'ejabberd_hooks' callback %%%---------------------------------------------------------------------- @@ -107,15 +123,17 @@ check_decoded_jwt(true, Fields, _Signature, Server, User) -> try JID = jid:decode(SJid), JID#jid.luser == User andalso JID#jid.lserver == Server - catch error:{bad_jid, _} -> - false + catch + error:{bad_jid, _} -> + false end; - _ -> % error | {ok, _UnknownType} + _ -> % error | {ok, _UnknownType} false end; check_decoded_jwt(Acc, _, _, _, _) -> Acc. + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- @@ -125,19 +143,18 @@ check_jwt_token(User, Server, Token) -> {true, {jose_jwt, Fields}, Signature} -> Now = erlang:system_time(second), ?DEBUG("jwt verify at system timestamp ~p: ~p - ~p~n", [Now, Fields, Signature]), - case maps:find(<<"exp">>, Fields) of + case maps:find(<<"exp">>, Fields) of error -> - %% No expiry in token => We consider token invalid: - false; + %% No expiry in token => We consider token invalid: + false; {ok, Exp} -> if Exp > Now -> ejabberd_hooks:run_fold( - check_decoded_jwt, - Server, - true, - [Fields, Signature, Server, User] - ); + check_decoded_jwt, + Server, + true, + [Fields, Signature, Server, User]); true -> %% return false, if token has expired false diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 8656684aa..93cf5256d 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -31,14 +31,24 @@ -behaviour(ejabberd_auth). %% gen_server callbacks --export([init/1, handle_info/2, handle_call/3, - handle_cast/2, terminate/2, code_change/3]). +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3]). --export([start/1, stop/1, start_link/1, set_password/3, - check_password/4, user_exists/2, - get_users/2, count_users/2, - store_type/1, plain_password_required/1, - reload/1]). +-export([start/1, + stop/1, + start_link/1, + set_password/3, + check_password/4, + user_exists/2, + get_users/2, + count_users/2, + store_type/1, + plain_password_required/1, + reload/1]). -include("logger.hrl"). @@ -48,13 +58,13 @@ %% @efmt:off %% @indent-begin -record(state, - {host = <<"">> :: binary(), + {host = <<"">> :: binary(), eldap_id = <<"">> :: binary(), bind_eldap_id = <<"">> :: binary(), servers = [] :: [binary()], backups = [] :: [binary()], port = ?LDAP_PORT :: inet:port_number(), - tls_options = [] :: list(), + tls_options = [] :: list(), dn = <<"">> :: binary(), password = <<"">> :: binary(), base = <<"">> :: binary(), @@ -66,120 +76,150 @@ dn_filter_attrs = [] :: [binary()]}). %% @indent-end %% @efmt:on -%% + %% handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -define(LDAP_SEARCH_TIMEOUT, 5). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- + start(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), - ChildSpec = {Proc, {?MODULE, start_link, [Host]}, - transient, 1000, worker, [?MODULE]}, + ChildSpec = {Proc, + {?MODULE, start_link, [Host]}, + transient, + 1000, + worker, + [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, ChildSpec). + stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), case supervisor:terminate_child(ejabberd_backend_sup, Proc) of - ok -> supervisor:delete_child(ejabberd_backend_sup, Proc); - Err -> Err + ok -> supervisor:delete_child(ejabberd_backend_sup, Proc); + Err -> Err end. + start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, Host, []). + terminate(_Reason, _State) -> ok. + init(Host) -> process_flag(trap_exit, true), State = parse_options(Host), eldap_pool:start_link(State#state.eldap_id, - State#state.servers, State#state.backups, - State#state.port, State#state.dn, - State#state.password, State#state.tls_options), + State#state.servers, + State#state.backups, + State#state.port, + State#state.dn, + State#state.password, + State#state.tls_options), eldap_pool:start_link(State#state.bind_eldap_id, - State#state.servers, State#state.backups, - State#state.port, State#state.dn, - State#state.password, State#state.tls_options), + State#state.servers, + State#state.backups, + State#state.port, + State#state.dn, + State#state.password, + State#state.tls_options), {ok, State}. + reload(Host) -> stop(Host), start(Host). + plain_password_required(_) -> true. + store_type(_) -> external. + check_password(User, AuthzId, Server, Password) -> - if AuthzId /= <<>> andalso AuthzId /= User -> - {nocache, false}; - Password == <<"">> -> - {nocache, false}; - true -> - case catch check_password_ldap(User, Server, Password) of - {'EXIT', _} -> {nocache, false}; - Result -> {cache, Result} - end + if + AuthzId /= <<>> andalso AuthzId /= User -> + {nocache, false}; + Password == <<"">> -> + {nocache, false}; + true -> + case catch check_password_ldap(User, Server, Password) of + {'EXIT', _} -> {nocache, false}; + Result -> {cache, Result} + end end. + set_password(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of - false -> {cache, {error, db_failure}}; - DN -> - case eldap_pool:modify_passwd(State#state.eldap_id, DN, - Password) of - ok -> {cache, {ok, Password}}; - _Err -> {nocache, {error, db_failure}} - end + false -> {cache, {error, db_failure}}; + DN -> + case eldap_pool:modify_passwd(State#state.eldap_id, + DN, + Password) of + ok -> {cache, {ok, Password}}; + _Err -> {nocache, {error, db_failure}} + end end. + get_users(Server, []) -> case catch get_users_ldap(Server) of - {'EXIT', _} -> []; - Result -> Result + {'EXIT', _} -> []; + Result -> Result end. + count_users(Server, Opts) -> length(get_users(Server, Opts)). + user_exists(User, Server) -> case catch user_exists_ldap(User, Server) of - {'EXIT', _Error} -> {nocache, {error, db_failure}}; - Result -> {cache, Result} + {'EXIT', _Error} -> {nocache, {error, db_failure}}; + Result -> {cache, Result} end. + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- check_password_ldap(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of - false -> false; - DN -> - case eldap_pool:bind(State#state.bind_eldap_id, DN, - Password) - of - ok -> true; - _ -> false - end + false -> false; + DN -> + case eldap_pool:bind(State#state.bind_eldap_id, + DN, + Password) of + ok -> true; + _ -> false + end end. + get_users_ldap(Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), UIDs = State#state.uids, @@ -187,55 +227,54 @@ get_users_ldap(Server) -> Server = State#state.host, ResAttrs = result_attrs(State), case eldap_filter:parse(State#state.sfilter) of - {ok, EldapFilter} -> - case eldap_pool:search(Eldap_ID, - [{base, State#state.base}, - {filter, EldapFilter}, - {timeout, ?LDAP_SEARCH_TIMEOUT}, - {deref_aliases, State#state.deref_aliases}, - {attributes, ResAttrs}]) - of - #eldap_search_result{entries = Entries} -> - lists:flatmap(fun (#eldap_entry{attributes = Attrs, - object_name = DN}) -> - case is_valid_dn(DN, Attrs, State) of - false -> []; - _ -> - case - eldap_utils:find_ldap_attrs(UIDs, - Attrs) - of - <<"">> -> []; - {User, UIDFormat} -> - case - eldap_utils:get_user_part(User, - UIDFormat) - of - {ok, U} -> - case jid:nodeprep(U) of - error -> []; - LU -> - [{LU, - jid:nameprep(Server)}] - end; - _ -> [] - end - end - end - end, - Entries); - _ -> [] - end; - _ -> [] + {ok, EldapFilter} -> + case eldap_pool:search(Eldap_ID, + [{base, State#state.base}, + {filter, EldapFilter}, + {timeout, ?LDAP_SEARCH_TIMEOUT}, + {deref_aliases, State#state.deref_aliases}, + {attributes, ResAttrs}]) of + #eldap_search_result{entries = Entries} -> + lists:flatmap(fun(#eldap_entry{ + attributes = Attrs, + object_name = DN + }) -> + case is_valid_dn(DN, Attrs, State) of + false -> []; + _ -> + case eldap_utils:find_ldap_attrs(UIDs, + Attrs) of + <<"">> -> []; + {User, UIDFormat} -> + case eldap_utils:get_user_part(User, + UIDFormat) of + {ok, U} -> + case jid:nodeprep(U) of + error -> []; + LU -> + [{LU, + jid:nameprep(Server)}] + end; + _ -> [] + end + end + end + end, + Entries); + _ -> [] + end; + _ -> [] end. + user_exists_ldap(User, Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of - false -> false; - _DN -> true + false -> false; + _DN -> true end. + handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(stop, _From, State) -> @@ -244,68 +283,75 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + find_user_dn(User, State) -> ResAttrs = result_attrs(State), case eldap_filter:parse(State#state.ufilter, - [{<<"%u">>, User}]) - of - {ok, Filter} -> - case eldap_pool:search(State#state.eldap_id, - [{base, State#state.base}, {filter, Filter}, - {deref_aliases, State#state.deref_aliases}, - {attributes, ResAttrs}]) - of - #eldap_search_result{entries = - [#eldap_entry{attributes = Attrs, - object_name = DN} - | _]} -> - is_valid_dn(DN, Attrs, State); - _ -> false - end; - _ -> false + [{<<"%u">>, User}]) of + {ok, Filter} -> + case eldap_pool:search(State#state.eldap_id, + [{base, State#state.base}, + {filter, Filter}, + {deref_aliases, State#state.deref_aliases}, + {attributes, ResAttrs}]) of + #eldap_search_result{ + entries = + [#eldap_entry{ + attributes = Attrs, + object_name = DN + } | _] + } -> + is_valid_dn(DN, Attrs, State); + _ -> false + end; + _ -> false end. + %% Check that the DN is valid, based on the dn filter is_valid_dn(DN, _, #state{dn_filter = undefined}) -> DN; is_valid_dn(DN, Attrs, State) -> DNAttrs = State#state.dn_filter_attrs, UIDs = State#state.uids, - Values = [{<<"%s">>, - eldap_utils:get_ldap_attr(Attr, Attrs), 1} - || Attr <- DNAttrs], + Values = [ {<<"%s">>, + eldap_utils:get_ldap_attr(Attr, Attrs), + 1} + || Attr <- DNAttrs ], SubstValues = case eldap_utils:find_ldap_attrs(UIDs, - Attrs) - of - <<"">> -> Values; - {S, UAF} -> - case eldap_utils:get_user_part(S, UAF) of - {ok, U} -> [{<<"%u">>, U} | Values]; - _ -> Values - end - end - ++ [{<<"%d">>, State#state.host}, {<<"%D">>, DN}], + Attrs) of + <<"">> -> Values; + {S, UAF} -> + case eldap_utils:get_user_part(S, UAF) of + {ok, U} -> [{<<"%u">>, U} | Values]; + _ -> Values + end + end ++ + [{<<"%d">>, State#state.host}, {<<"%D">>, DN}], case eldap_filter:parse(State#state.dn_filter, - SubstValues) - of - {ok, EldapFilter} -> - case eldap_pool:search(State#state.eldap_id, - [{base, State#state.base}, - {filter, EldapFilter}, - {deref_aliases, State#state.deref_aliases}, - {attributes, [<<"dn">>]}]) - of - #eldap_search_result{entries = [_ | _]} -> DN; - _ -> false - end; - _ -> false + SubstValues) of + {ok, EldapFilter} -> + case eldap_pool:search(State#state.eldap_id, + [{base, State#state.base}, + {filter, EldapFilter}, + {deref_aliases, State#state.deref_aliases}, + {attributes, [<<"dn">>]}]) of + #eldap_search_result{entries = [_ | _]} -> DN; + _ -> false + end; + _ -> false end. -result_attrs(#state{uids = UIDs, - dn_filter_attrs = DNFilterAttrs}) -> - lists:foldl(fun ({UID}, Acc) -> [UID | Acc]; - ({UID, _}, Acc) -> [UID | Acc] - end, - DNFilterAttrs, UIDs). + +result_attrs(#state{ + uids = UIDs, + dn_filter_attrs = DNFilterAttrs + }) -> + lists:foldl(fun({UID}, Acc) -> [UID | Acc]; + ({UID, _}, Acc) -> [UID | Acc] + end, + DNFilterAttrs, + UIDs). + %%%---------------------------------------------------------------------- %%% Auxiliary functions @@ -317,25 +363,30 @@ parse_options(Host) -> gen_mod:get_module_proc(Host, bind_ejabberd_auth_ldap)), UIDsTemp = ejabberd_option:ldap_uids(Host), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), - SubFilter = eldap_utils:generate_subfilter(UIDs), + SubFilter = eldap_utils:generate_subfilter(UIDs), UserFilter = case ejabberd_option:ldap_filter(Host) of <<"">> -> - SubFilter; + SubFilter; F -> <<"(&", SubFilter/binary, F/binary, ")">> end, SearchFilter = eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}]), {DNFilter, DNFilterAttrs} = ejabberd_option:ldap_dn_filter(Host), - #state{host = Host, eldap_id = Eldap_ID, - bind_eldap_id = Bind_Eldap_ID, - servers = Cfg#eldap_config.servers, - backups = Cfg#eldap_config.backups, - port = Cfg#eldap_config.port, - tls_options = Cfg#eldap_config.tls_options, - dn = Cfg#eldap_config.dn, - password = Cfg#eldap_config.password, - base = Cfg#eldap_config.base, - deref_aliases = Cfg#eldap_config.deref_aliases, - uids = UIDs, ufilter = UserFilter, - sfilter = SearchFilter, - dn_filter = DNFilter, dn_filter_attrs = DNFilterAttrs}. + #state{ + host = Host, + eldap_id = Eldap_ID, + bind_eldap_id = Bind_Eldap_ID, + servers = Cfg#eldap_config.servers, + backups = Cfg#eldap_config.backups, + port = Cfg#eldap_config.port, + tls_options = Cfg#eldap_config.tls_options, + dn = Cfg#eldap_config.dn, + password = Cfg#eldap_config.password, + base = Cfg#eldap_config.base, + deref_aliases = Cfg#eldap_config.deref_aliases, + uids = UIDs, + ufilter = UserFilter, + sfilter = SearchFilter, + dn_filter = DNFilter, + dn_filter_attrs = DNFilterAttrs + }. diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index 996dd620f..756bcf552 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -29,19 +29,34 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3, - get_users/2, init_db/0, - count_users/2, get_password/2, - remove_user/2, store_type/1, import/2, - plain_password_required/1, use_cache/1, drop_password_type/2, set_password_instance/3]). +-export([start/1, + stop/1, + set_password_multiple/3, + try_register_multiple/3, + get_users/2, + init_db/0, + count_users/2, + get_password/2, + remove_user/2, + store_type/1, + import/2, + plain_password_required/1, + use_cache/1, + drop_password_type/2, + set_password_instance/3]). -export([need_transform/1, transform/1]). -include("logger.hrl"). + -include_lib("xmpp/include/scram.hrl"). + -include("ejabberd_auth.hrl"). --record(reg_users_counter, {vhost = <<"">> :: binary(), - count = 0 :: integer() | '$1'}). +-record(reg_users_counter, { + vhost = <<"">> :: binary(), + count = 0 :: integer() | '$1' + }). + %%%---------------------------------------------------------------------- %%% API @@ -51,214 +66,251 @@ start(Host) -> update_reg_users_counter_table(Host), ok. + stop(_Host) -> ok. + init_db() -> - ejabberd_mnesia:create(?MODULE, passwd, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, passwd)}]), - ejabberd_mnesia:create(?MODULE, reg_users_counter, - [{ram_copies, [node()]}, - {attributes, record_info(fields, reg_users_counter)}]). + ejabberd_mnesia:create(?MODULE, + passwd, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, passwd)}]), + ejabberd_mnesia:create(?MODULE, + reg_users_counter, + [{ram_copies, [node()]}, + {attributes, record_info(fields, reg_users_counter)}]). + update_reg_users_counter_table(Server) -> Set = get_users(Server, []), Size = length(Set), LServer = jid:nameprep(Server), - F = fun () -> - mnesia:write(#reg_users_counter{vhost = LServer, - count = Size}) - end, + F = fun() -> + mnesia:write(#reg_users_counter{ + vhost = LServer, + count = Size + }) + end, mnesia:sync_dirty(F). + use_cache(Host) -> case mnesia:table_info(passwd, storage_type) of - disc_only_copies -> - ejabberd_option:auth_use_cache(Host); - _ -> - false + disc_only_copies -> + ejabberd_option:auth_use_cache(Host); + _ -> + false end. + plain_password_required(Server) -> store_type(Server) == scram. + store_type(Server) -> ejabberd_auth:password_format(Server). + set_password_multiple(User, Server, Passwords) -> F = fun() -> - lists:foreach( - fun(#scram{hash = Hash} = Password) -> - mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); - (Plain) -> - mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) - end, Passwords) - end, + lists:foreach( + fun(#scram{hash = Hash} = Password) -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + (Plain) -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end, + Passwords) + end, case mnesia:transaction(F) of - {atomic, ok} -> - {cache, {ok, Passwords}}; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {nocache, {error, db_failure}} + {atomic, ok} -> + {cache, {ok, Passwords}}; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {nocache, {error, db_failure}} end. + set_password_instance(User, Server, Password) -> F = fun() -> - case Password of - #scram{hash = Hash} = Password -> - mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); - Plain -> - mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) - end - end, + case Password of + #scram{hash = Hash} = Password -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + Plain -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end + end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, db_failure} + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} end. + try_register_multiple(User, Server, Passwords) -> F = fun() -> - case mnesia:select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'}, - [{'==', '$1', User}, - {'==', '$2', Server}], - ['$3']}]) of - [] -> - lists:foreach( - fun(#scram{hash = Hash} = Password) -> - mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); - (Plain) -> - mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) - end, Passwords), - mnesia:dirty_update_counter(reg_users_counter, Server, 1), - {ok, Passwords}; - [_] -> - {error, exists} - end - end, + case mnesia:select(passwd, + [{{'_', {'$1', '$2', '_'}, '$3'}, + [{'==', '$1', User}, + {'==', '$2', Server}], + ['$3']}]) of + [] -> + lists:foreach( + fun(#scram{hash = Hash} = Password) -> + mnesia:write(#passwd{us = {User, Server, Hash}, password = Password}); + (Plain) -> + mnesia:write(#passwd{us = {User, Server, plain}, password = Plain}) + end, + Passwords), + mnesia:dirty_update_counter(reg_users_counter, Server, 1), + {ok, Passwords}; + [_] -> + {error, exists} + end + end, case mnesia:transaction(F) of - {atomic, Res} -> - {cache, Res}; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {nocache, {error, db_failure}} + {atomic, Res} -> + {cache, Res}; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {nocache, {error, db_failure}} end. + get_users(Server, []) -> Users = mnesia:dirty_select(passwd, - [{#passwd{us = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, Server}], ['$1']}]), - misc:lists_uniq([{U, S} || {U, S, _} <- Users]); + [{#passwd{us = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, Server}], + ['$1']}]), + misc:lists_uniq([ {U, S} || {U, S, _} <- Users ]); get_users(Server, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> get_users(Server, [{limit, End - Start + 1}, {offset, Start}]); get_users(Server, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> case get_users(Server, []) of - [] -> - []; - Users -> - Set = lists:keysort(1, Users), - L = length(Set), - Start = if Offset < 1 -> 1; - Offset > L -> L; - true -> Offset - end, - lists:sublist(Set, Start, Limit) + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if + Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) end; get_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> - Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], + Set = [ {U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U) ], lists:keysort(1, Set); get_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}]) when is_binary(Prefix) and is_integer(Start) and is_integer(End) -> - get_users(Server, [{prefix, Prefix}, {limit, End - Start + 1}, - {offset, Start}]); + get_users(Server, + [{prefix, Prefix}, + {limit, End - Start + 1}, + {offset, Start}]); get_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) -> - case [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)] of - [] -> - []; - Users -> - Set = lists:keysort(1, Users), - L = length(Set), - Start = if Offset < 1 -> 1; - Offset > L -> L; - true -> Offset - end, - lists:sublist(Set, Start, Limit) + case [ {U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U) ] of + [] -> + []; + Users -> + Set = lists:keysort(1, Users), + L = length(Set), + Start = if + Offset < 1 -> 1; + Offset > L -> L; + true -> Offset + end, + lists:sublist(Set, Start, Limit) end; get_users(Server, _) -> get_users(Server, []). + count_users(Server, []) -> case mnesia:dirty_select( - reg_users_counter, - [{#reg_users_counter{vhost = Server, count = '$1'}, - [], ['$1']}]) of - [Count] -> Count; - _ -> 0 + reg_users_counter, + [{#reg_users_counter{vhost = Server, count = '$1'}, + [], + ['$1']}]) of + [Count] -> Count; + _ -> 0 end; count_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> - Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], + Set = [ {U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U) ], length(Set); count_users(Server, _) -> count_users(Server, []). + get_password(User, Server) -> - case mnesia:dirty_select(passwd, [{{'_', {'$1', '$2', '_'}, '$3'}, - [{'==', '$1', User}, - {'==', '$2', Server}], - ['$3']}]) of - [_|_] = List -> - List2 = lists:map( - fun({scram, SK, SEK, Salt, IC}) -> - #scram{storedkey = SK, serverkey = SEK, - salt = Salt, hash = sha, iterationcount = IC}; - (Other) -> Other - end, List), - {cache, {ok, List2}}; - _ -> - {cache, error} + case mnesia:dirty_select(passwd, + [{{'_', {'$1', '$2', '_'}, '$3'}, + [{'==', '$1', User}, + {'==', '$2', Server}], + ['$3']}]) of + [_ | _] = List -> + List2 = lists:map( + fun({scram, SK, SEK, Salt, IC}) -> + #scram{ + storedkey = SK, + serverkey = SEK, + salt = Salt, + hash = sha, + iterationcount = IC + }; + (Other) -> Other + end, + List), + {cache, {ok, List2}}; + _ -> + {cache, error} end. + drop_password_type(Server, Hash) -> F = fun() -> - Keys = mnesia:select(passwd, [{{'_', '$1', '_'}, - [{'==', {element, 3, '$1'}, Hash}, - {'==', {element, 2, '$1'}, Server}], - ['$1']}]), - lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), - ok - end, + Keys = mnesia:select(passwd, + [{{'_', '$1', '_'}, + [{'==', {element, 3, '$1'}, Hash}, + {'==', {element, 2, '$1'}, Server}], + ['$1']}]), + lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), + ok + end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, db_failure} + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} end. + remove_user(User, Server) -> - F = fun () -> - Keys = mnesia:select(passwd, [{{'_', '$1', '_'}, - [{'==', {element, 1, '$1'}, User}, - {'==', {element, 2, '$1'}, Server}], - ['$1']}]), - lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), - mnesia:dirty_update_counter(reg_users_counter, Server, -1), - ok - end, + F = fun() -> + Keys = mnesia:select(passwd, + [{{'_', '$1', '_'}, + [{'==', {element, 1, '$1'}, User}, + {'==', {element, 2, '$1'}, Server}], + ['$1']}]), + lists:foreach(fun(Key) -> mnesia:delete({passwd, Key}) end, Keys), + mnesia:dirty_update_counter(reg_users_counter, Server, -1), + ok + end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, db_failure} + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} end. + need_transform(#reg_users_counter{}) -> false; need_transform({passwd, {_U, _S, _T}, _Pass}) -> @@ -266,32 +318,44 @@ need_transform({passwd, {_U, _S, _T}, _Pass}) -> need_transform({passwd, {_U, _S}, _Pass}) -> true. + transform({passwd, {U, S}, Pass}) when is_list(U) orelse is_list(S) orelse is_list(Pass) -> NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, NewPass = case Pass of - #scram{storedkey = StoredKey, - serverkey = ServerKey, - salt = Salt} -> - Pass#scram{ - storedkey = iolist_to_binary(StoredKey), - serverkey = iolist_to_binary(ServerKey), - salt = iolist_to_binary(Salt)}; - _ -> - iolist_to_binary(Pass) - end, + #scram{ + storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt + } -> + Pass#scram{ + storedkey = iolist_to_binary(StoredKey), + serverkey = iolist_to_binary(ServerKey), + salt = iolist_to_binary(Salt) + }; + _ -> + iolist_to_binary(Pass) + end, transform(#passwd{us = NewUS, password = NewPass}); transform(#passwd{us = {U, S}, password = Password} = P) when is_binary(Password) -> P#passwd{us = {U, S, plain}, password = Password}; transform({passwd, {U, S}, {scram, SK, SEK, Salt, IC}}) -> - #passwd{us = {U, S, sha}, - password = #scram{storedkey = SK, serverkey = SEK, - salt = Salt, hash = sha, iterationcount = IC}}; + #passwd{ + us = {U, S, sha}, + password = #scram{ + storedkey = SK, + serverkey = SEK, + salt = Salt, + hash = sha, + iterationcount = IC + } + }; transform(#passwd{us = {U, S}, password = #scram{hash = Hash}} = P) -> P#passwd{us = {U, S, Hash}}; transform(Other) -> Other. + import(LServer, [LUser, Password, _TimeStamp]) -> mnesia:dirty_write( #passwd{us = {LUser, LServer}, password = Password}). diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index d795b0d6f..8e4b26b75 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -28,52 +28,65 @@ -behaviour(ejabberd_auth). --export([start/1, stop/1, check_password/4, - user_exists/2, store_type/1, plain_password_required/1]). +-export([start/1, + stop/1, + check_password/4, + user_exists/2, + store_type/1, + plain_password_required/1]). + start(_Host) -> ejabberd:start_app(epam). + stop(_Host) -> ok. + check_password(User, AuthzId, Host, Password) -> - if AuthzId /= <<>> andalso AuthzId /= User -> - false; - true -> - Service = get_pam_service(Host), - UserInfo = case get_pam_userinfotype(Host) of - username -> User; - jid -> <> - end, - case catch epam:authenticate(Service, UserInfo, Password) of - true -> {cache, true}; - false -> {cache, false}; - _ -> {nocache, false} - end + if + AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + Service = get_pam_service(Host), + UserInfo = case get_pam_userinfotype(Host) of + username -> User; + jid -> <> + end, + case catch epam:authenticate(Service, UserInfo, Password) of + true -> {cache, true}; + false -> {cache, false}; + _ -> {nocache, false} + end end. + user_exists(User, Host) -> Service = get_pam_service(Host), UserInfo = case get_pam_userinfotype(Host) of - username -> User; - jid -> <> - end, + username -> User; + jid -> <> + end, case catch epam:acct_mgmt(Service, UserInfo) of - true -> {cache, true}; - false -> {cache, false}; - _Err -> {nocache, {error, db_failure}} + true -> {cache, true}; + false -> {cache, false}; + _Err -> {nocache, {error, db_failure}} end. + plain_password_required(_) -> true. + store_type(_) -> external. + %%==================================================================== %% Internal functions %%==================================================================== get_pam_service(Host) -> ejabberd_option:pam_service(Host). + get_pam_userinfotype(Host) -> ejabberd_option:pam_userinfotype(Host). diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index 8ce78bc18..edf3d3a6d 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -25,22 +25,33 @@ -module(ejabberd_auth_sql). - -author('alexey@process-one.net'). -behaviour(ejabberd_auth). --export([start/1, stop/1, set_password_multiple/3, try_register_multiple/3, - get_users/2, count_users/2, get_password/2, - remove_user/2, store_type/1, plain_password_required/1, - export/1, which_users_exists/2, drop_password_type/2, set_password_instance/3]). +-export([start/1, + stop/1, + set_password_multiple/3, + try_register_multiple/3, + get_users/2, + count_users/2, + get_password/2, + remove_user/2, + store_type/1, + plain_password_required/1, + export/1, + which_users_exists/2, + drop_password_type/2, + set_password_instance/3]). -export([sql_schemas/0]). -include_lib("xmpp/include/scram.hrl"). + -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("ejabberd_auth.hrl"). + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- @@ -48,251 +59,341 @@ start(Host) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> - [ - #sql_schema{ - version = 2, - tables = - [#sql_table{ - name = <<"users">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"type">>, type = smallint}, - #sql_column{name = <<"password">>, type = text}, - #sql_column{name = <<"serverkey">>, type = {text, 128}, - default = true}, - #sql_column{name = <<"salt">>, type = {text, 128}, - default = true}, - #sql_column{name = <<"iterationcount">>, type = integer, - default = true}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"type">>], - unique = true}]}], - update = [ - {add_column, <<"users">>, <<"type">>}, - {update_primary_key,<<"users">>, - [<<"server_host">>, <<"username">>, <<"type">>]} - ]}, - #sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"users">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"password">>, type = text}, - #sql_column{name = <<"serverkey">>, type = {text, 128}, - default = true}, - #sql_column{name = <<"salt">>, type = {text, 128}, - default = true}, - #sql_column{name = <<"iterationcount">>, type = integer, - default = true}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}]}]. + [#sql_schema{ + version = 2, + tables = + [#sql_table{ + name = <<"users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"type">>, type = smallint}, + #sql_column{name = <<"password">>, type = text}, + #sql_column{ + name = <<"serverkey">>, + type = {text, 128}, + default = true + }, + #sql_column{ + name = <<"salt">>, + type = {text, 128}, + default = true + }, + #sql_column{ + name = <<"iterationcount">>, + type = integer, + default = true + }, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"type">>], + unique = true + }] + }], + update = [{add_column, <<"users">>, <<"type">>}, + {update_primary_key, <<"users">>, + [<<"server_host">>, <<"username">>, <<"type">>]}] + }, + #sql_schema{ + version = 1, + tables = + [#sql_table{ + name = <<"users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"password">>, type = text}, + #sql_column{ + name = <<"serverkey">>, + type = {text, 128}, + default = true + }, + #sql_column{ + name = <<"salt">>, + type = {text, 128}, + default = true + }, + #sql_column{ + name = <<"iterationcount">>, + type = integer, + default = true + }, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }] + }]. + stop(_Host) -> ok. + plain_password_required(Server) -> store_type(Server) == scram. + store_type(Server) -> ejabberd_auth:password_format(Server). + hash_to_num(plain) -> 1; hash_to_num(sha) -> 2; hash_to_num(sha256) -> 3; hash_to_num(sha512) -> 4. + num_to_hash(2) -> sha; num_to_hash(3) -> sha256; num_to_hash(4) -> sha512. -set_password_instance(User, Server, #scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC}) -> + +set_password_instance(User, + Server, + #scram{ + hash = Hash, + storedkey = SK, + serverkey = SEK, + salt = Salt, + iterationcount = IC + }) -> F = fun() -> - set_password_scram_t(User, Server, Hash, - SK, SEK, Salt, IC) - end, + set_password_scram_t(User, + Server, + Hash, + SK, + SEK, + Salt, + IC) + end, case ejabberd_sql:sql_transaction(Server, F) of - {atomic, _} -> - ok; - {aborted, _} -> - {error, db_failure} + {atomic, _} -> + ok; + {aborted, _} -> + {error, db_failure} end; set_password_instance(User, Server, Plain) -> F = fun() -> - set_password_t(User, Server, Plain) - end, + set_password_t(User, Server, Plain) + end, case ejabberd_sql:sql_transaction(Server, F) of - {atomic, _} -> - ok; - {aborted, _} -> - {error, db_failure} + {atomic, _} -> + ok; + {aborted, _} -> + {error, db_failure} end. + set_password_multiple(User, Server, Passwords) -> F = - fun() -> - ejabberd_sql:sql_query_t( - ?SQL("delete from users where username=%(User)s and %(Server)H")), - lists:foreach( - fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC}) -> - set_password_scram_t( - User, Server, Hash, - SK, SEK, Salt, IC); - (Plain) -> - set_password_t(User, Server, Plain) - end, Passwords) - end, + fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from users where username=%(User)s and %(Server)H")), + lists:foreach( + fun(#scram{ + hash = Hash, + storedkey = SK, + serverkey = SEK, + salt = Salt, + iterationcount = IC + }) -> + set_password_scram_t( + User, + Server, + Hash, + SK, + SEK, + Salt, + IC); + (Plain) -> + set_password_t(User, Server, Plain) + end, + Passwords) + end, case ejabberd_sql:sql_transaction(Server, F) of - {atomic, _} -> - {cache, {ok, Passwords}}; - {aborted, _} -> - {nocache, {error, db_failure}} + {atomic, _} -> + {cache, {ok, Passwords}}; + {aborted, _} -> + {nocache, {error, db_failure}} end. + try_register_multiple(User, Server, Passwords) -> F = - fun() -> - case ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users where username=%(User)s and %(Server)H")) of - {selected, [{0}]} -> - lists:foreach( - fun(#scram{hash = Hash, storedkey = SK, serverkey = SEK, - salt = Salt, iterationcount = IC}) -> - set_password_scram_t( - User, Server, Hash, - SK, SEK, Salt, IC); - (Plain) -> - set_password_t(User, Server, Plain) - end, Passwords), - {cache, {ok, Passwords}}; - {selected, _} -> - {nocache, {error, exists}}; - _ -> - {nocache, {error, db_failure}} - end - end, + fun() -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from users where username=%(User)s and %(Server)H")) of + {selected, [{0}]} -> + lists:foreach( + fun(#scram{ + hash = Hash, + storedkey = SK, + serverkey = SEK, + salt = Salt, + iterationcount = IC + }) -> + set_password_scram_t( + User, + Server, + Hash, + SK, + SEK, + Salt, + IC); + (Plain) -> + set_password_t(User, Server, Plain) + end, + Passwords), + {cache, {ok, Passwords}}; + {selected, _} -> + {nocache, {error, exists}}; + _ -> + {nocache, {error, db_failure}} + end + end, case ejabberd_sql:sql_transaction(Server, F) of - {atomic, Res} -> - Res; - {aborted, _} -> - {nocache, {error, db_failure}} + {atomic, Res} -> + Res; + {aborted, _} -> + {nocache, {error, db_failure}} end. + get_users(Server, Opts) -> case list_users(Server, Opts) of - {selected, Res} -> - [{U, Server} || {U} <- Res]; - _ -> [] + {selected, Res} -> + [ {U, Server} || {U} <- Res ]; + _ -> [] end. + count_users(Server, Opts) -> case users_number(Server, Opts) of - {selected, [{Res}]} -> - Res; - _Other -> 0 + {selected, [{Res}]} -> + Res; + _Other -> 0 end. + get_password(User, Server) -> case get_password_scram(Server, User) of - {selected, []} -> - {cache, error}; - {selected, Passwords} -> - Converted = lists:map( - fun({0, Password, <<>>, <<>>, 0}) -> - update_password_type(User, Server, 1), - Password; - ({_, Password, <<>>, <<>>, 0}) -> - Password; - ({0, StoredKey, ServerKey, Salt, IterationCount}) -> - {Hash, SK} = case StoredKey of - <<"sha256:", Rest/binary>> -> - update_password_type(User, Server, 3, Rest), - {sha256, Rest}; - <<"sha512:", Rest/binary>> -> - update_password_type(User, Server, 4, Rest), - {sha512, Rest}; - Other -> - update_password_type(User, Server, 2), - {sha, Other} - end, - #scram{storedkey = SK, - serverkey = ServerKey, - salt = Salt, - hash = Hash, - iterationcount = IterationCount}; - ({Type, StoredKey, ServerKey, Salt, IterationCount}) -> - Hash = num_to_hash(Type), - #scram{storedkey = StoredKey, - serverkey = ServerKey, - salt = Salt, - hash = Hash, - iterationcount = IterationCount} - end, Passwords), - {cache, {ok, Converted}}; - _ -> - {nocache, error} + {selected, []} -> + {cache, error}; + {selected, Passwords} -> + Converted = lists:map( + fun({0, Password, <<>>, <<>>, 0}) -> + update_password_type(User, Server, 1), + Password; + ({_, Password, <<>>, <<>>, 0}) -> + Password; + ({0, StoredKey, ServerKey, Salt, IterationCount}) -> + {Hash, SK} = case StoredKey of + <<"sha256:", Rest/binary>> -> + update_password_type(User, Server, 3, Rest), + {sha256, Rest}; + <<"sha512:", Rest/binary>> -> + update_password_type(User, Server, 4, Rest), + {sha512, Rest}; + Other -> + update_password_type(User, Server, 2), + {sha, Other} + end, + #scram{ + storedkey = SK, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount + }; + ({Type, StoredKey, ServerKey, Salt, IterationCount}) -> + Hash = num_to_hash(Type), + #scram{ + storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + hash = Hash, + iterationcount = IterationCount + } + end, + Passwords), + {cache, {ok, Converted}}; + _ -> + {nocache, error} end. + remove_user(User, Server) -> case del_user(Server, User) of - {updated, _} -> - ok; - _ -> - {error, db_failure} + {updated, _} -> + ok; + _ -> + {error, db_failure} end. + drop_password_type(LServer, Hash) -> Type = hash_to_num(Hash), ejabberd_sql:sql_query( - LServer, - ?SQL("delete from users" - " where type=%(Type)d and %(LServer)H")). + LServer, + ?SQL("delete from users" + " where type=%(Type)d and %(LServer)H")). -set_password_scram_t(LUser, LServer, Hash, - StoredKey, ServerKey, Salt, IterationCount) -> + +set_password_scram_t(LUser, + LServer, + Hash, + StoredKey, + ServerKey, + Salt, + IterationCount) -> Type = hash_to_num(Hash), ?SQL_UPSERT_T( - "users", - ["!username=%(LUser)s", - "!server_host=%(LServer)s", - "!type=%(Type)d", - "password=%(StoredKey)s", - "serverkey=%(ServerKey)s", - "salt=%(Salt)s", - "iterationcount=%(IterationCount)d"]). + "users", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!type=%(Type)d", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"]). + set_password_t(LUser, LServer, Password) -> ?SQL_UPSERT_T( - "users", - ["!username=%(LUser)s", - "!server_host=%(LServer)s", - "!type=1", - "password=%(Password)s", - "serverkey=''", - "salt=''", - "iterationcount=0"]). + "users", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!type=1", + "password=%(Password)s", + "serverkey=''", + "salt=''", + "iterationcount=0"]). + update_password_type(LUser, LServer, Type, Password) -> ejabberd_sql:sql_query( - LServer, - ?SQL("update users set type=%(Type)d, password=%(Password)s" - " where username=%(LUser)s and type=0 and %(LServer)H")). + LServer, + ?SQL("update users set type=%(Type)d, password=%(Password)s" + " where username=%(LUser)s and type=0 and %(LServer)H")). + update_password_type(LUser, LServer, Type) -> ejabberd_sql:sql_query( - LServer, - ?SQL("update users set type=%(Type)d" - " where username=%(LUser)s and type=0 and %(LServer)H")). + LServer, + ?SQL("update users set type=%(Type)d" + " where username=%(LUser)s and type=0 and %(LServer)H")). + get_password_scram(LServer, LUser) -> ejabberd_sql:sql_query( @@ -301,28 +402,31 @@ get_password_scram(LServer, LUser) -> " from users" " where username=%(LUser)s and %(LServer)H")). + del_user(LServer, LUser) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from users where username=%(LUser)s and %(LServer)H")). + list_users(LServer, []) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(distinct username)s from users where %(LServer)H")); list_users(LServer, [{from, Start}, {to, End}]) - when is_integer(Start) and is_integer(End) -> + when is_integer(Start) and is_integer(End) -> list_users(LServer, - [{limit, End - Start + 1}, {offset, Start - 1}]); + [{limit, End - Start + 1}, {offset, Start - 1}]); list_users(LServer, - [{prefix, Prefix}, {from, Start}, {to, End}]) - when is_binary(Prefix) and is_integer(Start) and - is_integer(End) -> + [{prefix, Prefix}, {from, Start}, {to, End}]) + when is_binary(Prefix) and is_integer(Start) and + is_integer(End) -> list_users(LServer, - [{prefix, Prefix}, {limit, End - Start + 1}, - {offset, Start - 1}]); + [{prefix, Prefix}, + {limit, End - Start + 1}, + {offset, Start - 1}]); list_users(LServer, [{limit, Limit}, {offset, Offset}]) - when is_integer(Limit) and is_integer(Offset) -> + when is_integer(Limit) and is_integer(Offset) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(distinct username)s from users " @@ -330,9 +434,9 @@ list_users(LServer, [{limit, Limit}, {offset, Offset}]) "order by username " "limit %(Limit)d offset %(Offset)d")); list_users(LServer, - [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) - when is_binary(Prefix) and is_integer(Limit) and - is_integer(Offset) -> + [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) + when is_binary(Prefix) and is_integer(Limit) and + is_integer(Offset) -> SPrefix = ejabberd_sql:escape_like_arg(Prefix), SPrefix2 = <>, ejabberd_sql:sql_query( @@ -342,12 +446,12 @@ list_users(LServer, "order by username " "limit %(Limit)d offset %(Offset)d")). + users_number(LServer) -> ejabberd_sql:sql_query( LServer, fun(pgsql, _) -> - case - ejabberd_option:pgsql_users_number_estimate(LServer) of + case ejabberd_option:pgsql_users_number_estimate(LServer) of true -> ejabberd_sql:sql_query_t( ?SQL("select @(reltuples :: bigint)d from pg_class" @@ -355,14 +459,15 @@ users_number(LServer) -> _ -> ejabberd_sql:sql_query_t( ?SQL("select @(count(distinct username))d from users where %(LServer)H")) - end; + end; (_Type, _) -> ejabberd_sql:sql_query_t( ?SQL("select @(count(distinct username))d from users where %(LServer)H")) end). + users_number(LServer, [{prefix, Prefix}]) - when is_binary(Prefix) -> + when is_binary(Prefix) -> SPrefix = ejabberd_sql:escape_like_arg(Prefix), SPrefix2 = <>, ejabberd_sql:sql_query( @@ -372,16 +477,18 @@ users_number(LServer, [{prefix, Prefix}]) users_number(LServer, []) -> users_number(LServer). + which_users_exists(LServer, LUsers) when length(LUsers) =< 100 -> try ejabberd_sql:sql_query( - LServer, - ?SQL("select @(distinct username)s from users where username in %(LUsers)ls")) of + LServer, + ?SQL("select @(distinct username)s from users where username in %(LUsers)ls")) of {selected, Matching} -> - [U || {U} <- Matching]; + [ U || {U} <- Matching ]; {error, _} = E -> E - catch _:B -> - {error, B} + catch + _:B -> + {error, B} end; which_users_exists(LServer, LUsers) -> {First, Rest} = lists:split(100, LUsers), @@ -397,6 +504,7 @@ which_users_exists(LServer, LUsers) -> end end. + export(_Server) -> [{passwd, fun(Host, #passwd{us = {LUser, LServer, plain}, password = Password}) @@ -404,43 +512,44 @@ export(_Server) -> is_binary(Password) -> [?SQL("delete from users where username=%(LUser)s and type=1 and %(LServer)H;"), ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "type=1", - "password=%(Password)s"])]; - (Host, {passwd, {LUser, LServer, _}, - {scram, StoredKey, ServerKey, Salt, IterationCount}}) + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "type=1", + "password=%(Password)s"])]; + (Host, + {passwd, {LUser, LServer, _}, + {scram, StoredKey, ServerKey, Salt, IterationCount}}) when LServer == Host -> Hash = sha, Type = hash_to_num(Hash), [?SQL("delete from users where username=%(LUser)s and type=%(Type)d and %(LServer)H;"), ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "type=%(Type)d", - "password=%(StoredKey)s", - "serverkey=%(ServerKey)s", - "salt=%(Salt)s", - "iterationcount=%(IterationCount)d"])]; + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "type=%(Type)d", + "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#scram.storedkey, + StoredKey = Scram#scram.storedkey, ServerKey = Scram#scram.serverkey, Salt = Scram#scram.salt, IterationCount = Scram#scram.iterationcount, Type = hash_to_num(Scram#scram.hash), [?SQL("delete from users where username=%(LUser)s and type=%(Type)d and %(LServer)H;"), ?SQL_INSERT( - "users", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "type=%(Type)d", - "password=%(StoredKey)s", - "serverkey=%(ServerKey)s", - "salt=%(Salt)s", - "iterationcount=%(IterationCount)d"])]; + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "type=%(Type)d", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"])]; (_Host, _R) -> [] end}]. diff --git a/src/ejabberd_backend_sup.erl b/src/ejabberd_backend_sup.erl index 1b3495e36..8791ec0d2 100644 --- a/src/ejabberd_backend_sup.erl +++ b/src/ejabberd_backend_sup.erl @@ -29,12 +29,14 @@ %% Supervisor callbacks -export([init/1]). + %%%=================================================================== %%% API functions %%%=================================================================== start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). + %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== diff --git a/src/ejabberd_batch.erl b/src/ejabberd_batch.erl index 5a907c74b..497a423b1 100644 --- a/src/ejabberd_batch.erl +++ b/src/ejabberd_batch.erl @@ -34,8 +34,12 @@ -export([start_link/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, - code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -export([register_task/5, task_status/1, abort_task/1]). -define(SERVER, ?MODULE). @@ -47,159 +51,178 @@ %%% API %%%=================================================================== + %% @doc Spawns the server and registers the local name (unique) -spec(start_link() -> - {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). + {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + register_task(Type, Steps, Rate, JobState, JobFun) -> gen_server:call(?MODULE, {register_task, Type, Steps, Rate, JobState, JobFun}). + task_status(Type) -> gen_server:call(?MODULE, {task_status, Type}). + abort_task(Type) -> gen_server:call(?MODULE, {abort_task, Type}). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== + %% @private %% @doc Initializes the server -spec(init(Args :: term()) -> - {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | - {stop, Reason :: term()} | ignore). + {ok, State :: #state{}} | + {ok, State :: #state{}, timeout() | hibernate} | + {stop, Reason :: term()} | + ignore). init([]) -> {ok, #state{}}. + %% @private %% @doc Handling call messages --spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, - State :: #state{}) -> - {reply, Reply :: term(), NewState :: #state{}} | - {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | - {noreply, NewState :: #state{}} | - {noreply, NewState :: #state{}, timeout() | hibernate} | - {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | - {stop, Reason :: term(), NewState :: #state{}}). +-spec(handle_call(Request :: term(), + From :: {pid(), Tag :: term()}, + State :: #state{}) -> + {reply, Reply :: term(), NewState :: #state{}} | + {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | + {stop, Reason :: term(), NewState :: #state{}}). handle_call({register_task, Type, Steps, Rate, JobState, JobFun}, _From, #state{tasks = Tasks} = State) -> case maps:get(Type, Tasks, #task{}) of - #task{state = S} when S == completed; S == not_started; S == aborted; S == failed -> - Pid = spawn(fun() -> work_loop(Type, JobState, JobFun, Rate, erlang:monotonic_time(second), 0) end), - Tasks2 = maps:put(Type, #task{state = working, pid = Pid, steps = Steps, done_steps = 0}, Tasks), - {reply, ok, #state{tasks = Tasks2}}; - #task{state = working} -> - {reply, {error, in_progress}, State} + #task{state = S} when S == completed; S == not_started; S == aborted; S == failed -> + Pid = spawn(fun() -> work_loop(Type, JobState, JobFun, Rate, erlang:monotonic_time(second), 0) end), + Tasks2 = maps:put(Type, #task{state = working, pid = Pid, steps = Steps, done_steps = 0}, Tasks), + {reply, ok, #state{tasks = Tasks2}}; + #task{state = working} -> + {reply, {error, in_progress}, State} end; handle_call({task_status, Type}, _From, #state{tasks = Tasks} = State) -> case maps:get(Type, Tasks, none) of - none -> - {reply, not_started, State}; - #task{state = not_started} -> - {reply, not_started, State}; - #task{state = failed, done_steps = Steps, pid = Error} -> - {reply, {failed, Steps, Error}, State}; - #task{state = aborted, done_steps = Steps} -> - {reply, {aborted, Steps}, State}; - #task{state = working, done_steps = Steps} -> - {reply, {working, Steps}, State}; - #task{state = completed, done_steps = Steps} -> - {reply, {completed, Steps}, State} + none -> + {reply, not_started, State}; + #task{state = not_started} -> + {reply, not_started, State}; + #task{state = failed, done_steps = Steps, pid = Error} -> + {reply, {failed, Steps, Error}, State}; + #task{state = aborted, done_steps = Steps} -> + {reply, {aborted, Steps}, State}; + #task{state = working, done_steps = Steps} -> + {reply, {working, Steps}, State}; + #task{state = completed, done_steps = Steps} -> + {reply, {completed, Steps}, State} end; handle_call({abort_task, Type}, _From, #state{tasks = Tasks} = State) -> case maps:get(Type, Tasks, none) of - #task{state = working, pid = Pid} = T -> - Pid ! abort, - Tasks2 = maps:put(Type, T#task{state = aborted, pid = none}, Tasks), - {reply, aborted, State#state{tasks = Tasks2}}; - _ -> - {reply, not_started, State} + #task{state = working, pid = Pid} = T -> + Pid ! abort, + Tasks2 = maps:put(Type, T#task{state = aborted, pid = none}, Tasks), + {reply, aborted, State#state{tasks = Tasks2}}; + _ -> + {reply, not_started, State} end; handle_call(_Request, _From, State = #state{}) -> {reply, ok, State}. + %% @private %% @doc Handling cast messages -spec(handle_cast(Request :: term(), State :: #state{}) -> - {noreply, NewState :: #state{}} | - {noreply, NewState :: #state{}, timeout() | hibernate} | - {stop, Reason :: term(), NewState :: #state{}}). + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). handle_cast({task_finished, Type, Pid}, #state{tasks = Tasks} = State) -> case maps:get(Type, Tasks, none) of - #task{state = working, pid = Pid2} = T when Pid == Pid2 -> - Tasks2 = maps:put(Type, T#task{state = completed, pid = none}, Tasks), - {noreply, State#state{tasks = Tasks2}}; - _ -> - {noreply, State} + #task{state = working, pid = Pid2} = T when Pid == Pid2 -> + Tasks2 = maps:put(Type, T#task{state = completed, pid = none}, Tasks), + {noreply, State#state{tasks = Tasks2}}; + _ -> + {noreply, State} end; handle_cast({task_progress, Type, Pid, Count}, #state{tasks = Tasks} = State) -> case maps:get(Type, Tasks, none) of - #task{state = working, pid = Pid2, done_steps = Steps} = T when Pid == Pid2 -> - Tasks2 = maps:put(Type, T#task{done_steps = Steps + Count}, Tasks), - {noreply, State#state{tasks = Tasks2}}; - _ -> - {noreply, State} + #task{state = working, pid = Pid2, done_steps = Steps} = T when Pid == Pid2 -> + Tasks2 = maps:put(Type, T#task{done_steps = Steps + Count}, Tasks), + {noreply, State#state{tasks = Tasks2}}; + _ -> + {noreply, State} end; handle_cast({task_error, Type, Pid, Error}, #state{tasks = Tasks} = State) -> case maps:get(Type, Tasks, none) of - #task{state = working, pid = Pid2} = T when Pid == Pid2 -> - Tasks2 = maps:put(Type, T#task{state = failed, pid = Error}, Tasks), - {noreply, State#state{tasks = Tasks2}}; - _ -> - {noreply, State} + #task{state = working, pid = Pid2} = T when Pid == Pid2 -> + Tasks2 = maps:put(Type, T#task{state = failed, pid = Error}, Tasks), + {noreply, State#state{tasks = Tasks2}}; + _ -> + {noreply, State} end; handle_cast(_Request, State = #state{}) -> {noreply, State}. + %% @private %% @doc Handling all non call/cast messages -spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> - {noreply, NewState :: #state{}} | - {noreply, NewState :: #state{}, timeout() | hibernate} | - {stop, Reason :: term(), NewState :: #state{}}). + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). handle_info(_Info, State = #state{}) -> {noreply, State}. + %% @private %% @doc This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), - State :: #state{}) -> term()). + State :: #state{}) -> term()). terminate(_Reason, _State = #state{}) -> ok. + %% @private %% @doc Convert process state when code is changed --spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{}, - Extra :: term()) -> - {ok, NewState :: #state{}} | {error, Reason :: term()}). +-spec(code_change(OldVsn :: term() | {down, term()}, + State :: #state{}, + Extra :: term()) -> + {ok, NewState :: #state{}} | {error, Reason :: term()}). code_change(_OldVsn, State = #state{}, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== + work_loop(Task, JobState, JobFun, Rate, StartDate, CurrentProgress) -> try JobFun(JobState) of - {ok, _NewState, 0} -> - gen_server:cast(?MODULE, {task_finished, Task, self()}); - {ok, NewState, Count} -> - gen_server:cast(?MODULE, {task_progress, Task, self(), Count}), - NewProgress = CurrentProgress + Count, - TimeSpent = erlang:monotonic_time(second) - StartDate, - SleepTime = max(0, NewProgress/Rate*60 - TimeSpent), - receive - abort -> ok - after round(SleepTime*1000) -> - work_loop(Task, NewState, JobFun, Rate, StartDate, NewProgress) - end; - {error, Error} -> - gen_server:cast(?MODULE, {task_error, Task, self(), Error}) - catch _:_ -> - gen_server:cast(?MODULE, {task_error, Task, self(), internal_error}) + {ok, _NewState, 0} -> + gen_server:cast(?MODULE, {task_finished, Task, self()}); + {ok, NewState, Count} -> + gen_server:cast(?MODULE, {task_progress, Task, self(), Count}), + NewProgress = CurrentProgress + Count, + TimeSpent = erlang:monotonic_time(second) - StartDate, + SleepTime = max(0, NewProgress / Rate * 60 - TimeSpent), + receive + abort -> ok + after + round(SleepTime * 1000) -> + work_loop(Task, NewState, JobFun, Rate, StartDate, NewProgress) + end; + {error, Error} -> + gen_server:cast(?MODULE, {task_error, Task, self(), Error}) + catch + _:_ -> + gen_server:cast(?MODULE, {task_error, Task, self(), internal_error}) end. diff --git a/src/ejabberd_bosh.erl b/src/ejabberd_bosh.erl index 1e29114bd..72b4fa950 100644 --- a/src/ejabberd_bosh.erl +++ b/src/ejabberd_bosh.erl @@ -31,19 +31,34 @@ %% API -export([start/2, start/3, start_link/3]). --export([send_xml/2, setopts/2, controlling_process/2, - reset_stream/1, change_shaper/2, close/1, - sockname/1, peername/1, process_request/3, send/2, - get_transport/1, get_owner/1]). +-export([send_xml/2, + setopts/2, + controlling_process/2, + reset_stream/1, + change_shaper/2, + close/1, + sockname/1, + peername/1, + process_request/3, + send/2, + get_transport/1, + get_owner/1]). %% gen_fsm callbacks --export([init/1, wait_for_session/2, wait_for_session/3, - active/2, active/3, handle_event/3, print_state/1, - handle_sync_event/4, handle_info/3, terminate/3, - code_change/4]). +-export([init/1, + wait_for_session/2, wait_for_session/3, + active/2, active/3, + handle_event/3, + print_state/1, + handle_sync_event/4, + handle_info/3, + terminate/3, + code_change/4]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("ejabberd_http.hrl"). -include("bosh.hrl"). @@ -63,7 +78,7 @@ -define(NS_BOSH, <<"urn:xmpp:xbosh">>). -define(NS_HTTP_BIND, - <<"http://jabber.org/protocol/httpbind">>). + <<"http://jabber.org/protocol/httpbind">>). -define(DEFAULT_WAIT, 300). @@ -76,8 +91,8 @@ -define(SEND_TIMEOUT, 15000). -type bosh_socket() :: {http_bind, pid(), - {inet:ip_address(), - inet:port_number()}}. + {inet:ip_address(), + inet:port_number()}}. -export_type([bosh_socket/0]). @@ -86,113 +101,127 @@ %% @indent-begin -record(state, - {host = <<"">> :: binary(), + {host = <<"">> :: binary(), sid = <<"">> :: binary(), el_ibuf :: p1_queue:queue(), el_obuf :: p1_queue:queue(), shaper_state = none :: ejabberd_shaper:shaper(), c2s_pid :: pid() | undefined, - xmpp_ver = <<"">> :: binary(), + xmpp_ver = <<"">> :: binary(), inactivity_timer :: reference() | undefined, wait_timer :: reference() | undefined, - wait_timeout = ?DEFAULT_WAIT :: pos_integer(), + wait_timeout = ?DEFAULT_WAIT :: pos_integer(), inactivity_timeout :: pos_integer(), - prev_rid = 0 :: non_neg_integer(), + prev_rid = 0 :: non_neg_integer(), prev_key = <<"">> :: binary(), prev_poll :: erlang:timestamp() | undefined, max_concat = unlimited :: unlimited | non_neg_integer(), - responses = gb_trees:empty() :: gb_trees:tree(), - receivers = gb_trees:empty() :: gb_trees:tree(), - shaped_receivers :: p1_queue:queue(), + responses = gb_trees:empty() :: gb_trees:tree(), + receivers = gb_trees:empty() :: gb_trees:tree(), + shaped_receivers :: p1_queue:queue(), ip :: inet:ip_address(), max_requests = 1 :: non_neg_integer()}). -record(body, - {http_reason = <<"">> :: binary(), + {http_reason = <<"">> :: binary(), attrs = [] :: [{any(), any()}], els = [] :: [fxml_stream:xml_stream_el()], size = 0 :: non_neg_integer()}). %% @indent-end %% @efmt:on -%% + %% + start(#body{attrs = Attrs} = Body, IP, SID) -> XMPPDomain = get_attr(to, Attrs), SupervisorProc = gen_mod:get_module_proc(XMPPDomain, mod_bosh), case catch supervisor:start_child(SupervisorProc, - [Body, IP, SID]) - of - {ok, Pid} -> {ok, Pid}; - {'EXIT', {noproc, _}} -> - check_bosh_module(XMPPDomain), - {error, module_not_loaded}; - Err -> - ?ERROR_MSG("Failed to start BOSH session: ~p", [Err]), - {error, Err} + [Body, IP, SID]) of + {ok, Pid} -> {ok, Pid}; + {'EXIT', {noproc, _}} -> + check_bosh_module(XMPPDomain), + {error, module_not_loaded}; + Err -> + ?ERROR_MSG("Failed to start BOSH session: ~p", [Err]), + {error, Err} end. + start(StateName, State) -> - p1_fsm:start_link(?MODULE, [StateName, State], - ?FSMOPTS). + p1_fsm:start_link(?MODULE, + [StateName, State], + ?FSMOPTS). + start_link(Body, IP, SID) -> - p1_fsm:start_link(?MODULE, [Body, IP, SID], - ?FSMOPTS). + p1_fsm:start_link(?MODULE, + [Body, IP, SID], + ?FSMOPTS). + send({http_bind, FsmRef, IP}, Packet) -> send_xml({http_bind, FsmRef, IP}, Packet). + send_xml({http_bind, FsmRef, _IP}, Packet) -> case catch p1_fsm:sync_send_all_state_event(FsmRef, - {send_xml, Packet}, - ?SEND_TIMEOUT) - of - {'EXIT', {timeout, _}} -> {error, timeout}; - {'EXIT', _} -> {error, einval}; - Res -> Res + {send_xml, Packet}, + ?SEND_TIMEOUT) of + {'EXIT', {timeout, _}} -> {error, timeout}; + {'EXIT', _} -> {error, einval}; + Res -> Res end. + setopts({http_bind, FsmRef, _IP}, Opts) -> case lists:member({active, once}, Opts) of - true -> - p1_fsm:send_all_state_event(FsmRef, - {activate, self()}); - _ -> - case lists:member({active, false}, Opts) of - true -> - case catch p1_fsm:sync_send_all_state_event(FsmRef, - deactivate_socket) - of - {'EXIT', _} -> {error, einval}; - Res -> Res - end; - _ -> ok - end + true -> + p1_fsm:send_all_state_event(FsmRef, + {activate, self()}); + _ -> + case lists:member({active, false}, Opts) of + true -> + case catch p1_fsm:sync_send_all_state_event(FsmRef, + deactivate_socket) of + {'EXIT', _} -> {error, einval}; + Res -> Res + end; + _ -> ok + end end. + controlling_process(_Socket, _Pid) -> ok. + reset_stream({http_bind, _FsmRef, _IP} = Socket) -> Socket. + change_shaper({http_bind, FsmRef, _IP}, Shaper) -> p1_fsm:send_all_state_event(FsmRef, {change_shaper, Shaper}). + close({http_bind, FsmRef, _IP}) -> catch p1_fsm:sync_send_all_state_event(FsmRef, - close). + close). + sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. + peername({http_bind, _FsmRef, IP}) -> {ok, IP}. + get_transport(_Socket) -> http_bind. + get_owner({http_bind, FsmRef, _IP}) -> FsmRef. + process_request(Data, IP, Type) -> Opts1 = ejabberd_c2s_config:get_c2s_limits(), Opts = case Type of @@ -201,79 +230,96 @@ process_request(Data, IP, Type) -> json -> Opts1 end, - MaxStanzaSize = case lists:keysearch(max_stanza_size, 1, - Opts) - of - {value, {_, Size}} -> Size; - _ -> infinity - end, + MaxStanzaSize = case lists:keysearch(max_stanza_size, + 1, + Opts) of + {value, {_, Size}} -> Size; + _ -> infinity + end, PayloadSize = iolist_size(Data), - if PayloadSize > MaxStanzaSize -> - http_error(403, <<"Request Too Large">>, Type); - true -> - case decode_body(Data, PayloadSize, Type) of - {ok, #body{attrs = Attrs} = Body} -> - SID = get_attr(sid, Attrs), - To = get_attr(to, Attrs), - if SID == <<"">>, To == <<"">> -> - bosh_response_with_msg(#body{http_reason = - <<"Missing 'to' attribute">>, - attrs = - [{type, <<"terminate">>}, - {condition, - <<"improper-addressing">>}]}, - Type, Body); - SID == <<"">> -> - case start(Body, IP, make_sid()) of - {ok, Pid} -> process_request(Pid, Body, IP, Type); - _Err -> - bosh_response_with_msg(#body{http_reason = - <<"Failed to start BOSH session">>, - attrs = - [{type, <<"terminate">>}, - {condition, - <<"internal-server-error">>}]}, - Type, Body) - end; - true -> - case mod_bosh:find_session(SID) of - {ok, Pid} -> process_request(Pid, Body, IP, Type); - error -> - bosh_response_with_msg(#body{http_reason = - <<"Session ID mismatch">>, - attrs = - [{type, <<"terminate">>}, - {condition, - <<"item-not-found">>}]}, - Type, Body) - end - end; - {error, Reason} -> http_error(400, Reason, Type) - end + if + PayloadSize > MaxStanzaSize -> + http_error(403, <<"Request Too Large">>, Type); + true -> + case decode_body(Data, PayloadSize, Type) of + {ok, #body{attrs = Attrs} = Body} -> + SID = get_attr(sid, Attrs), + To = get_attr(to, Attrs), + if + SID == <<"">>, To == <<"">> -> + bosh_response_with_msg(#body{ + http_reason = + <<"Missing 'to' attribute">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"improper-addressing">>}] + }, + Type, + Body); + SID == <<"">> -> + case start(Body, IP, make_sid()) of + {ok, Pid} -> process_request(Pid, Body, IP, Type); + _Err -> + bosh_response_with_msg(#body{ + http_reason = + <<"Failed to start BOSH session">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"internal-server-error">>}] + }, + Type, + Body) + end; + true -> + case mod_bosh:find_session(SID) of + {ok, Pid} -> process_request(Pid, Body, IP, Type); + error -> + bosh_response_with_msg(#body{ + http_reason = + <<"Session ID mismatch">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"item-not-found">>}] + }, + Type, + Body) + end + end; + {error, Reason} -> http_error(400, Reason, Type) + end end. + process_request(Pid, Req, _IP, Type) -> - case catch p1_fsm:sync_send_event(Pid, Req, - infinity) - of - #body{} = Resp -> bosh_response(Resp, Type); - {'EXIT', {Reason, _}} - when Reason == noproc; Reason == normal -> - bosh_response(#body{http_reason = - <<"BOSH session not found">>, - attrs = - [{type, <<"terminate">>}, - {condition, <<"item-not-found">>}]}, - Type); - {'EXIT', _} -> - bosh_response(#body{http_reason = - <<"Unexpected error">>, - attrs = - [{type, <<"terminate">>}, - {condition, <<"internal-server-error">>}]}, - Type) + case catch p1_fsm:sync_send_event(Pid, + Req, + infinity) of + #body{} = Resp -> bosh_response(Resp, Type); + {'EXIT', {Reason, _}} + when Reason == noproc; Reason == normal -> + bosh_response(#body{ + http_reason = + <<"BOSH session not found">>, + attrs = + [{type, <<"terminate">>}, + {condition, <<"item-not-found">>}] + }, + Type); + {'EXIT', _} -> + bosh_response(#body{ + http_reason = + <<"Unexpected error">>, + attrs = + [{type, <<"terminate">>}, + {condition, <<"internal-server-error">>}] + }, + Type) end. + init([#body{attrs = Attrs}, IP, SID]) -> Opts1 = ejabberd_c2s_config:get_c2s_limits(), Opts2 = [{xml_socket, true} | Opts1], @@ -290,677 +336,798 @@ init([#body{attrs = Attrs}, IP, SID]) -> {buf_in([make_xmlstreamstart(XMPPDomain, XMPPVer)], buf_new(XMPPDomain)), Opts2} - end, - case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of - {ok, C2SPid} -> - ejabberd_c2s:accept(C2SPid), - Inactivity = mod_bosh_opt:max_inactivity(XMPPDomain) div 1000, - MaxConcat = mod_bosh_opt:max_concat(XMPPDomain), - ShapedReceivers = buf_new(XMPPDomain, ?MAX_SHAPED_REQUESTS_QUEUE_LEN), - State = #state{host = XMPPDomain, sid = SID, ip = IP, - xmpp_ver = XMPPVer, el_ibuf = InBuf, - max_concat = MaxConcat, el_obuf = buf_new(XMPPDomain), - inactivity_timeout = Inactivity, - shaped_receivers = ShapedReceivers, - shaper_state = ShaperState}, - NewState = restart_inactivity_timer(State), - case mod_bosh:open_session(SID, self()) of - ok -> - {ok, wait_for_session, NewState}; - {error, Reason} -> - {stop, Reason} - end; - {error, Reason} -> - {stop, Reason}; - ignore -> - ignore + end, + case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()} | Opts]) of + {ok, C2SPid} -> + ejabberd_c2s:accept(C2SPid), + Inactivity = mod_bosh_opt:max_inactivity(XMPPDomain) div 1000, + MaxConcat = mod_bosh_opt:max_concat(XMPPDomain), + ShapedReceivers = buf_new(XMPPDomain, ?MAX_SHAPED_REQUESTS_QUEUE_LEN), + State = #state{ + host = XMPPDomain, + sid = SID, + ip = IP, + xmpp_ver = XMPPVer, + el_ibuf = InBuf, + max_concat = MaxConcat, + el_obuf = buf_new(XMPPDomain), + inactivity_timeout = Inactivity, + shaped_receivers = ShapedReceivers, + shaper_state = ShaperState + }, + NewState = restart_inactivity_timer(State), + case mod_bosh:open_session(SID, self()) of + ok -> + {ok, wait_for_session, NewState}; + {error, Reason} -> + {stop, Reason} + end; + {error, Reason} -> + {stop, Reason}; + ignore -> + ignore end. + wait_for_session(Event, State) -> ?ERROR_MSG("Unexpected event in 'wait_for_session': ~p", - [Event]), + [Event]), {next_state, wait_for_session, State}. -wait_for_session(#body{attrs = Attrs} = Req, From, - State) -> + +wait_for_session(#body{attrs = Attrs} = Req, + From, + State) -> RID = get_attr(rid, Attrs), ?DEBUG("Got request:~n** RequestID: ~p~n** Request: " - "~p~n** From: ~p~n** State: ~p", - [RID, Req, From, State]), + "~p~n** From: ~p~n** State: ~p", + [RID, Req, From, State]), Wait = min(get_attr(wait, Attrs, undefined), - ?DEFAULT_WAIT), + ?DEFAULT_WAIT), Hold = min(get_attr(hold, Attrs, undefined), - ?DEFAULT_HOLD), + ?DEFAULT_HOLD), NewKey = get_attr(newkey, Attrs), Type = get_attr(type, Attrs), Requests = Hold + 1, PollTime = if - Wait == 0, Hold == 0 -> erlang:timestamp(); - true -> undefined - end, + Wait == 0, Hold == 0 -> erlang:timestamp(); + true -> undefined + end, MaxPause = mod_bosh_opt:max_pause(State#state.host) div 1000, - Resp = #body{attrs = - [{sid, State#state.sid}, {wait, Wait}, - {ver, ?BOSH_VERSION}, {polling, ?DEFAULT_POLLING}, - {inactivity, State#state.inactivity_timeout}, - {hold, Hold}, {'xmpp:restartlogic', true}, - {requests, Requests}, {secure, true}, - {maxpause, MaxPause}, {'xmlns:xmpp', ?NS_BOSH}, - {'xmlns:stream', ?NS_STREAM}, {from, State#state.host}]}, + Resp = #body{ + attrs = + [{sid, State#state.sid}, + {wait, Wait}, + {ver, ?BOSH_VERSION}, + {polling, ?DEFAULT_POLLING}, + {inactivity, State#state.inactivity_timeout}, + {hold, Hold}, + {'xmpp:restartlogic', true}, + {requests, Requests}, + {secure, true}, + {maxpause, MaxPause}, + {'xmlns:xmpp', ?NS_BOSH}, + {'xmlns:stream', ?NS_STREAM}, + {from, State#state.host}] + }, {ShaperState, _} = - ejabberd_shaper:update(State#state.shaper_state, Req#body.size), - State1 = State#state{wait_timeout = Wait, - prev_rid = RID, prev_key = NewKey, - prev_poll = PollTime, shaper_state = ShaperState, - max_requests = Requests}, + ejabberd_shaper:update(State#state.shaper_state, Req#body.size), + State1 = State#state{ + wait_timeout = Wait, + prev_rid = RID, + prev_key = NewKey, + prev_poll = PollTime, + shaper_state = ShaperState, + max_requests = Requests + }, Els = maybe_add_xmlstreamend(Req#body.els, Type), State2 = route_els(State1, Els), {State3, RespEls} = get_response_els(State2), State4 = stop_inactivity_timer(State3), case RespEls of - [{xmlstreamstart, _, _} = El1] -> - OutBuf = buf_in([El1], State4#state.el_obuf), - State5 = restart_wait_timer(State4), - Receivers = gb_trees:insert(RID, {From, Resp}, - State5#state.receivers), - {next_state, active, - State5#state{receivers = Receivers, el_obuf = OutBuf}}; - [] -> - State5 = restart_wait_timer(State4), - Receivers = gb_trees:insert(RID, {From, Resp}, - State5#state.receivers), - {next_state, active, - State5#state{receivers = Receivers}}; - _ -> - reply_next_state(State4, Resp#body{els = RespEls}, RID, - From) + [{xmlstreamstart, _, _} = El1] -> + OutBuf = buf_in([El1], State4#state.el_obuf), + State5 = restart_wait_timer(State4), + Receivers = gb_trees:insert(RID, + {From, Resp}, + State5#state.receivers), + {next_state, active, + State5#state{receivers = Receivers, el_obuf = OutBuf}}; + [] -> + State5 = restart_wait_timer(State4), + Receivers = gb_trees:insert(RID, + {From, Resp}, + State5#state.receivers), + {next_state, active, + State5#state{receivers = Receivers}}; + _ -> + reply_next_state(State4, + Resp#body{els = RespEls}, + RID, + From) end; wait_for_session(Event, _From, State) -> ?ERROR_MSG("Unexpected sync event in 'wait_for_session': ~p", - [Event]), + [Event]), {reply, {error, badarg}, wait_for_session, State}. + active({#body{} = Body, From}, State) -> active1(Body, From, State); active(Event, State) -> ?ERROR_MSG("Unexpected event in 'active': ~p", - [Event]), + [Event]), {next_state, active, State}. -active(#body{attrs = Attrs, size = Size} = Req, From, + +active(#body{attrs = Attrs, size = Size} = Req, + From, State) -> ?DEBUG("Got request:~n** Request: ~p~n** From: " - "~p~n** State: ~p", - [Req, From, State]), + "~p~n** State: ~p", + [Req, From, State]), {ShaperState, Pause} = - ejabberd_shaper:update(State#state.shaper_state, Size), + ejabberd_shaper:update(State#state.shaper_state, Size), State1 = State#state{shaper_state = ShaperState}, - if Pause > 0 -> - TRef = start_shaper_timer(Pause), - try p1_queue:in({TRef, From, Req}, - State1#state.shaped_receivers) of - Q -> - State2 = stop_inactivity_timer(State1), - {next_state, active, - State2#state{shaped_receivers = Q}} - catch error:full -> - misc:cancel_timer(TRef), - RID = get_attr(rid, Attrs), - reply_stop(State1, - #body{http_reason = <<"Too many requests">>, - attrs = - [{<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"policy-violation">>}]}, - From, RID) - end; - true -> active1(Req, From, State1) + if + Pause > 0 -> + TRef = start_shaper_timer(Pause), + try p1_queue:in({TRef, From, Req}, + State1#state.shaped_receivers) of + Q -> + State2 = stop_inactivity_timer(State1), + {next_state, active, + State2#state{shaped_receivers = Q}} + catch + error:full -> + misc:cancel_timer(TRef), + RID = get_attr(rid, Attrs), + reply_stop(State1, + #body{ + http_reason = <<"Too many requests">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"policy-violation">>}] + }, + From, + RID) + end; + true -> active1(Req, From, State1) end; active(Event, _From, State) -> ?ERROR_MSG("Unexpected sync event in 'active': ~p", - [Event]), + [Event]), {reply, {error, badarg}, active, State}. + active1(#body{attrs = Attrs} = Req, From, State) -> RID = get_attr(rid, Attrs), Key = get_attr(key, Attrs), IsValidKey = is_valid_key(State#state.prev_key, Key), IsOveractivity = is_overactivity(State#state.prev_poll), Type = get_attr(type, Attrs), - if RID > - State#state.prev_rid + State#state.max_requests -> - reply_stop(State, - #body{http_reason = <<"Request ID is out of range">>, - attrs = - [{<<"type">>, <<"terminate">>}, - {<<"condition">>, <<"item-not-found">>}]}, - From, RID); - RID > State#state.prev_rid + 1 -> - State1 = restart_inactivity_timer(State), - Receivers = gb_trees:insert(RID, {From, Req}, - State1#state.receivers), - {next_state, active, - State1#state{receivers = Receivers}}; - RID =< State#state.prev_rid -> + if + RID > + State#state.prev_rid + State#state.max_requests -> + reply_stop(State, + #body{ + http_reason = <<"Request ID is out of range">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, <<"item-not-found">>}] + }, + From, + RID); + RID > State#state.prev_rid + 1 -> + State1 = restart_inactivity_timer(State), + Receivers = gb_trees:insert(RID, + {From, Req}, + State1#state.receivers), + {next_state, active, + State1#state{receivers = Receivers}}; + RID =< State#state.prev_rid -> %% TODO: do we need to check 'key' here? It seems so... case gb_trees:lookup(RID, State#state.responses) of {value, PrevBody} -> {next_state, active, - do_reply(State, From, PrevBody, RID)}; + do_reply(State, From, PrevBody, RID)}; none -> State1 = drop_holding_receiver(State, RID), State2 = stop_inactivity_timer(State1), State3 = restart_wait_timer(State2), - Receivers = gb_trees:insert(RID, {From, Req}, + Receivers = gb_trees:insert(RID, + {From, Req}, State3#state.receivers), {next_state, active, State3#state{receivers = Receivers}} end; - not IsValidKey -> - reply_stop(State, - #body{http_reason = <<"Session key mismatch">>, - attrs = - [{<<"type">>, <<"terminate">>}, - {<<"condition">>, <<"item-not-found">>}]}, - From, RID); - IsOveractivity -> - reply_stop(State, - #body{http_reason = <<"Too many requests">>, - attrs = - [{<<"type">>, <<"terminate">>}, - {<<"condition">>, <<"policy-violation">>}]}, - From, RID); - true -> - State1 = stop_inactivity_timer(State), - State2 = stop_wait_timer(State1), - Els = case get_attr('xmpp:restart', Attrs, false) of - true -> - XMPPDomain = get_attr(to, Attrs, State#state.host), - XMPPVer = get_attr('xmpp:version', Attrs, - State#state.xmpp_ver), - [make_xmlstreamstart(XMPPDomain, XMPPVer)]; - false -> Req#body.els - end, - State3 = route_els(State2, - maybe_add_xmlstreamend(Els, Type)), - {State4, RespEls} = get_response_els(State3), - NewKey = get_attr(newkey, Attrs, Key), - Pause = get_attr(pause, Attrs, undefined), - NewPoll = case State#state.prev_poll of - undefined -> undefined; - _ -> erlang:timestamp() - end, - State5 = State4#state{prev_poll = NewPoll, - prev_key = NewKey}, - if Type == <<"terminate">> -> - reply_stop(State5, - #body{http_reason = <<"Session close">>, - attrs = [{<<"type">>, <<"terminate">>}], - els = RespEls}, - From, RID); - Pause /= undefined -> - State6 = drop_holding_receiver(State5), - State7 = restart_inactivity_timer(State6, Pause), - InBuf = buf_in(RespEls, State7#state.el_ibuf), - {next_state, active, - State7#state{prev_rid = RID, el_ibuf = InBuf}}; - RespEls == [] -> - State6 = drop_holding_receiver(State5), - State7 = stop_inactivity_timer(State6), - State8 = restart_wait_timer(State7), - Receivers = gb_trees:insert(RID, {From, #body{}}, - State8#state.receivers), - {next_state, active, - State8#state{prev_rid = RID, receivers = Receivers}}; - true -> - State6 = drop_holding_receiver(State5), - reply_next_state(State6#state{prev_rid = RID}, - #body{els = RespEls}, RID, From) - end + not IsValidKey -> + reply_stop(State, + #body{ + http_reason = <<"Session key mismatch">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, <<"item-not-found">>}] + }, + From, + RID); + IsOveractivity -> + reply_stop(State, + #body{ + http_reason = <<"Too many requests">>, + attrs = + [{<<"type">>, <<"terminate">>}, + {<<"condition">>, <<"policy-violation">>}] + }, + From, + RID); + true -> + State1 = stop_inactivity_timer(State), + State2 = stop_wait_timer(State1), + Els = case get_attr('xmpp:restart', Attrs, false) of + true -> + XMPPDomain = get_attr(to, Attrs, State#state.host), + XMPPVer = get_attr('xmpp:version', + Attrs, + State#state.xmpp_ver), + [make_xmlstreamstart(XMPPDomain, XMPPVer)]; + false -> Req#body.els + end, + State3 = route_els(State2, + maybe_add_xmlstreamend(Els, Type)), + {State4, RespEls} = get_response_els(State3), + NewKey = get_attr(newkey, Attrs, Key), + Pause = get_attr(pause, Attrs, undefined), + NewPoll = case State#state.prev_poll of + undefined -> undefined; + _ -> erlang:timestamp() + end, + State5 = State4#state{ + prev_poll = NewPoll, + prev_key = NewKey + }, + if + Type == <<"terminate">> -> + reply_stop(State5, + #body{ + http_reason = <<"Session close">>, + attrs = [{<<"type">>, <<"terminate">>}], + els = RespEls + }, + From, + RID); + Pause /= undefined -> + State6 = drop_holding_receiver(State5), + State7 = restart_inactivity_timer(State6, Pause), + InBuf = buf_in(RespEls, State7#state.el_ibuf), + {next_state, active, + State7#state{prev_rid = RID, el_ibuf = InBuf}}; + RespEls == [] -> + State6 = drop_holding_receiver(State5), + State7 = stop_inactivity_timer(State6), + State8 = restart_wait_timer(State7), + Receivers = gb_trees:insert(RID, + {From, #body{}}, + State8#state.receivers), + {next_state, active, + State8#state{prev_rid = RID, receivers = Receivers}}; + true -> + State6 = drop_holding_receiver(State5), + reply_next_state(State6#state{prev_rid = RID}, + #body{els = RespEls}, + RID, + From) + end end. -handle_event({activate, C2SPid}, StateName, - State) -> + +handle_event({activate, C2SPid}, + StateName, + State) -> State1 = route_els(State#state{c2s_pid = C2SPid}), {next_state, StateName, State1}; -handle_event({change_shaper, Shaper}, StateName, - State) -> +handle_event({change_shaper, Shaper}, + StateName, + State) -> {next_state, StateName, State#state{shaper_state = Shaper}}; handle_event(Event, StateName, State) -> ?ERROR_MSG("Unexpected event in '~ts': ~p", - [StateName, Event]), + [StateName, Event]), {next_state, StateName, State}. + handle_sync_event({send_xml, - {xmlstreamstart, _, _} = El}, - _From, StateName, State) - when State#state.xmpp_ver >= <<"1.0">> -> + {xmlstreamstart, _, _} = El}, + _From, + StateName, + State) + when State#state.xmpp_ver >= <<"1.0">> -> OutBuf = buf_in([El], State#state.el_obuf), {reply, ok, StateName, State#state{el_obuf = OutBuf}}; -handle_sync_event({send_xml, El}, _From, StateName, - State) -> +handle_sync_event({send_xml, El}, + _From, + StateName, + State) -> OutBuf = buf_in([El], State#state.el_obuf), State1 = State#state{el_obuf = OutBuf}, case gb_trees:lookup(State1#state.prev_rid, - State1#state.receivers) - of - {value, {From, Body}} -> - {State2, Els} = get_response_els(State1), - {reply, ok, StateName, - reply(State2, Body#body{els = Els}, - State2#state.prev_rid, From)}; - none -> - State2 = case p1_queue:out(State1#state.shaped_receivers) - of - {{value, {TRef, From, Body}}, Q} -> - misc:cancel_timer(TRef), - p1_fsm:send_event(self(), {Body, From}), - State1#state{shaped_receivers = Q}; - _ -> State1 - end, - {reply, ok, StateName, State2} + State1#state.receivers) of + {value, {From, Body}} -> + {State2, Els} = get_response_els(State1), + {reply, ok, + StateName, + reply(State2, + Body#body{els = Els}, + State2#state.prev_rid, + From)}; + none -> + State2 = case p1_queue:out(State1#state.shaped_receivers) of + {{value, {TRef, From, Body}}, Q} -> + misc:cancel_timer(TRef), + p1_fsm:send_event(self(), {Body, From}), + State1#state{shaped_receivers = Q}; + _ -> State1 + end, + {reply, ok, StateName, State2} end; handle_sync_event(close, _From, _StateName, State) -> {stop, normal, State}; -handle_sync_event(deactivate_socket, _From, StateName, - StateData) -> - {reply, ok, StateName, - StateData#state{c2s_pid = undefined}}; +handle_sync_event(deactivate_socket, + _From, + StateName, + StateData) -> + {reply, ok, + StateName, + StateData#state{c2s_pid = undefined}}; handle_sync_event(Event, _From, StateName, State) -> ?ERROR_MSG("Unexpected sync event in '~ts': ~p", - [StateName, Event]), + [StateName, Event]), {reply, {error, badarg}, StateName, State}. -handle_info({timeout, TRef, wait_timeout}, StateName, - #state{wait_timer = TRef} = State) -> + +handle_info({timeout, TRef, wait_timeout}, + StateName, + #state{wait_timer = TRef} = State) -> State2 = State#state{wait_timer = undefined}, {next_state, StateName, drop_holding_receiver(State2)}; -handle_info({timeout, TRef, inactive}, _StateName, - #state{inactivity_timer = TRef} = State) -> +handle_info({timeout, TRef, inactive}, + _StateName, + #state{inactivity_timer = TRef} = State) -> {stop, normal, State}; -handle_info({timeout, TRef, shaper_timeout}, StateName, - State) -> +handle_info({timeout, TRef, shaper_timeout}, + StateName, + State) -> case p1_queue:out(State#state.shaped_receivers) of - {{value, {TRef, From, Req}}, Q} -> - p1_fsm:send_event(self(), {Req, From}), - {next_state, StateName, - State#state{shaped_receivers = Q}}; - {{value, _}, _} -> - ?ERROR_MSG("shaper_timeout mismatch:~n** TRef: ~p~n** " - "State: ~p", - [TRef, State]), - {stop, normal, State}; - _ -> {next_state, StateName, State} + {{value, {TRef, From, Req}}, Q} -> + p1_fsm:send_event(self(), {Req, From}), + {next_state, StateName, + State#state{shaped_receivers = Q}}; + {{value, _}, _} -> + ?ERROR_MSG("shaper_timeout mismatch:~n** TRef: ~p~n** " + "State: ~p", + [TRef, State]), + {stop, normal, State}; + _ -> {next_state, StateName, State} end; handle_info(Info, StateName, State) -> ?ERROR_MSG("Unexpected info:~n** Msg: ~p~n** StateName: ~p", - [Info, StateName]), + [Info, StateName]), {next_state, StateName, State}. + terminate(_Reason, _StateName, State) -> mod_bosh:close_session(State#state.sid), case State#state.c2s_pid of - C2SPid when is_pid(C2SPid) -> - p1_fsm:send_event(C2SPid, closed); - _ -> ok + C2SPid when is_pid(C2SPid) -> + p1_fsm:send_event(C2SPid, closed); + _ -> ok end, bounce_receivers(State, closed), bounce_els_from_obuf(State). + code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. + print_state(State) -> State. + route_els(#state{el_ibuf = Buf, c2s_pid = C2SPid} = State) -> NewBuf = p1_queue:dropwhile( - fun(El) -> - p1_fsm:send_event(C2SPid, El), - true - end, Buf), + fun(El) -> + p1_fsm:send_event(C2SPid, El), + true + end, + Buf), State#state{el_ibuf = NewBuf}. + route_els(State, Els) -> case State#state.c2s_pid of - C2SPid when is_pid(C2SPid) -> - lists:foreach(fun (El) -> - p1_fsm:send_event(C2SPid, El) - end, - Els), - State; - _ -> - InBuf = buf_in(Els, State#state.el_ibuf), - State#state{el_ibuf = InBuf} + C2SPid when is_pid(C2SPid) -> + lists:foreach(fun(El) -> + p1_fsm:send_event(C2SPid, El) + end, + Els), + State; + _ -> + InBuf = buf_in(Els, State#state.el_ibuf), + State#state{el_ibuf = InBuf} end. -get_response_els(#state{el_obuf = OutBuf, - max_concat = MaxConcat} = - State) -> + +get_response_els(#state{ + el_obuf = OutBuf, + max_concat = MaxConcat + } = + State) -> {Els, NewOutBuf} = buf_out(OutBuf, MaxConcat), {State#state{el_obuf = NewOutBuf}, Els}. + reply(State, Body, RID, From) -> State1 = restart_inactivity_timer(State), Receivers = gb_trees:delete_any(RID, - State1#state.receivers), + State1#state.receivers), State2 = do_reply(State1, From, Body, RID), case catch gb_trees:take_smallest(Receivers) of - {NextRID, {From1, Req}, Receivers1} - when NextRID == RID + 1 -> - p1_fsm:send_event(self(), {Req, From1}), - State2#state{receivers = Receivers1}; - _ -> State2#state{receivers = Receivers} + {NextRID, {From1, Req}, Receivers1} + when NextRID == RID + 1 -> + p1_fsm:send_event(self(), {Req, From1}), + State2#state{receivers = Receivers1}; + _ -> State2#state{receivers = Receivers} end. + reply_next_state(State, Body, RID, From) -> State1 = restart_inactivity_timer(State), Receivers = gb_trees:delete_any(RID, - State1#state.receivers), + State1#state.receivers), State2 = do_reply(State1, From, Body, RID), case catch gb_trees:take_smallest(Receivers) of - {NextRID, {From1, Req}, Receivers1} - when NextRID == RID + 1 -> - active(Req, From1, - State2#state{receivers = Receivers1}); - _ -> - {next_state, active, - State2#state{receivers = Receivers}} + {NextRID, {From1, Req}, Receivers1} + when NextRID == RID + 1 -> + active(Req, + From1, + State2#state{receivers = Receivers1}); + _ -> + {next_state, active, + State2#state{receivers = Receivers}} end. + reply_stop(State, Body, From, RID) -> {stop, normal, do_reply(State, From, Body, RID)}. + drop_holding_receiver(State) -> drop_holding_receiver(State, State#state.prev_rid). + + drop_holding_receiver(State, RID) -> case gb_trees:lookup(RID, State#state.receivers) of - {value, {From, Body}} -> - State1 = restart_inactivity_timer(State), - Receivers = gb_trees:delete_any(RID, - State1#state.receivers), - State2 = State1#state{receivers = Receivers}, - do_reply(State2, From, Body, RID); - none -> - restart_inactivity_timer(State) + {value, {From, Body}} -> + State1 = restart_inactivity_timer(State), + Receivers = gb_trees:delete_any(RID, + State1#state.receivers), + State2 = State1#state{receivers = Receivers}, + do_reply(State2, From, Body, RID); + none -> + restart_inactivity_timer(State) end. + do_reply(State, From, Body, RID) -> ?DEBUG("Send reply:~n** RequestID: ~p~n** Reply: " - "~p~n** To: ~p~n** State: ~p", - [RID, Body, From, State]), + "~p~n** To: ~p~n** State: ~p", + [RID, Body, From, State]), p1_fsm:reply(From, Body), Responses = gb_trees:delete_any(RID, - State#state.responses), + State#state.responses), Responses1 = case gb_trees:size(Responses) of - N when N < State#state.max_requests; N == 0 -> - Responses; - _ -> element(3, gb_trees:take_smallest(Responses)) - end, + N when N < State#state.max_requests; N == 0 -> + Responses; + _ -> element(3, gb_trees:take_smallest(Responses)) + end, Responses2 = gb_trees:insert(RID, Body, Responses1), State#state{responses = Responses2}. + bounce_receivers(State, _Reason) -> Receivers = gb_trees:to_list(State#state.receivers), - ShapedReceivers = lists:map(fun ({_, From, - #body{attrs = Attrs} = Body}) -> - RID = get_attr(rid, Attrs), - {RID, {From, Body}} - end, - p1_queue:to_list(State#state.shaped_receivers)), - lists:foldl(fun ({RID, {From, _Body}}, AccState) -> - NewBody = #body{http_reason = - <<"Session closed">>, - attrs = - [{type, <<"terminate">>}, - {condition, - <<"other-request">>}]}, - do_reply(AccState, From, NewBody, RID) - end, - State, Receivers ++ ShapedReceivers). + ShapedReceivers = lists:map(fun({_, + From, + #body{attrs = Attrs} = Body}) -> + RID = get_attr(rid, Attrs), + {RID, {From, Body}} + end, + p1_queue:to_list(State#state.shaped_receivers)), + lists:foldl(fun({RID, {From, _Body}}, AccState) -> + NewBody = #body{ + http_reason = + <<"Session closed">>, + attrs = + [{type, <<"terminate">>}, + {condition, + <<"other-request">>}] + }, + do_reply(AccState, From, NewBody, RID) + end, + State, + Receivers ++ ShapedReceivers). + bounce_els_from_obuf(State) -> Opts = ejabberd_config:codec_options(), p1_queue:foreach( fun({xmlstreamelement, El}) -> - try xmpp:decode(El, ?NS_CLIENT, Opts) of - Pkt when ?is_stanza(Pkt) -> - case {xmpp:get_from(Pkt), xmpp:get_to(Pkt)} of - {#jid{}, #jid{}} -> - ejabberd_router:route(Pkt); - _ -> - ok - end; - _ -> - ok - catch _:{xmpp_codec, _} -> - ok - end; - (_) -> - ok - end, State#state.el_obuf). + try xmpp:decode(El, ?NS_CLIENT, Opts) of + Pkt when ?is_stanza(Pkt) -> + case {xmpp:get_from(Pkt), xmpp:get_to(Pkt)} of + {#jid{}, #jid{}} -> + ejabberd_router:route(Pkt); + _ -> + ok + end; + _ -> + ok + catch + _:{xmpp_codec, _} -> + ok + end; + (_) -> + ok + end, + State#state.el_obuf). + is_valid_key(<<"">>, <<"">>) -> true; is_valid_key(PrevKey, Key) -> str:sha(Key) == PrevKey. + is_overactivity(undefined) -> false; is_overactivity(PrevPoll) -> PollPeriod = timer:now_diff(erlang:timestamp(), PrevPoll) div - 1000000, - if PollPeriod < (?DEFAULT_POLLING) -> true; - true -> false + 1000000, + if + PollPeriod < (?DEFAULT_POLLING) -> true; + true -> false end. + make_xmlstreamstart(XMPPDomain, Version) -> VersionEl = case Version of - <<"">> -> []; - _ -> [{<<"version">>, Version}] - end, + <<"">> -> []; + _ -> [{<<"version">>, Version}] + end, {xmlstreamstart, <<"stream:stream">>, - [{<<"to">>, XMPPDomain}, {<<"xmlns">>, ?NS_CLIENT}, - {<<"xmlns:xmpp">>, ?NS_BOSH}, - {<<"xmlns:stream">>, ?NS_STREAM} - | VersionEl]}. + [{<<"to">>, XMPPDomain}, + {<<"xmlns">>, ?NS_CLIENT}, + {<<"xmlns:xmpp">>, ?NS_BOSH}, + {<<"xmlns:stream">>, ?NS_STREAM} | VersionEl]}. + maybe_add_xmlstreamend(Els, <<"terminate">>) -> Els ++ [{xmlstreamend, <<"stream:stream">>}]; maybe_add_xmlstreamend(Els, _) -> Els. + encode_body(#body{attrs = Attrs, els = Els}, Type) -> - Attrs1 = lists:map(fun ({K, V}) when is_atom(K) -> - AmK = iolist_to_binary(atom_to_list(K)), - case V of - true -> {AmK, <<"true">>}; - false -> {AmK, <<"false">>}; - I when is_integer(I), I >= 0 -> - {AmK, integer_to_binary(I)}; - _ -> {AmK, V} - end; - ({K, V}) -> {K, V} - end, - Attrs), + Attrs1 = lists:map(fun({K, V}) when is_atom(K) -> + AmK = iolist_to_binary(atom_to_list(K)), + case V of + true -> {AmK, <<"true">>}; + false -> {AmK, <<"false">>}; + I when is_integer(I), I >= 0 -> + {AmK, integer_to_binary(I)}; + _ -> {AmK, V} + end; + ({K, V}) -> {K, V} + end, + Attrs), Attrs2 = [{<<"xmlns">>, ?NS_HTTP_BIND} | Attrs1], - {Attrs3, XMLs} = lists:foldr(fun ({xmlstreamraw, XML}, - {AttrsAcc, XMLBuf}) -> - {AttrsAcc, [XML | XMLBuf]}; - ({xmlstreamelement, - #xmlel{name = <<"stream:error">>} = El}, - {AttrsAcc, XMLBuf}) -> - {[{<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"remote-stream-error">>}, - {<<"xmlns:stream">>, ?NS_STREAM} - | AttrsAcc], - [encode_element(El, Type) | XMLBuf]}; - ({xmlstreamelement, - #xmlel{name = <<"stream:features">>} = - El}, - {AttrsAcc, XMLBuf}) -> - {lists:keystore(<<"xmlns:stream">>, 1, - AttrsAcc, - {<<"xmlns:stream">>, - ?NS_STREAM}), - [encode_element(El, Type) | XMLBuf]}; - ({xmlstreamelement, - #xmlel{name = Name, attrs = EAttrs} = El}, - {AttrsAcc, XMLBuf}) + {Attrs3, XMLs} = lists:foldr(fun({xmlstreamraw, XML}, + {AttrsAcc, XMLBuf}) -> + {AttrsAcc, [XML | XMLBuf]}; + ({xmlstreamelement, + #xmlel{name = <<"stream:error">>} = El}, + {AttrsAcc, XMLBuf}) -> + {[{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"remote-stream-error">>}, + {<<"xmlns:stream">>, ?NS_STREAM} | AttrsAcc], + [encode_element(El, Type) | XMLBuf]}; + ({xmlstreamelement, + #xmlel{name = <<"stream:features">>} = + El}, + {AttrsAcc, XMLBuf}) -> + {lists:keystore(<<"xmlns:stream">>, + 1, + AttrsAcc, + {<<"xmlns:stream">>, + ?NS_STREAM}), + [encode_element(El, Type) | XMLBuf]}; + ({xmlstreamelement, + #xmlel{name = Name, attrs = EAttrs} = El}, + {AttrsAcc, XMLBuf}) when Name == <<"message">>; Name == <<"presence">>; Name == <<"iq">> -> NewAttrs = lists:keystore( - <<"xmlns">>, 1, EAttrs, + <<"xmlns">>, + 1, + EAttrs, {<<"xmlns">>, ?NS_CLIENT}), NewEl = El#xmlel{attrs = NewAttrs}, {AttrsAcc, [encode_element(NewEl, Type) | XMLBuf]}; - ({xmlstreamelement, El}, - {AttrsAcc, XMLBuf}) -> + ({xmlstreamelement, El}, + {AttrsAcc, XMLBuf}) -> {AttrsAcc, [encode_element(El, Type) | XMLBuf]}; - ({xmlstreamend, _}, {AttrsAcc, XMLBuf}) -> - {[{<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"remote-stream-error">>} - | AttrsAcc], - XMLBuf}; - ({xmlstreamstart, <<"stream:stream">>, - SAttrs}, - {AttrsAcc, XMLBuf}) -> - StreamID = fxml:get_attr_s(<<"id">>, - SAttrs), - NewAttrs = case - fxml:get_attr_s(<<"version">>, - SAttrs) - of - <<"">> -> - [{<<"authid">>, - StreamID} - | AttrsAcc]; - V -> - lists:keystore(<<"xmlns:xmpp">>, - 1, - [{<<"xmpp:version">>, - V}, - {<<"authid">>, - StreamID} - | AttrsAcc], - {<<"xmlns:xmpp">>, - ?NS_BOSH}) - end, - {NewAttrs, XMLBuf}; - ({xmlstreamerror, _}, - {AttrsAcc, XMLBuf}) -> - {[{<<"type">>, <<"terminate">>}, - {<<"condition">>, - <<"remote-stream-error">>} - | AttrsAcc], - XMLBuf}; - (_, Acc) -> Acc - end, - {Attrs2, []}, Els), + ({xmlstreamend, _}, {AttrsAcc, XMLBuf}) -> + {[{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"remote-stream-error">>} | AttrsAcc], + XMLBuf}; + ({xmlstreamstart, <<"stream:stream">>, + SAttrs}, + {AttrsAcc, XMLBuf}) -> + StreamID = fxml:get_attr_s(<<"id">>, + SAttrs), + NewAttrs = case fxml:get_attr_s(<<"version">>, + SAttrs) of + <<"">> -> + [{<<"authid">>, + StreamID} | AttrsAcc]; + V -> + lists:keystore(<<"xmlns:xmpp">>, + 1, + [{<<"xmpp:version">>, + V}, + {<<"authid">>, + StreamID} | AttrsAcc], + {<<"xmlns:xmpp">>, + ?NS_BOSH}) + end, + {NewAttrs, XMLBuf}; + ({xmlstreamerror, _}, + {AttrsAcc, XMLBuf}) -> + {[{<<"type">>, <<"terminate">>}, + {<<"condition">>, + <<"remote-stream-error">>} | AttrsAcc], + XMLBuf}; + (_, Acc) -> Acc + end, + {Attrs2, []}, + Els), case XMLs of - [] when Type == xml -> + [] when Type == xml -> [<<">, attrs_to_list(Attrs3), <<"/>">>]; - _ when Type == xml -> - [<<">, attrs_to_list(Attrs3), $>, XMLs, - <<"">>] + _ when Type == xml -> + [<<">, + attrs_to_list(Attrs3), + $>, + XMLs, + <<"">>] end. + encode_element(El, xml) -> fxml:element_to_binary(El); encode_element(El, json) -> El. + decode_body(Data, Size, Type) -> case decode(Data, Type) of - #xmlel{name = <<"body">>, attrs = Attrs, - children = Els} -> - case attrs_to_body_attrs(Attrs) of - {error, _} = Err -> Err; - BodyAttrs -> - case get_attr(rid, BodyAttrs) of - <<"">> -> {error, <<"Missing \"rid\" attribute">>}; - _ -> - Els1 = lists:flatmap(fun (#xmlel{} = El) -> - [{xmlstreamelement, El}]; - (_) -> [] - end, - Els), - {ok, #body{attrs = BodyAttrs, size = Size, els = Els1}} - end - end; - #xmlel{} -> {error, <<"Unexpected payload">>}; - _ when Type == xml -> + #xmlel{ + name = <<"body">>, + attrs = Attrs, + children = Els + } -> + case attrs_to_body_attrs(Attrs) of + {error, _} = Err -> Err; + BodyAttrs -> + case get_attr(rid, BodyAttrs) of + <<"">> -> {error, <<"Missing \"rid\" attribute">>}; + _ -> + Els1 = lists:flatmap(fun(#xmlel{} = El) -> + [{xmlstreamelement, El}]; + (_) -> [] + end, + Els), + {ok, #body{attrs = BodyAttrs, size = Size, els = Els1}} + end + end; + #xmlel{} -> {error, <<"Unexpected payload">>}; + _ when Type == xml -> {error, <<"XML is not well-formed">>}; - _ when Type == json -> + _ when Type == json -> {error, <<"JSON is not well-formed">>} end. + decode(Data, xml) -> fxml_stream:parse_element(Data); decode(Data, json) -> Data. + attrs_to_body_attrs(Attrs) -> - lists:foldl(fun (_, {error, Reason}) -> {error, Reason}; - ({Attr, Val}, Acc) -> - try case Attr of - <<"ver">> -> [{ver, Val} | Acc]; - <<"xmpp:version">> -> - [{'xmpp:version', Val} | Acc]; - <<"type">> -> [{type, Val} | Acc]; - <<"key">> -> [{key, Val} | Acc]; - <<"newkey">> -> [{newkey, Val} | Acc]; - <<"xmlns">> -> Val = (?NS_HTTP_BIND), Acc; - <<"secure">> -> [{secure, to_bool(Val)} | Acc]; - <<"xmpp:restart">> -> - [{'xmpp:restart', to_bool(Val)} | Acc]; - <<"to">> -> - [{to, jid:nameprep(Val)} | Acc]; - <<"wait">> -> [{wait, to_int(Val, 0)} | Acc]; - <<"ack">> -> [{ack, to_int(Val, 0)} | Acc]; - <<"sid">> -> [{sid, Val} | Acc]; - <<"hold">> -> [{hold, to_int(Val, 0)} | Acc]; - <<"rid">> -> [{rid, to_int(Val, 0)} | Acc]; - <<"pause">> -> [{pause, to_int(Val, 0)} | Acc]; - _ -> [{Attr, Val} | Acc] - end - catch - _:_ -> - {error, - <<"Invalid \"", Attr/binary, "\" attribute">>} - end - end, - [], Attrs). + lists:foldl(fun(_, {error, Reason}) -> {error, Reason}; + ({Attr, Val}, Acc) -> + try + case Attr of + <<"ver">> -> [{ver, Val} | Acc]; + <<"xmpp:version">> -> + [{'xmpp:version', Val} | Acc]; + <<"type">> -> [{type, Val} | Acc]; + <<"key">> -> [{key, Val} | Acc]; + <<"newkey">> -> [{newkey, Val} | Acc]; + <<"xmlns">> -> Val = (?NS_HTTP_BIND), Acc; + <<"secure">> -> [{secure, to_bool(Val)} | Acc]; + <<"xmpp:restart">> -> + [{'xmpp:restart', to_bool(Val)} | Acc]; + <<"to">> -> + [{to, jid:nameprep(Val)} | Acc]; + <<"wait">> -> [{wait, to_int(Val, 0)} | Acc]; + <<"ack">> -> [{ack, to_int(Val, 0)} | Acc]; + <<"sid">> -> [{sid, Val} | Acc]; + <<"hold">> -> [{hold, to_int(Val, 0)} | Acc]; + <<"rid">> -> [{rid, to_int(Val, 0)} | Acc]; + <<"pause">> -> [{pause, to_int(Val, 0)} | Acc]; + _ -> [{Attr, Val} | Acc] + end + catch + _:_ -> + {error, + <<"Invalid \"", Attr/binary, "\" attribute">>} + end + end, + [], + Attrs). + to_int(S, Min) -> case binary_to_integer(S) of - I when I >= Min -> I; - _ -> erlang:error(badarg) + I when I >= Min -> I; + _ -> erlang:error(badarg) end. + to_bool(<<"true">>) -> true; to_bool(<<"1">>) -> true; to_bool(<<"false">>) -> false; to_bool(<<"0">>) -> false. -attrs_to_list(Attrs) -> [attr_to_list(A) || A <- Attrs]. + +attrs_to_list(Attrs) -> [ attr_to_list(A) || A <- Attrs ]. + attr_to_list({Name, Value}) -> [$\s, Name, $=, $', fxml:crypt(Value), $']. + bosh_response(Body, Type) -> CType = case Type of xml -> ?CT_XML; json -> ?CT_JSON end, - {200, Body#body.http_reason, ?HEADER(CType), + {200, + Body#body.http_reason, + ?HEADER(CType), encode_body(Body, Type)}. + bosh_response_with_msg(Body, Type, RcvBody) -> ?DEBUG("Send error reply:~p~n** Receiced body: ~p", - [Body, RcvBody]), + [Body, RcvBody]), bosh_response(Body, Type). + http_error(Status, Reason, Type) -> CType = case Type of xml -> ?CT_XML; @@ -968,93 +1135,119 @@ http_error(Status, Reason, Type) -> end, {Status, Reason, ?HEADER(CType), <<"">>}. + make_sid() -> str:sha(p1_rand:get_string()). + -compile({no_auto_import, [{min, 2}]}). + min(undefined, B) -> B; min(A, B) -> erlang:min(A, B). + check_bosh_module(XmppDomain) -> case gen_mod:is_loaded(XmppDomain, mod_bosh) of - true -> ok; - false -> - ?ERROR_MSG("You are trying to use BOSH (HTTP Bind) " - "in host ~p, but the module mod_bosh " - "is not started in that host. Configure " - "your BOSH client to connect to the correct " - "host, or add your desired host to the " - "configuration, or check your 'modules' " - "section in your ejabberd configuration " - "file.", - [XmppDomain]) + true -> ok; + false -> + ?ERROR_MSG("You are trying to use BOSH (HTTP Bind) " + "in host ~p, but the module mod_bosh " + "is not started in that host. Configure " + "your BOSH client to connect to the correct " + "host, or add your desired host to the " + "configuration, or check your 'modules' " + "section in your ejabberd configuration " + "file.", + [XmppDomain]) end. + get_attr(Attr, Attrs) -> get_attr(Attr, Attrs, <<"">>). + get_attr(Attr, Attrs, Default) -> case lists:keysearch(Attr, 1, Attrs) of - {value, {_, Val}} -> Val; - _ -> Default + {value, {_, Val}} -> Val; + _ -> Default end. + buf_new(Host) -> buf_new(Host, unlimited). + buf_new(Host, Limit) -> QueueType = mod_bosh_opt:queue_type(Host), p1_queue:new(QueueType, Limit). + buf_in(Xs, Buf) -> lists:foldl(fun p1_queue:in/2, Buf, Xs). + buf_out(Buf, Num) when is_integer(Num), Num > 0 -> buf_out(Buf, Num, []); buf_out(Buf, _) -> {p1_queue:to_list(Buf), p1_queue:clear(Buf)}. + buf_out(Buf, 0, Els) -> {lists:reverse(Els), Buf}; buf_out(Buf, I, Els) -> case p1_queue:out(Buf) of - {{value, El}, NewBuf} -> - buf_out(NewBuf, I - 1, [El | Els]); - {empty, _} -> buf_out(Buf, 0, Els) + {{value, El}, NewBuf} -> + buf_out(NewBuf, I - 1, [El | Els]); + {empty, _} -> buf_out(Buf, 0, Els) end. + restart_timer(TRef, Timeout, Msg) -> misc:cancel_timer(TRef), erlang:start_timer(timer:seconds(Timeout), self(), Msg). -restart_inactivity_timer(#state{inactivity_timeout = - Timeout} = - State) -> + +restart_inactivity_timer(#state{ + inactivity_timeout = + Timeout + } = + State) -> restart_inactivity_timer(State, Timeout). -restart_inactivity_timer(#state{inactivity_timer = - TRef} = - State, - Timeout) -> + +restart_inactivity_timer(#state{ + inactivity_timer = + TRef + } = + State, + Timeout) -> NewTRef = restart_timer(TRef, Timeout, inactive), State#state{inactivity_timer = NewTRef}. + stop_inactivity_timer(#state{inactivity_timer = TRef} = - State) -> + State) -> misc:cancel_timer(TRef), State#state{inactivity_timer = undefined}. -restart_wait_timer(#state{wait_timer = TRef, - wait_timeout = Timeout} = - State) -> + +restart_wait_timer(#state{ + wait_timer = TRef, + wait_timeout = Timeout + } = + State) -> NewTRef = restart_timer(TRef, Timeout, wait_timeout), State#state{wait_timer = NewTRef}. + stop_wait_timer(#state{wait_timer = TRef} = State) -> misc:cancel_timer(TRef), State#state{wait_timer = undefined}. + start_shaper_timer(Timeout) -> erlang:start_timer(Timeout, self(), shaper_timeout). + make_random_jid(Host) -> User = p1_rand:get_string(), jid:make(User, Host, p1_rand:get_string()). + make_socket(Pid, IP) -> {http_bind, Pid, IP}. diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index ef9312ef5..78b9c9770 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -32,34 +32,76 @@ %% ejabberd_listener callbacks -export([start/3, start_link/3, accept/1, listen_opt_type/1, listen_options/0]). %% xmpp_stream_in callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). --export([tls_options/1, tls_required/1, tls_enabled/1, - allow_unencrypted_sasl2/1, compress_methods/1, bind/2, - sasl_mechanisms/2, get_password_fun/2, check_password_fun/2, - check_password_digest_fun/2, unauthenticated_stream_features/1, - authenticated_stream_features/1, handle_stream_start/2, - handle_stream_end/2, handle_unauthenticated_packet/2, - handle_authenticated_packet/2, handle_auth_success/4, - handle_auth_failure/4, handle_send/3, handle_recv/3, handle_cdata/2, - handle_unbinded_packet/2, inline_stream_features/1, - handle_sasl2_inline/2, handle_sasl2_inline_post/3, - handle_bind2_inline/2, handle_bind2_inline_post/3, sasl_options/1, - handle_sasl2_task_next/4, handle_sasl2_task_data/3, - get_fast_tokens_fun/2, fast_mechanisms/1]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). +-export([tls_options/1, + tls_required/1, + tls_enabled/1, + allow_unencrypted_sasl2/1, + compress_methods/1, + bind/2, + sasl_mechanisms/2, + get_password_fun/2, + check_password_fun/2, + check_password_digest_fun/2, + unauthenticated_stream_features/1, + authenticated_stream_features/1, + handle_stream_start/2, + handle_stream_end/2, + handle_unauthenticated_packet/2, + handle_authenticated_packet/2, + handle_auth_success/4, + handle_auth_failure/4, + handle_send/3, + handle_recv/3, + handle_cdata/2, + handle_unbinded_packet/2, + inline_stream_features/1, + handle_sasl2_inline/2, + handle_sasl2_inline_post/3, + handle_bind2_inline/2, + handle_bind2_inline_post/3, + sasl_options/1, + handle_sasl2_task_next/4, + handle_sasl2_task_data/3, + get_fast_tokens_fun/2, + fast_mechanisms/1]). %% Hooks --export([handle_unexpected_cast/2, handle_unexpected_call/3, - process_auth_result/3, c2s_handle_bind/1, - reject_unauthenticated_packet/2, process_closed/2, - process_terminated/2, process_info/2]). +-export([handle_unexpected_cast/2, + handle_unexpected_call/3, + process_auth_result/3, + c2s_handle_bind/1, + reject_unauthenticated_packet/2, + process_closed/2, + process_terminated/2, + process_info/2]). %% API --export([get_presence/1, set_presence/2, resend_presence/1, resend_presence/2, - open_session/1, call/3, cast/2, send/2, close/1, close/2, stop_async/1, - reply/2, copy_state/2, set_timeout/2, route/2, format_reason/2, - host_up/1, host_down/1, send_ws_ping/1, bounce_message_queue/2, - reset_vcard_xupdate_resend_presence/1]). +-export([get_presence/1, + set_presence/2, + resend_presence/1, resend_presence/2, + open_session/1, + call/3, + cast/2, + send/2, + close/1, close/2, + stop_async/1, + reply/2, + copy_state/2, + set_timeout/2, + route/2, + format_reason/2, + host_up/1, + host_down/1, + send_ws_ping/1, + bounce_message_queue/2, + reset_vcard_xupdate_resend_presence/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("mod_roster.hrl"). -include("translate.hrl"). @@ -69,20 +111,26 @@ -type state() :: xmpp_stream_in:state(). -export_type([state/0]). + %%%=================================================================== %%% ejabberd_listener API %%%=================================================================== start(SockMod, Socket, Opts) -> - xmpp_stream_in:start(?MODULE, [{SockMod, Socket}, Opts], - ejabberd_config:fsm_limit_opts(Opts)). + xmpp_stream_in:start(?MODULE, + [{SockMod, Socket}, Opts], + ejabberd_config:fsm_limit_opts(Opts)). + start_link(SockMod, Socket, Opts) -> - xmpp_stream_in:start_link(?MODULE, [{SockMod, Socket}, Opts], - ejabberd_config:fsm_limit_opts(Opts)). + xmpp_stream_in:start_link(?MODULE, + [{SockMod, Socket}, Opts], + ejabberd_config:fsm_limit_opts(Opts)). + accept(Ref) -> xmpp_stream_in:accept(Ref). + %%%=================================================================== %%% Common API %%%=================================================================== @@ -90,438 +138,579 @@ accept(Ref) -> call(Ref, Msg, Timeout) -> xmpp_stream_in:call(Ref, Msg, Timeout). + -spec cast(pid(), term()) -> ok. cast(Ref, Msg) -> xmpp_stream_in:cast(Ref, Msg). + reply(Ref, Reply) -> xmpp_stream_in:reply(Ref, Reply). + -spec get_presence(pid()) -> presence(). get_presence(Ref) -> call(Ref, get_presence, 1000). + -spec set_presence(pid(), presence()) -> ok. set_presence(Ref, Pres) -> call(Ref, {set_presence, Pres}, 1000). + -spec resend_presence(pid()) -> boolean(). resend_presence(Pid) -> resend_presence(Pid, undefined). + -spec resend_presence(pid(), jid() | undefined) -> boolean(). resend_presence(Pid, To) -> route(Pid, {resend_presence, To}). + -spec reset_vcard_xupdate_resend_presence(pid()) -> boolean(). reset_vcard_xupdate_resend_presence(Pid) -> route(Pid, reset_vcard_xupdate_resend_presence). + -spec close(pid()) -> ok; - (state()) -> state(). + (state()) -> state(). close(Ref) -> xmpp_stream_in:close(Ref). + -spec close(pid(), atom()) -> ok. close(Ref, Reason) -> xmpp_stream_in:close(Ref, Reason). + -spec stop_async(pid()) -> ok. stop_async(Pid) -> xmpp_stream_in:stop_async(Pid). + -spec send(pid(), xmpp_element()) -> ok; - (state(), xmpp_element()) -> state(). + (state(), xmpp_element()) -> state(). send(Pid, Pkt) when is_pid(Pid) -> xmpp_stream_in:send(Pid, Pkt); send(#{lserver := LServer} = State, Pkt) -> Pkt1 = fix_from_to(Pkt, State), case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt1, State}, []) of - {drop, State1} -> State1; - {Pkt2, State1} -> xmpp_stream_in:send(State1, Pkt2) + {drop, State1} -> State1; + {Pkt2, State1} -> xmpp_stream_in:send(State1, Pkt2) end. + -spec send_error(state(), xmpp_element(), stanza_error()) -> state(). send_error(#{lserver := LServer} = State, Pkt, Err) -> case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt, State}, []) of - {drop, State1} -> State1; - {Pkt1, State1} -> xmpp_stream_in:send_error(State1, Pkt1, Err) + {drop, State1} -> State1; + {Pkt1, State1} -> xmpp_stream_in:send_error(State1, Pkt1, Err) end. + -spec send_ws_ping(pid()) -> ok; - (state()) -> state(). + (state()) -> state(). send_ws_ping(Ref) -> xmpp_stream_in:send_ws_ping(Ref). + -spec route(pid(), term()) -> boolean(). route(Pid, Term) -> ejabberd_cluster:send(Pid, Term). + -spec set_timeout(state(), timeout()) -> state(). set_timeout(State, Timeout) -> xmpp_stream_in:set_timeout(State, Timeout). + -spec host_up(binary()) -> ok. host_up(Host) -> ejabberd_hooks:add(c2s_closed, Host, ?MODULE, process_closed, 100), - ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, - process_terminated, 100), + ejabberd_hooks:add(c2s_terminated, + Host, + ?MODULE, + process_terminated, + 100), ejabberd_hooks:add(c2s_handle_bind, Host, ?MODULE, c2s_handle_bind, 100), - ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, - reject_unauthenticated_packet, 100), - ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, - process_info, 100), - ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE, - process_auth_result, 100), - ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, - handle_unexpected_cast, 100), - ejabberd_hooks:add(c2s_handle_call, Host, ?MODULE, - handle_unexpected_call, 100). + ejabberd_hooks:add(c2s_unauthenticated_packet, + Host, + ?MODULE, + reject_unauthenticated_packet, + 100), + ejabberd_hooks:add(c2s_handle_info, + Host, + ?MODULE, + process_info, + 100), + ejabberd_hooks:add(c2s_auth_result, + Host, + ?MODULE, + process_auth_result, + 100), + ejabberd_hooks:add(c2s_handle_cast, + Host, + ?MODULE, + handle_unexpected_cast, + 100), + ejabberd_hooks:add(c2s_handle_call, + Host, + ?MODULE, + handle_unexpected_call, + 100). + -spec host_down(binary()) -> ok. host_down(Host) -> ejabberd_hooks:delete(c2s_closed, Host, ?MODULE, process_closed, 100), - ejabberd_hooks:delete(c2s_terminated, Host, ?MODULE, - process_terminated, 100), + ejabberd_hooks:delete(c2s_terminated, + Host, + ?MODULE, + process_terminated, + 100), ejabberd_hooks:delete(c2s_handle_bind, Host, ?MODULE, c2s_handle_bind, 100), - ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, - reject_unauthenticated_packet, 100), - ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, - process_info, 100), - ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE, - process_auth_result, 100), - ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, - handle_unexpected_cast, 100), - ejabberd_hooks:delete(c2s_handle_call, Host, ?MODULE, - handle_unexpected_call, 100). + ejabberd_hooks:delete(c2s_unauthenticated_packet, + Host, + ?MODULE, + reject_unauthenticated_packet, + 100), + ejabberd_hooks:delete(c2s_handle_info, + Host, + ?MODULE, + process_info, + 100), + ejabberd_hooks:delete(c2s_auth_result, + Host, + ?MODULE, + process_auth_result, + 100), + ejabberd_hooks:delete(c2s_handle_cast, + Host, + ?MODULE, + handle_unexpected_cast, + 100), + ejabberd_hooks:delete(c2s_handle_call, + Host, + ?MODULE, + handle_unexpected_call, + 100). + %% Copies content of one c2s state to another. %% This is needed for session migration from one pid to another. -spec copy_state(state(), state()) -> state(). copy_state(NewState, - #{jid := JID, resource := Resource, auth_module := AuthModule, - lserver := LServer, pres_a := PresA} = OldState) -> + #{ + jid := JID, + resource := Resource, + auth_module := AuthModule, + lserver := LServer, + pres_a := PresA + } = OldState) -> State1 = case OldState of - #{pres_last := Pres, pres_timestamp := PresTS} -> - NewState#{pres_last => Pres, pres_timestamp => PresTS}; - _ -> - NewState - end, + #{pres_last := Pres, pres_timestamp := PresTS} -> + NewState#{pres_last => Pres, pres_timestamp => PresTS}; + _ -> + NewState + end, Conn = get_conn_type(State1), - State2 = State1#{jid => JID, resource => Resource, - conn => Conn, - auth_module => AuthModule, - pres_a => PresA}, + State2 = State1#{ + jid => JID, + resource => Resource, + conn => Conn, + auth_module => AuthModule, + pres_a => PresA + }, ejabberd_hooks:run_fold(c2s_copy_session, LServer, State2, [OldState]). + -spec open_session(state()) -> {ok, state()} | state(). -open_session(#{user := U, server := S, resource := R, - sid := SID, ip := IP, auth_module := AuthModule} = State) -> +open_session(#{ + user := U, + server := S, + resource := R, + sid := SID, + ip := IP, + auth_module := AuthModule + } = State) -> JID = jid:make(U, S, R), State1 = change_shaper(State), Conn = get_conn_type(State1), State2 = State1#{conn => Conn, resource => R, jid => JID}, Prio = case maps:get(pres_last, State, undefined) of - undefined -> undefined; - Pres -> get_priority_from_presence(Pres) - end, + undefined -> undefined; + Pres -> get_priority_from_presence(Pres) + end, Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}], case State of - #{bind2_session_id := Tag} -> - ejabberd_sm:open_session(SID, U, S, R, Prio, Info, Tag); - _ -> - ejabberd_sm:open_session(SID, U, S, R, Prio, Info) + #{bind2_session_id := Tag} -> + ejabberd_sm:open_session(SID, U, S, R, Prio, Info, Tag); + _ -> + ejabberd_sm:open_session(SID, U, S, R, Prio, Info) end, xmpp_stream_in:establish(State2). + %%%=================================================================== %%% Hooks %%%=================================================================== process_info(#{lserver := LServer} = State, {route, Packet}) -> {Pass, State1} = case Packet of - #presence{} -> - process_presence_in(State, Packet); - #message{} -> - process_message_in(State, Packet); - #iq{} -> - process_iq_in(State, Packet) - end, - if Pass -> - {Packet1, State2} = ejabberd_hooks:run_fold( - user_receive_packet, LServer, - {Packet, State1}, []), - case Packet1 of - drop -> State2; - _ -> send(State2, Packet1) - end; - true -> - State1 + #presence{} -> + process_presence_in(State, Packet); + #message{} -> + process_message_in(State, Packet); + #iq{} -> + process_iq_in(State, Packet) + end, + if + Pass -> + {Packet1, State2} = ejabberd_hooks:run_fold( + user_receive_packet, + LServer, + {Packet, State1}, + []), + case Packet1 of + drop -> State2; + _ -> send(State2, Packet1) + end; + true -> + State1 end; process_info(State, reset_vcard_xupdate_resend_presence) -> case maps:get(pres_last, State, error) of - error -> State; - Pres -> - Pres2 = xmpp:remove_subtag(Pres, #vcard_xupdate{}), - process_self_presence(State#{pres_last => Pres2}, Pres2) + error -> State; + Pres -> + Pres2 = xmpp:remove_subtag(Pres, #vcard_xupdate{}), + process_self_presence(State#{pres_last => Pres2}, Pres2) end; process_info(#{jid := JID} = State, {resend_presence, To}) -> case maps:get(pres_last, State, error) of - error -> State; - Pres when To == undefined -> - process_self_presence(State, Pres); - Pres when To#jid.luser == JID#jid.luser andalso - To#jid.lserver == JID#jid.lserver andalso - To#jid.lresource == <<"">> -> - process_self_presence(State, Pres); - Pres -> - process_presence_out(State, xmpp:set_to(Pres, To)) + error -> State; + Pres when To == undefined -> + process_self_presence(State, Pres); + Pres when To#jid.luser == JID#jid.luser andalso + To#jid.lserver == JID#jid.lserver andalso + To#jid.lresource == <<"">> -> + process_self_presence(State, Pres); + Pres -> + process_presence_out(State, xmpp:set_to(Pres, To)) end; process_info(State, Info) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), State. + handle_unexpected_call(State, From, Msg) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Msg]), State. + handle_unexpected_cast(State, Msg) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), State. + c2s_handle_bind({<<"">>, {ok, State}}) -> {new_uniq_id(), {ok, State}}; c2s_handle_bind(Acc) -> Acc. + reject_unauthenticated_packet(State, _Pkt) -> Err = xmpp:serr_not_authorized(), send(State, Err). -process_auth_result(#{sasl_mech := Mech, auth_module := AuthModule, - socket := Socket, ip := IP, lserver := LServer} = State, - true, User) -> + +process_auth_result(#{ + sasl_mech := Mech, + auth_module := AuthModule, + socket := Socket, + ip := IP, + lserver := LServer + } = State, + true, + User) -> misc:set_proc_label({?MODULE, User, LServer}), ?INFO_MSG("(~ts) Accepted c2s ~ts authentication for ~ts@~ts by ~ts backend from ~ts", - [xmpp_socket:pp(Socket), Mech, User, LServer, + [xmpp_socket:pp(Socket), + Mech, + User, + LServer, ejabberd_auth:backend_type(AuthModule), ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), State; -process_auth_result(#{sasl_mech := Mech, - socket := Socket, ip := IP, lserver := LServer} = State, - {false, Reason}, User) -> +process_auth_result(#{ + sasl_mech := Mech, + socket := Socket, + ip := IP, + lserver := LServer + } = State, + {false, Reason}, + User) -> ?WARNING_MSG("(~ts) Failed c2s ~ts authentication ~tsfrom ~ts: ~ts", - [xmpp_socket:pp(Socket), Mech, - if User /= <<"">> -> ["for ", User, "@", LServer, " "]; - true -> "" + [xmpp_socket:pp(Socket), + Mech, + if + User /= <<"">> -> ["for ", User, "@", LServer, " "]; + true -> "" end, - ejabberd_config:may_hide_data(misc:ip_to_list(IP)), Reason]), + ejabberd_config:may_hide_data(misc:ip_to_list(IP)), + Reason]), State. + process_closed(State, Reason) -> stop_async(self()), State#{stop_reason => Reason}. + process_terminated(#{sid := SID, jid := JID, user := U, server := S, resource := R} = State, - Reason) -> + Reason) -> Status = format_reason(State, Reason), ?INFO_MSG("(~ts) Closing c2s session for ~ts: ~ts", - [case maps:find(socket, State) of - {ok, Socket} -> xmpp_socket:pp(Socket); - _ -> <<"unknown">> - end, jid:encode(JID), Status]), - Pres = #presence{type = unavailable, - from = JID, - to = jid:remove_resource(JID)}, + [case maps:find(socket, State) of + {ok, Socket} -> xmpp_socket:pp(Socket); + _ -> <<"unknown">> + end, + jid:encode(JID), + Status]), + Pres = #presence{ + type = unavailable, + from = JID, + to = jid:remove_resource(JID) + }, State1 = case maps:is_key(pres_last, State) of - true -> - ejabberd_sm:close_session_unset_presence(SID, U, S, R, - Status), - broadcast_presence_unavailable(State, Pres, true); - false -> - ejabberd_sm:close_session(SID, U, S, R), - broadcast_presence_unavailable(State, Pres, false) - end, + true -> + ejabberd_sm:close_session_unset_presence(SID, + U, + S, + R, + Status), + broadcast_presence_unavailable(State, Pres, true); + false -> + ejabberd_sm:close_session(SID, U, S, R), + broadcast_presence_unavailable(State, Pres, false) + end, bounce_message_queue(SID, JID), State1; process_terminated(#{stop_reason := {tls, _}} = State, Reason) -> ?WARNING_MSG("(~ts) Failed to secure c2s connection: ~ts", - [case maps:find(socket, State) of - {ok, Socket} -> xmpp_socket:pp(Socket); - _ -> <<"unknown">> - end, format_reason(State, Reason)]), + [case maps:find(socket, State) of + {ok, Socket} -> xmpp_socket:pp(Socket); + _ -> <<"unknown">> + end, + format_reason(State, Reason)]), State; process_terminated(State, _Reason) -> State. + %%%=================================================================== %%% xmpp_stream_in callbacks %%%=================================================================== -tls_options(#{lserver := LServer, tls_options := DefaultOpts, - stream_encrypted := Encrypted}) -> +tls_options(#{ + lserver := LServer, + tls_options := DefaultOpts, + stream_encrypted := Encrypted + }) -> TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of - {true, CertFile} when CertFile /= undefined -> DefaultOpts; - {_, _} -> - case ejabberd_pkix:get_certfile(LServer) of - error -> DefaultOpts; - {ok, CertFile} -> - lists:keystore(certfile, 1, DefaultOpts, - {certfile, CertFile}) - end - end, + {true, CertFile} when CertFile /= undefined -> DefaultOpts; + {_, _} -> + case ejabberd_pkix:get_certfile(LServer) of + error -> DefaultOpts; + {ok, CertFile} -> + lists:keystore(certfile, + 1, + DefaultOpts, + {certfile, CertFile}) + end + end, TLSOpts2 = case ejabberd_option:c2s_ciphers(LServer) of undefined -> TLSOpts1; - Ciphers -> lists:keystore(ciphers, 1, TLSOpts1, - {ciphers, Ciphers}) + Ciphers -> + lists:keystore(ciphers, + 1, + TLSOpts1, + {ciphers, Ciphers}) end, TLSOpts3 = case ejabberd_option:c2s_protocol_options(LServer) of undefined -> TLSOpts2; - ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2, - {protocol_options, ProtoOpts}) + ProtoOpts -> + lists:keystore(protocol_options, + 1, + TLSOpts2, + {protocol_options, ProtoOpts}) end, TLSOpts4 = case ejabberd_option:c2s_dhfile(LServer) of undefined -> TLSOpts3; - DHFile -> lists:keystore(dhfile, 1, TLSOpts3, - {dhfile, DHFile}) + DHFile -> + lists:keystore(dhfile, + 1, + TLSOpts3, + {dhfile, DHFile}) end, TLSOpts5 = case ejabberd_option:c2s_cafile(LServer) of - undefined -> TLSOpts4; - CAFile -> lists:keystore(cafile, 1, TLSOpts4, - {cafile, CAFile}) - end, + undefined -> TLSOpts4; + CAFile -> + lists:keystore(cafile, + 1, + TLSOpts4, + {cafile, CAFile}) + end, case ejabberd_option:c2s_tls_compression(LServer) of - undefined -> TLSOpts5; - false -> [compression_none | TLSOpts5]; - true -> lists:delete(compression_none, TLSOpts5) + undefined -> TLSOpts5; + false -> [compression_none | TLSOpts5]; + true -> lists:delete(compression_none, TLSOpts5) end. + tls_required(#{tls_required := TLSRequired}) -> TLSRequired. -tls_enabled(#{tls_enabled := TLSEnabled, - tls_required := TLSRequired, - tls_verify := TLSVerify}) -> + +tls_enabled(#{ + tls_enabled := TLSEnabled, + tls_required := TLSRequired, + tls_verify := TLSVerify + }) -> TLSEnabled or TLSRequired or TLSVerify. + allow_unencrypted_sasl2(#{allow_unencrypted_sasl2 := AllowUnencryptedSasl2}) -> AllowUnencryptedSasl2. + compress_methods(#{zlib := true}) -> [<<"zlib">>]; compress_methods(_) -> []. + unauthenticated_stream_features(#{lserver := LServer}) -> ejabberd_hooks:run_fold(c2s_pre_auth_features, LServer, [], [LServer]). + authenticated_stream_features(#{lserver := LServer}) -> ejabberd_hooks:run_fold(c2s_post_auth_features, LServer, [], [LServer]). + inline_stream_features(#{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_inline_features, LServer, {[], [], []}, [LServer, State]). + sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = State) -> Type = ejabberd_auth:store_type(LServer), Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer), {Digest, ShaAv, Sha256Av, Sha512Av} = - case ejabberd_option:auth_stored_password_types(LServer) of - [] -> - ScramHash = ejabberd_option:auth_scram_hash(LServer), - {Type == plain, - Type == plain orelse (Type == scram andalso ScramHash == sha), - Type == plain orelse (Type == scram andalso ScramHash == sha256), - Type == plain orelse (Type == scram andalso ScramHash == sha512)}; - Methods -> - HasPlain = lists:member(plain, Methods), - {HasPlain, - HasPlain orelse lists:member(scram_sha1, Methods), - HasPlain orelse lists:member(scram_sha256, Methods), - HasPlain orelse lists:member(scram_sha512, Methods)} - end, + case ejabberd_option:auth_stored_password_types(LServer) of + [] -> + ScramHash = ejabberd_option:auth_scram_hash(LServer), + {Type == plain, + Type == plain orelse (Type == scram andalso ScramHash == sha), + Type == plain orelse (Type == scram andalso ScramHash == sha256), + Type == plain orelse (Type == scram andalso ScramHash == sha512)}; + Methods -> + HasPlain = lists:member(plain, Methods), + {HasPlain, + HasPlain orelse lists:member(scram_sha1, Methods), + HasPlain orelse lists:member(scram_sha256, Methods), + HasPlain orelse lists:member(scram_sha512, Methods)} + end, %% I re-created it from cyrsasl ets magic, but I think it's wrong %% TODO: need to check before 18.09 release Mechs2 = lists:filter( - fun(<<"ANONYMOUS">>) -> - ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); - (<<"DIGEST-MD5">>) -> Digest; - (<<"SCRAM-SHA-1">>) -> ShaAv; - (<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted; - (<<"SCRAM-SHA-256">>) -> Sha256Av; - (<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted; - (<<"SCRAM-SHA-512">>) -> Sha512Av; - (<<"SCRAM-SHA-512-PLUS">>) -> Sha512Av andalso Encrypted; - (<<"PLAIN">>) -> true; - (<<"X-OAUTH2">>) -> [ejabberd_auth_anonymous] /= ejabberd_auth:auth_modules(LServer); - (<<"EXTERNAL">>) -> maps:get(tls_verify, State, false); - (_) -> false - end, Mechs -- Mechs1), + fun(<<"ANONYMOUS">>) -> + ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); + (<<"DIGEST-MD5">>) -> Digest; + (<<"SCRAM-SHA-1">>) -> ShaAv; + (<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted; + (<<"SCRAM-SHA-256">>) -> Sha256Av; + (<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted; + (<<"SCRAM-SHA-512">>) -> Sha512Av; + (<<"SCRAM-SHA-512-PLUS">>) -> Sha512Av andalso Encrypted; + (<<"PLAIN">>) -> true; + (<<"X-OAUTH2">>) -> [ejabberd_auth_anonymous] /= ejabberd_auth:auth_modules(LServer); + (<<"EXTERNAL">>) -> maps:get(tls_verify, State, false); + (_) -> false + end, + Mechs -- Mechs1), case ejabberd_option:auth_password_types_hidden_in_sasl1() of - [] -> Mechs2; - List -> - Mechs3 = lists:foldl( - fun(plain, Acc) -> Acc -- [<<"PLAIN">>]; - (scram_sha1, Acc) -> Acc -- [<<"SCRAM-SHA-1">>, <<"SCRAM-SHA-1-PLUS">>]; - (scram_sha256, Acc) -> Acc -- [<<"SCRAM-SHA-256">>, <<"SCRAM-SHA-256-PLUS">>]; - (scram_sha512, Acc) -> Acc -- [<<"SCRAM-SHA-512">>, <<"SCRAM-SHA-512-PLUS">>] - end, Mechs2, List), - {Mechs3, Mechs2} + [] -> Mechs2; + List -> + Mechs3 = lists:foldl( + fun(plain, Acc) -> Acc -- [<<"PLAIN">>]; + (scram_sha1, Acc) -> Acc -- [<<"SCRAM-SHA-1">>, <<"SCRAM-SHA-1-PLUS">>]; + (scram_sha256, Acc) -> Acc -- [<<"SCRAM-SHA-256">>, <<"SCRAM-SHA-256-PLUS">>]; + (scram_sha512, Acc) -> Acc -- [<<"SCRAM-SHA-512">>, <<"SCRAM-SHA-512-PLUS">>] + end, + Mechs2, + List), + {Mechs3, Mechs2} end. + sasl_options(#{lserver := LServer}) -> case ejabberd_option:disable_sasl_scram_downgrade_protection(LServer) of - true -> [{scram_downgrade_protection, false}]; - _ -> [] + true -> [{scram_downgrade_protection, false}]; + _ -> [] end. + get_password_fun(_Mech, #{lserver := LServer}) -> fun(U) -> - ejabberd_auth:get_password_with_authmodule(U, LServer) + ejabberd_auth:get_password_with_authmodule(U, LServer) end. + check_password_fun(<<"X-OAUTH2">>, #{lserver := LServer}) -> fun(User, _AuthzId, Token) -> - case ejabberd_oauth:check_token( - User, LServer, [<<"sasl_auth">>], Token) of - true -> {true, ejabberd_oauth}; - _ -> {false, ejabberd_oauth} - end + case ejabberd_oauth:check_token( + User, LServer, [<<"sasl_auth">>], Token) of + true -> {true, ejabberd_oauth}; + _ -> {false, ejabberd_oauth} + end end; check_password_fun(_Mech, #{lserver := LServer}) -> fun(U, AuthzId, P) -> - ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P) + ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P) end. + check_password_digest_fun(_Mech, #{lserver := LServer}) -> fun(U, AuthzId, P, D, DG) -> - ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG) + ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG) end. + get_fast_tokens_fun(_Mech, #{lserver := LServer}) -> fun(User, UA) -> - case gen_mod:is_loaded(LServer, mod_auth_fast) of - false -> false; - _ -> mod_auth_fast:get_tokens(LServer, User, UA) - end + case gen_mod:is_loaded(LServer, mod_auth_fast) of + false -> false; + _ -> mod_auth_fast:get_tokens(LServer, User, UA) + end end. + fast_mechanisms(#{lserver := LServer}) -> case gen_mod:is_loaded(LServer, mod_auth_fast) of - false -> []; - _ -> mod_auth_fast:get_mechanisms(LServer) + false -> []; + _ -> mod_auth_fast:get_mechanisms(LServer) end. -bind( - R, - #{ - user := U, - server := S, - lserver := LServer, - access := Access, - lang := Lang, - socket := Socket, - ip := IP - }=State -) -> + +bind(R, + #{ + user := U, + server := S, + lserver := LServer, + access := Access, + lang := Lang, + socket := Socket, + ip := IP + } = State) -> case ejabberd_hooks:run_fold(c2s_handle_bind, LServer, {R, {ok, State}}, []) of {R2, {ok, State2}} -> case resource_conflict_action(U, S, R2) of @@ -532,155 +721,200 @@ bind( case acl:match_rule(LServer, Access, #{usr => jid:split(JID), ip => IP}) of allow -> State3 = open_session( - State2#{resource => Resource, sid => ejabberd_sm:make_sid()} - ), + State2#{resource => Resource, sid => ejabberd_sm:make_sid()}), State4 = ejabberd_hooks:run_fold( - c2s_session_opened, LServer, State3, [] - ), + c2s_session_opened, LServer, State3, []), ?INFO_MSG( - "(~ts) Opened c2s session for ~ts", [xmpp_socket:pp(Socket), jid:encode(JID)] - ), + "(~ts) Opened c2s session for ~ts", [xmpp_socket:pp(Socket), jid:encode(JID)]), {ok, State4}; deny -> ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]), ?WARNING_MSG( - "(~ts) Forbidden c2s session for ~ts", - [xmpp_socket:pp(Socket), jid:encode(JID)] - ), + "(~ts) Forbidden c2s session for ~ts", + [xmpp_socket:pp(Socket), jid:encode(JID)]), Txt = ?T("Access denied by service policy"), {error, xmpp:err_not_allowed(Txt, Lang), State2} end end; - {R2, {error, XmppErr, _State2}=Err} -> + {R2, {error, XmppErr, _State2} = Err} -> case XmppErr of #stanza_error{reason = 'not-allowed'} -> JID = jid:make(U, S, R2), ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]), ?WARNING_MSG( - "(~ts) Forbidden c2s session for ~ts", - [xmpp_socket:pp(Socket), jid:encode(JID)] - ); + "(~ts) Forbidden c2s session for ~ts", + [xmpp_socket:pp(Socket), jid:encode(JID)]); _ -> ok end, Err end. + handle_stream_start(StreamStart, #{lserver := LServer} = State) -> case ejabberd_router:is_my_host(LServer) of - false -> - send(State#{lserver => ejabberd_config:get_myname()}, xmpp:serr_host_unknown()); - true -> - State1 = change_shaper(State), - Opts = ejabberd_config:codec_options(), - State2 = State1#{codec_options => Opts}, - ejabberd_hooks:run_fold( - c2s_stream_started, LServer, State2, [StreamStart]) + false -> + send(State#{lserver => ejabberd_config:get_myname()}, xmpp:serr_host_unknown()); + true -> + State1 = change_shaper(State), + Opts = ejabberd_config:codec_options(), + State2 = State1#{codec_options => Opts}, + ejabberd_hooks:run_fold( + c2s_stream_started, LServer, State2, [StreamStart]) end. + handle_stream_end(Reason, #{lserver := LServer} = State) -> State1 = State#{stop_reason => Reason}, ejabberd_hooks:run_fold(c2s_closed, LServer, State1, [Reason]). -handle_auth_success(User, _Mech, AuthModule, - #{lserver := LServer} = State) -> + +handle_auth_success(User, + _Mech, + AuthModule, + #{lserver := LServer} = State) -> State1 = State#{auth_module => AuthModule}, ejabberd_hooks:run_fold(c2s_auth_result, LServer, State1, [true, User]). -handle_auth_failure(User, _Mech, Reason, - #{lserver := LServer} = State) -> + +handle_auth_failure(User, + _Mech, + Reason, + #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_auth_result, LServer, State, [{false, Reason}, User]). + handle_unbinded_packet(Pkt, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_unbinded_packet, LServer, State, [Pkt]). + handle_unauthenticated_packet(Pkt, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_unauthenticated_packet, LServer, State, [Pkt]). + handle_authenticated_packet(Pkt, #{lserver := LServer} = State) when not ?is_stanza(Pkt) -> ejabberd_hooks:run_fold(c2s_authenticated_packet, - LServer, State, [Pkt]); -handle_authenticated_packet(Pkt, #{lserver := LServer, jid := JID, - ip := {IP, _}} = State) -> + LServer, + State, + [Pkt]); +handle_authenticated_packet(Pkt, + #{ + lserver := LServer, + jid := JID, + ip := {IP, _} + } = State) -> Pkt1 = xmpp:put_meta(Pkt, ip, IP), State1 = ejabberd_hooks:run_fold(c2s_authenticated_packet, - LServer, State, [Pkt1]), + LServer, + State, + [Pkt1]), #jid{luser = LUser} = JID, {Pkt2, State2} = ejabberd_hooks:run_fold( - user_send_packet, LServer, {Pkt1, State1}, []), + user_send_packet, LServer, {Pkt1, State1}, []), case Pkt2 of - drop -> - State2; - #iq{type = set, sub_els = [_]} -> - try xmpp:try_subtag(Pkt2, #xmpp_session{}) of - #xmpp_session{} -> - % It seems that some client are expecting to have response - % to session request be sent from server jid, let's make - % sure it is that. - Pkt3 = xmpp:set_to(Pkt2, jid:make(<<>>, LServer, <<>>)), - send(State2, xmpp:make_iq_result(Pkt3)); - _ -> - check_privacy_then_route(State2, Pkt2) - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Lang = maps:get(lang, State), - Err = xmpp:err_bad_request(Txt, Lang), - send_error(State2, Pkt2, Err) - end; - #presence{to = #jid{luser = LUser, lserver = LServer, - lresource = <<"">>}} -> - process_self_presence(State2, Pkt2); - #presence{} -> - process_presence_out(State2, Pkt2); - _ -> - check_privacy_then_route(State2, Pkt2) + drop -> + State2; + #iq{type = set, sub_els = [_]} -> + try xmpp:try_subtag(Pkt2, #xmpp_session{}) of + #xmpp_session{} -> + % It seems that some client are expecting to have response + % to session request be sent from server jid, let's make + % sure it is that. + Pkt3 = xmpp:set_to(Pkt2, jid:make(<<>>, LServer, <<>>)), + send(State2, xmpp:make_iq_result(Pkt3)); + _ -> + check_privacy_then_route(State2, Pkt2) + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Lang = maps:get(lang, State), + Err = xmpp:err_bad_request(Txt, Lang), + send_error(State2, Pkt2, Err) + end; + #presence{ + to = #jid{ + luser = LUser, + lserver = LServer, + lresource = <<"">> + } + } -> + process_self_presence(State2, Pkt2); + #presence{} -> + process_presence_out(State2, Pkt2); + _ -> + check_privacy_then_route(State2, Pkt2) end. + handle_cdata(Data, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_cdata, LServer, - State, [Data]). + ejabberd_hooks:run_fold(c2s_handle_cdata, + LServer, + State, + [Data]). + handle_sasl2_inline(Els, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_sasl2_inline, LServer, - {State, Els, []}, []). + ejabberd_hooks:run_fold(c2s_handle_sasl2_inline, + LServer, + {State, Els, []}, + []). + handle_sasl2_inline_post(Els, Results, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_sasl2_inline_post, LServer, - State, [Els, Results]). + ejabberd_hooks:run_fold(c2s_handle_sasl2_inline_post, + LServer, + State, + [Els, Results]). + handle_bind2_inline(Els, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_bind2_inline, LServer, - {State, Els, []}, []). + ejabberd_hooks:run_fold(c2s_handle_bind2_inline, + LServer, + {State, Els, []}, + []). + handle_bind2_inline_post(Els, Results, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_bind2_inline_post, LServer, - State, [Els, Results]). + ejabberd_hooks:run_fold(c2s_handle_bind2_inline_post, + LServer, + State, + [Els, Results]). + handle_sasl2_task_next(Task, Els, InlineEls, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_sasl2_task_next, LServer, - {abort, State}, [Task, Els, InlineEls]). + ejabberd_hooks:run_fold(c2s_handle_sasl2_task_next, + LServer, + {abort, State}, + [Task, Els, InlineEls]). + handle_sasl2_task_data(Els, InlineEls, #{lserver := LServer} = State) -> - ejabberd_hooks:run_fold(c2s_handle_sasl2_task_data, LServer, - {abort, State}, [Els, InlineEls]). + ejabberd_hooks:run_fold(c2s_handle_sasl2_task_data, + LServer, + {abort, State}, + [Els, InlineEls]). + handle_recv(El, Pkt, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_recv, LServer, State, [El, Pkt]). + handle_send(Pkt, Result, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_send, LServer, State, [Pkt, Result]). + init([State, Opts]) -> Access = proplists:get_value(access, Opts, all), Shaper = proplists:get_value(shaper, Opts, none), TLSOpts1 = lists:filter( - fun({certfile, _}) -> true; - ({ciphers, _}) -> true; - ({dhfile, _}) -> true; - ({cafile, _}) -> true; - ({protocol_options, _}) -> true; - (_) -> false - end, Opts), + fun({certfile, _}) -> true; + ({ciphers, _}) -> true; + ({dhfile, _}) -> true; + ({cafile, _}) -> true; + ({protocol_options, _}) -> true; + (_) -> false + end, + Opts), TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of false -> [compression_none | TLSOpts1]; true -> TLSOpts1 @@ -691,29 +925,32 @@ init([State, Opts]) -> AllowUnencryptedSasl2 = proplists:get_bool(allow_unencrypted_sasl2, Opts), Zlib = proplists:get_bool(zlib, Opts), Timeout = ejabberd_option:negotiation_timeout(), - State1 = State#{tls_options => TLSOpts2, - tls_required => TLSRequired, - tls_enabled => TLSEnabled, - tls_verify => TLSVerify, - allow_unencrypted_sasl2 => AllowUnencryptedSasl2, - pres_a => ?SETS:new(), - zlib => Zlib, - lang => ejabberd_option:language(), - server => ejabberd_config:get_myname(), - lserver => ejabberd_config:get_myname(), - access => Access, - shaper => Shaper}, + State1 = State#{ + tls_options => TLSOpts2, + tls_required => TLSRequired, + tls_enabled => TLSEnabled, + tls_verify => TLSVerify, + allow_unencrypted_sasl2 => AllowUnencryptedSasl2, + pres_a => ?SETS:new(), + zlib => Zlib, + lang => ejabberd_option:language(), + server => ejabberd_config:get_myname(), + lserver => ejabberd_config:get_myname(), + access => Access, + shaper => Shaper + }, State2 = xmpp_stream_in:set_timeout(State1, Timeout), misc:set_proc_label({?MODULE, init_state}), ejabberd_hooks:run_fold(c2s_init, {ok, State2}, [Opts]). + handle_call(get_presence, From, #{jid := JID} = State) -> Pres = case maps:get(pres_last, State, error) of - error -> - BareJID = jid:remove_resource(JID), - #presence{from = JID, to = BareJID, type = unavailable}; - P -> P - end, + error -> + BareJID = jid:remove_resource(JID), + #presence{from = JID, to = BareJID, type = unavailable}; + P -> P + end, reply(From, Pres), State; handle_call({set_presence, Pres}, From, State) -> @@ -723,31 +960,37 @@ handle_call(Request, From, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold( c2s_handle_call, LServer, State, [Request, From]). + handle_cast(Msg, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_cast, LServer, State, [Msg]). + handle_info(Info, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_info, LServer, State, [Info]). + terminate(Reason, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_terminated, LServer, State, [Reason]). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec process_iq_in(state(), iq()) -> {boolean(), state()}. process_iq_in(State, #iq{} = IQ) -> case privacy_check_packet(State, IQ, in) of - allow -> - {true, State}; - deny -> - ejabberd_router:route_error(IQ, xmpp:err_service_unavailable()), - {false, State} + allow -> + {true, State}; + deny -> + ejabberd_router:route_error(IQ, xmpp:err_service_unavailable()), + {false, State} end. + -spec process_message_in(state(), message()) -> {boolean(), state()}. process_message_in(State, #message{type = T} = Msg) -> %% This function should be as simple as process_iq_in/2, @@ -756,367 +999,441 @@ process_message_in(State, #message{type = T} = Msg) -> %% most likely means having only some particular participant %% blocked, i.e. room@conference.server.org/participant. case privacy_check_packet(State, Msg, in) of - allow -> - {true, State}; - deny when T == groupchat; T == headline -> - {false, State}; - deny -> - case xmpp:has_subtag(Msg, #muc_user{}) of - true -> - ok; - false -> - ejabberd_router:route_error( - Msg, xmpp:err_service_unavailable()) - end, - {false, State} + allow -> + {true, State}; + deny when T == groupchat; T == headline -> + {false, State}; + deny -> + case xmpp:has_subtag(Msg, #muc_user{}) of + true -> + ok; + false -> + ejabberd_router:route_error( + Msg, xmpp:err_service_unavailable()) + end, + {false, State} end. + -spec process_presence_in(state(), presence()) -> {boolean(), state()}. process_presence_in(#{lserver := LServer, pres_a := PresA} = State0, - #presence{from = From, type = T} = Pres) -> + #presence{from = From, type = T} = Pres) -> State = ejabberd_hooks:run_fold(c2s_presence_in, LServer, State0, [Pres]), case T of - probe -> - route_probe_reply(From, State), - {false, State}; - error -> - A = ?SETS:del_element(jid:tolower(From), PresA), - {true, State#{pres_a => A}}; - _ -> - case privacy_check_packet(State, Pres, in) of - allow -> - {true, State}; - deny -> - {false, State} - end + probe -> + route_probe_reply(From, State), + {false, State}; + error -> + A = ?SETS:del_element(jid:tolower(From), PresA), + {true, State#{pres_a => A}}; + _ -> + case privacy_check_packet(State, Pres, in) of + allow -> + {true, State}; + deny -> + {false, State} + end end. + -spec route_probe_reply(jid(), state()) -> ok. -route_probe_reply(From, #{jid := To, - pres_last := LastPres, - pres_timestamp := TS} = State) -> +route_probe_reply(From, + #{ + jid := To, + pres_last := LastPres, + pres_timestamp := TS + } = State) -> {LUser, LServer, LResource} = jid:tolower(To), IsAnotherResource = case jid:tolower(From) of - {LUser, LServer, R} when R /= LResource -> true; - _ -> false - end, + {LUser, LServer, R} when R /= LResource -> true; + _ -> false + end, Subscription = get_subscription(To, From), - if IsAnotherResource orelse - Subscription == both orelse Subscription == from -> - Packet = xmpp:set_from_to(LastPres, To, From), - Packet2 = misc:add_delay_info(Packet, To, TS), - case privacy_check_packet(State, Packet2, out) of - deny -> - ok; - allow -> - ejabberd_hooks:run(presence_probe_hook, - LServer, - [From, To, self()]), - ejabberd_router:route(Packet2) - end; - true -> - ok + if + IsAnotherResource orelse + Subscription == both orelse Subscription == from -> + Packet = xmpp:set_from_to(LastPres, To, From), + Packet2 = misc:add_delay_info(Packet, To, TS), + case privacy_check_packet(State, Packet2, out) of + deny -> + ok; + allow -> + ejabberd_hooks:run(presence_probe_hook, + LServer, + [From, To, self()]), + ejabberd_router:route(Packet2) + end; + true -> + ok end; route_probe_reply(_, _) -> ok. + -spec process_presence_out(state(), presence()) -> state(). -process_presence_out(#{lserver := LServer, jid := JID, - lang := Lang, pres_a := PresA} = State0, - #presence{from = From, to = To, type = Type} = Pres) -> +process_presence_out(#{ + lserver := LServer, + jid := JID, + lang := Lang, + pres_a := PresA + } = State0, + #presence{from = From, to = To, type = Type} = Pres) -> State1 = - if Type == subscribe; Type == subscribed; - Type == unsubscribe; Type == unsubscribed -> - Access = mod_roster_opt:access(LServer), - MyBareJID = jid:remove_resource(JID), - case acl:match_rule(LServer, Access, MyBareJID) of - deny -> - AccessErrTxt = ?T("Access denied by service policy"), - AccessErr = xmpp:err_forbidden(AccessErrTxt, Lang), - send_error(State0, Pres, AccessErr); - allow -> - ejabberd_hooks:run(roster_out_subscription, LServer, [Pres]), - State0 - end; - true -> - State0 - end, + if + Type == subscribe; + Type == subscribed; + Type == unsubscribe; + Type == unsubscribed -> + Access = mod_roster_opt:access(LServer), + MyBareJID = jid:remove_resource(JID), + case acl:match_rule(LServer, Access, MyBareJID) of + deny -> + AccessErrTxt = ?T("Access denied by service policy"), + AccessErr = xmpp:err_forbidden(AccessErrTxt, Lang), + send_error(State0, Pres, AccessErr); + allow -> + ejabberd_hooks:run(roster_out_subscription, LServer, [Pres]), + State0 + end; + true -> + State0 + end, case privacy_check_packet(State1, Pres, out) of - deny -> - PrivErrTxt = ?T("Your active privacy list has denied " - "the routing of this stanza."), - PrivErr = xmpp:err_not_acceptable(PrivErrTxt, Lang), - send_error(State1, Pres, PrivErr); - allow when Type == subscribe; Type == subscribed; - Type == unsubscribe; Type == unsubscribed -> - BareFrom = jid:remove_resource(From), - ejabberd_router:route(xmpp:set_from_to(Pres, BareFrom, To)), - State1; - allow when Type == error; Type == probe -> - ejabberd_router:route(Pres), - State1; - allow -> - ejabberd_router:route(Pres), - LTo = jid:tolower(To), - LBareTo = jid:remove_resource(LTo), - LBareFrom = jid:remove_resource(jid:tolower(From)), - if LBareTo /= LBareFrom -> - Subscription = get_subscription(From, To), - if Subscription /= both andalso Subscription /= from -> - A = case Type of - available -> ?SETS:add_element(LTo, PresA); - unavailable -> ?SETS:del_element(LTo, PresA) - end, - State1#{pres_a => A}; - true -> - State1 - end; - true -> - State1 - end + deny -> + PrivErrTxt = ?T("Your active privacy list has denied " + "the routing of this stanza."), + PrivErr = xmpp:err_not_acceptable(PrivErrTxt, Lang), + send_error(State1, Pres, PrivErr); + allow when Type == subscribe; + Type == subscribed; + Type == unsubscribe; + Type == unsubscribed -> + BareFrom = jid:remove_resource(From), + ejabberd_router:route(xmpp:set_from_to(Pres, BareFrom, To)), + State1; + allow when Type == error; Type == probe -> + ejabberd_router:route(Pres), + State1; + allow -> + ejabberd_router:route(Pres), + LTo = jid:tolower(To), + LBareTo = jid:remove_resource(LTo), + LBareFrom = jid:remove_resource(jid:tolower(From)), + if + LBareTo /= LBareFrom -> + Subscription = get_subscription(From, To), + if + Subscription /= both andalso Subscription /= from -> + A = case Type of + available -> ?SETS:add_element(LTo, PresA); + unavailable -> ?SETS:del_element(LTo, PresA) + end, + State1#{pres_a => A}; + true -> + State1 + end; + true -> + State1 + end end. + -spec process_self_presence(state(), presence()) -> state(). -process_self_presence(#{lserver := LServer, sid := SID, - user := U, server := S, resource := R} = State, - #presence{type = unavailable} = Pres) -> +process_self_presence(#{ + lserver := LServer, + sid := SID, + user := U, + server := S, + resource := R + } = State, + #presence{type = unavailable} = Pres) -> Status = xmpp:get_text(Pres#presence.status), _ = ejabberd_sm:unset_presence(SID, U, S, R, Status), {Pres1, State1} = ejabberd_hooks:run_fold( - c2s_self_presence, LServer, {Pres, State}, []), + c2s_self_presence, LServer, {Pres, State}, []), State2 = broadcast_presence_unavailable(State1, Pres1, true), maps:remove(pres_last, maps:remove(pres_timestamp, State2)); process_self_presence(#{lserver := LServer} = State, - #presence{type = available} = Pres) -> + #presence{type = available} = Pres) -> PreviousPres = maps:get(pres_last, State, undefined), _ = update_priority(State, Pres), {Pres1, State1} = ejabberd_hooks:run_fold( - c2s_self_presence, LServer, {Pres, State}, []), - State2 = State1#{pres_last => Pres1, - pres_timestamp => erlang:timestamp()}, + c2s_self_presence, LServer, {Pres, State}, []), + State2 = State1#{ + pres_last => Pres1, + pres_timestamp => erlang:timestamp() + }, FromUnavailable = PreviousPres == undefined, broadcast_presence_available(State2, Pres1, FromUnavailable); process_self_presence(State, _Pres) -> State. + -spec update_priority(state(), presence()) -> ok | {error, notfound}. update_priority(#{sid := SID, user := U, server := S, resource := R}, - Pres) -> + Pres) -> Priority = get_priority_from_presence(Pres), ejabberd_sm:set_presence(SID, U, S, R, Priority, Pres). + -spec broadcast_presence_unavailable(state(), presence(), boolean()) -> state(). -broadcast_presence_unavailable(#{jid := JID, pres_a := PresA} = State, Pres, - BroadcastToRoster) -> +broadcast_presence_unavailable(#{jid := JID, pres_a := PresA} = State, + Pres, + BroadcastToRoster) -> #jid{luser = LUser, lserver = LServer} = JID, BareJID = jid:tolower(jid:remove_resource(JID)), Items1 = case BroadcastToRoster of - true -> - Roster = ejabberd_hooks:run_fold(roster_get, LServer, - [], [{LUser, LServer}]), - lists:foldl( - fun(#roster_item{jid = ItemJID, subscription = Sub}, Acc) - when Sub == both; Sub == from -> - maps:put(jid:tolower(ItemJID), 1, Acc); - (_, Acc) -> - Acc - end, #{BareJID => 1}, Roster); - _ -> - #{BareJID => 1} - end, + true -> + Roster = ejabberd_hooks:run_fold(roster_get, + LServer, + [], + [{LUser, LServer}]), + lists:foldl( + fun(#roster_item{jid = ItemJID, subscription = Sub}, Acc) + when Sub == both; Sub == from -> + maps:put(jid:tolower(ItemJID), 1, Acc); + (_, Acc) -> + Acc + end, + #{BareJID => 1}, + Roster); + _ -> + #{BareJID => 1} + end, Items2 = ?SETS:fold( - fun(LJID, Acc) -> - maps:put(LJID, 1, Acc) - end, Items1, PresA), + fun(LJID, Acc) -> + maps:put(LJID, 1, Acc) + end, + Items1, + PresA), JIDs = lists:filtermap( - fun(LJid) -> - To = jid:make(LJid), - P = xmpp:set_to(Pres, To), - case privacy_check_packet(State, P, out) of - allow -> {true, To}; - deny -> false - end - end, maps:keys(Items2)), + fun(LJid) -> + To = jid:make(LJid), + P = xmpp:set_to(Pres, To), + case privacy_check_packet(State, P, out) of + allow -> {true, To}; + deny -> false + end + end, + maps:keys(Items2)), route_multiple(State, JIDs, Pres), State#{pres_a => ?SETS:new()}. + -spec broadcast_presence_available(state(), presence(), boolean()) -> state(). broadcast_presence_available(#{jid := JID} = State, - Pres, _FromUnavailable = true) -> + Pres, + _FromUnavailable = true) -> Probe = #presence{from = JID, type = probe}, #jid{luser = LUser, lserver = LServer} = JID, BareJID = jid:remove_resource(JID), - Items = ejabberd_hooks:run_fold(roster_get, LServer, - [], [{LUser, LServer}]), + Items = ejabberd_hooks:run_fold(roster_get, + LServer, + [], + [{LUser, LServer}]), {FJIDs, TJIDs} = - lists:foldl( - fun(#roster_item{jid = To, subscription = Sub}, {F, T}) -> - F1 = if Sub == both orelse Sub == from -> - Pres1 = xmpp:set_to(Pres, To), - case privacy_check_packet(State, Pres1, out) of - allow -> [To|F]; - deny -> F - end; - true -> F - end, - T1 = if Sub == both orelse Sub == to -> - Probe1 = xmpp:set_to(Probe, To), - case privacy_check_packet(State, Probe1, out) of - allow -> [To|T]; - deny -> T - end; - true -> T - end, - {F1, T1} - end, {[BareJID], [BareJID]}, Items), + lists:foldl( + fun(#roster_item{jid = To, subscription = Sub}, {F, T}) -> + F1 = if + Sub == both orelse Sub == from -> + Pres1 = xmpp:set_to(Pres, To), + case privacy_check_packet(State, Pres1, out) of + allow -> [To | F]; + deny -> F + end; + true -> F + end, + T1 = if + Sub == both orelse Sub == to -> + Probe1 = xmpp:set_to(Probe, To), + case privacy_check_packet(State, Probe1, out) of + allow -> [To | T]; + deny -> T + end; + true -> T + end, + {F1, T1} + end, + {[BareJID], [BareJID]}, + Items), route_multiple(State, TJIDs, Probe), route_multiple(State, FJIDs, Pres), State; broadcast_presence_available(#{jid := JID} = State, - Pres, _FromUnavailable = false) -> + Pres, + _FromUnavailable = false) -> #jid{luser = LUser, lserver = LServer} = JID, BareJID = jid:remove_resource(JID), Items = ejabberd_hooks:run_fold( - roster_get, LServer, [], [{LUser, LServer}]), + roster_get, LServer, [], [{LUser, LServer}]), JIDs = lists:foldl( - fun(#roster_item{jid = To, subscription = Sub}, Tos) - when Sub == both orelse Sub == from -> - P = xmpp:set_to(Pres, To), - case privacy_check_packet(State, P, out) of - allow -> [To|Tos]; - deny -> Tos - end; - (_, Tos) -> - Tos - end, [BareJID], Items), + fun(#roster_item{jid = To, subscription = Sub}, Tos) + when Sub == both orelse Sub == from -> + P = xmpp:set_to(Pres, To), + case privacy_check_packet(State, P, out) of + allow -> [To | Tos]; + deny -> Tos + end; + (_, Tos) -> + Tos + end, + [BareJID], + Items), route_multiple(State, JIDs, Pres), State. + -spec check_privacy_then_route(state(), stanza()) -> state(). check_privacy_then_route(#{lang := Lang} = State, Pkt) -> case privacy_check_packet(State, Pkt, out) of deny -> ErrText = ?T("Your active privacy list has denied " - "the routing of this stanza."), - Err = xmpp:err_not_acceptable(ErrText, Lang), - send_error(State, Pkt, Err); + "the routing of this stanza."), + Err = xmpp:err_not_acceptable(ErrText, Lang), + send_error(State, Pkt, Err); allow -> - ejabberd_router:route(Pkt), - State + ejabberd_router:route(Pkt), + State end. + -spec privacy_check_packet(state(), stanza(), in | out) -> allow | deny. privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) -> ejabberd_hooks:run_fold(privacy_check_packet, LServer, allow, [State, Pkt, Dir]). + -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of - undefined -> 0; - _ -> Prio + undefined -> 0; + _ -> Prio end. + -spec route_multiple(state(), [jid()], stanza()) -> ok. route_multiple(#{lserver := LServer}, JIDs, Pkt) -> From = xmpp:get_from(Pkt), ejabberd_router_multicast:route_multicast(From, LServer, JIDs, Pkt, false). + get_subscription(#jid{luser = LUser, lserver = LServer}, JID) -> {Subscription, _, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, {none, none, []}, - [LUser, LServer, JID]), + roster_get_jid_info, + LServer, + {none, none, []}, + [LUser, LServer, JID]), Subscription. + -spec resource_conflict_action(binary(), binary(), binary()) -> - {accept_resource, binary()} | closenew. + {accept_resource, binary()} | closenew. resource_conflict_action(U, S, R) -> OptionRaw = case ejabberd_sm:is_existing_resource(U, S, R) of - true -> - ejabberd_option:resource_conflict(S); - false -> - acceptnew - end, + true -> + ejabberd_option:resource_conflict(S); + false -> + acceptnew + end, Option = case OptionRaw of - setresource -> setresource; - closeold -> acceptnew; %% ejabberd_sm will close old session - closenew -> closenew; - acceptnew -> acceptnew - end, + setresource -> setresource; + closeold -> acceptnew; %% ejabberd_sm will close old session + closenew -> closenew; + acceptnew -> acceptnew + end, case Option of - acceptnew -> {accept_resource, R}; - closenew -> closenew; - setresource -> - Rnew = new_uniq_id(), - {accept_resource, Rnew} + acceptnew -> {accept_resource, R}; + closenew -> closenew; + setresource -> + Rnew = new_uniq_id(), + {accept_resource, Rnew} end. + -spec bounce_message_queue(ejabberd_sm:sid(), jid:jid()) -> ok. bounce_message_queue({_, Pid} = SID, JID) -> {U, S, R} = jid:tolower(JID), SIDs = ejabberd_sm:get_session_sids(U, S, R), case lists:member(SID, SIDs) of - true -> - ?WARNING_MSG("The session for ~ts@~ts/~ts is supposed to " - "be unregistered, but session identifier ~p " - "still presents in the 'session' table", - [U, S, R, Pid]); - false -> - receive {route, Pkt} -> - ejabberd_router:route(Pkt), - bounce_message_queue(SID, JID) - after 100 -> - ok - end + true -> + ?WARNING_MSG("The session for ~ts@~ts/~ts is supposed to " + "be unregistered, but session identifier ~p " + "still presents in the 'session' table", + [U, S, R, Pid]); + false -> + receive + {route, Pkt} -> + ejabberd_router:route(Pkt), + bounce_message_queue(SID, JID) + after + 100 -> + ok + end end. + -spec new_uniq_id() -> binary(). new_uniq_id() -> iolist_to_binary( [p1_rand:get_string(), integer_to_binary(erlang:unique_integer([positive]))]). --spec get_conn_type(state()) -> c2s | c2s_tls | c2s_compressed | websocket | - c2s_compressed_tls | http_bind. + +-spec get_conn_type(state()) -> c2s | + c2s_tls | + c2s_compressed | + websocket | + c2s_compressed_tls | + http_bind. get_conn_type(State) -> case xmpp_stream_in:get_transport(State) of - tcp -> c2s; - tls -> c2s_tls; - tcp_zlib -> c2s_compressed; - tls_zlib -> c2s_compressed_tls; - http_bind -> http_bind; - websocket -> websocket + tcp -> c2s; + tls -> c2s_tls; + tcp_zlib -> c2s_compressed; + tls_zlib -> c2s_compressed_tls; + http_bind -> http_bind; + websocket -> websocket end. + -spec fix_from_to(xmpp_element(), state()) -> stanza() | xmpp_element(). fix_from_to(Pkt, #{jid := JID}) when ?is_stanza(Pkt) -> #jid{luser = U, lserver = S, lresource = R} = JID, case xmpp:get_from(Pkt) of - undefined -> - Pkt; - From -> - From1 = case jid:tolower(From) of - {U, S, R} -> JID; - {U, S, _} -> jid:replace_resource(JID, From#jid.resource); - _ -> From - end, - To1 = case xmpp:get_to(Pkt) of - #jid{lresource = <<>>} = To2 -> To2; - _ -> JID - end, - xmpp:set_from_to(Pkt, From1, To1) + undefined -> + Pkt; + From -> + From1 = case jid:tolower(From) of + {U, S, R} -> JID; + {U, S, _} -> jid:replace_resource(JID, From#jid.resource); + _ -> From + end, + To1 = case xmpp:get_to(Pkt) of + #jid{lresource = <<>>} = To2 -> To2; + _ -> JID + end, + xmpp:set_from_to(Pkt, From1, To1) end; fix_from_to(Pkt, _State) -> Pkt. + -spec change_shaper(state()) -> state(). -change_shaper(#{shaper := ShaperName, ip := {IP, _}, lserver := LServer, - user := U, server := S, resource := R} = State) -> +change_shaper(#{ + shaper := ShaperName, + ip := {IP, _}, + lserver := LServer, + user := U, + server := S, + resource := R + } = State) -> JID = jid:make(U, S, R), - Shaper = ejabberd_shaper:match(LServer, ShaperName, - #{usr => jid:split(JID), ip => IP}), + Shaper = ejabberd_shaper:match(LServer, + ShaperName, + #{usr => jid:split(JID), ip => IP}), xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)). + -spec format_reason(state(), term()) -> binary(). format_reason(#{stop_reason := Reason}, _) -> xmpp_stream_in:format_error(Reason); @@ -1129,6 +1446,7 @@ format_reason(_, {shutdown, _}) -> format_reason(_, _) -> <<"internal server error">>. + listen_opt_type(starttls) -> econf:bool(); listen_opt_type(starttls_required) -> @@ -1141,11 +1459,12 @@ listen_opt_type(zlib) -> econf:and_then( econf:bool(), fun(false) -> false; - (true) -> - ejabberd:start_app(ezlib), - true + (true) -> + ejabberd:start_app(ezlib), + true end). + listen_options() -> [{access, all}, {shaper, none}, diff --git a/src/ejabberd_c2s_config.erl b/src/ejabberd_c2s_config.erl index 0c80ebec9..38d482799 100644 --- a/src/ejabberd_c2s_config.erl +++ b/src/ejabberd_c2s_config.erl @@ -30,23 +30,27 @@ -export([get_c2s_limits/0]). + %% Get first c2s configuration limitations to apply it to other c2s %% connectors. get_c2s_limits() -> C2SFirstListen = ejabberd_option:listen(), case lists:keysearch(ejabberd_c2s, 2, C2SFirstListen) of - false -> []; - {value, {_Port, ejabberd_c2s, Opts}} -> - select_opts_values(Opts) + false -> []; + {value, {_Port, ejabberd_c2s, Opts}} -> + select_opts_values(Opts) end. + %% Only get access, shaper and max_stanza_size values select_opts_values(Opts) -> maps:fold( fun(Opt, Val, Acc) when Opt == access; - Opt == shaper; - Opt == max_stanza_size -> - [{Opt, Val}|Acc]; - (_, _, Acc) -> - Acc - end, [], Opts). + Opt == shaper; + Opt == max_stanza_size -> + [{Opt, Val} | Acc]; + (_, _, Acc) -> + Acc + end, + [], + Opts). diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index d1d62e59b..5538eee03 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -34,195 +34,262 @@ -export([start_link/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). --export([create_captcha/6, build_captcha_html/2, - check_captcha/2, process_reply/1, process/2, - is_feature_available/0, create_captcha_x/5, - host_up/1, host_down/1, - config_reloaded/0, process_iq/1]). +-export([create_captcha/6, + build_captcha_html/2, + check_captcha/2, + process_reply/1, + process/2, + is_feature_available/0, + create_captcha_x/5, + host_up/1, + host_down/1, + config_reloaded/0, + process_iq/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("translate.hrl"). -define(CAPTCHA_LIFETIME, 120000). --define(LIMIT_PERIOD, 60*1000*1000). +-define(LIMIT_PERIOD, 60 * 1000 * 1000). -type image_error() :: efbig | enodata | limit | malformed_image | timeout. -type priority() :: neg_integer(). -type callback() :: fun((captcha_succeed | captcha_failed) -> any()). --record(state, {limits = treap:empty() :: treap:treap(), - enabled = false :: boolean()}). +-record(state, { + limits = treap:empty() :: treap:treap(), + enabled = false :: boolean() + }). + +-record(captcha, { + id :: binary(), + pid :: pid() | undefined, + key :: binary(), + tref :: reference(), + args :: any() + }). --record(captcha, {id :: binary(), - pid :: pid() | undefined, - key :: binary(), - tref :: reference(), - args :: any()}). start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], - []). + gen_server:start_link({local, ?MODULE}, + ?MODULE, + [], + []). + -spec captcha_text(binary()) -> binary(). captcha_text(Lang) -> translate:translate(Lang, ?T("Enter the text you see")). + -spec mk_ocr_field(binary(), binary(), binary()) -> xdata_field(). mk_ocr_field(Lang, CID, Type) -> URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>}, [_, F] = captcha_form:encode([{ocr, <<>>}], Lang, [ocr]), xmpp:set_els(F, [#media{uri = [URI]}]). + update_captcha_key(_Id, Key, Key) -> ok; update_captcha_key(Id, _Key, Key2) -> true = ets:update_element(captcha, Id, [{4, Key2}]). --spec create_captcha(binary(), jid(), jid(), - binary(), any(), - callback() | term()) -> {error, image_error()} | - {ok, binary(), [text()], [xmpp_element()]}. + +-spec create_captcha(binary(), + jid(), + jid(), + binary(), + any(), + callback() | term()) -> {error, image_error()} | + {ok, binary(), [text()], [xmpp_element()]}. create_captcha(SID, From, To, Lang, Limiter, Args) -> case create_image(Limiter) of - {ok, Type, Key, Image} -> - Id = <<(p1_rand:get_string())/binary>>, - JID = jid:encode(From), - CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, - Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, - Fs = captcha_form:encode( - [{from, To}, {challenge, Id}, {sid, SID}, - mk_ocr_field(Lang, CID, Type)], - Lang, [challenge]), - X = #xdata{type = form, fields = Fs}, - Captcha = #xcaptcha{xdata = X}, - BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. " - "To unblock your subscription request, visit ~s"), [JID, get_url(Id)]}, - Body = xmpp:mk_text(BodyString, Lang), - OOB = #oob_x{url = get_url(Id)}, - Hint = #hint{type = 'no-store'}, - Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), - ets:insert(captcha, - #captcha{id = Id, pid = self(), key = Key, tref = Tref, - args = Args}), - {ok, Id, Body, [Hint, OOB, Captcha, Data]}; - Err -> Err + {ok, Type, Key, Image} -> + Id = <<(p1_rand:get_string())/binary>>, + JID = jid:encode(From), + CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, + Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, + Fs = captcha_form:encode( + [{from, To}, + {challenge, Id}, + {sid, SID}, + mk_ocr_field(Lang, CID, Type)], + Lang, + [challenge]), + X = #xdata{type = form, fields = Fs}, + Captcha = #xcaptcha{xdata = X}, + BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. " + "To unblock your subscription request, visit ~s"), + [JID, get_url(Id)]}, + Body = xmpp:mk_text(BodyString, Lang), + OOB = #oob_x{url = get_url(Id)}, + Hint = #hint{type = 'no-store'}, + Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), + ets:insert(captcha, + #captcha{ + id = Id, + pid = self(), + key = Key, + tref = Tref, + args = Args + }), + {ok, Id, Body, [Hint, OOB, Captcha, Data]}; + Err -> Err end. + -spec create_captcha_x(binary(), jid(), binary(), any(), xdata()) -> - {ok, [xmpp_element()]} | {error, image_error()}. + {ok, [xmpp_element()]} | {error, image_error()}. create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) -> case create_image(Limiter) of - {ok, Type, Key, Image} -> - Id = <<(p1_rand:get_string())/binary>>, - CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, - Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, - HelpTxt = translate:translate( - Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")), - Imageurl = get_url(<>), - [H|T] = captcha_form:encode( - [{'captcha-fallback-text', HelpTxt}, - {'captcha-fallback-url', Imageurl}, - {from, To}, {challenge, Id}, {sid, SID}, - mk_ocr_field(Lang, CID, Type)], - Lang, [challenge]), - Captcha = X#xdata{type = form, fields = [H|Fs ++ T]}, - Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), - ets:insert(captcha, #captcha{id = Id, key = Key, tref = Tref}), - {ok, [Captcha, Data]}; - Err -> Err + {ok, Type, Key, Image} -> + Id = <<(p1_rand:get_string())/binary>>, + CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, + Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, + HelpTxt = translate:translate( + Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")), + Imageurl = get_url(<>), + [H | T] = captcha_form:encode( + [{'captcha-fallback-text', HelpTxt}, + {'captcha-fallback-url', Imageurl}, + {from, To}, + {challenge, Id}, + {sid, SID}, + mk_ocr_field(Lang, CID, Type)], + Lang, + [challenge]), + Captcha = X#xdata{type = form, fields = [H | Fs ++ T]}, + Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), + ets:insert(captcha, #captcha{id = Id, key = Key, tref = Tref}), + {ok, [Captcha, Data]}; + Err -> Err end. + -spec build_captcha_html(binary(), binary()) -> captcha_not_found | {xmlel(), - {xmlel(), cdata(), - xmlel(), xmlel()}}. + {xmlel(), + cdata(), + xmlel(), + xmlel()}}. build_captcha_html(Id, Lang) -> case lookup_captcha(Id) of - {ok, _} -> - ImgEl = #xmlel{name = <<"img">>, - attrs = - [{<<"src">>, get_url(<>)}], - children = []}, - Text = {xmlcdata, captcha_text(Lang)}, - IdEl = #xmlel{name = <<"input">>, - attrs = - [{<<"type">>, <<"hidden">>}, {<<"name">>, <<"id">>}, - {<<"value">>, Id}], - children = []}, - KeyEl = #xmlel{name = <<"input">>, - attrs = - [{<<"type">>, <<"text">>}, {<<"name">>, <<"key">>}, - {<<"size">>, <<"10">>}], - children = []}, - FormEl = #xmlel{name = <<"form">>, - attrs = - [{<<"action">>, get_url(Id)}, - {<<"name">>, <<"captcha">>}, - {<<"method">>, <<"POST">>}], - children = - [ImgEl, - #xmlel{name = <<"br">>, attrs = [], - children = []}, - Text, - #xmlel{name = <<"br">>, attrs = [], - children = []}, - IdEl, KeyEl, - #xmlel{name = <<"br">>, attrs = [], - children = []}, - #xmlel{name = <<"input">>, - attrs = - [{<<"type">>, <<"submit">>}, - {<<"name">>, <<"enter">>}, - {<<"value">>, ?T("OK")}], - children = []}]}, - {FormEl, {ImgEl, Text, IdEl, KeyEl}}; - _ -> captcha_not_found + {ok, _} -> + ImgEl = #xmlel{ + name = <<"img">>, + attrs = + [{<<"src">>, get_url(<>)}], + children = [] + }, + Text = {xmlcdata, captcha_text(Lang)}, + IdEl = #xmlel{ + name = <<"input">>, + attrs = + [{<<"type">>, <<"hidden">>}, + {<<"name">>, <<"id">>}, + {<<"value">>, Id}], + children = [] + }, + KeyEl = #xmlel{ + name = <<"input">>, + attrs = + [{<<"type">>, <<"text">>}, + {<<"name">>, <<"key">>}, + {<<"size">>, <<"10">>}], + children = [] + }, + FormEl = #xmlel{ + name = <<"form">>, + attrs = + [{<<"action">>, get_url(Id)}, + {<<"name">>, <<"captcha">>}, + {<<"method">>, <<"POST">>}], + children = + [ImgEl, + #xmlel{ + name = <<"br">>, + attrs = [], + children = [] + }, + Text, + #xmlel{ + name = <<"br">>, + attrs = [], + children = [] + }, + IdEl, + KeyEl, + #xmlel{ + name = <<"br">>, + attrs = [], + children = [] + }, + #xmlel{ + name = <<"input">>, + attrs = + [{<<"type">>, <<"submit">>}, + {<<"name">>, <<"enter">>}, + {<<"value">>, ?T("OK")}], + children = [] + }] + }, + {FormEl, {ImgEl, Text, IdEl, KeyEl}}; + _ -> captcha_not_found end. + -spec process_reply(xmpp_element()) -> ok | {error, bad_match | not_found | malformed}. process_reply(#xdata{} = X) -> Required = [<<"challenge">>, <<"ocr">>], Fs = lists:filter( - fun(#xdata_field{var = Var}) -> - lists:member(Var, [<<"FORM_TYPE">>|Required]) - end, X#xdata.fields), + fun(#xdata_field{var = Var}) -> + lists:member(Var, [<<"FORM_TYPE">> | Required]) + end, + X#xdata.fields), try captcha_form:decode(Fs, [?NS_CAPTCHA], Required) of - Props -> - Id = proplists:get_value(challenge, Props), - OCR = proplists:get_value(ocr, Props), - case check_captcha(Id, OCR) of - captcha_valid -> ok; - captcha_non_valid -> {error, bad_match}; - captcha_not_found -> {error, not_found} - end - catch _:{captcha_form, Why} -> - ?WARNING_MSG("Malformed CAPTCHA form: ~ts", - [captcha_form:format_error(Why)]), - {error, malformed} + Props -> + Id = proplists:get_value(challenge, Props), + OCR = proplists:get_value(ocr, Props), + case check_captcha(Id, OCR) of + captcha_valid -> ok; + captcha_non_valid -> {error, bad_match}; + captcha_not_found -> {error, not_found} + end + catch + _:{captcha_form, Why} -> + ?WARNING_MSG("Malformed CAPTCHA form: ~ts", + [captcha_form:format_error(Why)]), + {error, malformed} end; process_reply(#xcaptcha{xdata = #xdata{} = X}) -> process_reply(X); process_reply(_) -> {error, malformed}. + -spec process_iq(iq()) -> iq(). process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) -> case process_reply(El) of - ok -> - xmpp:make_iq_result(IQ); - {error, malformed} -> - Txt = ?T("Incorrect CAPTCHA submit"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); - {error, _} -> - Txt = ?T("The CAPTCHA verification has failed"), - xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + ok -> + xmpp:make_iq_result(IQ); + {error, malformed} -> + Txt = ?T("Incorrect CAPTCHA submit"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + {error, _} -> + Txt = ?T("The CAPTCHA verification has failed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) end; process_iq(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), @@ -231,172 +298,212 @@ process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + process(_Handlers, - #request{method = 'GET', lang = Lang, - path = [_, Id]}) -> + #request{ + method = 'GET', + lang = Lang, + path = [_, Id] + }) -> case build_captcha_html(Id, Lang) of - {FormEl, _} -> - Form = #xmlel{name = <<"div">>, - attrs = [{<<"align">>, <<"center">>}], - children = [FormEl]}, - ejabberd_web:make_xhtml([Form]); - captcha_not_found -> ejabberd_web:error(not_found) + {FormEl, _} -> + Form = #xmlel{ + name = <<"div">>, + attrs = [{<<"align">>, <<"center">>}], + children = [FormEl] + }, + ejabberd_web:make_xhtml([Form]); + captcha_not_found -> ejabberd_web:error(not_found) end; process(_Handlers, - #request{method = 'GET', path = [_, Id, <<"image">>], - ip = IP}) -> + #request{ + method = 'GET', + path = [_, Id, <<"image">>], + ip = IP + }) -> {Addr, _Port} = IP, case lookup_captcha(Id) of - {ok, #captcha{key = Key}} -> - case create_image(Addr, Key) of - {ok, Type, Key2, Img} -> - update_captcha_key(Id, Key, Key2), - {200, - [{<<"Content-Type">>, Type}, - {<<"Cache-Control">>, <<"no-cache">>}, - {<<"Last-Modified">>, list_to_binary(httpd_util:rfc1123_date())}], - Img}; - {error, limit} -> ejabberd_web:error(not_allowed); - _ -> ejabberd_web:error(not_found) - end; - _ -> ejabberd_web:error(not_found) + {ok, #captcha{key = Key}} -> + case create_image(Addr, Key) of + {ok, Type, Key2, Img} -> + update_captcha_key(Id, Key, Key2), + {200, + [{<<"Content-Type">>, Type}, + {<<"Cache-Control">>, <<"no-cache">>}, + {<<"Last-Modified">>, list_to_binary(httpd_util:rfc1123_date())}], + Img}; + {error, limit} -> ejabberd_web:error(not_allowed); + _ -> ejabberd_web:error(not_found) + end; + _ -> ejabberd_web:error(not_found) end; process(_Handlers, - #request{method = 'POST', q = Q, lang = Lang, - path = [_, Id]}) -> + #request{ + method = 'POST', + q = Q, + lang = Lang, + path = [_, Id] + }) -> ProvidedKey = proplists:get_value(<<"key">>, Q, none), case check_captcha(Id, ProvidedKey) of - captcha_valid -> - Form = #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - ?T("The CAPTCHA is valid."))}]}, - ejabberd_web:make_xhtml([Form]); - captcha_non_valid -> ejabberd_web:error(not_allowed); - captcha_not_found -> ejabberd_web:error(not_found) + captcha_valid -> + Form = #xmlel{ + name = <<"p">>, + attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + ?T("The CAPTCHA is valid."))}] + }, + ejabberd_web:make_xhtml([Form]); + captcha_non_valid -> ejabberd_web:error(not_allowed); + captcha_not_found -> ejabberd_web:error(not_found) end; process(_Handlers, _Request) -> ejabberd_web:error(not_found). + host_up(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA, - ?MODULE, process_iq). + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_CAPTCHA, + ?MODULE, + process_iq). + host_down(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA). + config_reloaded() -> gen_server:call(?MODULE, config_reloaded, timer:minutes(1)). + init([]) -> _ = mnesia:delete_table(captcha), _ = ets:new(captcha, [named_table, public, {keypos, #captcha.id}]), case check_captcha_setup() of - true -> - register_handlers(), - ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 70), - {ok, #state{enabled = true}}; - false -> - {ok, #state{enabled = false}}; - {error, Reason} -> - {stop, Reason} + true -> + register_handlers(), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 70), + {ok, #state{enabled = true}}; + false -> + {ok, #state{enabled = false}}; + {error, Reason} -> + {stop, Reason} end. -handle_call({is_limited, Limiter, RateLimit}, _From, - State) -> + +handle_call({is_limited, Limiter, RateLimit}, + _From, + State) -> NowPriority = now_priority(), CleanPriority = NowPriority + (?LIMIT_PERIOD), Limits = clean_treap(State#state.limits, CleanPriority), case treap:lookup(Limiter, Limits) of - {ok, _, Rate} when Rate >= RateLimit -> - {reply, true, State#state{limits = Limits}}; - {ok, Priority, Rate} -> - NewLimits = treap:insert(Limiter, Priority, Rate + 1, - Limits), - {reply, false, State#state{limits = NewLimits}}; - _ -> - NewLimits = treap:insert(Limiter, NowPriority, 1, - Limits), - {reply, false, State#state{limits = NewLimits}} + {ok, _, Rate} when Rate >= RateLimit -> + {reply, true, State#state{limits = Limits}}; + {ok, Priority, Rate} -> + NewLimits = treap:insert(Limiter, + Priority, + Rate + 1, + Limits), + {reply, false, State#state{limits = NewLimits}}; + _ -> + NewLimits = treap:insert(Limiter, + NowPriority, + 1, + Limits), + {reply, false, State#state{limits = NewLimits}} end; handle_call(config_reloaded, _From, #state{enabled = Enabled} = State) -> State1 = case is_feature_available() of - true when not Enabled -> - case check_captcha_setup() of - true -> - register_handlers(), - State#state{enabled = true}; - _ -> - State - end; - false when Enabled -> - unregister_handlers(), - State#state{enabled = false}; - _ -> - State - end, + true when not Enabled -> + case check_captcha_setup() of + true -> + register_handlers(), + State#state{enabled = true}; + _ -> + State + end; + false when Enabled -> + unregister_handlers(), + State#state{enabled = false}; + _ -> + State + end, {reply, ok, State1}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({remove_id, Id}, State) -> ?DEBUG("CAPTCHA ~p timed out", [Id]), case ets:lookup(captcha, Id) of - [#captcha{args = Args, pid = Pid}] -> - callback(captcha_failed, Pid, Args), - ets:delete(captcha, Id); - _ -> ok + [#captcha{args = Args, pid = Pid}] -> + callback(captcha_failed, Pid, Args), + ets:delete(captcha, Id); + _ -> ok end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, #state{enabled = Enabled}) -> - if Enabled -> unregister_handlers(); - true -> ok + if + Enabled -> unregister_handlers(); + true -> ok end, ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 70). + register_handlers() -> ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), ejabberd_hooks:add(host_down, ?MODULE, host_down, 50), lists:foreach(fun host_up/1, ejabberd_option:hosts()). + unregister_handlers() -> ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50), lists:foreach(fun host_down/1, ejabberd_option:hosts()). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + -spec create_image() -> {ok, binary(), binary(), binary()} | - {error, image_error()}. + {error, image_error()}. create_image() -> create_image(undefined). + -spec create_image(term()) -> {ok, binary(), binary(), binary()} | - {error, image_error()}. + {error, image_error()}. create_image(Limiter) -> Key = str:substr(p1_rand:get_string(), 1, 6), create_image(Limiter, Key). + -spec create_image(term(), binary()) -> {ok, binary(), binary(), binary()} | - {error, image_error()}. + {error, image_error()}. create_image(Limiter, Key) -> case is_limited(Limiter) of - true -> {error, limit}; - false -> do_create_image(Key) + true -> {error, limit}; + false -> do_create_image(Key) end. + -spec do_create_image(binary()) -> {ok, binary(), binary(), binary()} | - {error, image_error()}. + {error, image_error()}. do_create_image(Key) -> FileName = get_prog_name(), case length(binary:split(FileName, <<"/">>)) == 1 of @@ -406,6 +513,7 @@ do_create_image(Key) -> do_create_image(Key, FileName) end. + do_create_image(Key, Module) when is_atom(Module) -> Function = create_image, erlang:apply(Module, Function, [Key]); @@ -413,45 +521,47 @@ do_create_image(Key, Module) when is_atom(Module) -> do_create_image(Key, FileName) when is_binary(FileName) -> Cmd = lists:flatten(io_lib:format("~ts ~ts", [FileName, Key])), case cmd(Cmd) of - {ok, - <<137, $P, $N, $G, $\r, $\n, 26, $\n, _/binary>> = - Img} -> - {ok, <<"image/png">>, Key, Img}; - {ok, <<255, 216, _/binary>> = Img} -> - {ok, <<"image/jpeg">>, Key, Img}; - {ok, <<$G, $I, $F, $8, X, $a, _/binary>> = Img} - when X == $7; X == $9 -> - {ok, <<"image/gif">>, Key, Img}; - {error, enodata = Reason} -> - ?ERROR_MSG("Failed to process output from \"~ts\". " - "Maybe ImageMagick's Convert program " - "is not installed.", - [Cmd]), - {error, Reason}; - {error, Reason} -> - ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", - [Cmd, Reason]), - {error, Reason}; - _ -> - Reason = malformed_image, - ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", - [Cmd, Reason]), - {error, Reason} + {ok, + <<137, $P, $N, $G, $\r, $\n, 26, $\n, _/binary>> = + Img} -> + {ok, <<"image/png">>, Key, Img}; + {ok, <<255, 216, _/binary>> = Img} -> + {ok, <<"image/jpeg">>, Key, Img}; + {ok, <<$G, $I, $F, $8, X, $a, _/binary>> = Img} + when X == $7; X == $9 -> + {ok, <<"image/gif">>, Key, Img}; + {error, enodata = Reason} -> + ?ERROR_MSG("Failed to process output from \"~ts\". " + "Maybe ImageMagick's Convert program " + "is not installed.", + [Cmd]), + {error, Reason}; + {error, Reason} -> + ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", + [Cmd, Reason]), + {error, Reason}; + _ -> + Reason = malformed_image, + ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", + [Cmd, Reason]), + {error, Reason} end. + get_prog_name() -> case ejabberd_option:captcha_cmd() of undefined -> ?WARNING_MSG("The option captcha_cmd is not configured, " - "but some module wants to use the CAPTCHA " - "feature.", - []), + "but some module wants to use the CAPTCHA " + "feature.", + []), false; FileName -> maybe_warning_norequesthandler(), FileName end. + maybe_warning_norequesthandler() -> Host = hd(ejabberd_option:hosts()), AutoURL = get_auto_url(any, ?MODULE, Host), @@ -459,47 +569,50 @@ maybe_warning_norequesthandler() -> case (AutoURL == undefined) and not is_binary(ManualURL) of true -> ?CRITICAL_MSG("The option captcha_cmd is configured " - "and captcha_url is set to auto, " - "but I couldn't find a request_handler in listen option " - "configured with ejabberd_captcha and integer port. " - "Please setup the URL with option captcha_url, see " - "https://docs.ejabberd.im/admin/configuration/basic/#captcha", - []); + "and captcha_url is set to auto, " + "but I couldn't find a request_handler in listen option " + "configured with ejabberd_captcha and integer port. " + "Please setup the URL with option captcha_url, see " + "https://docs.ejabberd.im/admin/configuration/basic/#captcha", + []); _ -> ok end. + -spec get_url(binary()) -> binary(). get_url(Str) -> case ejabberd_option:captcha_url() of - auto -> - Host = ejabberd_config:get_myname(), + auto -> + Host = ejabberd_config:get_myname(), URL = get_auto_url(any, ?MODULE, Host), <>; - undefined -> - URL = parse_captcha_host(), - <>; - URL -> - <> + undefined -> + URL = parse_captcha_host(), + <>; + URL -> + <> end. + -spec parse_captcha_host() -> binary(). parse_captcha_host() -> CaptchaHost = ejabberd_option:captcha_host(), case str:tokens(CaptchaHost, <<":">>) of - [Host] -> - <<"http://", Host/binary>>; - [<<"http", _/binary>> = TransferProt, Host] -> - <>; - [Host, PortString] -> - TransferProt = atom_to_binary(get_transfer_protocol(PortString), latin1), - <>; - [TransferProt, Host, PortString] -> - <>; - _ -> - <<"http://", (ejabberd_config:get_myname())/binary>> + [Host] -> + <<"http://", Host/binary>>; + [<<"http", _/binary>> = TransferProt, Host] -> + <>; + [Host, PortString] -> + TransferProt = atom_to_binary(get_transfer_protocol(PortString), latin1), + <>; + [TransferProt, Host, PortString] -> + <>; + _ -> + <<"http://", (ejabberd_config:get_myname())/binary>> end. + get_auto_url(Tls, Module, Host) -> case find_handler_port_path(Tls, Module) of [] -> undefined; @@ -515,12 +628,15 @@ get_auto_url(Tls, Module, Host) -> true -> <<"https">> end, <>))/binary>> end. + find_handler_port_path(Tls, Module) -> lists:filtermap( fun({{Port, _, _}, @@ -532,155 +648,176 @@ find_handler_port_path(Tls, Module) -> {Path, Module} -> {true, {ThisTls, Port, Path}} end; (_) -> false - end, ets:tab2list(ejabberd_listener)). + end, + ets:tab2list(ejabberd_listener)). + get_transfer_protocol(PortString) -> PortNumber = binary_to_integer(PortString), PortListeners = get_port_listeners(PortNumber), get_captcha_transfer_protocol(PortListeners). + get_port_listeners(PortNumber) -> AllListeners = ejabberd_option:listen(), lists:filter( fun({{Port, _IP, _Transport}, _Module, _Opts}) -> - Port == PortNumber - end, AllListeners). + Port == PortNumber + end, + AllListeners). + get_captcha_transfer_protocol([]) -> throw(<<"The port number mentioned in captcha_host " - "is not a ejabberd_http listener with " - "'captcha' option. Change the port number " - "or specify http:// in that option.">>); + "is not a ejabberd_http listener with " + "'captcha' option. Change the port number " + "or specify http:// in that option.">>); get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) -> Handlers = maps:get(request_handlers, Opts, []), case lists:any( - fun({_, ?MODULE}) -> true; - ({_, _}) -> false - end, Handlers) of - true -> - case maps:get(tls, Opts) of - true -> https; - false -> http - end; - false -> - get_captcha_transfer_protocol(Listeners) + fun({_, ?MODULE}) -> true; + ({_, _}) -> false + end, + Handlers) of + true -> + case maps:get(tls, Opts) of + true -> https; + false -> http + end; + false -> + get_captcha_transfer_protocol(Listeners) end; get_captcha_transfer_protocol([_ | Listeners]) -> get_captcha_transfer_protocol(Listeners). + is_limited(undefined) -> false; is_limited(Limiter) -> case ejabberd_option:captcha_limit() of - infinity -> false; - Int -> - case catch gen_server:call(?MODULE, - {is_limited, Limiter, Int}, 5000) - of - true -> true; - false -> false; - Err -> ?ERROR_MSG("Call failed: ~p", [Err]), false - end + infinity -> false; + Int -> + case catch gen_server:call(?MODULE, + {is_limited, Limiter, Int}, + 5000) of + true -> true; + false -> false; + Err -> ?ERROR_MSG("Call failed: ~p", [Err]), false + end end. + -define(CMD_TIMEOUT, 5000). -define(MAX_FILE_SIZE, 64 * 1024). + -spec cmd(string()) -> {ok, binary()} | {error, image_error()}. cmd(Cmd) -> Port = open_port({spawn, Cmd}, [stream, eof, binary]), - TRef = erlang:start_timer(?CMD_TIMEOUT, self(), - timeout), + TRef = erlang:start_timer(?CMD_TIMEOUT, + self(), + timeout), recv_data(Port, TRef, <<>>). + -spec recv_data(port(), reference(), binary()) -> {ok, binary()} | {error, image_error()}. recv_data(Port, TRef, Buf) -> receive - {Port, {data, Bytes}} -> - NewBuf = <>, - if byte_size(NewBuf) > (?MAX_FILE_SIZE) -> - return(Port, TRef, {error, efbig}); - true -> recv_data(Port, TRef, NewBuf) - end; - {Port, {data, _}} -> return(Port, TRef, {error, efbig}); - {Port, eof} when Buf /= <<>> -> - return(Port, TRef, {ok, Buf}); - {Port, eof} -> return(Port, TRef, {error, enodata}); - {timeout, TRef, _} -> - return(Port, TRef, {error, timeout}) + {Port, {data, Bytes}} -> + NewBuf = <>, + if + byte_size(NewBuf) > (?MAX_FILE_SIZE) -> + return(Port, TRef, {error, efbig}); + true -> recv_data(Port, TRef, NewBuf) + end; + {Port, {data, _}} -> return(Port, TRef, {error, efbig}); + {Port, eof} when Buf /= <<>> -> + return(Port, TRef, {ok, Buf}); + {Port, eof} -> return(Port, TRef, {error, enodata}); + {timeout, TRef, _} -> + return(Port, TRef, {error, timeout}) end. + -spec return(port(), reference(), {ok, binary()} | {error, image_error()}) -> - {ok, binary()} | {error, image_error()}. + {ok, binary()} | {error, image_error()}. return(Port, TRef, Result) -> misc:cancel_timer(TRef), catch port_close(Port), Result. + is_feature_available() -> case get_prog_name() of - PathOrModule when is_binary(PathOrModule) -> true; - false -> false + PathOrModule when is_binary(PathOrModule) -> true; + false -> false end. + check_captcha_setup() -> case is_feature_available() of - true -> - case create_image() of - {ok, _, _, _} -> - true; - Err -> - ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " - "but it can't generate images.", - []), - Err - end; - false -> - false + true -> + case create_image() of + {ok, _, _, _} -> + true; + Err -> + ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " + "but it can't generate images.", + []), + Err + end; + false -> + false end. + -spec lookup_captcha(binary()) -> {ok, #captcha{}} | {error, enoent}. lookup_captcha(Id) -> case ets:lookup(captcha, Id) of - [C] -> {ok, C}; - [] -> {error, enoent} + [C] -> {ok, C}; + [] -> {error, enoent} end. + -spec check_captcha(binary(), binary()) -> captcha_not_found | captcha_valid | captcha_non_valid. check_captcha(Id, ProvidedKey) -> case lookup_captcha(Id) of - {ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} -> - ets:delete(captcha, Id), - misc:cancel_timer(Tref), - if ValidKey == ProvidedKey -> - callback(captcha_succeed, Pid, Args), - captcha_valid; - true -> - callback(captcha_failed, Pid, Args), - captcha_non_valid - end; - {error, _} -> - captcha_not_found + {ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} -> + ets:delete(captcha, Id), + misc:cancel_timer(Tref), + if + ValidKey == ProvidedKey -> + callback(captcha_succeed, Pid, Args), + captcha_valid; + true -> + callback(captcha_failed, Pid, Args), + captcha_non_valid + end; + {error, _} -> + captcha_not_found end. + -spec clean_treap(treap:treap(), priority()) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of - true -> Treap; - false -> - {_Key, Priority, _Value} = treap:get_root(Treap), - if Priority > CleanPriority -> - clean_treap(treap:delete_root(Treap), CleanPriority); - true -> Treap - end + true -> Treap; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if + Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority); + true -> Treap + end end. + -spec callback(captcha_succeed | captcha_failed, - pid() | undefined, - callback() | term()) -> any(). + pid() | undefined, + callback() | term()) -> any(). callback(Result, _Pid, F) when is_function(F) -> F(Result); callback(Result, Pid, Args) when is_pid(Pid) -> @@ -688,6 +825,7 @@ callback(Result, Pid, Args) when is_pid(Pid) -> callback(_, _, _) -> ok. + -spec now_priority() -> priority(). now_priority() -> -erlang:system_time(microsecond). diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl index 38a378d30..773120790 100644 --- a/src/ejabberd_cluster.erl +++ b/src/ejabberd_cluster.erl @@ -24,14 +24,27 @@ -behaviour(gen_server). %% API --export([start_link/0, call/4, call/5, multicall/3, multicall/4, multicall/5, - eval_everywhere/3, eval_everywhere/4]). +-export([start_link/0, + call/4, call/5, + multicall/3, multicall/4, multicall/5, + eval_everywhere/3, eval_everywhere/4]). %% Backend dependent API --export([get_nodes/0, get_known_nodes/0, join/1, leave/1, subscribe/0, - subscribe/1, node_id/0, get_node_by_id/1, send/2, wait_for_sync/1]). +-export([get_nodes/0, + get_known_nodes/0, + join/1, + leave/1, + subscribe/0, subscribe/1, + node_id/0, + get_node_by_id/1, + send/2, + wait_for_sync/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% hooks -export([set_ticktime/0]). @@ -39,6 +52,7 @@ -type dst() :: pid() | atom() | {atom(), node()}. + -callback init() -> ok | {error, any()}. -callback get_nodes() -> [node()]. -callback get_known_nodes() -> [node()]. @@ -52,42 +66,51 @@ -record(state, {}). + %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + -spec call(node(), module(), atom(), [any()]) -> any(). call(Node, Module, Function, Args) -> call(Node, Module, Function, Args, rpc_timeout()). + -spec call(node(), module(), atom(), [any()], timeout()) -> any(). call(Node, Module, Function, Args, Timeout) -> rpc:call(Node, Module, Function, Args, Timeout). + -spec multicall(module(), atom(), [any()]) -> {list(), [node()]}. multicall(Module, Function, Args) -> multicall(get_nodes(), Module, Function, Args). + -spec multicall([node()], module(), atom(), list()) -> {list(), [node()]}. multicall(Nodes, Module, Function, Args) -> multicall(Nodes, Module, Function, Args, rpc_timeout()). + -spec multicall([node()], module(), atom(), list(), timeout()) -> {list(), [node()]}. multicall(Nodes, Module, Function, Args, Timeout) -> rpc:multicall(Nodes, Module, Function, Args, Timeout). + -spec eval_everywhere(module(), atom(), [any()]) -> ok. eval_everywhere(Module, Function, Args) -> eval_everywhere(get_nodes(), Module, Function, Args), ok. + -spec eval_everywhere([node()], module(), atom(), [any()]) -> ok. eval_everywhere(Nodes, Module, Function, Args) -> rpc:eval_everywhere(Nodes, Module, Function, Args), ok. + %%%=================================================================== %%% Backend dependent API %%%=================================================================== @@ -96,31 +119,37 @@ get_nodes() -> Mod = get_mod(), Mod:get_nodes(). + -spec get_known_nodes() -> [node()]. get_known_nodes() -> Mod = get_mod(), Mod:get_known_nodes(). + -spec join(node()) -> ok | {error, any()}. join(Node) -> Mod = get_mod(), Mod:join(Node). + -spec leave(node()) -> ok | {error, any()}. leave(Node) -> Mod = get_mod(), Mod:leave(Node). + -spec node_id() -> binary(). node_id() -> Mod = get_mod(), Mod:node_id(). + -spec get_node_by_id(binary()) -> node(). get_node_by_id(ID) -> Mod = get_mod(), Mod:get_node_by_id(ID). + %% Note that false positive returns are possible, while false negatives are not. %% In other words: positive return value (i.e. 'true') doesn't guarantee %% successful delivery, while negative return value ('false') means @@ -134,45 +163,51 @@ send(Name, Msg) when is_atom(Name) -> send(whereis(Name), Msg); send(Pid, Msg) when is_pid(Pid) andalso node(Pid) == node() -> case erlang:is_process_alive(Pid) of - true -> - erlang:send(Pid, Msg), - true; - false -> - false + true -> + erlang:send(Pid, Msg), + true; + false -> + false end; send(Dst, Msg) -> Mod = get_mod(), Mod:send(Dst, Msg). + -spec wait_for_sync(timeout()) -> ok | {error, any()}. wait_for_sync(Timeout) -> Mod = get_mod(), Mod:wait_for_sync(Timeout). + -spec subscribe() -> ok. subscribe() -> subscribe(self()). + -spec subscribe(dst()) -> ok. subscribe(Proc) -> Mod = get_mod(), Mod:subscribe(Proc). + %%%=================================================================== %%% Hooks %%%=================================================================== set_ticktime() -> Ticktime = ejabberd_option:net_ticktime() div 1000, case net_kernel:set_net_ticktime(Ticktime) of - {ongoing_change_to, Time} when Time /= Ticktime -> - ?ERROR_MSG("Failed to set new net_ticktime because " - "the net kernel is busy changing it to the " - "previously configured value. Please wait for " - "~B seconds and retry", [Time]); - _ -> - ok + {ongoing_change_to, Time} when Time /= Ticktime -> + ?ERROR_MSG("Failed to set new net_ticktime because " + "the net kernel is busy changing it to the " + "previously configured value. Please wait for " + "~B seconds and retry", + [Time]); + _ -> + ok end. + %%%=================================================================== %%% gen_server API %%%=================================================================== @@ -181,25 +216,29 @@ init([]) -> Nodes = ejabberd_option:cluster_nodes(), lists:foreach(fun(Node) -> net_kernel:connect_node(Node) - end, Nodes), + end, + Nodes), Mod = get_mod(), case Mod:init() of - ok -> - ejabberd_hooks:add(config_reloaded, ?MODULE, set_ticktime, 50), - Mod:subscribe(?MODULE), - {ok, #state{}}; - {error, Reason} -> - {stop, Reason} + ok -> + ejabberd_hooks:add(config_reloaded, ?MODULE, set_ticktime, 50), + Mod:subscribe(?MODULE), + {ok, #state{}}; + {error, Reason} -> + {stop, Reason} end. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({node_up, Node}, State) -> ?INFO_MSG("Node ~ts has joined", [Node]), {noreply, State}; @@ -210,12 +249,15 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, set_ticktime, 50). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -223,5 +265,6 @@ get_mod() -> Backend = ejabberd_option:cluster_backend(), list_to_existing_atom("ejabberd_cluster_" ++ atom_to_list(Backend)). + rpc_timeout() -> ejabberd_option:rpc_timeout(). diff --git a/src/ejabberd_cluster_mnesia.erl b/src/ejabberd_cluster_mnesia.erl index ada0703be..0b529b1b9 100644 --- a/src/ejabberd_cluster_mnesia.erl +++ b/src/ejabberd_cluster_mnesia.erl @@ -27,26 +27,37 @@ -behaviour(ejabberd_cluster). %% API --export([init/0, get_nodes/0, join/1, leave/1, - get_known_nodes/0, node_id/0, get_node_by_id/1, - send/2, wait_for_sync/1, subscribe/1]). +-export([init/0, + get_nodes/0, + join/1, + leave/1, + get_known_nodes/0, + node_id/0, + get_node_by_id/1, + send/2, + wait_for_sync/1, + subscribe/1]). -include("logger.hrl"). + -spec init() -> ok. init() -> ok. + -spec get_nodes() -> [node()]. get_nodes() -> mnesia:system_info(running_db_nodes). + -spec get_known_nodes() -> [node()]. get_known_nodes() -> - lists:usort(mnesia:system_info(db_nodes) - ++ mnesia:system_info(extra_db_nodes)). + lists:usort(mnesia:system_info(db_nodes) ++ + mnesia:system_info(extra_db_nodes)). + -spec join(node()) -> ok | {error, any()}. @@ -72,12 +83,13 @@ join(Node) -> {error, {no_ping, Node}} end. + -spec leave(node()) -> ok | {error, any()}. leave(Node) -> case {node(), net_adm:ping(Node)} of {Node, _} -> - Cluster = get_nodes()--[Node], + Cluster = get_nodes() -- [Node], leave(Cluster, Node); {_, pong} -> rpc:call(Node, ?MODULE, leave, [Node], 10000); @@ -87,68 +99,81 @@ leave(Node) -> {aborted, Reason} -> {error, Reason} end end. + + leave([], Node) -> {error, {no_cluster, Node}}; -leave([Master|_], Node) -> +leave([Master | _], Node) -> application:stop(ejabberd), application:stop(mnesia), spawn(fun() -> - rpc:call(Master, mnesia, del_table_copy, [schema, Node]), - mnesia:delete_schema([node()]), - erlang:halt(0) + rpc:call(Master, mnesia, del_table_copy, [schema, Node]), + mnesia:delete_schema([node()]), + erlang:halt(0) end), ok. + -spec node_id() -> binary(). node_id() -> integer_to_binary(erlang:phash2(node())). + -spec get_node_by_id(binary()) -> node(). get_node_by_id(Hash) -> try binary_to_integer(Hash) of - I -> match_node_id(I) - catch _:_ -> - node() + I -> match_node_id(I) + catch + _:_ -> + node() end. + -spec send({atom(), node()}, term()) -> boolean(). send(Dst, Msg) -> case erlang:send(Dst, Msg, [nosuspend, noconnect]) of - ok -> true; - _ -> false + ok -> true; + _ -> false end. + -spec wait_for_sync(timeout()) -> ok. wait_for_sync(Timeout) -> ?INFO_MSG("Waiting for Mnesia synchronization to complete", []), mnesia:wait_for_tables(mnesia:system_info(local_tables), Timeout), ok. + -spec subscribe(_) -> ok. subscribe(_) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== + replicate_database(Node) -> mnesia:change_table_copy_type(schema, node(), disc_copies), lists:foreach( - fun(Table) -> - Type = rpc:call(Node, mnesia, table_info, [Table, storage_type]), - mnesia:add_table_copy(Table, node(), Type) - end, mnesia:system_info(tables)--[schema]). + fun(Table) -> + Type = rpc:call(Node, mnesia, table_info, [Table, storage_type]), + mnesia:add_table_copy(Table, node(), Type) + end, + mnesia:system_info(tables) -- [schema]). + -spec match_node_id(integer()) -> node(). match_node_id(I) -> match_node_id(I, get_nodes()). + -spec match_node_id(integer(), [node()]) -> node(). -match_node_id(I, [Node|Nodes]) -> +match_node_id(I, [Node | Nodes]) -> case erlang:phash2(Node) of - I -> Node; - _ -> match_node_id(I, Nodes) + I -> Node; + _ -> match_node_id(I, Nodes) end; match_node_id(_I, []) -> node(). diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index f1e724da3..84f6ba714 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -31,120 +31,139 @@ -define(DEFAULT_VERSION, 1000000). -export([start_link/0, - list_commands/0, - list_commands/1, - list_commands/2, - get_command_format/1, - get_command_format/2, - get_command_format/3, - get_command_definition/1, - get_command_definition/2, - get_tags_commands/0, - get_tags_commands/1, - register_commands/1, - register_commands/2, - register_commands/3, - unregister_commands/1, - unregister_commands/3, - get_commands_spec/0, - get_commands_definition/0, - get_commands_definition/1, - execute_command2/3, - execute_command2/4]). + list_commands/0, list_commands/1, list_commands/2, + get_command_format/1, get_command_format/2, get_command_format/3, + get_command_definition/1, get_command_definition/2, + get_tags_commands/0, get_tags_commands/1, + register_commands/1, register_commands/2, register_commands/3, + unregister_commands/1, unregister_commands/3, + get_commands_spec/0, + get_commands_definition/0, get_commands_definition/1, + execute_command2/3, execute_command2/4]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("ejabberd_commands.hrl"). -include("logger.hrl"). + -include_lib("stdlib/include/ms_transform.hrl"). -type auth() :: {binary(), binary(), binary() | {oauth, binary()}, boolean()} | map(). -record(state, {}). + get_commands_spec() -> - [ - #ejabberd_commands{name = gen_html_doc_for_commands, tags = [documentation], - desc = "Generates html documentation for ejabberd_commands", - module = ejabberd_commands_doc, function = generate_html_output, - args = [{file, binary}, {regexp, binary}, {examples, binary}], - result = {res, rescode}, - args_desc = ["Path to file where generated " - "documentation should be stored", - "Regexp matching names of commands or modules " - "that will be included inside generated document", - "Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) " - "that will have example invocation include in markdown document"], - result_desc = "0 if command failed, 1 when succeeded", - args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], - result_example = ok}, - #ejabberd_commands{name = gen_markdown_doc_for_commands, tags = [documentation], - desc = "Generates markdown documentation for ejabberd_commands", - module = ejabberd_commands_doc, function = generate_md_output, - args = [{file, binary}, {regexp, binary}, {examples, binary}], - result = {res, rescode}, - args_desc = ["Path to file where generated " - "documentation should be stored", - "Regexp matching names of commands or modules " - "that will be included inside generated document, " - "or `runtime` to get commands registered at runtime", - "Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) " - "that will have example invocation include in markdown document"], - result_desc = "0 if command failed, 1 when succeeded", - args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], - result_example = ok}, - #ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation], - desc = "Generates markdown documentation for ejabberd_commands", - note = "added in 21.12", - module = ejabberd_commands_doc, function = generate_tags_md, - args = [{file, binary}], - result = {res, rescode}, - args_desc = ["Path to file where generated " - "documentation should be stored"], - result_desc = "0 if command failed, 1 when succeeded", - args_example = ["/home/me/docs/tags.md"], - result_example = ok}]. + [#ejabberd_commands{ + name = gen_html_doc_for_commands, + tags = [documentation], + desc = "Generates html documentation for ejabberd_commands", + module = ejabberd_commands_doc, + function = generate_html_output, + args = [{file, binary}, {regexp, binary}, {examples, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored", + "Regexp matching names of commands or modules " + "that will be included inside generated document", + "Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) " + "that will have example invocation include in markdown document"], + result_desc = "0 if command failed, 1 when succeeded", + args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + result_example = ok + }, + #ejabberd_commands{ + name = gen_markdown_doc_for_commands, + tags = [documentation], + desc = "Generates markdown documentation for ejabberd_commands", + module = ejabberd_commands_doc, + function = generate_md_output, + args = [{file, binary}, {regexp, binary}, {examples, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored", + "Regexp matching names of commands or modules " + "that will be included inside generated document, " + "or `runtime` to get commands registered at runtime", + "Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) " + "that will have example invocation include in markdown document"], + result_desc = "0 if command failed, 1 when succeeded", + args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + result_example = ok + }, + #ejabberd_commands{ + name = gen_markdown_doc_for_tags, + tags = [documentation], + desc = "Generates markdown documentation for ejabberd_commands", + note = "added in 21.12", + module = ejabberd_commands_doc, + function = generate_tags_md, + args = [{file, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored"], + result_desc = "0 if command failed, 1 when succeeded", + args_example = ["/home/me/docs/tags.md"], + result_example = ok + }]. + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + init([]) -> - try mnesia:transform_table(ejabberd_commands, ignore, - record_info(fields, ejabberd_commands)) - catch exit:{aborted, {no_exists, _}} -> ok + try + mnesia:transform_table(ejabberd_commands, + ignore, + record_info(fields, ejabberd_commands)) + catch + exit:{aborted, {no_exists, _}} -> ok end, - ejabberd_mnesia:create(?MODULE, ejabberd_commands, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, ejabberd_commands)}, - {type, bag}]), + ejabberd_mnesia:create(?MODULE, + ejabberd_commands, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, ejabberd_commands)}, + {type, bag}]), register_commands(get_commands_spec()), {ok, #state{}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + -spec register_commands([ejabberd_commands()]) -> ok. register_commands(Commands) -> register_commands(unknown, Commands). + -spec register_commands(atom(), [ejabberd_commands()]) -> ok. register_commands(Definer, Commands) -> @@ -164,6 +183,7 @@ register_commands(Definer, Commands) -> ejabberd_access_permissions:invalidate(), ok. + -spec register_commands(binary(), atom(), [ejabberd_commands()]) -> ok. register_commands(Host, Definer, Commands) -> @@ -174,11 +194,12 @@ register_commands(Host, Definer, Commands) -> ok end. + register_command_prepare(Command, Definer) -> Tags1 = Command#ejabberd_commands.tags, Tags2 = case Command#ejabberd_commands.version of 0 -> Tags1; - Version -> Tags1 ++ [list_to_atom("v"++integer_to_list(Version))] + Version -> Tags1 ++ [list_to_atom("v" ++ integer_to_list(Version))] end, Command#ejabberd_commands{definer = Definer, tags = Tags2}. @@ -188,11 +209,12 @@ register_command_prepare(Command, Definer) -> unregister_commands(Commands) -> lists:foreach( fun(Command) -> - mnesia:dirty_delete(ejabberd_commands, Command#ejabberd_commands.name) + mnesia:dirty_delete(ejabberd_commands, Command#ejabberd_commands.name) end, Commands), ejabberd_access_permissions:invalidate(). + -spec unregister_commands(binary(), atom(), [ejabberd_commands()]) -> ok. unregister_commands(Host, Definer, Commands) -> @@ -203,47 +225,57 @@ unregister_commands(Host, Definer, Commands) -> ok end. + -spec list_commands() -> [{atom(), [aterm()], string()}]. list_commands() -> list_commands(?DEFAULT_VERSION). + -spec list_commands(integer()) -> [{atom(), [aterm()], string()}]. list_commands(Version) -> Commands = get_commands_definition(Version), - [{Name, Args, Desc} || #ejabberd_commands{name = Name, - args = Args, - tags = Tags, - desc = Desc} <- Commands, - not lists:member(internal, Tags)]. + [ {Name, Args, Desc} || #ejabberd_commands{ + name = Name, + args = Args, + tags = Tags, + desc = Desc + } <- Commands, + not lists:member(internal, Tags) ]. + -spec list_commands(integer(), map()) -> [{atom(), [aterm()], string()}]. list_commands(Version, CallerInfo) -> lists:filter( fun({Name, _Args, _Desc}) -> - allow == ejabberd_access_permissions:can_access(Name, CallerInfo) + allow == ejabberd_access_permissions:can_access(Name, CallerInfo) end, - list_commands(Version) - ). + list_commands(Version)). --spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}. + +-spec get_command_format(atom()) -> {[aterm()], [{atom(), atom()}], rterm()}. get_command_format(Name) -> get_command_format(Name, noauth, ?DEFAULT_VERSION). + + get_command_format(Name, Version) when is_integer(Version) -> get_command_format(Name, noauth, Version); -get_command_format(Name, Auth) -> +get_command_format(Name, Auth) -> get_command_format(Name, Auth, ?DEFAULT_VERSION). --spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(),atom()}], rterm()}. + +-spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(), atom()}], rterm()}. get_command_format(Name, Auth, Version) -> Admin = is_admin(Name, Auth, #{}), - #ejabberd_commands{args = Args, - result = Result, - args_rename = Rename, - policy = Policy} = + #ejabberd_commands{ + args = Args, + result = Result, + args_rename = Rename, + policy = Policy + } = get_command_definition(Name, Version), case Policy of user when Admin; @@ -253,11 +285,13 @@ get_command_format(Name, Auth, Version) -> {Args, Rename, Result} end. + -spec get_command_definition(atom()) -> ejabberd_commands(). get_command_definition(Name) -> get_command_definition(Name, ?DEFAULT_VERSION). + -spec get_command_definition(atom(), integer()) -> ejabberd_commands(). get_command_definition(Name, Version) -> @@ -270,13 +304,15 @@ get_command_definition(Name, Version) -> when N == Name, V =< Version -> {V, C} end)))) of - [{_, Command} | _ ] -> Command; + [{_, Command} | _] -> Command; _E -> throw({error, unknown_command}) end. + get_commands_definition() -> get_commands_definition(?DEFAULT_VERSION). + -spec get_commands_definition(integer()) -> [ejabberd_commands()]. get_commands_definition(Version) -> @@ -291,28 +327,30 @@ get_commands_definition(Version) -> end)))), F = fun({_Name, _V, Command}, []) -> [Command]; - ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> + ({Name, _V, _Command}, [#ejabberd_commands{name = Name} | _T] = Acc) -> Acc; ({_Name, _V, Command}, Acc) -> [Command | Acc] end, lists:foldl(F, [], L). + execute_command2(Name, Arguments, CallerInfo) -> execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION). + execute_command2(Name, Arguments, CallerInfo, Version) -> Command = get_command_definition(Name, Version), FrontedCalledInternal = - maps:get(caller_module, CallerInfo, none) /= ejabberd_web_admin - andalso lists:member(internal, Command#ejabberd_commands.tags), + maps:get(caller_module, CallerInfo, none) /= ejabberd_web_admin andalso + lists:member(internal, Command#ejabberd_commands.tags), case {ejabberd_access_permissions:can_access(Name, CallerInfo), FrontedCalledInternal} of {allow, false} -> - do_execute_command(Command, Arguments); + do_execute_command(Command, Arguments); {_, true} -> - throw({error, frontend_cannot_call_an_internal_command}); + throw({error, frontend_cannot_call_an_internal_command}); {deny, false} -> - throw({error, access_rules_unauthorized}) + throw({error, access_rules_unauthorized}) end. @@ -323,38 +361,40 @@ do_execute_command(Command, Arguments) -> ejabberd_hooks:run(api_call, [Module, Function, Arguments]), apply(Module, Function, Arguments). + -spec get_tags_commands() -> [{string(), [string()]}]. get_tags_commands() -> get_tags_commands(?DEFAULT_VERSION). + -spec get_tags_commands(integer()) -> [{string(), [string()]}]. get_tags_commands(Version) -> - CommandTags = [{Name, Tags} || - #ejabberd_commands{name = Name, tags = Tags} - <- get_commands_definition(Version), - not lists:member(internal, Tags)], + CommandTags = [ {Name, Tags} + || #ejabberd_commands{name = Name, tags = Tags} <- get_commands_definition(Version), + not lists:member(internal, Tags) ], Dict = lists:foldl( - fun({CommandNameAtom, CTags}, D) -> - CommandName = atom_to_list(CommandNameAtom), - case CTags of - [] -> - orddict:append("untagged", CommandName, D); - _ -> - lists:foldl( - fun(TagAtom, DD) -> - Tag = atom_to_list(TagAtom), - orddict:append(Tag, CommandName, DD) - end, - D, - CTags) - end - end, - orddict:new(), - CommandTags), + fun({CommandNameAtom, CTags}, D) -> + CommandName = atom_to_list(CommandNameAtom), + case CTags of + [] -> + orddict:append("untagged", CommandName, D); + _ -> + lists:foldl( + fun(TagAtom, DD) -> + Tag = atom_to_list(TagAtom), + orddict:append(Tag, CommandName, DD) + end, + D, + CTags) + end + end, + orddict:new(), + CommandTags), orddict:to_list(Dict). + %% ----------------------------- %% Access verification %% ----------------------------- diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl index 79bfe6147..b8eb5925e 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -33,7 +33,7 @@ -include("ejabberd_commands.hrl"). -define(RAW(V), if HTMLOutput -> fxml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end). --define(TAG_BIN(N), (atom_to_binary(N, latin1))/binary). +-define(TAG_BIN(N), (atom_to_binary(N, latin1)) / binary). -define(TAG_STR(N), atom_to_list(N)). -define(TAG(N), if HTMLOutput -> [<<"<", ?TAG_BIN(N), "/>">>]; true -> md_tag(N, <<"">>) end). -define(TAG(N, V), if HTMLOutput -> [<<"<", ?TAG_BIN(N), ">">>, V, <<"">>]; true -> md_tag(N, V) end). @@ -42,13 +42,13 @@ -define(TAG_R(N, C, V), ?TAG(N, C, ?RAW(V))). -define(SPAN(N, V), ?TAG_R(span, ??N, V)). --define(STR(A), ?SPAN(str,[<<"\"">>, A, <<"\"">>])). --define(NUM(A), ?SPAN(num,integer_to_binary(A))). --define(FIELD(A), ?SPAN(field,A)). --define(ID(A), ?SPAN(id,A)). --define(OP(A), ?SPAN(op,A)). +-define(STR(A), ?SPAN(str, [<<"\"">>, A, <<"\"">>])). +-define(NUM(A), ?SPAN(num, integer_to_binary(A))). +-define(FIELD(A), ?SPAN(field, A)). +-define(ID(A), ?SPAN(id, A)). +-define(OP(A), ?SPAN(op, A)). -define(ARG(A), ?FIELD(atom_to_list(A))). --define(KW(A), ?SPAN(kw,A)). +-define(KW(A), ?SPAN(kw, A)). -define(BR, <<"\n">>). -define(ARG_S(A), ?STR(atom_to_list(A))). @@ -63,12 +63,16 @@ -define(STR_A(A), ?STR(atom_to_list(A))). -define(ID_A(A), ?ID(atom_to_list(A))). + list_join_with([], _M) -> []; -list_join_with([El|Tail], M) -> +list_join_with([El | Tail], M) -> lists:reverse(lists:foldl(fun(E, Acc) -> [E, M | Acc] - end, [El], Tail)). + end, + [El], + Tail)). + md_tag(dt, V) -> [<<"- ">>, V]; @@ -91,6 +95,7 @@ md_tag('div', V) -> md_tag(_, V) -> V. + perl_gen({Name, integer}, Int, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?NUM(Int)]; perl_gen({Name, string}, Str, _Indent, HTMLOutput) -> @@ -100,27 +105,55 @@ perl_gen({Name, binary}, Str, _Indent, HTMLOutput) -> perl_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?STR_A(Atom)]; perl_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> - Res = lists:map(fun({A,B})->perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), + Res = lists:map(fun({A, B}) -> perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), [?ARG(Name), ?OP_L(" => {"), list_join_with(Res, [?OP_L(", ")]), ?OP_L("}")]; perl_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> Res = lists:map(fun(E) -> [?OP_L("{"), perl_gen(ElDesc, E, Indent, HTMLOutput), ?OP_L("}")] end, List), [?ARG(Name), ?OP_L(" => ["), list_join_with(Res, [?OP_L(", ")]), ?OP_L("]")]. + perl_call(Name, ArgsDesc, Values, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ perl\n">>} end, [Preamble, - Indent, ?ID_L("XMLRPC::Lite"), ?OP_L("->"), ?ID_L("proxy"), ?OP_L("("), ?ID_L("$url"), ?OP_L(")->"), - ?ID_L("call"), ?OP_L("("), ?STR_A(Name), ?OP_L(", {"), ?BR, Indent, <<" ">>, - list_join_with(lists:map(fun({A,B})->perl_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), - ?BR, Indent, ?OP_L("})->"), ?ID_L("results"), ?OP_L("()")]. + Indent, + ?ID_L("XMLRPC::Lite"), + ?OP_L("->"), + ?ID_L("proxy"), + ?OP_L("("), + ?ID_L("$url"), + ?OP_L(")->"), + ?ID_L("call"), + ?OP_L("("), + ?STR_A(Name), + ?OP_L(", {"), + ?BR, + Indent, + <<" ">>, + list_join_with(lists:map(fun({A, B}) -> perl_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), + ?BR, + Indent, + ?OP_L("})->"), + ?ID_L("results"), + ?OP_L("()")]. + java_gen_map(Vals, Indent, HTMLOutput) -> {Split, NL} = case Indent of none -> {<<" ">>, <<" ">>}; _ -> {[?BR, <<" ", Indent/binary>>], [?BR, Indent]} end, - [?KW_L("new "), ?ID_L("HashMap"), ?OP_L("<"), ?ID_L("String"), ?OP_L(", "), ?ID_L("Object"), - ?OP_L(">() {{"), Split, list_join_with(Vals, Split), NL, ?OP_L("}}")]. + [?KW_L("new "), + ?ID_L("HashMap"), + ?OP_L("<"), + ?ID_L("String"), + ?OP_L(", "), + ?ID_L("Object"), + ?OP_L(">() {{"), + Split, + list_join_with(Vals, Split), + NL, + ?OP_L("}}")]. + java_gen({Name, integer}, Int, _Indent, HTMLOutput) -> [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Integer"), ?OP_L("("), ?NUM(Int), ?OP_L("));")]; @@ -136,26 +169,74 @@ java_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), java_gen_map(Res, Indent, HTMLOutput), ?OP_L(")")]; java_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> {NI, NI2, I} = case List of - [_] -> {" ", " ", Indent}; - _ -> {[?BR, <<" ", Indent/binary>>], - [?BR, <<" ", Indent/binary>>], - <<" ", Indent/binary>>} - end, + [_] -> {" ", " ", Indent}; + _ -> + {[?BR, <<" ", Indent/binary>>], + [?BR, <<" ", Indent/binary>>], + <<" ", Indent/binary>>} + end, Res = lists:map(fun(E) -> java_gen_map([java_gen(ElDesc, E, I, HTMLOutput)], none, HTMLOutput) end, List), - [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Object"), ?OP_L("[] {"), NI, - list_join_with(Res, [?OP_L(","), NI]), NI2, ?OP_L("});")]. + [?ID_L("put"), + ?OP_L("("), + ?STR_A(Name), + ?OP_L(", "), + ?KW_L("new "), + ?ID_L("Object"), + ?OP_L("[] {"), + NI, + list_join_with(Res, [?OP_L(","), NI]), + NI2, + ?OP_L("});")]. + java_call(Name, ArgsDesc, Values, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ java\n">>} end, [Preamble, - Indent, ?ID_L("XmlRpcClientConfigImpl config"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClientConfigImpl"), ?OP_L("();"), ?BR, - Indent, ?ID_L("config"), ?OP_L("."), ?ID_L("setServerURL"), ?OP_L("("), ?ID_L("url"), ?OP_L(");"), ?BR, Indent, ?BR, - Indent, ?ID_L("XmlRpcClient client"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClient"), ?OP_L("();"), ?BR, - Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("setConfig"), ?OP_L("("), ?ID_L("config"), ?OP_L(");"), ?BR, Indent, ?BR, - Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("execute"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), - java_gen_map(lists:map(fun({A,B})->java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput), + Indent, + ?ID_L("XmlRpcClientConfigImpl config"), + ?OP_L(" = "), + ?KW_L("new "), + ?ID_L("XmlRpcClientConfigImpl"), + ?OP_L("();"), + ?BR, + Indent, + ?ID_L("config"), + ?OP_L("."), + ?ID_L("setServerURL"), + ?OP_L("("), + ?ID_L("url"), + ?OP_L(");"), + ?BR, + Indent, + ?BR, + Indent, + ?ID_L("XmlRpcClient client"), + ?OP_L(" = "), + ?KW_L("new "), + ?ID_L("XmlRpcClient"), + ?OP_L("();"), + ?BR, + Indent, + ?ID_L("client"), + ?OP_L("."), + ?ID_L("setConfig"), + ?OP_L("("), + ?ID_L("config"), + ?OP_L(");"), + ?BR, + Indent, + ?BR, + Indent, + ?ID_L("client"), + ?OP_L("."), + ?ID_L("execute"), + ?OP_L("("), + ?STR_A(Name), + ?OP_L(", "), + java_gen_map(lists:map(fun({A, B}) -> java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput), ?OP_L(");")]. + -define(XML_S(N, V), ?OP_L("<"), ?FIELD_L(??N), ?OP_L(">"), V). -define(XML_E(N), ?OP_L("")). -define(XML(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?BR, Indent, ?XML_E(N)). @@ -163,51 +244,75 @@ java_call(Name, ArgsDesc, Values, HTMLOutput) -> -define(XML_L(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?XML_E(N)). -define(XML_L(N, Indent, D, V), ?XML_L(N, [Indent, lists:duplicate(D, <<" ">>)], V)). + xml_gen({Name, integer}, Int, Indent, HTMLOutput) -> - [?XML(member, Indent, - [?XML_L(name, Indent, 1, ?ID_A(Name)), - ?XML(value, Indent, 1, - [?XML_L(integer, Indent, 2, ?ID(integer_to_binary(Int)))])])]; + [?XML(member, + Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, + Indent, + 1, + [?XML_L(integer, Indent, 2, ?ID(integer_to_binary(Int)))])])]; xml_gen({Name, string}, Str, Indent, HTMLOutput) -> - [?XML(member, Indent, - [?XML_L(name, Indent, 1, ?ID_A(Name)), - ?XML(value, Indent, 1, - [?XML_L(string, Indent, 2, ?ID(Str))])])]; + [?XML(member, + Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, + Indent, + 1, + [?XML_L(string, Indent, 2, ?ID(Str))])])]; xml_gen({Name, binary}, Str, Indent, HTMLOutput) -> - [?XML(member, Indent, - [?XML_L(name, Indent, 1, ?ID_A(Name)), - ?XML(value, Indent, 1, - [?XML_L(string, Indent, 2, ?ID(Str))])])]; + [?XML(member, + Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, + Indent, + 1, + [?XML_L(string, Indent, 2, ?ID(Str))])])]; xml_gen({Name, atom}, Atom, Indent, HTMLOutput) -> - [?XML(member, Indent, - [?XML_L(name, Indent, 1, ?ID_A(Name)), - ?XML(value, Indent, 1, - [?XML_L(string, Indent, 2, ?ID(atom_to_list(Atom)))])])]; + [?XML(member, + Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, + Indent, + 1, + [?XML_L(string, Indent, 2, ?ID(atom_to_list(Atom)))])])]; xml_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> NewIndent = <<" ", Indent/binary>>, Res = lists:map(fun({A, B}) -> xml_gen(A, B, NewIndent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), - [?XML(member, Indent, - [?XML_L(name, Indent, 1, ?ID_A(Name)), - ?XML(value, Indent, 1, [?XML(struct, NewIndent, Res)])])]; + [?XML(member, + Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, [?XML(struct, NewIndent, Res)])])]; xml_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> Ind1 = <<" ", Indent/binary>>, Ind2 = <<" ", Ind1/binary>>, Res = lists:map(fun(E) -> [?XML(value, Ind1, [?XML(struct, Ind1, 1, xml_gen(ElDesc, E, Ind2, HTMLOutput))])] end, List), - [?XML(member, Indent, - [?XML_L(name, Indent, 1, ?ID_A(Name)), - ?XML(value, Indent, 1, [?XML(array, Indent, 2, [?XML(data, Indent, 3, Res)])])])]. + [?XML(member, + Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, [?XML(array, Indent, 2, [?XML(data, Indent, 3, Res)])])])]. + xml_call(Name, ArgsDesc, Values, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ xml">>} end, Res = lists:map(fun({A, B}) -> xml_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [Preamble, - ?XML(methodCall, Indent, + ?XML(methodCall, + Indent, [?XML_L(methodName, Indent, 1, ?ID_A(Name)), - ?XML(params, Indent, 1, - [?XML(param, Indent, 2, - [?XML(value, Indent, 3, + ?XML(params, + Indent, + 1, + [?XML(param, + Indent, + 2, + [?XML(value, + Indent, + 3, [?XML(struct, Indent, 4, Res)])])])])]. + % [?ARG_S(Name), ?OP_L(": "), ?STR(Str)]; json_gen({_Name, integer}, Int, _Indent, HTMLOutput) -> [?NUM(Int)]; @@ -220,15 +325,22 @@ json_gen({_Name, atom}, Atom, _Indent, HTMLOutput) -> json_gen({_Name, rescode}, Val, _Indent, HTMLOutput) -> [?ID_A(Val == ok orelse Val == true)]; json_gen({_Name, restuple}, {Val, Str}, _Indent, HTMLOutput) -> - [?OP_L("{"), ?STR_L("res"), ?OP_L(": "), ?ID_A(Val == ok orelse Val == true), ?OP_L(", "), - ?STR_L("text"), ?OP_L(": "), ?STR(Str), ?OP_L("}")]; + [?OP_L("{"), + ?STR_L("res"), + ?OP_L(": "), + ?ID_A(Val == ok orelse Val == true), + ?OP_L(", "), + ?STR_L("text"), + ?OP_L(": "), + ?STR(Str), + ?OP_L("}")]; json_gen({_Name, {list, {_, {tuple, [{_, atom}, ValFmt]}}}}, List, Indent, HTMLOutput) -> Indent2 = <<" ", Indent/binary>>, - Res = lists:map(fun({N, V})->[?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List), + Res = lists:map(fun({N, V}) -> [?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List), [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; json_gen({_Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> Indent2 = <<" ", Indent/binary>>, - Res = lists:map(fun({{N, _} = A, B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end, + Res = lists:map(fun({{N, _} = A, B}) -> [?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end, lists:zip(Fields, tuple_to_list(Tuple))), [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> @@ -236,6 +348,7 @@ json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> Res = lists:map(fun(E) -> json_gen(ElDesc, E, Indent2, HTMLOutput) end, List), [?OP_L("["), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("]")]. + json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<"">>, <<"~~~ json\n">>} end, {Code, ResultStr} = case {ResultDesc, Result} of @@ -255,23 +368,40 @@ json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> 500 -> <<" 500 Internal Server Error">> end, [Preamble, - Indent, ?ID_L("POST /api/"), ?ID_A(Name), ?BR, - Indent, ?OP_L("{"), ?BR, Indent, <<" ">>, - list_join_with(lists:map(fun({{N,_}=A,B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, <>, HTMLOutput)] end, - lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), - ?BR, Indent, ?OP_L("}"), ?BR, Indent, ?BR, Indent, - ?ID_L("HTTP/1.1"), ?ID(CodeStr), ?BR, Indent, - ResultStr - ]. + Indent, + ?ID_L("POST /api/"), + ?ID_A(Name), + ?BR, + Indent, + ?OP_L("{"), + ?BR, + Indent, + <<" ">>, + list_join_with(lists:map(fun({{N, _} = A, B}) -> [?STR_A(N), ?OP_L(": "), json_gen(A, B, <>, HTMLOutput)] end, + lists:zip(ArgsDesc, Values)), + [?OP_L(","), ?BR, Indent, <<" ">>]), + ?BR, + Indent, + ?OP_L("}"), + ?BR, + Indent, + ?BR, + Indent, + ?ID_L("HTTP/1.1"), + ?ID(CodeStr), + ?BR, + Indent, + ResultStr]. + generate_example_input({_Name, integer}, {LastStr, LastNum}) -> - {LastNum+1, {LastStr, LastNum+1}}; + {LastNum + 1, {LastStr, LastNum + 1}}; generate_example_input({_Name, string}, {LastStr, LastNum}) -> - {string:chars(LastStr+1, 5), {LastStr+1, LastNum}}; + {string:chars(LastStr + 1, 5), {LastStr + 1, LastNum}}; generate_example_input({_Name, binary}, {LastStr, LastNum}) -> - {iolist_to_binary(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; + {iolist_to_binary(string:chars(LastStr + 1, 5)), {LastStr + 1, LastNum}}; generate_example_input({_Name, atom}, {LastStr, LastNum}) -> - {list_to_atom(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; + {list_to_atom(string:chars(LastStr + 1, 5)), {LastStr + 1, LastNum}}; generate_example_input({_Name, rescode}, {LastStr, LastNum}) -> {ok, {LastStr, LastNum}}; generate_example_input({_Name, restuple}, {LastStr, LastNum}) -> @@ -280,68 +410,84 @@ generate_example_input({_Name, {tuple, Fields}}, Data) -> {R, D} = lists:foldl(fun(Field, {Res2, Data2}) -> {Res3, Data3} = generate_example_input(Field, Data2), {[Res3 | Res2], Data3} - end, {[], Data}, Fields), + end, + {[], Data}, + Fields), {list_to_tuple(lists:reverse(R)), D}; generate_example_input({_Name, {list, Desc}}, Data) -> {R1, D1} = generate_example_input(Desc, Data), {R2, D2} = generate_example_input(Desc, D1), {[R1, R2], D2}. -gen_calls(#ejabberd_commands{args_example=none, args=ArgsDesc} = C, HTMLOutput, Langs) -> + +gen_calls(#ejabberd_commands{args_example = none, args = ArgsDesc} = C, HTMLOutput, Langs) -> {R, _} = lists:foldl(fun(Arg, {Res, Data}) -> {Res3, Data3} = generate_example_input(Arg, Data), {[Res3 | Res], Data3} - end, {[], {$a-1, 0}}, ArgsDesc), - gen_calls(C#ejabberd_commands{args_example=lists:reverse(R)}, HTMLOutput, Langs); -gen_calls(#ejabberd_commands{result_example=none, result=ResultDesc} = C, HTMLOutput, Langs) -> - {R, _} = generate_example_input(ResultDesc, {$a-1, 0}), - gen_calls(C#ejabberd_commands{result_example=R}, HTMLOutput, Langs); -gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc, - result_example=Result, result=ResultDesc, - name=Name}, HTMLOutput, Langs) -> + end, + {[], {$a - 1, 0}}, + ArgsDesc), + gen_calls(C#ejabberd_commands{args_example = lists:reverse(R)}, HTMLOutput, Langs); +gen_calls(#ejabberd_commands{result_example = none, result = ResultDesc} = C, HTMLOutput, Langs) -> + {R, _} = generate_example_input(ResultDesc, {$a - 1, 0}), + gen_calls(C#ejabberd_commands{result_example = R}, HTMLOutput, Langs); +gen_calls(#ejabberd_commands{ + args_example = Values, + args = ArgsDesc, + result_example = Result, + result = ResultDesc, + name = Name + }, + HTMLOutput, + Langs) -> Perl = perl_call(Name, ArgsDesc, Values, HTMLOutput), Java = java_call(Name, ArgsDesc, Values, HTMLOutput), XML = xml_call(Name, ArgsDesc, Values, HTMLOutput), JSON = json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput), - if HTMLOutput -> - [?TAG(ul, "code-samples-names", + if + HTMLOutput -> + [?TAG(ul, + "code-samples-names", [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, <<"Java">>); _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, <<"Perl">>); _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, <<"XML">>); _ -> [] end, case lists:member(<<"json">>, Langs) of true -> ?TAG(li, <<"JSON">>); _ -> [] end]), - ?TAG(ul, "code-samples", + ?TAG(ul, + "code-samples", [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, ?TAG(pre, Java)); _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, ?TAG(pre, Perl)); _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, ?TAG(pre, XML)); _ -> [] end, case lists:member(<<"json">>, Langs) of true -> ?TAG(li, ?TAG(pre, JSON)); _ -> [] end])]; - true -> - case Langs of - Val when length(Val) == 0 orelse length(Val) == 1 -> - [case lists:member(<<"java">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"perl">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"json">>, Langs) of true -> [<<"\n">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, - <<"\n\n">>]; - _ -> - [<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, - case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, - case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, - case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, - <<"{: .code-samples-labels}\n">>, - case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, - case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, - <<"{: .code-samples-tabs}\n\n">>] - end + true -> + case Langs of + Val when length(Val) == 0 orelse length(Val) == 1 -> + [case lists:member(<<"java">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"\n\n">>]; + _ -> + [<<"\n">>, + case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, + <<"{: .code-samples-labels}\n">>, + case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"{: .code-samples-tabs}\n\n">>] + end end. + format_type({list, {_, {tuple, Els}}}) -> io_lib:format("[~ts]", [format_type({tuple, Els})]); format_type({list, El}) -> io_lib:format("[~ts]", [format_type(El)]); format_type({tuple, Els}) -> - Args = [format_type(El) || El <- Els], + Args = [ format_type(El) || El <- Els ], io_lib:format("{~ts}", [string:join(Args, ", ")]); format_type({Name, Type}) -> io_lib:format("~ts::~ts", [Name, format_type(Type)]); @@ -352,68 +498,94 @@ format_type(atom) -> format_type(Type) -> io_lib:format("~p", [Type]). + gen_param(Name, Type, undefined, HTMLOutput) -> [?TAG(li, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))])]; gen_param(Name, Type, Desc, HTMLOutput) -> [?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]), ?TAG(dd, ?RAW(Desc))]. + make_tags(HTMLOutput) -> TagsList = ejabberd_commands:get_tags_commands(1000000), lists:map(fun(T) -> gen_tags(T, HTMLOutput) end, TagsList). --dialyzer({no_match, gen_tags/2}). -gen_tags({TagName, Commands}, HTMLOutput) -> - [?TAG(h1, TagName) | [?TAG(p, ?RAW("* _`"++C++"`_")) || C <- Commands]]. -gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, - args=Args, args_desc=ArgsDesc, note=Note, definer=Definer, - result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) -> +-dialyzer({no_match, gen_tags/2}). + + +gen_tags({TagName, Commands}, HTMLOutput) -> + [?TAG(h1, TagName) | [ ?TAG(p, ?RAW("* _`" ++ C ++ "`_")) || C <- Commands ]]. + + +gen_doc(#ejabberd_commands{ + name = Name, + tags = Tags, + desc = Desc, + longdesc = LongDesc, + args = Args, + args_desc = ArgsDesc, + note = Note, + definer = Definer, + result = Result, + result_desc = ResultDesc + } = Cmd, + HTMLOutput, + Langs) -> try ArgsText = case ArgsDesc of none -> - [?TAG(ul, "args-list", [gen_param(AName, Type, undefined, HTMLOutput) - || {AName, Type} <- Args])]; + [?TAG(ul, + "args-list", + [ gen_param(AName, Type, undefined, HTMLOutput) + || {AName, Type} <- Args ])]; _ -> - [?TAG(dl, "args-list", [gen_param(AName, Type, ADesc, HTMLOutput) - || {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc)])] + [?TAG(dl, + "args-list", + [ gen_param(AName, Type, ADesc, HTMLOutput) + || {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc) ])] end, ResultText = case Result of - {res,rescode} -> - [?TAG(dl, [gen_param(res, integer, - "Status code (`0` on success, `1` otherwise)", - HTMLOutput)])]; - {res,restuple} -> - [?TAG(dl, [gen_param(res, string, - "Raw result string", - HTMLOutput)])]; - {RName, Type} -> - case ResultDesc of - none -> - [?TAG(ul, [gen_param(RName, Type, undefined, HTMLOutput)])]; - _ -> - [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] - end + {res, rescode} -> + [?TAG(dl, + [gen_param(res, + integer, + "Status code (`0` on success, `1` otherwise)", + HTMLOutput)])]; + {res, restuple} -> + [?TAG(dl, + [gen_param(res, + string, + "Raw result string", + HTMLOutput)])]; + {RName, Type} -> + case ResultDesc of + none -> + [?TAG(ul, [gen_param(RName, Type, undefined, HTMLOutput)])]; + _ -> + [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] + end end, - TagsText = ?RAW(string:join(["_`"++atom_to_list(Tag)++"`_" || Tag <- Tags], ", ")), + TagsText = ?RAW(string:join([ "_`" ++ atom_to_list(Tag) ++ "`_" || Tag <- Tags ], ", ")), IsDefinerMod = case Definer of - unknown -> false; - _ -> lists:member(gen_mod, lists:flatten(proplists:get_all_values(behaviour, Definer:module_info(attributes)))) - end, + unknown -> false; + _ -> lists:member(gen_mod, lists:flatten(proplists:get_all_values(behaviour, Definer:module_info(attributes)))) + end, ModuleText = case IsDefinerMod of - true -> - [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("_`"++atom_to_list(Definer)++"`_"))]; - false -> - [] - end, + true -> + [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("_`" ++ atom_to_list(Definer) ++ "`_"))]; + false -> + [] + end, NoteEl = case Note of - "" -> []; - _ -> ?TAG('div', "note-down", ?RAW(Note)) - end, + "" -> []; + _ -> ?TAG('div', "note-down", ?RAW(Note)) + end, {NotePre, NotePost} = - if HTMLOutput -> {[], NoteEl}; - true -> {NoteEl, []} - end, + if + HTMLOutput -> {[], NoteEl}; + true -> {NoteEl, []} + end, [?TAG(h1, make_command_name(Name, Note)), NotePre, @@ -423,18 +595,21 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, _ -> ?TAG(p, ?RAW(LongDesc)) end, NotePost, - ?TAG(h2, <<"Arguments:">>), ArgsText, - ?TAG(h2, <<"Result:">>), ResultText, - ?TAG(h2, <<"Tags:">>), ?TAG(p, TagsText)] - ++ ModuleText ++ [ - ?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)] + ?TAG(h2, <<"Arguments:">>), + ArgsText, + ?TAG(h2, <<"Result:">>), + ResultText, + ?TAG(h2, <<"Tags:">>), + ?TAG(p, TagsText)] ++ + ModuleText ++ [?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)] catch - _:Ex -> - throw(iolist_to_binary(io_lib:format( - <<"Error when generating documentation for command '~p': ~p">>, - [Name, Ex]))) + _:Ex -> + throw(iolist_to_binary(io_lib:format( + <<"Error when generating documentation for command '~p': ~p">>, + [Name, Ex]))) end. + get_version_mark("") -> ""; get_version_mark(Note) -> @@ -445,33 +620,39 @@ get_version_mark(Note) -> _ -> " 🟤" end. + make_command_name(Name, Note) -> atom_to_list(Name) ++ get_version_mark(Note). + find_commands_definitions() -> lists:flatmap( - fun(Mod) -> - code:ensure_loaded(Mod), - Cs = case erlang:function_exported(Mod, get_commands_spec, 0) of - true -> - apply(Mod, get_commands_spec, []); - _ -> - [] - end, - [C#ejabberd_commands{definer = Mod} || C <- Cs] - end, ejabberd_config:beams(all)). + fun(Mod) -> + code:ensure_loaded(Mod), + Cs = case erlang:function_exported(Mod, get_commands_spec, 0) of + true -> + apply(Mod, get_commands_spec, []); + _ -> + [] + end, + [ C#ejabberd_commands{definer = Mod} || C <- Cs ] + end, + ejabberd_config:beams(all)). + generate_html_output(File, RegExp, Languages) -> Cmds = find_commands_definitions(), {ok, RE} = re:compile(RegExp), - Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> + Cmds2 = lists:filter(fun(#ejabberd_commands{name = Name, module = Module}) -> re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse - re:run(atom_to_list(Module), RE, [{capture, none}]) == match - end, Cmds), - Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> + re:run(atom_to_list(Module), RE, [{capture, none}]) == match + end, + Cmds), + Cmds3 = lists:sort(fun(#ejabberd_commands{name = N1}, #ejabberd_commands{name = N2}) -> N1 =< N2 - end, Cmds2), - Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], + end, + Cmds2), + Cmds4 = [ maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3 ], Langs = binary:split(Languages, <<",">>, [global]), Out = lists:map(fun(C) -> gen_doc(C, true, Langs) end, Cmds4), {ok, Fh} = file:open(File, [write]), @@ -479,52 +660,64 @@ generate_html_output(File, RegExp, Languages) -> file:close(Fh), ok. -maybe_add_policy_arguments(#ejabberd_commands{args=Args1, policy=user}=Cmd) -> + +maybe_add_policy_arguments(#ejabberd_commands{args = Args1, policy = user} = Cmd) -> Args2 = [{user, binary}, {host, binary} | Args1], Cmd#ejabberd_commands{args = Args2}; maybe_add_policy_arguments(Cmd) -> Cmd. + generate_md_output(File, <<"runtime">>, Languages) -> Cmds = lists:map(fun({N, _, _}) -> ejabberd_commands:get_command_definition(N) - end, ejabberd_commands:list_commands()), + end, + ejabberd_commands:list_commands()), generate_md_output(File, <<".">>, Languages, Cmds); generate_md_output(File, RegExp, Languages) -> Cmds = find_commands_definitions(), generate_md_output(File, RegExp, Languages, Cmds). + generate_md_output(File, RegExp, Languages, Cmds) -> {ok, RE} = re:compile(RegExp), - Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> + Cmds2 = lists:filter(fun(#ejabberd_commands{name = Name, module = Module}) -> re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse - re:run(atom_to_list(Module), RE, [{capture, none}]) == match - end, Cmds), - Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> + re:run(atom_to_list(Module), RE, [{capture, none}]) == match + end, + Cmds), + Cmds3 = lists:sort(fun(#ejabberd_commands{name = N1}, #ejabberd_commands{name = N2}) -> N1 =< N2 - end, Cmds2), - Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], + end, + Cmds2), + Cmds4 = [ maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3 ], Langs = binary:split(Languages, <<",">>, [global]), Version = binary_to_list(ejabberd_config:version()), Header = ["# API Reference\n\n" - "This section describes API commands of ejabberd ", Version, ". " - "The commands that changed in this version are marked with 🟤.\n\n"], + "This section describes API commands of ejabberd ", + Version, + ". " + "The commands that changed in this version are marked with 🟤.\n\n"], Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds4), {ok, Fh} = file:open(File, [write, {encoding, utf8}]), io:format(Fh, "~ts~ts", [Header, Out]), file:close(Fh), ok. + generate_tags_md(File) -> Version = binary_to_list(ejabberd_config:version()), Header = ["# API Tags\n\n" - "This section enumerates the API tags of ejabberd ", Version, ". \n\n"], + "This section enumerates the API tags of ejabberd ", + Version, + ". \n\n"], Tags = make_tags(false), {ok, Fh} = file:open(File, [write, {encoding, utf8}]), io:format(Fh, "~ts~ts", [Header, Tags]), file:close(Fh), ok. + html_pre() -> " @@ -650,8 +843,9 @@ html_pre() -> } ". + html_post() -> -"">> - ] ++ PluginsHtml ++ [ - <<"">>, - <<"">>, - <<"">>, - <<"">>, - <<"">>]}; + fxml:crypt(CSS), + <<"'>">>, + <<"">>] ++ PluginsHtml ++ [<<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>]}; process(LocalPath, #request{host = Host}) -> case is_served_file(LocalPath) of true -> serve(Host, LocalPath); false -> ejabberd_web:error(not_found) end. + %%---------------------------------------------------------------------- %% File server %%---------------------------------------------------------------------- + is_served_file([<<"converse.min.js">>]) -> true; is_served_file([<<"converse.min.css">>]) -> true; is_served_file([<<"converse.min.js.map">>]) -> true; @@ -120,6 +139,7 @@ is_served_file([<<"webfonts">>, _]) -> true; is_served_file([<<"plugins">>, _]) -> true; is_served_file(_) -> false. + serve(Host, LocalPath) -> case get_conversejs_resources(Host) of undefined -> @@ -128,14 +148,17 @@ serve(Host, LocalPath) -> MainPath -> serve2(LocalPath, MainPath) end. + get_conversejs_resources(Host) -> Opts = gen_mod:get_module_opts(Host, ?MODULE), mod_conversejs_opt:conversejs_resources(Opts). + %% Copied from mod_muc_log_http.erl + serve2(LocalPathBin, MainPathBin) -> - LocalPath = [binary_to_list(LPB) || LPB <- LocalPathBin], + LocalPath = [ binary_to_list(LPB) || LPB <- LocalPathBin ], MainPath = binary_to_list(MainPathBin), FileName = filename:join(filename:split(MainPath) ++ LocalPath), case file:read_file(FileName) of @@ -155,20 +178,23 @@ serve2(LocalPathBin, MainPathBin) -> end end. + content_type(Filename) -> case string:to_lower(filename:extension(Filename)) of - ".css" -> "text/css"; - ".js" -> "text/javascript"; - ".map" -> "application/json"; - ".ttf" -> "font/ttf"; - ".woff" -> "font/woff"; - ".woff2" -> "font/woff2" + ".css" -> "text/css"; + ".js" -> "text/javascript"; + ".map" -> "application/json"; + ".ttf" -> "font/ttf"; + ".woff" -> "font/woff"; + ".woff2" -> "font/woff2" end. + %%---------------------------------------------------------------------- %% Options parsing %%---------------------------------------------------------------------- + get_auth_options(Domain) -> case {ejabberd_auth_anonymous:is_login_anonymous_enabled(Domain), ejabberd_auth_anonymous:is_sasl_anonymous_enabled(Domain)} of @@ -181,6 +207,7 @@ get_auth_options(Domain) -> {<<"jid">>, Domain}] end. + get_register_options(Server) -> AuthSupportsRegister = lists:any( @@ -194,15 +221,19 @@ get_register_options(Server) -> ModRegisterAllowsMe = (Modules == all) orelse lists:member(?MODULE, Modules), [{<<"allow_registration">>, AuthSupportsRegister and ModRegisterAllowsMe}]. + get_register_modules(Server) -> - try mod_register_opt:allow_modules(Server) + try + mod_register_opt:allow_modules(Server) catch error:{module_not_loaded, mod_register, _} -> ?DEBUG("mod_conversejs couldn't get mod_register configuration for " - "vhost ~p: module not loaded in that vhost.", [Server]), + "vhost ~p: module not loaded in that vhost.", + [Server]), [] end. + get_extra_options(Host) -> RawOpts = gen_mod:get_module_opt(Host, ?MODULE, conversejs_options), lists:map(fun({Name, <<"true">>}) -> {Name, true}; @@ -214,6 +245,7 @@ get_extra_options(Host) -> end, RawOpts). + get_file_url(Host, Option, Filename, Default) -> FileRaw = case gen_mod:get_module_opt(Host, ?MODULE, Option) of auto -> get_auto_file_url(Host, Filename, Default); @@ -221,41 +253,47 @@ get_file_url(Host, Option, Filename, Default) -> end, misc:expand_keyword(<<"@HOST@">>, FileRaw, Host). + get_auto_file_url(Host, Filename, Default) -> case get_conversejs_resources(Host) of undefined -> Default; _ -> Filename end. + get_plugins_html(Host, RawPath) -> Resources = get_conversejs_resources(Host), lists:map(fun(F) -> - Plugin = - case {F, Resources} of - {<<"libsignal">>, undefined} -> - <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>; - {<<"libsignal">>, Path} -> - ?WARNING_MSG("~p is configured to use local Converse files " - "from path ~ts but the public plugin ~ts!", - [?MODULE, Path, F]), - <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>; - _ -> - fxml:crypt(<>) - end, - <<"">> + Plugin = + case {F, Resources} of + {<<"libsignal">>, undefined} -> + <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>; + {<<"libsignal">>, Path} -> + ?WARNING_MSG("~p is configured to use local Converse files " + "from path ~ts but the public plugin ~ts!", + [?MODULE, Path, F]), + <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>; + _ -> + fxml:crypt(<>) + end, + <<"">> end, gen_mod:get_module_opt(Host, ?MODULE, conversejs_plugins)). + %%---------------------------------------------------------------------- %% WebAdmin link and autologin %%---------------------------------------------------------------------- %% @format-begin + web_menu_system(Result, - #request{host = Host, - auth = Auth, - tp = Protocol}) -> + #request{ + host = Host, + auth = Auth, + tp = Protocol + }) -> AutoUrl = mod_host_meta:get_auto_url(any, ?MODULE), ConverseUrl = misc:expand_keyword(<<"@HOST@">>, AutoUrl, Host), AutologinQuery = @@ -276,10 +314,10 @@ web_menu_system(Result, [?C(unicode:characters_to_binary("Converse"))])]), [ConverseEl | Result]. + get_autologin_options(Query) -> case {proplists:get_value(<<"autologinjid">>, Query), - proplists:get_value(<<"autologintoken">>, Query)} - of + proplists:get_value(<<"autologintoken">>, Query)} of {undefined, _} -> []; {Jid, Token} -> @@ -288,16 +326,18 @@ get_autologin_options(Query) -> {<<"password">>, check_token_get_password(Jid, Token)}] end. + build_token(Jid, Password) -> Minutes = integer_to_binary(calendar:datetime_to_gregorian_seconds( - calendar:universal_time()) - div 60), + calendar:universal_time()) div + 60), Cookie = misc:atom_to_binary( - erlang:get_cookie()), + erlang:get_cookie()), str:sha(<>). + check_token_get_password(_, undefined) -> <<"">>; check_token_get_password(JidString, TokenProvided) -> @@ -311,10 +351,12 @@ check_token_get_password(JidString, TokenProvided) -> end. %% @format-end + %%---------------------------------------------------------------------- %% %%---------------------------------------------------------------------- + mod_opt_type(bosh_service_url) -> econf:either(auto, econf:binary()); mod_opt_type(websocket_url) -> @@ -332,6 +374,7 @@ mod_opt_type(conversejs_plugins) -> mod_opt_type(default_domain) -> econf:host(). + mod_options(Host) -> [{bosh_service_url, auto}, {websocket_url, auto}, @@ -342,19 +385,24 @@ mod_options(Host) -> {conversejs_plugins, []}, {conversejs_css, auto}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module serves a simple page for the " - "https://conversejs.org/[Converse] XMPP web browser client."), "", + "https://conversejs.org/[Converse] XMPP web browser client."), + "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " - "_`listen-options.md#request_handlers|request_handlers`_."), "", + "_`listen-options.md#request_handlers|request_handlers`_."), + "", ?T("Make sure either _`mod_bosh`_ or _`listen.md#ejabberd_http_ws|ejabberd_http_ws`_ " - "are enabled in at least one 'request_handlers'."), "", + "are enabled in at least one 'request_handlers'."), + "", ?T("When 'conversejs_css' and 'conversejs_script' are 'auto', " - "by default they point to the public Converse client."), "", - ?T("This module is available since ejabberd 21.12.") - ], + "by default they point to the public Converse client."), + "", + ?T("This module is available since ejabberd 21.12.")], note => "improved in 25.07", example => [{?T("Manually setup WebSocket url, and use the public Converse client:"), @@ -397,66 +445,81 @@ mod_doc() -> " i18n: \"pt\"", " locked_domain: \"@HOST@\"", " message_archiving: always", - " theme: dracula"]} - ], + " theme: dracula"]}], opts => [{websocket_url, - #{value => ?T("auto | WebSocketURL"), + #{ + value => ?T("auto | WebSocketURL"), desc => ?T("A WebSocket URL to which Converse can connect to. " "The '@HOST@' keyword is replaced with the real virtual " "host name. " "If set to 'auto', it will build the URL of the first " "configured WebSocket request handler. " - "The default value is 'auto'.")}}, + "The default value is 'auto'.") + }}, {bosh_service_url, - #{value => ?T("auto | BoshURL"), + #{ + value => ?T("auto | BoshURL"), desc => ?T("BOSH service URL to which Converse can connect to. " "The keyword '@HOST@' is replaced with the real " "virtual host name. " "If set to 'auto', it will build the URL of the first " "configured BOSH request handler. " - "The default value is 'auto'.")}}, + "The default value is 'auto'.") + }}, {default_domain, - #{value => ?T("Domain"), + #{ + value => ?T("Domain"), desc => ?T("Specify a domain to act as the default for user JIDs. " "The keyword '@HOST@' is replaced with the hostname. " - "The default value is '@HOST@'.")}}, + "The default value is '@HOST@'.") + }}, {conversejs_resources, - #{value => ?T("Path"), + #{ + value => ?T("Path"), note => "added in 22.05", desc => ?T("Local path to the Converse files. " - "If not set, the public Converse client will be used instead.")}}, + "If not set, the public Converse client will be used instead.") + }}, {conversejs_options, - #{value => "{Name: Value}", + #{ + value => "{Name: Value}", note => "added in 22.05", desc => ?T("Specify additional options to be passed to Converse. " "See https://conversejs.org/docs/html/configuration.html[Converse configuration]. " "Only boolean, integer and string values are supported; " - "lists are not supported.")}}, + "lists are not supported.") + }}, {conversejs_plugins, - #{value => ?T("[Filename]"), + #{ + value => ?T("[Filename]"), desc => ?T("List of additional local files to include as scripts in the homepage. " "Please make sure those files are available in the path specified in " "'conversejs_resources' option, in subdirectory 'plugins/'. " "If using the public Converse client, then '\"libsignal\"' " "gets replaced with the URL of the public library. " - "The default value is '[]'.")}}, + "The default value is '[]'.") + }}, {conversejs_script, - #{value => ?T("auto | URL"), + #{ + value => ?T("auto | URL"), desc => ?T("Converse main script URL. " "The keyword '@HOST@' is replaced with the hostname. " - "The default value is 'auto'.")}}, + "The default value is 'auto'.") + }}, {conversejs_css, - #{value => ?T("auto | URL"), + #{ + value => ?T("auto | URL"), desc => ?T("Converse CSS URL. " "The keyword '@HOST@' is replaced with the hostname. " - "The default value is 'auto'.")}}] + "The default value is 'auto'.") + }}] }. diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl index 37deac7ef..6d49570b9 100644 --- a/src/mod_conversejs_opt.erl +++ b/src/mod_conversejs_opt.erl @@ -12,51 +12,58 @@ -export([default_domain/1]). -export([websocket_url/1]). + -spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'auto' | 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()) -> 'auto' | 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_options(gen_mod:opts() | global | binary()) -> [{binary(),binary() | integer()}]. + +-spec conversejs_options(gen_mod:opts() | global | binary()) -> [{binary(), binary() | integer()}]. conversejs_options(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_options, Opts); conversejs_options(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, conversejs_options). + -spec conversejs_plugins(gen_mod:opts() | global | binary()) -> [binary()]. conversejs_plugins(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_plugins, Opts); conversejs_plugins(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, conversejs_plugins). + -spec conversejs_resources(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). conversejs_resources(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_resources, Opts); conversejs_resources(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, conversejs_resources). + -spec conversejs_script(gen_mod:opts() | global | binary()) -> 'auto' | 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()) -> 'auto' | binary(). websocket_url(Opts) when is_map(Opts) -> gen_mod:get_opt(websocket_url, Opts); websocket_url(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, websocket_url). - diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl index f6879ea7f..969b8f355 100644 --- a/src/mod_delegation.erl +++ b/src/mod_delegation.erl @@ -34,229 +34,301 @@ -export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2, mod_options/1]). -export([mod_doc/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). --export([component_connected/1, component_disconnected/2, - ejabberd_local/1, ejabberd_sm/1, decode_iq_subel/1, - disco_local_features/5, disco_sm_features/5, - disco_local_identity/5, disco_sm_identity/5]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). +-export([component_connected/1, + component_disconnected/2, + ejabberd_local/1, + ejabberd_sm/1, + decode_iq_subel/1, + disco_local_features/5, + disco_sm_features/5, + disco_local_identity/5, + disco_sm_identity/5]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -type route_type() :: ejabberd_sm | ejabberd_local. -type delegations() :: #{{binary(), route_type()} => {binary(), disco_info()}}. -record(state, {server_host = <<"">> :: binary()}). + %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(_Host, _NewOpts, _OldOpts) -> ok. + mod_opt_type(namespaces) -> econf:and_then( econf:map( - econf:binary(), - econf:options( - #{filtering => econf:list(econf:binary()), - access => econf:acl()})), + econf:binary(), + econf:options( + #{ + filtering => econf:list(econf:binary()), + access => econf:acl() + })), fun(L) -> - lists:map( - fun({NS, Opts}) -> - Attrs = proplists:get_value(filtering, Opts, []), - Access = proplists:get_value(access, Opts, none), - {NS, Attrs, Access} - end, L) + lists:map( + fun({NS, Opts}) -> + Attrs = proplists:get_value(filtering, Opts, []), + Access = proplists:get_value(access, Opts, none), + {NS, Attrs, Access} + end, + L) end). + -spec mod_options(binary()) -> [{namespaces, - [{binary(), [binary()], acl:acl()}]} | - {atom(), term()}]. + [{binary(), [binary()], acl:acl()}]} | + {atom(), term()}]. mod_options(_Host) -> [{namespaces, []}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module is an implementation of " - "https://xmpp.org/extensions/xep-0355.html" - "[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."), "", - ?T("WARNING: Security issue: Namespace delegation gives components " - "access to sensitive data, so permission should be granted " - "carefully, only if you trust the component."), "", - ?T("NOTE: This module is complementary to _`mod_privilege`_ but can " - "also be used separately.")], + "https://xmpp.org/extensions/xep-0355.html" + "[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."), + "", + ?T("WARNING: Security issue: Namespace delegation gives components " + "access to sensitive data, so permission should be granted " + "carefully, only if you trust the component."), + "", + ?T("NOTE: This module is complementary to _`mod_privilege`_ but can " + "also be used separately.")], opts => [{namespaces, - #{value => "{Namespace: Options}", + #{ + value => "{Namespace: Options}", desc => ?T("If you want to delegate namespaces to a component, " "specify them in this option, and associate them " - "to an access rule. The 'Options' are:")}, + "to an access rule. The 'Options' are:") + }, [{filtering, - #{value => ?T("Attributes"), + #{ + value => ?T("Attributes"), desc => - ?T("The list of attributes. Currently not used.")}}, + ?T("The list of attributes. Currently not used.") + }}, {access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("The option defines which components are allowed " - "for namespace delegation. The default value is 'none'.")}}]}], + "for namespace delegation. The default value is 'none'.") + }}]}], example => - [{?T("Make sure you do not delegate the same namespace to several " - "services at the same time. As in the example provided later, " - "to have the 'sat-pubsub.example.org' component perform " - "correctly disable the _`mod_pubsub`_ module."), - ["access_rules:", - " external_pubsub:", - " allow: external_component", - " external_mam:", - " allow: external_component", - "", - "acl:", - " external_component:", - " server: sat-pubsub.example.org", - "", - "modules:", - " mod_delegation:", - " namespaces:", - " urn:xmpp:mam:1:", - " access: external_mam", - " http://jabber.org/protocol/pubsub:", - " access: external_pubsub"]}]}. + [{?T("Make sure you do not delegate the same namespace to several " + "services at the same time. As in the example provided later, " + "to have the 'sat-pubsub.example.org' component perform " + "correctly disable the _`mod_pubsub`_ module."), + ["access_rules:", + " external_pubsub:", + " allow: external_component", + " external_mam:", + " allow: external_component", + "", + "acl:", + " external_component:", + " server: sat-pubsub.example.org", + "", + "modules:", + " mod_delegation:", + " namespaces:", + " urn:xmpp:mam:1:", + " access: external_mam", + " http://jabber.org/protocol/pubsub:", + " access: external_pubsub"]}] + }. + depends(_, _) -> []. + -spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel(). %% Tell gen_iq_handler not to auto-decode IQ payload decode_iq_subel(El) -> El. + -spec component_connected(binary()) -> ok. component_connected(Host) -> lists:foreach( fun(ServerHost) -> - Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), - gen_server:cast(Proc, {component_connected, Host}) - end, ejabberd_option:hosts()). + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_connected, Host}) + end, + ejabberd_option:hosts()). + -spec component_disconnected(binary(), binary()) -> ok. component_disconnected(Host, _Reason) -> lists:foreach( fun(ServerHost) -> - Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), - gen_server:cast(Proc, {component_disconnected, Host}) - end, ejabberd_option:hosts()). + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_disconnected, Host}) + end, + ejabberd_option:hosts()). + -spec ejabberd_local(iq()) -> iq(). ejabberd_local(IQ) -> process_iq(IQ, ejabberd_local). + -spec ejabberd_sm(iq()) -> iq(). ejabberd_sm(IQ) -> process_iq(IQ, ejabberd_sm). --spec disco_local_features(mod_disco:features_acc(), jid(), jid(), - binary(), binary()) -> mod_disco:features_acc(). + +-spec disco_local_features(mod_disco:features_acc(), + jid(), + jid(), + binary(), + binary()) -> mod_disco:features_acc(). disco_local_features(Acc, From, To, Node, Lang) -> disco_features(Acc, From, To, Node, Lang, ejabberd_local). --spec disco_sm_features(mod_disco:features_acc(), jid(), jid(), - binary(), binary()) -> mod_disco:features_acc(). + +-spec disco_sm_features(mod_disco:features_acc(), + jid(), + jid(), + binary(), + binary()) -> mod_disco:features_acc(). disco_sm_features(Acc, From, To, Node, Lang) -> disco_features(Acc, From, To, Node, Lang, ejabberd_sm). + -spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_local_identity(Acc, From, To, Node, Lang) -> disco_identity(Acc, From, To, Node, Lang, ejabberd_local). + -spec disco_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_sm_identity(Acc, From, To, Node, Lang) -> disco_identity(Acc, From, To, Node, Lang, ejabberd_sm). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), catch ets:new(?MODULE, - [named_table, public, + [named_table, + public, {heir, erlang:group_leader(), none}]), - ejabberd_hooks:add(component_connected, ?MODULE, - component_connected, 50), - ejabberd_hooks:add(component_disconnected, ?MODULE, - component_disconnected, 50), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - disco_local_features, 50), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - disco_sm_features, 50), - ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, - disco_local_identity, 50), - ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, - disco_sm_identity, 50), + ejabberd_hooks:add(component_connected, + ?MODULE, + component_connected, + 50), + ejabberd_hooks:add(component_disconnected, + ?MODULE, + component_disconnected, + 50), + ejabberd_hooks:add(disco_local_features, + Host, + ?MODULE, + disco_local_features, + 50), + ejabberd_hooks:add(disco_sm_features, + Host, + ?MODULE, + disco_sm_features, + 50), + ejabberd_hooks:add(disco_local_identity, + Host, + ?MODULE, + disco_local_identity, + 50), + ejabberd_hooks:add(disco_sm_identity, + Host, + ?MODULE, + disco_sm_identity, + 50), {ok, #state{server_host = Host}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast({component_connected, Host}, State) -> ServerHost = State#state.server_host, To = jid:make(Host), NSAttrsAccessList = mod_delegation_opt:namespaces(ServerHost), lists:foreach( fun({NS, _Attrs, Access}) -> - case acl:match_rule(ServerHost, Access, To) of - allow -> - send_disco_queries(ServerHost, Host, NS); - deny -> - ?DEBUG("Denied delegation for ~ts on ~ts", [Host, NS]) - end - end, NSAttrsAccessList), + case acl:match_rule(ServerHost, Access, To) of + allow -> + send_disco_queries(ServerHost, Host, NS); + deny -> + ?DEBUG("Denied delegation for ~ts on ~ts", [Host, NS]) + end + end, + NSAttrsAccessList), {noreply, State}; handle_cast({component_disconnected, Host}, State) -> ServerHost = State#state.server_host, Delegations = - maps:filter( - fun({NS, Type}, {H, _}) when H == Host -> - ?INFO_MSG("Remove delegation of namespace '~ts' " - "from external component '~ts'", - [NS, Host]), - gen_iq_handler:remove_iq_handler(Type, ServerHost, NS), - false; - (_, _) -> - true - end, get_delegations(ServerHost)), + maps:filter( + fun({NS, Type}, {H, _}) when H == Host -> + ?INFO_MSG("Remove delegation of namespace '~ts' " + "from external component '~ts'", + [NS, Host]), + gen_iq_handler:remove_iq_handler(Type, ServerHost, NS), + false; + (_, _) -> + true + end, + get_delegations(ServerHost)), set_delegations(ServerHost, Delegations), {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({iq_reply, ResIQ, {disco_info, Type, Host, NS}}, State) -> case ResIQ of - #iq{type = result, sub_els = [SubEl]} -> - try xmpp:decode(SubEl) of - #disco_info{} = Info -> - ServerHost = State#state.server_host, - process_disco_info(ServerHost, Type, Host, NS, Info) - catch _:{xmpp_codec, _} -> - ok - end; - _ -> - ok + #iq{type = result, sub_els = [SubEl]} -> + try xmpp:decode(SubEl) of + #disco_info{} = Info -> + ServerHost = State#state.server_host, + process_disco_info(ServerHost, Type, Host, NS, Info) + catch + _:{xmpp_codec, _} -> + ok + end; + _ -> + ok end, {noreply, State}; handle_info({iq_reply, ResIQ, #iq{} = IQ}, State) -> @@ -266,91 +338,122 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, State) -> ServerHost = State#state.server_host, case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of - false -> - ejabberd_hooks:delete(component_connected, ?MODULE, - component_connected, 50), - ejabberd_hooks:delete(component_disconnected, ?MODULE, - component_disconnected, 50); - true -> - ok + false -> + ejabberd_hooks:delete(component_connected, + ?MODULE, + component_connected, + 50), + ejabberd_hooks:delete(component_disconnected, + ?MODULE, + component_disconnected, + 50); + true -> + ok end, - ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, - disco_local_features, 50), - ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, - disco_sm_features, 50), - ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, - disco_local_identity, 50), - ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, - disco_sm_identity, 50), + ejabberd_hooks:delete(disco_local_features, + ServerHost, + ?MODULE, + disco_local_features, + 50), + ejabberd_hooks:delete(disco_sm_features, + ServerHost, + ?MODULE, + disco_sm_features, + 50), + ejabberd_hooks:delete(disco_local_identity, + ServerHost, + ?MODULE, + disco_local_identity, + 50), + ejabberd_hooks:delete(disco_sm_identity, + ServerHost, + ?MODULE, + disco_sm_identity, + 50), lists:foreach( fun({NS, Type}) -> - gen_iq_handler:remove_iq_handler(Type, ServerHost, NS) - end, maps:keys(get_delegations(ServerHost))), + gen_iq_handler:remove_iq_handler(Type, ServerHost, NS) + end, + maps:keys(get_delegations(ServerHost))), ets:delete(?MODULE, ServerHost). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_delegations(binary()) -> delegations(). get_delegations(Host) -> - try ets:lookup_element(?MODULE, Host, 2) - catch _:badarg -> #{} + try + ets:lookup_element(?MODULE, Host, 2) + catch + _:badarg -> #{} end. + -spec set_delegations(binary(), delegations()) -> true. set_delegations(ServerHost, Delegations) -> case maps:size(Delegations) of - 0 -> ets:delete(?MODULE, ServerHost); - _ -> ets:insert(?MODULE, {ServerHost, Delegations}) + 0 -> ets:delete(?MODULE, ServerHost); + _ -> ets:insert(?MODULE, {ServerHost, Delegations}) end. + -spec process_iq(iq(), route_type()) -> ignore | iq(). process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) -> LServer = To#jid.lserver, NS = xmpp:get_ns(SubEl), Delegations = get_delegations(LServer), case maps:find({NS, Type}, Delegations) of - {ok, {Host, _}} -> - Delegation = #delegation{ - forwarded = #forwarded{sub_els = [IQ]}}, - NewFrom = jid:make(LServer), - NewTo = jid:make(Host), - ejabberd_router:route_iq( - #iq{type = set, - from = NewFrom, - to = NewTo, - sub_els = [Delegation]}, - IQ, gen_mod:get_module_proc(LServer, ?MODULE)), - ignore; - error -> - Txt = ?T("Failed to map delegated namespace to external component"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + {ok, {Host, _}} -> + Delegation = #delegation{ + forwarded = #forwarded{sub_els = [IQ]} + }, + NewFrom = jid:make(LServer), + NewTo = jid:make(Host), + ejabberd_router:route_iq( + #iq{ + type = set, + from = NewFrom, + to = NewTo, + sub_els = [Delegation] + }, + IQ, + gen_mod:get_module_proc(LServer, ?MODULE)), + ignore; + error -> + Txt = ?T("Failed to map delegated namespace to external component"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. + -spec process_iq_result(iq(), iq()) -> ok. process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ, - #iq{type = result} = ResIQ) -> + #iq{type = result} = ResIQ) -> try - CodecOpts = ejabberd_config:codec_options(), - #delegation{forwarded = #forwarded{sub_els = [SubEl]}} = - xmpp:get_subtag(ResIQ, #delegation{}), - case xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of - #iq{from = To, to = From, type = Type, id = ID} = Reply - when Type == error; Type == result -> - ejabberd_router:route(Reply) - end - catch _:_ -> - ?ERROR_MSG("Got iq-result with invalid delegated " - "payload:~n~ts", [xmpp:pp(ResIQ)]), - Txt = ?T("External component failure"), - Err = xmpp:err_internal_server_error(Txt, Lang), - ejabberd_router:route_error(IQ, Err) + CodecOpts = ejabberd_config:codec_options(), + #delegation{forwarded = #forwarded{sub_els = [SubEl]}} = + xmpp:get_subtag(ResIQ, #delegation{}), + case xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of + #iq{from = To, to = From, type = Type, id = ID} = Reply + when Type == error; Type == result -> + ejabberd_router:route(Reply) + end + catch + _:_ -> + ?ERROR_MSG("Got iq-result with invalid delegated " + "payload:~n~ts", + [xmpp:pp(ResIQ)]), + Txt = ?T("External component failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + ejabberd_router:route_error(IQ, Err) end; process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) -> Err = xmpp:set_from_to(ResIQ, To, From), @@ -360,75 +463,101 @@ process_iq_result(#iq{lang = Lang} = IQ, timeout) -> Err = xmpp:err_internal_server_error(Txt, Lang), ejabberd_router:route_error(IQ, Err). --spec process_disco_info(binary(), route_type(), - binary(), binary(), disco_info()) -> ok. + +-spec process_disco_info(binary(), + route_type(), + binary(), + binary(), + disco_info()) -> ok. process_disco_info(ServerHost, Type, Host, NS, Info) -> From = jid:make(ServerHost), To = jid:make(Host), Delegations = get_delegations(ServerHost), case maps:find({NS, Type}, Delegations) of - error -> - Msg = #message{from = From, to = To, - sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]}, - Delegations1 = maps:put({NS, Type}, {Host, Info}, Delegations), - gen_iq_handler:add_iq_handler(Type, ServerHost, NS, ?MODULE, Type), - ejabberd_router:route(Msg), - set_delegations(ServerHost, Delegations1), - ?INFO_MSG("Namespace '~ts' is delegated to external component '~ts'", - [NS, Host]); - {ok, {AnotherHost, _}} -> - ?WARNING_MSG("Failed to delegate namespace '~ts' to " - "external component '~ts' because it's already " - "delegated to '~ts'", - [NS, Host, AnotherHost]) + error -> + Msg = #message{ + from = From, + to = To, + sub_els = [#delegation{delegated = [#delegated{ns = NS}]}] + }, + Delegations1 = maps:put({NS, Type}, {Host, Info}, Delegations), + gen_iq_handler:add_iq_handler(Type, ServerHost, NS, ?MODULE, Type), + ejabberd_router:route(Msg), + set_delegations(ServerHost, Delegations1), + ?INFO_MSG("Namespace '~ts' is delegated to external component '~ts'", + [NS, Host]); + {ok, {AnotherHost, _}} -> + ?WARNING_MSG("Failed to delegate namespace '~ts' to " + "external component '~ts' because it's already " + "delegated to '~ts'", + [NS, Host, AnotherHost]) end. + -spec send_disco_queries(binary(), binary(), binary()) -> ok. send_disco_queries(LServer, Host, NS) -> From = jid:make(LServer), To = jid:make(Host), lists:foreach( fun({Type, Node}) -> - ejabberd_router:route_iq( - #iq{type = get, from = From, to = To, - sub_els = [#disco_info{node = Node}]}, - {disco_info, Type, Host, NS}, - gen_mod:get_module_proc(LServer, ?MODULE)) - end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>}, - {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]). + ejabberd_router:route_iq( + #iq{ + type = get, + from = From, + to = To, + sub_els = [#disco_info{node = Node}] + }, + {disco_info, Type, Host, NS}, + gen_mod:get_module_proc(LServer, ?MODULE)) + end, + [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>}, + {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]). --spec disco_features(mod_disco:features_acc(), jid(), jid(), binary(), binary(), - route_type()) -> mod_disco:features_acc(). + +-spec disco_features(mod_disco:features_acc(), + jid(), + jid(), + binary(), + binary(), + route_type()) -> mod_disco:features_acc(). disco_features(Acc, _From, To, <<"">>, _Lang, Type) -> Delegations = get_delegations(To#jid.lserver), Features = my_features(Type) ++ - lists:flatmap( - fun({{_, T}, {_, Info}}) when T == Type -> - Info#disco_info.features; - (_) -> - [] - end, maps:to_list(Delegations)), + lists:flatmap( + fun({{_, T}, {_, Info}}) when T == Type -> + Info#disco_info.features; + (_) -> + [] + end, + maps:to_list(Delegations)), case Acc of - empty when Features /= [] -> {result, Features}; - {result, Fs} -> {result, Fs ++ Features}; - _ -> Acc + empty when Features /= [] -> {result, Features}; + {result, Fs} -> {result, Fs ++ Features}; + _ -> Acc end; disco_features(Acc, _, _, _, _, _) -> Acc. --spec disco_identity([identity()], jid(), jid(), binary(), binary(), - route_type()) -> [identity()]. + +-spec disco_identity([identity()], + jid(), + jid(), + binary(), + binary(), + route_type()) -> [identity()]. disco_identity(Acc, _From, To, <<"">>, _Lang, Type) -> Delegations = get_delegations(To#jid.lserver), Identities = lists:flatmap( - fun({{_, T}, {_, Info}}) when T == Type -> - Info#disco_info.identities; - (_) -> - [] - end, maps:to_list(Delegations)), + fun({{_, T}, {_, Info}}) when T == Type -> + Info#disco_info.identities; + (_) -> + [] + end, + maps:to_list(Delegations)), Acc ++ Identities; disco_identity(Acc, _From, _To, _Node, _Lang, _Type) -> Acc. + my_features(ejabberd_local) -> [?NS_DELEGATION]; my_features(ejabberd_sm) -> []. diff --git a/src/mod_delegation_opt.erl b/src/mod_delegation_opt.erl index 90965007e..959fa6fc6 100644 --- a/src/mod_delegation_opt.erl +++ b/src/mod_delegation_opt.erl @@ -5,9 +5,9 @@ -export([namespaces/1]). --spec namespaces(gen_mod:opts() | global | binary()) -> [{binary(),[binary()],acl:acl()}]. + +-spec namespaces(gen_mod:opts() | global | binary()) -> [{binary(), [binary()], acl:acl()}]. namespaces(Opts) when is_map(Opts) -> gen_mod:get_opt(namespaces, Opts); namespaces(Host) -> gen_mod:get_module_opt(Host, mod_delegation, namespaces). - diff --git a/src/mod_disco.erl b/src/mod_disco.erl index 3dbecf6a9..ff38b3b8b 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -32,33 +32,49 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_local_iq_items/1, - process_local_iq_info/1, get_local_identity/5, - get_local_features/5, get_local_services/5, - process_sm_iq_items/1, process_sm_iq_info/1, - get_sm_identity/5, get_sm_features/5, get_sm_items/5, - get_info/5, mod_opt_type/1, mod_options/1, depends/2, +-export([start/2, + stop/1, + reload/3, + process_local_iq_items/1, + process_local_iq_info/1, + get_local_identity/5, + get_local_features/5, + get_local_services/5, + process_sm_iq_items/1, + process_sm_iq_info/1, + get_sm_identity/5, + get_sm_features/5, + get_sm_items/5, + get_info/5, + mod_opt_type/1, + mod_options/1, + depends/2, mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). + -include("mod_roster.hrl"). -type features_acc() :: {error, stanza_error()} | {result, [binary()]} | empty. -type items_acc() :: {error, stanza_error()} | {result, [disco_item()]} | empty. -export_type([features_acc/0, items_acc/0]). + start(Host, Opts) -> catch ets:new(disco_extra_domains, - [named_table, ordered_set, public, - {heir, erlang:group_leader(), none}]), + [named_table, + ordered_set, + public, + {heir, erlang:group_leader(), none}]), ExtraDomains = mod_disco_opt:extra_domains(Opts), - lists:foreach(fun (Domain) -> - register_extra_domain(Host, Domain) - end, - ExtraDomains), + lists:foreach(fun(Domain) -> + register_extra_domain(Host, Domain) + end, + ExtraDomains), {ok, [{iq_handler, ejabberd_local, ?NS_DISCO_ITEMS, process_local_iq_items}, {iq_handler, ejabberd_local, ?NS_DISCO_INFO, process_local_iq_info}, {iq_handler, ejabberd_sm, ?NS_DISCO_ITEMS, process_sm_iq_items}, @@ -71,308 +87,404 @@ start(Host, Opts) -> {hook, disco_sm_identity, get_sm_identity, 100}, {hook, disco_info, get_info, 100}]}. + stop(Host) -> catch ets:match_delete(disco_extra_domains, - {{'_', Host}}), + {{'_', Host}}), ok. + reload(Host, NewOpts, OldOpts) -> NewDomains = mod_disco_opt:extra_domains(NewOpts), OldDomains = mod_disco_opt:extra_domains(OldOpts), lists:foreach( fun(Domain) -> - register_extra_domain(Host, Domain) - end, NewDomains -- OldDomains), + register_extra_domain(Host, Domain) + end, + NewDomains -- OldDomains), lists:foreach( fun(Domain) -> - unregister_extra_domain(Host, Domain) - end, OldDomains -- NewDomains). + unregister_extra_domain(Host, Domain) + end, + OldDomains -- NewDomains). + -spec register_extra_domain(binary(), binary()) -> true. register_extra_domain(Host, Domain) -> ets:insert(disco_extra_domains, {{Domain, Host}}). + -spec unregister_extra_domain(binary(), binary()) -> true. unregister_extra_domain(Host, Domain) -> ets:delete_object(disco_extra_domains, {{Domain, Host}}). + -spec process_local_iq_items(iq()) -> iq(). process_local_iq_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_local_iq_items(#iq{type = get, lang = Lang, - from = From, to = To, - sub_els = [#disco_items{node = Node}]} = IQ) -> +process_local_iq_items(#iq{ + type = get, + lang = Lang, + from = From, + to = To, + sub_els = [#disco_items{node = Node}] + } = IQ) -> Host = To#jid.lserver, - case ejabberd_hooks:run_fold(disco_local_items, Host, - empty, [From, To, Node, Lang]) of - {result, Items} -> - xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); - {error, Error} -> - xmpp:make_error(IQ, Error) + case ejabberd_hooks:run_fold(disco_local_items, + Host, + empty, + [From, To, Node, Lang]) of + {result, Items} -> + xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -spec process_local_iq_info(iq()) -> iq(). process_local_iq_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_local_iq_info(#iq{type = get, lang = Lang, - from = From, to = To, - sub_els = [#disco_info{node = Node}]} = IQ) -> +process_local_iq_info(#iq{ + type = get, + lang = Lang, + from = From, + to = To, + sub_els = [#disco_info{node = Node}] + } = IQ) -> Host = To#jid.lserver, Identity = ejabberd_hooks:run_fold(disco_local_identity, - Host, [], [From, To, Node, Lang]), - Info = ejabberd_hooks:run_fold(disco_info, Host, [], - [Host, ?MODULE, Node, Lang]), - case ejabberd_hooks:run_fold(disco_local_features, Host, - empty, [From, To, Node, Lang]) of - {result, Features} -> - xmpp:make_iq_result(IQ, #disco_info{node = Node, - identities = Identity, - xdata = Info, - features = Features}); - {error, Error} -> - xmpp:make_error(IQ, Error) + Host, + [], + [From, To, Node, Lang]), + Info = ejabberd_hooks:run_fold(disco_info, + Host, + [], + [Host, ?MODULE, Node, Lang]), + case ejabberd_hooks:run_fold(disco_local_features, + Host, + empty, + [From, To, Node, Lang]) of + {result, Features} -> + xmpp:make_iq_result(IQ, + #disco_info{ + node = Node, + identities = Identity, + xdata = Info, + features = Features + }); + {error, Error} -> + xmpp:make_error(IQ, Error) end. --spec get_local_identity([identity()], jid(), jid(), - binary(), binary()) -> [identity()]. + +-spec get_local_identity([identity()], + jid(), + jid(), + binary(), + binary()) -> [identity()]. get_local_identity(Acc, _From, To, <<"">>, _Lang) -> Host = To#jid.lserver, Name = mod_disco_opt:name(Host), - Acc ++ [#identity{category = <<"server">>, - type = <<"im">>, - name = Name}]; + Acc ++ [#identity{ + category = <<"server">>, + type = <<"im">>, + name = Name + }]; get_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec get_local_features(features_acc(), jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [binary()]}. -get_local_features({error, _Error} = Acc, _From, _To, - _Node, _Lang) -> + {error, stanza_error()} | {result, [binary()]}. +get_local_features({error, _Error} = Acc, + _From, + _To, + _Node, + _Lang) -> Acc; get_local_features(Acc, _From, To, <<"">>, _Lang) -> Feats = case Acc of - {result, Features} -> Features; - empty -> [] - end, + {result, Features} -> Features; + empty -> [] + end, {result, lists:usort( - lists:flatten( - [?NS_FEATURE_IQ, ?NS_FEATURE_PRESENCE, - ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, Feats, - ejabberd_local:get_features(To#jid.lserver)]))}; + lists:flatten( + [?NS_FEATURE_IQ, + ?NS_FEATURE_PRESENCE, + ?NS_DISCO_INFO, + ?NS_DISCO_ITEMS, + Feats, + ejabberd_local:get_features(To#jid.lserver)]))}; get_local_features(Acc, _From, _To, _Node, Lang) -> case Acc of - {result, _Features} -> Acc; - empty -> - Txt = ?T("No features available"), - {error, xmpp:err_item_not_found(Txt, Lang)} + {result, _Features} -> Acc; + empty -> + Txt = ?T("No features available"), + {error, xmpp:err_item_not_found(Txt, Lang)} end. + -spec get_local_services(items_acc(), jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [disco_item()]}. -get_local_services({error, _Error} = Acc, _From, _To, - _Node, _Lang) -> + {error, stanza_error()} | {result, [disco_item()]}. +get_local_services({error, _Error} = Acc, + _From, + _To, + _Node, + _Lang) -> Acc; get_local_services(Acc, _From, To, <<"">>, _Lang) -> Items = case Acc of - {result, Its} -> Its; - empty -> [] - end, + {result, Its} -> Its; + empty -> [] + end, Host = To#jid.lserver, {result, lists:usort( lists:map( - fun(Domain) -> #disco_item{jid = jid:make(Domain)} end, - get_vh_services(Host) ++ - ets:select(disco_extra_domains, - ets:fun2ms( - fun({{D, H}}) when H == Host -> D end)))) - ++ Items}; -get_local_services({result, _} = Acc, _From, _To, _Node, - _Lang) -> + fun(Domain) -> #disco_item{jid = jid:make(Domain)} end, + get_vh_services(Host) ++ + ets:select(disco_extra_domains, + ets:fun2ms( + fun({{D, H}}) when H == Host -> D end)))) ++ + Items}; +get_local_services({result, _} = Acc, + _From, + _To, + _Node, + _Lang) -> Acc; get_local_services(empty, _From, _To, _Node, Lang) -> {error, xmpp:err_item_not_found(?T("No services available"), Lang)}. + -spec get_vh_services(binary()) -> [binary()]. get_vh_services(Host) -> - Hosts = lists:sort(fun (H1, H2) -> - byte_size(H1) >= byte_size(H2) - end, - ejabberd_option:hosts()), - lists:filter(fun (H) -> - case lists:dropwhile(fun (VH) -> - not - str:suffix( - <<".", VH/binary>>, - H) - end, - Hosts) - of - [] -> false; - [VH | _] -> VH == Host - end - end, - ejabberd_router:get_all_routes()). + Hosts = lists:sort(fun(H1, H2) -> + byte_size(H1) >= byte_size(H2) + end, + ejabberd_option:hosts()), + lists:filter(fun(H) -> + case lists:dropwhile(fun(VH) -> + not str:suffix( + <<".", VH/binary>>, + H) + end, + Hosts) of + [] -> false; + [VH | _] -> VH == Host + end + end, + ejabberd_router:get_all_routes()). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + -spec process_sm_iq_items(iq()) -> iq(). process_sm_iq_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_sm_iq_items(#iq{type = get, lang = Lang, - from = From, to = To, - sub_els = [#disco_items{node = Node}]} = IQ) -> +process_sm_iq_items(#iq{ + type = get, + lang = Lang, + from = From, + to = To, + sub_els = [#disco_items{node = Node}] + } = IQ) -> case mod_roster:is_subscribed(From, To) of - true -> - Host = To#jid.lserver, - case ejabberd_hooks:run_fold(disco_sm_items, Host, - empty, [From, To, Node, Lang]) of - {result, Items} -> - xmpp:make_iq_result( - IQ, #disco_items{node = Node, items = Items}); - {error, Error} -> - xmpp:make_error(IQ, Error) - end; - false -> - Txt = ?T("Not subscribed"), - xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) + true -> + Host = To#jid.lserver, + case ejabberd_hooks:run_fold(disco_sm_items, + Host, + empty, + [From, To, Node, Lang]) of + {result, Items} -> + xmpp:make_iq_result( + IQ, #disco_items{node = Node, items = Items}); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + false -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. + -spec get_sm_items(items_acc(), jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [disco_item()]}. -get_sm_items({error, _Error} = Acc, _From, _To, _Node, - _Lang) -> + {error, stanza_error()} | {result, [disco_item()]}. +get_sm_items({error, _Error} = Acc, + _From, + _To, + _Node, + _Lang) -> Acc; -get_sm_items(Acc, From, - #jid{user = User, server = Server} = To, <<"">>, _Lang) -> +get_sm_items(Acc, + From, + #jid{user = User, server = Server} = To, + <<"">>, + _Lang) -> Items = case Acc of - {result, Its} -> Its; - empty -> [] - end, + {result, Its} -> Its; + empty -> [] + end, Items1 = case mod_roster:is_subscribed(From, To) of - true -> get_user_resources(User, Server); - _ -> [] - end, + true -> get_user_resources(User, Server); + _ -> [] + end, {result, Items ++ Items1}; -get_sm_items({result, _} = Acc, _From, _To, _Node, - _Lang) -> +get_sm_items({result, _} = Acc, + _From, + _To, + _Node, + _Lang) -> Acc; get_sm_items(empty, From, To, _Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of - {LTo, LSTo} -> {error, xmpp:err_item_not_found()}; - _ -> - Txt = ?T("Query to another users is forbidden"), - {error, xmpp:err_not_allowed(Txt, Lang)} + {LTo, LSTo} -> {error, xmpp:err_item_not_found()}; + _ -> + Txt = ?T("Query to another users is forbidden"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. + -spec process_sm_iq_info(iq()) -> iq(). process_sm_iq_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_sm_iq_info(#iq{type = get, lang = Lang, - from = From, to = To, - sub_els = [#disco_info{node = Node}]} = IQ) -> +process_sm_iq_info(#iq{ + type = get, + lang = Lang, + from = From, + to = To, + sub_els = [#disco_info{node = Node}] + } = IQ) -> case mod_roster:is_subscribed(From, To) of - true -> - Host = To#jid.lserver, - Identity = ejabberd_hooks:run_fold(disco_sm_identity, - Host, [], - [From, To, Node, Lang]), - Info = ejabberd_hooks:run_fold(disco_info, Host, [], - [From, To, Node, Lang]), - case ejabberd_hooks:run_fold(disco_sm_features, Host, - empty, [From, To, Node, Lang]) of - {result, Features} -> - xmpp:make_iq_result(IQ, #disco_info{node = Node, - identities = Identity, - xdata = Info, - features = Features}); - {error, Error} -> - xmpp:make_error(IQ, Error) - end; - false -> - Txt = ?T("Not subscribed"), - xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) + true -> + Host = To#jid.lserver, + Identity = ejabberd_hooks:run_fold(disco_sm_identity, + Host, + [], + [From, To, Node, Lang]), + Info = ejabberd_hooks:run_fold(disco_info, + Host, + [], + [From, To, Node, Lang]), + case ejabberd_hooks:run_fold(disco_sm_features, + Host, + empty, + [From, To, Node, Lang]) of + {result, Features} -> + xmpp:make_iq_result(IQ, + #disco_info{ + node = Node, + identities = Identity, + xdata = Info, + features = Features + }); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + false -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. --spec get_sm_identity([identity()], jid(), jid(), - binary(), binary()) -> [identity()]. -get_sm_identity(Acc, _From, - #jid{luser = LUser, lserver = LServer}, _Node, _Lang) -> + +-spec get_sm_identity([identity()], + jid(), + jid(), + binary(), + binary()) -> [identity()]. +get_sm_identity(Acc, + _From, + #jid{luser = LUser, lserver = LServer}, + _Node, + _Lang) -> Acc ++ - case ejabberd_auth:user_exists(LUser, LServer) of - true -> - [#identity{category = <<"account">>, type = <<"registered">>}]; - _ -> [] - end. + case ejabberd_auth:user_exists(LUser, LServer) of + true -> + [#identity{category = <<"account">>, type = <<"registered">>}]; + _ -> [] + end. + -spec get_sm_features(features_acc(), jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [binary()]}. + {error, stanza_error()} | {result, [binary()]}. get_sm_features(empty, From, To, Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of - {LTo, LSTo} -> - case Node of - <<"">> -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS]}; - _ -> {error, xmpp:err_item_not_found()} - end; - _ -> - Txt = ?T("Query to another users is forbidden"), - {error, xmpp:err_not_allowed(Txt, Lang)} + {LTo, LSTo} -> + case Node of + <<"">> -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS]}; + _ -> {error, xmpp:err_item_not_found()} + end; + _ -> + Txt = ?T("Query to another users is forbidden"), + {error, xmpp:err_not_allowed(Txt, Lang)} end; get_sm_features({result, Features}, _From, _To, <<"">>, _Lang) -> - {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS|Features]}; + {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS | Features]}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec get_user_resources(binary(), binary()) -> [disco_item()]. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), - [#disco_item{jid = jid:make(User, Server, Resource), name = User} - || Resource <- lists:sort(Rs)]. + [ #disco_item{jid = jid:make(User, Server, Resource), name = User} + || Resource <- lists:sort(Rs) ]. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Support for: XEP-0157 Contact Addresses for XMPP Services + -spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; - ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. + ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. get_info(_A, Host, Mod, Node, _Lang) when is_atom(Mod), Node == <<"">> -> Module = case Mod of - undefined -> ?MODULE; - _ -> Mod - end, - [#xdata{type = result, - fields = [#xdata_field{type = hidden, - var = <<"FORM_TYPE">>, - values = [?NS_SERVERINFO]} - | get_fields(Host, Module)]}]; + undefined -> ?MODULE; + _ -> Mod + end, + [#xdata{ + type = result, + fields = [#xdata_field{ + type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_SERVERINFO] + } | get_fields(Host, Module)] + }]; get_info(Acc, _, _, _Node, _) -> Acc. + -spec get_fields(binary(), module()) -> [xdata_field()]. get_fields(Host, Module) -> Fields = mod_disco_opt:server_info(Host), - Fields1 = lists:filter(fun ({Modules, _, _}) -> - case Modules of - all -> true; - Modules -> - lists:member(Module, Modules) - end - end, - Fields), - [#xdata_field{var = Var, - type = 'list-multi', - values = Values} || {_, Var, Values} <- Fields1]. + Fields1 = lists:filter(fun({Modules, _, _}) -> + case Modules of + all -> true; + Modules -> + lists:member(Module, Modules) + end + end, + Fields), + [ #xdata_field{ + var = Var, + type = 'list-multi', + values = Values + } || {_, Var, Values} <- Fields1 ]. + -spec depends(binary(), gen_mod:opts()) -> []. depends(_Host, _Opts) -> []. + mod_opt_type(extra_domains) -> econf:list(econf:binary()); mod_opt_type(name) -> @@ -380,49 +492,59 @@ mod_opt_type(name) -> mod_opt_type(server_info) -> econf:list( econf:and_then( - econf:options( - #{name => econf:binary(), - urls => econf:list(econf:binary()), - modules => - econf:either( - all, - econf:list(econf:beam()))}), - fun(Opts) -> - Mods = proplists:get_value(modules, Opts, all), - Name = proplists:get_value(name, Opts, <<>>), - URLs = proplists:get_value(urls, Opts, []), - {Mods, Name, URLs} - end)). + econf:options( + #{ + name => econf:binary(), + urls => econf:list(econf:binary()), + modules => + econf:either( + all, + econf:list(econf:beam())) + }), + fun(Opts) -> + Mods = proplists:get_value(modules, Opts, all), + Name = proplists:get_value(name, Opts, <<>>), + URLs = proplists:get_value(urls, Opts, []), + {Mods, Name, URLs} + end)). + -spec mod_options(binary()) -> [{server_info, - [{all | [module()], binary(), [binary()]}]} | - {atom(), any()}]. + [{all | [module()], binary(), [binary()]}]} | + {atom(), any()}]. mod_options(_Host) -> [{extra_domains, []}, {server_info, []}, {name, ?T("ejabberd")}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0030.html" "[XEP-0030: Service Discovery]. With this module enabled, " "services on your server can be discovered by XMPP clients."), opts => [{extra_domains, - #{value => "[Domain, ...]", + #{ + value => "[Domain, ...]", desc => ?T("With this option, you can specify a list of extra " "domains that are added to the Service Discovery item list. " - "The default value is an empty list.")}}, + "The default value is an empty list.") + }}, {name, - #{value => ?T("Name"), + #{ + value => ?T("Name"), desc => ?T("A name of the server in the Service Discovery. " "This will only be displayed by special XMPP clients. " - "The default value is 'ejabberd'.")}}, + "The default value is 'ejabberd'.") + }}, {server_info, - #{value => "[Info, ...]", + #{ + value => "[Info, ...]", example => ["server_info:", " -", @@ -452,20 +574,28 @@ mod_doc() -> ?T("Specify additional information about the server, " "as described in https://xmpp.org/extensions/xep-0157.html" "[XEP-0157: Contact Addresses for XMPP Services]. Every 'Info' " - "element in the list is constructed from the following options:")}, + "element in the list is constructed from the following options:") + }, [{modules, - #{value => "all | [Module, ...]", + #{ + value => "all | [Module, ...]", desc => ?T("The value can be the keyword 'all', in which case the " "information is reported in all the services, " "or a list of ejabberd modules, in which case the " "information is only specified for the services provided " - "by those modules.")}}, + "by those modules.") + }}, {name, - #{value => ?T("Name"), + #{ + value => ?T("Name"), desc => ?T("The field 'var' name that will be defined. " - "See XEP-0157 for some standardized names.")}}, + "See XEP-0157 for some standardized names.") + }}, {urls, - #{value => "[URI, ...]", + #{ + value => "[URI, ...]", desc => ?T("A list of contact URIs, such as " - "HTTP URLs, XMPP URIs and so on.")}}]}]}. + "HTTP URLs, XMPP URIs and so on.") + }}]}] + }. diff --git a/src/mod_disco_opt.erl b/src/mod_disco_opt.erl index a66c3293a..541708ff2 100644 --- a/src/mod_disco_opt.erl +++ b/src/mod_disco_opt.erl @@ -7,21 +7,23 @@ -export([name/1]). -export([server_info/1]). + -spec extra_domains(gen_mod:opts() | global | binary()) -> [binary()]. extra_domains(Opts) when is_map(Opts) -> gen_mod:get_opt(extra_domains, Opts); extra_domains(Host) -> gen_mod:get_module_opt(Host, mod_disco, extra_domains). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_disco, name). --spec server_info(gen_mod:opts() | global | binary()) -> [{'all' | [module()],binary(),[binary()]}]. + +-spec server_info(gen_mod:opts() | global | binary()) -> [{'all' | [module()], binary(), [binary()]}]. server_info(Opts) when is_map(Opts) -> gen_mod:get_opt(server_info, Opts); server_info(Host) -> gen_mod:get_module_opt(Host, mod_disco, server_info). - diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index 2dbd8575c..361622761 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -29,115 +29,141 @@ -behaviour(gen_server). %% API --export([start/2, stop/1, reload/3, c2s_auth_result/3, - c2s_stream_started/2]). +-export([start/2, + stop/1, + reload/3, + c2s_auth_result/3, + c2s_stream_started/2]). --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3, - mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + mod_opt_type/1, + mod_options/1, + depends/2, + mod_doc/0]). %% ejabberd command. -export([get_commands_spec/0, unban/1]). -include_lib("stdlib/include/ms_transform.hrl"). + -include("ejabberd_commands.hrl"). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -define(CLEAN_INTERVAL, timer:minutes(10)). -record(state, {host = <<"">> :: binary()}). + %%%=================================================================== %%% API %%%=================================================================== --spec c2s_auth_result(ejabberd_c2s:state(), true | {false, binary()}, binary()) - -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. +-spec c2s_auth_result(ejabberd_c2s:state(), true | {false, binary()}, binary()) -> + ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. c2s_auth_result(#{sasl_mech := Mech} = State, {false, _}, _User) when Mech == <<"EXTERNAL">> -> State; c2s_auth_result(#{ip := {Addr, _}, lserver := LServer} = State, {false, _}, _User) -> case is_whitelisted(LServer, Addr) of - true -> - State; - false -> - BanLifetime = mod_fail2ban_opt:c2s_auth_ban_lifetime(LServer), - MaxFailures = mod_fail2ban_opt:c2s_max_auth_failures(LServer), - UnbanTS = current_time() + BanLifetime, - Attempts = case ets:lookup(failed_auth, Addr) of - [{Addr, N, _, _}] -> - ets:insert(failed_auth, - {Addr, N+1, UnbanTS, MaxFailures}), - N+1; - [] -> - ets:insert(failed_auth, - {Addr, 1, UnbanTS, MaxFailures}), - 1 - end, - if Attempts >= MaxFailures -> - log_and_disconnect(State, Attempts, UnbanTS); - true -> - State - end + true -> + State; + false -> + BanLifetime = mod_fail2ban_opt:c2s_auth_ban_lifetime(LServer), + MaxFailures = mod_fail2ban_opt:c2s_max_auth_failures(LServer), + UnbanTS = current_time() + BanLifetime, + Attempts = case ets:lookup(failed_auth, Addr) of + [{Addr, N, _, _}] -> + ets:insert(failed_auth, + {Addr, N + 1, UnbanTS, MaxFailures}), + N + 1; + [] -> + ets:insert(failed_auth, + {Addr, 1, UnbanTS, MaxFailures}), + 1 + end, + if + Attempts >= MaxFailures -> + log_and_disconnect(State, Attempts, UnbanTS); + true -> + State + end end; c2s_auth_result(#{ip := {Addr, _}} = State, true, _User) -> ets:delete(failed_auth, Addr), State. --spec c2s_stream_started(ejabberd_c2s:state(), stream_start()) - -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. + +-spec c2s_stream_started(ejabberd_c2s:state(), stream_start()) -> + ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. c2s_stream_started(#{ip := {Addr, _}} = State, _) -> case ets:lookup(failed_auth, Addr) of - [{Addr, N, TS, MaxFailures}] when N >= MaxFailures -> - case TS > current_time() of - true -> - log_and_disconnect(State, N, TS); - false -> - ets:delete(failed_auth, Addr), - State - end; - _ -> - State + [{Addr, N, TS, MaxFailures}] when N >= MaxFailures -> + case TS > current_time() of + true -> + log_and_disconnect(State, N, TS); + false -> + ets:delete(failed_auth, Addr), + State + end; + _ -> + State end. + %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - catch ets:new(failed_auth, [named_table, public, - {heir, erlang:group_leader(), none}]), + catch ets:new(failed_auth, + [named_table, + public, + {heir, erlang:group_leader(), none}]), ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()), gen_mod:stop_child(?MODULE, Host). + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE, c2s_auth_result, 100), ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 100), erlang:send_after(?CLEAN_INTERVAL, self(), clean), {ok, #state{host = Host}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast = ~p", [Msg]), {noreply, State}. + handle_info(clean, State) -> ?DEBUG("Cleaning ~p ETS table", [failed_auth]), Now = current_time(), @@ -150,94 +176,111 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info = ~p", [Info]), {noreply, State}. + terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE, c2s_auth_result, 100), ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 100), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - true -> - ok; - false -> - ets:delete(failed_auth) + true -> + ok; + false -> + ets:delete(failed_auth) end. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%-------------------------------------------------------------------- %% ejabberd command callback. %%-------------------------------------------------------------------- -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> - [#ejabberd_commands{name = unban_ip, tags = [accounts], - desc = "Remove banned IP addresses from the fail2ban table", - longdesc = "Accepts an IP address with a network mask. " - "Returns the number of unbanned addresses, or a negative integer if there were any error.", - module = ?MODULE, function = unban, - args = [{address, binary}], - args_example = [<<"::FFFF:127.0.0.1/128">>], - args_desc = ["IP address, optionally with network mask."], - result_example = 3, - result_desc = "Amount of unbanned entries, or negative in case of error.", - result = {unbanned, integer}}]. + [#ejabberd_commands{ + name = unban_ip, + tags = [accounts], + desc = "Remove banned IP addresses from the fail2ban table", + longdesc = "Accepts an IP address with a network mask. " + "Returns the number of unbanned addresses, or a negative integer if there were any error.", + module = ?MODULE, + function = unban, + args = [{address, binary}], + args_example = [<<"::FFFF:127.0.0.1/128">>], + args_desc = ["IP address, optionally with network mask."], + result_example = 3, + result_desc = "Amount of unbanned entries, or negative in case of error.", + result = {unbanned, integer} + }]. + -spec unban(binary()) -> integer(). unban(S) -> case misc:parse_ip_mask(S) of - {ok, {Net, Mask}} -> - unban(Net, Mask); - error -> - ?WARNING_MSG("Invalid network address when trying to unban: ~p", [S]), - -1 + {ok, {Net, Mask}} -> + unban(Net, Mask); + error -> + ?WARNING_MSG("Invalid network address when trying to unban: ~p", [S]), + -1 end. + -spec unban(inet:ip_address(), 0..128) -> non_neg_integer(). unban(Net, Mask) -> ets:foldl( - fun({Addr, _, _, _}, Acc) -> - case misc:match_ip_mask(Addr, Net, Mask) of - true -> - ets:delete(failed_auth, Addr), - Acc+1; - false -> Acc - end - end, 0, failed_auth). + fun({Addr, _, _, _}, Acc) -> + case misc:match_ip_mask(Addr, Net, Mask) of + true -> + ets:delete(failed_auth, Addr), + Acc + 1; + false -> Acc + end + end, + 0, + failed_auth). + %%%=================================================================== %%% Internal functions %%%=================================================================== --spec log_and_disconnect(ejabberd_c2s:state(), pos_integer(), non_neg_integer()) - -> {stop, ejabberd_c2s:state()}. +-spec log_and_disconnect(ejabberd_c2s:state(), pos_integer(), non_neg_integer()) -> + {stop, ejabberd_c2s:state()}. log_and_disconnect(#{ip := {Addr, _}, lang := Lang} = State, Attempts, UnbanTS) -> IP = misc:ip_to_list(Addr), UnbanDate = format_date( - calendar:now_to_universal_time(msec_to_now(UnbanTS))), + calendar:now_to_universal_time(msec_to_now(UnbanTS))), Format = ?T("Too many (~p) failed authentications " - "from this IP address (~s). The address " - "will be unblocked at ~s UTC"), + "from this IP address (~s). The address " + "will be unblocked at ~s UTC"), Args = [Attempts, IP, UnbanDate], ?WARNING_MSG("Connection attempt from blacklisted IP ~ts: ~ts", - [IP, io_lib:fwrite(Format, Args)]), + [IP, io_lib:fwrite(Format, Args)]), Err = xmpp:serr_policy_violation({Format, Args}, Lang), {stop, ejabberd_c2s:send(State, Err)}. + -spec is_whitelisted(binary(), inet:ip_address()) -> boolean(). is_whitelisted(Host, Addr) -> Access = mod_fail2ban_opt:access(Host), acl:match_rule(Host, Access, Addr) == allow. + -spec msec_to_now(pos_integer()) -> erlang:timestamp(). msec_to_now(MSecs) -> Secs = MSecs div 1000, {Secs div 1000000, Secs rem 1000000, 0}. + -spec format_date(calendar:datetime()) -> iolist(). format_date({{Year, Month, Day}, {Hour, Minute, Second}}) -> io_lib:format("~2..0w:~2..0w:~2..0w ~2..0w.~2..0w.~4..0w", - [Hour, Minute, Second, Day, Month, Year]). + [Hour, Minute, Second, Day, Month, Year]). + current_time() -> erlang:system_time(millisecond). + mod_opt_type(access) -> econf:acl(); mod_opt_type(c2s_auth_ban_lifetime) -> @@ -245,43 +288,55 @@ mod_opt_type(c2s_auth_ban_lifetime) -> mod_opt_type(c2s_max_auth_failures) -> econf:pos_int(). + mod_options(_Host) -> [{access, none}, {c2s_auth_ban_lifetime, timer:hours(1)}, {c2s_max_auth_failures, 20}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("The module bans IPs that show the malicious signs. " - "Currently only C2S authentication failures are detected."), "", + "Currently only C2S authentication failures are detected."), + "", ?T("Unlike the standalone program, 'mod_fail2ban' clears the " "record of authentication failures after some time since the " "first failure or on a successful authentication. " "It also does not simply block network traffic, but " - "provides the client with a descriptive error message."), "", - ?T("WARNING: You should not use this module behind a proxy or load " - "balancer. ejabberd will see the failures as coming from the " - "load balancer and, when the threshold of auth failures is " - "reached, will reject all connections coming from the load " - "balancer. You can lock all your user base out of ejabberd " - "when using this module behind a proxy.")], + "provides the client with a descriptive error message."), + "", + ?T("WARNING: You should not use this module behind a proxy or load " + "balancer. ejabberd will see the failures as coming from the " + "load balancer and, when the threshold of auth failures is " + "reached, will reject all connections coming from the load " + "balancer. You can lock all your user base out of ejabberd " + "when using this module behind a proxy.")], opts => [{access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Specify an access rule for whitelisting IP " "addresses or networks. If the rule returns 'allow' " "for a given IP address, that address will never be " "banned. The 'AccessName' should be of type 'ip'. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {c2s_auth_ban_lifetime, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("The lifetime of the IP ban caused by too many " "C2S authentication failures. The default value is " - "'1' hour.")}}, + "'1' hour.") + }}, {c2s_max_auth_failures, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("The number of C2S authentication failures to " - "trigger the IP ban. The default value is '20'.")}}]}. + "trigger the IP ban. The default value is '20'.") + }}] + }. diff --git a/src/mod_fail2ban_opt.erl b/src/mod_fail2ban_opt.erl index 15abbedd0..852fdb835 100644 --- a/src/mod_fail2ban_opt.erl +++ b/src/mod_fail2ban_opt.erl @@ -7,21 +7,23 @@ -export([c2s_auth_ban_lifetime/1]). -export([c2s_max_auth_failures/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_fail2ban, access). + -spec c2s_auth_ban_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). c2s_auth_ban_lifetime(Opts) when is_map(Opts) -> gen_mod:get_opt(c2s_auth_ban_lifetime, Opts); c2s_auth_ban_lifetime(Host) -> gen_mod:get_module_opt(Host, mod_fail2ban, c2s_auth_ban_lifetime). + -spec c2s_max_auth_failures(gen_mod:opts() | global | binary()) -> pos_integer(). c2s_max_auth_failures(Opts) when is_map(Opts) -> gen_mod:get_opt(c2s_max_auth_failures, Opts); c2s_max_auth_failures(Host) -> gen_mod:get_module_opt(Host, mod_fail2ban, c2s_max_auth_failures). - diff --git a/src/mod_host_meta.erl b/src/mod_host_meta.erl index ae7d7d697..fd593a580 100644 --- a/src/mod_host_meta.erl +++ b/src/mod_host_meta.erl @@ -31,8 +31,13 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process/2, - mod_opt_type/1, mod_options/1, depends/2]). +-export([start/2, + stop/1, + reload/3, + process/2, + mod_opt_type/1, + mod_options/1, + depends/2]). -export([mod_doc/0]). -export([get_url/4, get_auto_url/2]). @@ -50,24 +55,30 @@ %%% gen_mod callbacks %%%---------------------------------------------------------------------- + start(_Host, _Opts) -> report_hostmeta_listener(), ok. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> report_hostmeta_listener(), ok. + depends(_Host, _Opts) -> [{mod_bosh, soft}]. + %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- + process([], #request{method = 'GET', host = Host, path = Path}) -> case lists:last(Path) of <<"host-meta">> -> @@ -78,66 +89,77 @@ process([], #request{method = 'GET', host = Host, path = Path}) -> process(_Path, _Request) -> {404, [], "Not Found"}. + %%%---------------------------------------------------------------------- %%% Internal %%%---------------------------------------------------------------------- %% When set to 'auto', it only takes the first valid listener options it finds + file_xml(Host) -> BoshList = case get_url(?MODULE, bosh, true, Host) of undefined -> []; BoshUrl -> [?XA(<<"Link">>, [{<<"rel">>, <<"urn:xmpp:alt-connections:xbosh">>}, - {<<"href">>, BoshUrl}] - )] + {<<"href">>, BoshUrl}])] end, WsList = case get_url(?MODULE, websocket, true, Host) of undefined -> []; WsUrl -> [?XA(<<"Link">>, [{<<"rel">>, <<"urn:xmpp:alt-connections:websocket">>}, - {<<"href">>, WsUrl}] - )] + {<<"href">>, WsUrl}])] end, - {200, [html, - {<<"Content-Type">>, <<"application/xrd+xml">>}, - {<<"Access-Control-Allow-Origin">>, <<"*">>}], + {200, + [html, + {<<"Content-Type">>, <<"application/xrd+xml">>}, + {<<"Access-Control-Allow-Origin">>, <<"*">>}], [<<"\n">>, fxml:element_to_binary( ?XAE(<<"XRD">>, - [{<<"xmlns">>,<<"http://docs.oasis-open.org/ns/xri/xrd-1.0">>}], - BoshList ++ WsList) - )]}. + [{<<"xmlns">>, <<"http://docs.oasis-open.org/ns/xri/xrd-1.0">>}], + BoshList ++ WsList))]}. + file_json(Host) -> BoshList = case get_url(?MODULE, bosh, true, Host) of undefined -> []; - BoshUrl -> [#{rel => <<"urn:xmpp:alt-connections:xbosh">>, - href => BoshUrl}] + BoshUrl -> + [#{ + rel => <<"urn:xmpp:alt-connections:xbosh">>, + href => BoshUrl + }] end, WsList = case get_url(?MODULE, websocket, true, Host) of undefined -> []; - WsUrl -> [#{rel => <<"urn:xmpp:alt-connections:websocket">>, - href => WsUrl}] + WsUrl -> + [#{ + rel => <<"urn:xmpp:alt-connections:websocket">>, + href => WsUrl + }] end, - {200, [html, - {<<"Content-Type">>, <<"application/json">>}, - {<<"Access-Control-Allow-Origin">>, <<"*">>}], + {200, + [html, + {<<"Content-Type">>, <<"application/json">>}, + {<<"Access-Control-Allow-Origin">>, <<"*">>}], [misc:json_encode(#{links => BoshList ++ WsList})]}. + get_url(M, bosh, Tls, Host) -> get_url(M, Tls, Host, bosh_service_url, mod_bosh); get_url(M, websocket, Tls, Host) -> get_url(M, Tls, Host, websocket_url, ejabberd_http_ws). + get_url(M, Tls, Host, Option, Module) -> case get_url_preliminar(M, Tls, Host, Option, Module) of undefined -> undefined; Url -> misc:expand_keyword(<<"@HOST@">>, Url, Host) end. + get_url_preliminar(M, Tls, Host, Option, Module) -> case gen_mod:get_module_opt(Host, M, Option) of undefined -> undefined; @@ -146,6 +168,7 @@ get_url_preliminar(M, Tls, Host, Option, Module) -> U when is_binary(U) -> U end. + get_auto_url(Tls, Module) -> case find_handler_port_path(Tls, Module) of [] -> undefined; @@ -163,6 +186,7 @@ get_auto_url(Tls, Module) -> (str:join(Path, <<"/">>))/binary>> end. + find_handler_port_path(Tls, Module) -> lists:filtermap( fun({{Port, _, _}, @@ -174,7 +198,9 @@ find_handler_port_path(Tls, Module) -> {Path, Module} -> {true, {ThisTls, Port, Path}} end; (_) -> false - end, ets:tab2list(ejabberd_listener)). + end, + ets:tab2list(ejabberd_listener)). + report_hostmeta_listener() -> case {find_handler_port_path(false, ?MODULE), @@ -182,38 +208,46 @@ report_hostmeta_listener() -> {[], []} -> ?CRITICAL_MSG("It seems you enabled ~p in 'modules' but forgot to " "add it as a request_handler in an ejabberd_http " - "listener.", [?MODULE]); - {[_|_], _} -> + "listener.", + [?MODULE]); + {[_ | _], _} -> ?WARNING_MSG("Apparently ~p is enabled in a request_handler in a " "non-encrypted ejabberd_http listener. This is " "disallowed by XEP-0156. Please enable 'tls' in that " "listener, or setup a proxy encryption mechanism.", [?MODULE]); - {[], [_|_]} -> + {[], [_ | _]} -> ok end. + %%%---------------------------------------------------------------------- %%% Options and Doc %%%---------------------------------------------------------------------- + mod_opt_type(bosh_service_url) -> econf:either(undefined, econf:binary()); mod_opt_type(websocket_url) -> econf:either(undefined, econf:binary()). + mod_options(_) -> [{bosh_service_url, <<"auto">>}, {websocket_url, <<"auto">>}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module serves small 'host-meta' files as described in " "https://xmpp.org/extensions/xep-0156.html[XEP-0156: Discovering " - "Alternative XMPP Connection Methods]."), "", + "Alternative XMPP Connection Methods]."), + "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " - "_`listen-options.md#request_handlers|request_handlers`_."), "", + "_`listen-options.md#request_handlers|request_handlers`_."), + "", ?T("Notice it only works if _`listen.md#ejabberd_http|ejabberd_http`_ " "has _`listen-options.md#tls|tls`_ enabled.")], note => "added in 22.05", @@ -237,21 +271,25 @@ mod_doc() -> opts => [{websocket_url, - #{value => "undefined | auto | WebSocketURL", + #{ + value => "undefined | auto | WebSocketURL", desc => ?T("WebSocket URL to announce. " "The keyword '@HOST@' is replaced with the real virtual " "host name. " "If set to 'auto', it will build the URL of the first " "configured WebSocket request handler. " - "The default value is 'auto'.")}}, + "The default value is 'auto'.") + }}, {bosh_service_url, - #{value => "undefined | auto | BoshURL", + #{ + value => "undefined | auto | BoshURL", desc => ?T("BOSH service URL to announce. " "The keyword '@HOST@' is replaced with the real " "virtual host name. " "If set to 'auto', it will build the URL of the first " "configured BOSH request handler. " - "The default value is 'auto'.")}}] + "The default value is 'auto'.") + }}] }. diff --git a/src/mod_host_meta_opt.erl b/src/mod_host_meta_opt.erl index 965e95cf8..1da1fd427 100644 --- a/src/mod_host_meta_opt.erl +++ b/src/mod_host_meta_opt.erl @@ -6,15 +6,16 @@ -export([bosh_service_url/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_host_meta, bosh_service_url). + -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_host_meta, websocket_url). - diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 4d7e86777..ab31eea9c 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -29,11 +29,19 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process/2, depends/2, - format_arg/2, handle/4, - mod_opt_type/1, mod_options/1, mod_doc/0]). +-export([start/2, + stop/1, + reload/3, + process/2, + depends/2, + format_arg/2, + handle/4, + mod_opt_type/1, + mod_options/1, + mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_stacktrace.hrl"). @@ -65,8 +73,11 @@ {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(OPTIONS_HEADER, - [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, - ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). + [?CT_PLAIN, + ?AC_ALLOW_ORIGIN, + ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, + ?AC_MAX_AGE]). -define(HEADER(CType), [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). @@ -75,61 +86,70 @@ %% Module control %% ------------------- + start(_Host, _Opts) -> ok. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + %% ---------- %% basic auth %% ---------- + extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) -> Info = case HTTPAuth of - {SJID, Pass} -> - try jid:decode(SJID) of - #jid{luser = User, lserver = Server} -> - case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of - true -> - #{usr => {User, Server, <<"">>}, caller_server => Server}; - false -> - {error, invalid_auth} - end - catch _:{bad_jid, _} -> - {error, invalid_auth} - end; - {oauth, Token, _} -> - case ejabberd_oauth:check_token(Token) of - {ok, {U, S}, Scope} -> - #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; - {false, Reason} -> - {error, Reason} - end; - invalid -> - {error, invalid_auth}; - _ -> - #{} - end, + {SJID, Pass} -> + try jid:decode(SJID) of + #jid{luser = User, lserver = Server} -> + case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of + true -> + #{usr => {User, Server, <<"">>}, caller_server => Server}; + false -> + {error, invalid_auth} + end + catch + _:{bad_jid, _} -> + {error, invalid_auth} + end; + {oauth, Token, _} -> + case ejabberd_oauth:check_token(Token) of + {ok, {U, S}, Scope} -> + #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; + {false, Reason} -> + {error, Reason} + end; + invalid -> + {error, invalid_auth}; + _ -> + #{} + end, case Info of - Map when is_map(Map) -> - Tag = proplists:get_value(tag, Opts, <<>>), - Map#{caller_module => ?MODULE, ip => IP, tag => Tag}; - _ -> - ?DEBUG("Invalid auth data: ~p", [Info]), - Info + Map when is_map(Map) -> + Tag = proplists:get_value(tag, Opts, <<>>), + Map#{caller_module => ?MODULE, ip => IP, tag => Tag}; + _ -> + ?DEBUG("Invalid auth data: ~p", [Info]), + Info end. + %% ------------------ %% command processing %% ------------------ + %process(Call, Request) -> % ?DEBUG("~p~n~p", [Call, Request]), ok; process(_, #request{method = 'POST', data = <<>>}) -> @@ -140,16 +160,16 @@ process([Call | _], #request{method = 'POST', data = Data, ip = IPPort} = Req) - try Args = extract_args(Data), log(Call, Args, IPPort), - perform_call(Call, Args, Req, Version) + perform_call(Call, Args, Req, Version) catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> json_format({404, 44, <<"Command not found.">>}); - _:{error,{_,invalid_json}} = Err -> - ?DEBUG("Bad Request: ~p", [Err]), - badrequest_response(<<"Invalid JSON input">>); - ?EX_RULE(_Class, Error, Stack) -> - StackTrace = ?EX_STACK(Stack), + _:{error, {_, invalid_json}} = Err -> + ?DEBUG("Bad Request: ~p", [Err]), + badrequest_response(<<"Invalid JSON input">>); + ?EX_RULE(_Class, Error, Stack) -> + StackTrace = ?EX_STACK(Stack), ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]), badrequest_response() end; @@ -161,13 +181,13 @@ process([Call | _], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> _ -> Data end, log(Call, Args, IP), - perform_call(Call, Args, Req, Version) + perform_call(Call, Args, Req, Version) catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> json_format({404, 44, <<"Command not found.">>}); ?EX_RULE(_, Error, Stack) -> - StackTrace = ?EX_STACK(Stack), + StackTrace = ?EX_STACK(Stack), ?DEBUG("Bad Request: ~p ~p", [Error, StackTrace]), badrequest_response() end; @@ -179,223 +199,249 @@ process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), json_error(400, 40, <<"Missing command name.">>). + perform_call(Command, Args, Req, Version) -> case catch binary_to_existing_atom(Command, utf8) of - Call when is_atom(Call) -> - case extract_auth(Req) of - {error, expired} -> invalid_token_response(); - {error, not_found} -> invalid_token_response(); - {error, invalid_auth} -> unauthorized_response(); - Auth when is_map(Auth) -> - Result = handle(Call, Auth, Args, Version), - json_format(Result) - end; - _ -> - json_error(404, 40, <<"Endpoint not found.">>) + Call when is_atom(Call) -> + case extract_auth(Req) of + {error, expired} -> invalid_token_response(); + {error, not_found} -> invalid_token_response(); + {error, invalid_auth} -> unauthorized_response(); + Auth when is_map(Auth) -> + Result = handle(Call, Auth, Args, Version), + json_format(Result) + end; + _ -> + json_error(404, 40, <<"Endpoint not found.">>) end. + %% Be tolerant to make API more easily usable from command-line pipe. extract_args(<<"\n">>) -> []; extract_args(Data) -> Maps = misc:json_decode(Data), maps:to_list(Maps). + % get API version N from last "vN" element in URL path get_api_version(#request{path = Path, host = Host}) -> get_api_version(lists:reverse(Path), Host). + get_api_version([<<"v", String/binary>> | Tail], Host) -> case catch binary_to_integer(String) of - N when is_integer(N) -> - N; - _ -> - get_api_version(Tail, Host) + N when is_integer(N) -> + N; + _ -> + get_api_version(Tail, Host) end; get_api_version([_Head | Tail], Host) -> get_api_version(Tail, Host); get_api_version([], Host) -> - try mod_http_api_opt:default_version(Host) - catch error:{module_not_loaded, ?MODULE, Host} -> - ?WARNING_MSG("Using module ~p for host ~s, but it isn't configured " - "in the configuration file", [?MODULE, Host]), - ?DEFAULT_API_VERSION + try + mod_http_api_opt:default_version(Host) + catch + error:{module_not_loaded, ?MODULE, Host} -> + ?WARNING_MSG("Using module ~p for host ~s, but it isn't configured " + "in the configuration file", + [?MODULE, Host]), + ?DEFAULT_API_VERSION end. + %% ---------------- %% command handlers %% ---------------- %% TODO Check accept types of request before decided format of reply. + % generic ejabberd command handler handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> - Args2 = [{misc:binary_to_atom(Key), Value} || {Key, Value} <- Args], - try handle2(Call, Auth, Args2, Version) - catch throw:not_found -> - {404, <<"not_found">>}; - throw:{not_found, Why} when is_atom(Why) -> - {404, misc:atom_to_binary(Why)}; - throw:{not_found, Msg} -> - {404, iolist_to_binary(Msg)}; - throw:not_allowed -> - {401, <<"not_allowed">>}; - throw:{not_allowed, Why} when is_atom(Why) -> - {401, misc:atom_to_binary(Why)}; - throw:{not_allowed, Msg} -> - {401, iolist_to_binary(Msg)}; - throw:{error, account_unprivileged} -> - {403, 31, <<"Command need to be run with admin privilege.">>}; - throw:{error, access_rules_unauthorized} -> - {403, 32, <<"AccessRules: Account does not have the right to perform the operation.">>}; - throw:{invalid_parameter, Msg} -> - {400, iolist_to_binary(Msg)}; - throw:{error, Why} when is_atom(Why) -> - {400, misc:atom_to_binary(Why)}; - throw:{error, Msg} -> - {400, iolist_to_binary(Msg)}; - throw:Error when is_atom(Error) -> - {400, misc:atom_to_binary(Error)}; - throw:Msg when is_list(Msg); is_binary(Msg) -> - {400, iolist_to_binary(Msg)}; - ?EX_RULE(Class, Error, Stack) -> - StackTrace = ?EX_STACK(Stack), - ?ERROR_MSG("REST API Error: " - "~ts(~p) -> ~p:~p ~p", - [Call, hide_sensitive_args(Args), - Class, Error, StackTrace]), - {500, <<"internal_error">>} + Args2 = [ {misc:binary_to_atom(Key), Value} || {Key, Value} <- Args ], + try + handle2(Call, Auth, Args2, Version) + catch + throw:not_found -> + {404, <<"not_found">>}; + throw:{not_found, Why} when is_atom(Why) -> + {404, misc:atom_to_binary(Why)}; + throw:{not_found, Msg} -> + {404, iolist_to_binary(Msg)}; + throw:not_allowed -> + {401, <<"not_allowed">>}; + throw:{not_allowed, Why} when is_atom(Why) -> + {401, misc:atom_to_binary(Why)}; + throw:{not_allowed, Msg} -> + {401, iolist_to_binary(Msg)}; + throw:{error, account_unprivileged} -> + {403, 31, <<"Command need to be run with admin privilege.">>}; + throw:{error, access_rules_unauthorized} -> + {403, 32, <<"AccessRules: Account does not have the right to perform the operation.">>}; + throw:{invalid_parameter, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:{error, Why} when is_atom(Why) -> + {400, misc:atom_to_binary(Why)}; + throw:{error, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:Error when is_atom(Error) -> + {400, misc:atom_to_binary(Error)}; + throw:Msg when is_list(Msg); is_binary(Msg) -> + {400, iolist_to_binary(Msg)}; + ?EX_RULE(Class, Error, Stack) -> + StackTrace = ?EX_STACK(Stack), + ?ERROR_MSG("REST API Error: " + "~ts(~p) -> ~p:~p ~p", + [Call, + hide_sensitive_args(Args), + Class, + Error, + StackTrace]), + {500, <<"internal_error">>} end. + handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> {ArgsF, ArgsR, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), ArgsFormatted = format_args(Call, rename_old_args(Args, ArgsR), ArgsF), case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of - {error, Error} -> - throw(Error); - Res -> - format_command_result(Call, Auth, Res, Version) + {error, Error} -> + throw(Error); + Res -> + format_command_result(Call, Auth, Res, Version) end. + rename_old_args(Args, []) -> Args; rename_old_args(Args, [{OldName, NewName} | ArgsR]) -> Args2 = case lists:keytake(OldName, 1, Args) of - {value, {OldName, Value}, ArgsTail} -> - [{NewName, Value} | ArgsTail]; - false -> - Args - end, + {value, {OldName, Value}, ArgsTail} -> + [{NewName, Value} | ArgsTail]; + false -> + Args + end, rename_old_args(Args2, ArgsR). + get_elem_delete(Call, A, L, F) -> case proplists:get_all_values(A, L) of - [Value] -> {Value, proplists:delete(A, L)}; - [_, _ | _] -> - ?INFO_MSG("Command ~ts call rejected, it has duplicate attribute ~w", - [Call, A]), - throw({invalid_parameter, - io_lib:format("Request have duplicate argument: ~w", [A])}); - [] -> - case F of - {list, _} -> - {[], L}; - _ -> - ?INFO_MSG("Command ~ts call rejected, missing attribute ~w", - [Call, A]), - throw({invalid_parameter, - io_lib:format("Request have missing argument: ~w", [A])}) - end + [Value] -> {Value, proplists:delete(A, L)}; + [_, _ | _] -> + ?INFO_MSG("Command ~ts call rejected, it has duplicate attribute ~w", + [Call, A]), + throw({invalid_parameter, + io_lib:format("Request have duplicate argument: ~w", [A])}); + [] -> + case F of + {list, _} -> + {[], L}; + _ -> + ?INFO_MSG("Command ~ts call rejected, missing attribute ~w", + [Call, A]), + throw({invalid_parameter, + io_lib:format("Request have missing argument: ~w", [A])}) + end end. + format_args(Call, Args, ArgsFormat) -> - {ArgsRemaining, R} = lists:foldl(fun ({ArgName, - ArgFormat}, - {Args1, Res}) -> - {ArgValue, Args2} = - get_elem_delete(Call, ArgName, - Args1, ArgFormat), - Formatted = format_arg(ArgValue, - ArgFormat), - {Args2, Res ++ [Formatted]} - end, - {Args, []}, ArgsFormat), + {ArgsRemaining, R} = lists:foldl(fun({ArgName, + ArgFormat}, + {Args1, Res}) -> + {ArgValue, Args2} = + get_elem_delete(Call, + ArgName, + Args1, + ArgFormat), + Formatted = format_arg(ArgValue, + ArgFormat), + {Args2, Res ++ [Formatted]} + end, + {Args, []}, + ArgsFormat), case ArgsRemaining of - [] -> R; - L when is_list(L) -> - ExtraArgs = [N || {N, _} <- L], - ?INFO_MSG("Command ~ts call rejected, it has unknown arguments ~w", - [Call, ExtraArgs]), - throw({invalid_parameter, - io_lib:format("Request have unknown arguments: ~w", [ExtraArgs])}) + [] -> R; + L when is_list(L) -> + ExtraArgs = [ N || {N, _} <- L ], + ?INFO_MSG("Command ~ts call rejected, it has unknown arguments ~w", + [Call, ExtraArgs]), + throw({invalid_parameter, + io_lib:format("Request have unknown arguments: ~w", [ExtraArgs])}) end. + format_arg({Elements}, - {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}}) - when is_list(Elements) andalso - (Tuple1S == binary orelse Tuple1S == string) -> + {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}}) + when is_list(Elements) andalso + (Tuple1S == binary orelse Tuple1S == string) -> lists:map(fun({F1, F2}) -> - {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)}; - ({Val}) when is_list(Val) -> - format_arg({Val}, Tuple) - end, Elements); + {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)}; + ({Val}) when is_list(Val) -> + format_arg({Val}, Tuple) + end, + Elements); format_arg(Map, - {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}}}) - when is_map(Map) andalso - (Tuple1S == binary orelse Tuple1S == string) -> + {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}}}) + when is_map(Map) andalso + (Tuple1S == binary orelse Tuple1S == string) -> maps:fold( - fun(K, V, Acc) -> - [{format_arg(K, Tuple1S), format_arg(V, Tuple2S)} | Acc] - end, [], Map); + fun(K, V, Acc) -> + [{format_arg(K, Tuple1S), format_arg(V, Tuple2S)} | Acc] + end, + [], + Map); format_arg(Elements, - {list, {_ElementDefName, {list, _} = ElementDefFormat}}) - when is_list(Elements) -> - [{format_arg(Element, ElementDefFormat)} - || Element <- Elements]; + {list, {_ElementDefName, {list, _} = ElementDefFormat}}) + when is_list(Elements) -> + [ {format_arg(Element, ElementDefFormat)} + || Element <- Elements ]; %% Covered by command_test_list and command_test_list_tuple format_arg(Element, {list, Def}) - when not is_list(Element) -> + when not is_list(Element) -> format_arg([Element], {list, Def}); format_arg(Elements, - {list, {_ElementDefName, ElementDefFormat}}) - when is_list(Elements) -> - [format_arg(Element, ElementDefFormat) - || Element <- Elements]; + {list, {_ElementDefName, ElementDefFormat}}) + when is_list(Elements) -> + [ format_arg(Element, ElementDefFormat) + || Element <- Elements ]; format_arg({[{Name, Value}]}, - {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}) + {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}) when Tuple1S == binary; Tuple1S == string -> {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)}; %% Covered by command_test_tuple and command_test_list_tuple format_arg(Elements, - {tuple, ElementsDef}) + {tuple, ElementsDef}) when is_map(Elements) -> - list_to_tuple([element(2, maps:find(atom_to_binary(Name, latin1), Elements)) - || {Name, _Format} <- ElementsDef]); + list_to_tuple([ element(2, maps:find(atom_to_binary(Name, latin1), Elements)) + || {Name, _Format} <- ElementsDef ]); format_arg({Elements}, - {tuple, ElementsDef}) - when is_list(Elements) -> + {tuple, ElementsDef}) + when is_list(Elements) -> F = lists:map(fun({TElName, TElDef}) -> - case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of - {_, Value} -> - format_arg(Value, TElDef); - _ when TElDef == binary; TElDef == string -> - <<"">>; - _ -> - ?ERROR_MSG("Missing field ~p in tuple ~p", [TElName, Elements]), - throw({invalid_parameter, - io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])}) - end - end, ElementsDef), + case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of + {_, Value} -> + format_arg(Value, TElDef); + _ when TElDef == binary; TElDef == string -> + <<"">>; + _ -> + ?ERROR_MSG("Missing field ~p in tuple ~p", [TElName, Elements]), + throw({invalid_parameter, + io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])}) + end + end, + ElementsDef), list_to_tuple(F); format_arg(Elements, {list, ElementsDef}) - when is_list(Elements) and is_atom(ElementsDef) -> - [format_arg(Element, ElementsDef) - || Element <- Elements]; + when is_list(Elements) and is_atom(ElementsDef) -> + [ format_arg(Element, ElementsDef) + || Element <- Elements ]; format_arg(Arg, integer) when is_integer(Arg) -> Arg; format_arg(Arg, integer) when is_binary(Arg) -> binary_to_integer(Arg); @@ -408,38 +454,42 @@ format_arg(undefined, string) -> ""; format_arg(Arg, Format) -> ?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]), throw({invalid_parameter, - io_lib:format("Arg ~w is not in format ~w", - [Arg, Format])}). + io_lib:format("Arg ~w is not in format ~w", + [Arg, Format])}). + process_unicode_codepoints(Str) -> iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); (Y) -> Y - end, Str)). + end, + Str)). + %% ---------------- %% internal helpers %% ---------------- + format_command_result(Cmd, Auth, Result, Version) -> {_, _, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), case {ResultFormat, Result} of - {{_, rescode}, V} when V == true; V == ok -> - {200, 0}; - {{_, rescode}, _} -> - {200, 1}; + {{_, rescode}, V} when V == true; V == ok -> + {200, 0}; + {{_, rescode}, _} -> + {200, 1}; {_, {error, ErrorAtom, Code, Msg}} -> format_error_result(ErrorAtom, Code, Msg); {{_, restuple}, {V, Text}} when V == true; V == ok -> {200, iolist_to_binary(Text)}; {{_, restuple}, {ErrorAtom, Msg}} -> format_error_result(ErrorAtom, 0, Msg); - {{_, {list, _}}, _V} -> - {_, L} = format_result(Result, ResultFormat), - {200, L}; - {{_, {tuple, _}}, _V} -> - {_, T} = format_result(Result, ResultFormat), - {200, T}; - _ -> + {{_, {list, _}}, _V} -> + {_, L} = format_result(Result, ResultFormat), + {200, L}; + {{_, {tuple, _}}, _V} -> + {_, T} = format_result(Result, ResultFormat), + {200, T}; + _ -> OtherResult1 = format_result(Result, ResultFormat), OtherResult2 = case Version of 0 -> @@ -448,9 +498,10 @@ format_command_result(Cmd, Auth, Result, Version) -> {_, Other3} = OtherResult1, Other3 end, - {200, OtherResult2} + {200, OtherResult2} end. + format_result(Atom, {Name, atom}) -> {misc:atom_to_binary(Name), misc:atom_to_binary(Atom)}; @@ -482,16 +533,16 @@ format_result(Code, {Name, restuple}) -> format_result(Els1, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> Els = lists:keysort(1, Els1), - {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; + {misc:atom_to_binary(Name), {[ format_result(El, Fmt) || El <- Els ]}}; format_result(Els1, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) -> Els = lists:keysort(1, Els1), - {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; + {misc:atom_to_binary(Name), {[ format_result(El, Fmt) || El <- Els ]}}; %% Covered by command_test_list and command_test_list_tuple format_result(Els1, {Name, {list, Def}}) -> Els = lists:sort(Els1), - {misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]}; + {misc:atom_to_binary(Name), [ element(2, format_result(El, Def)) || El <- Els ]}; format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) -> {Name2, Val} = Tuple, @@ -506,7 +557,7 @@ format_result(Tuple, {_Name, {tuple, [{name, string}, {value, _} = ValFmt]}}) -> %% Covered by command_test_tuple and command_test_list_tuple format_result(Tuple, {Name, {tuple, Def}}) -> Els = lists:zip(tuple_to_list(Tuple), Def), - Els2 = [format_result(El, ElDef) || {El, ElDef} <- Els], + Els2 = [ format_result(El, ElDef) || {El, ElDef} <- Els ], {misc:atom_to_binary(Name), maps:from_list(Els2)}; format_result(404, {_Name, _}) -> @@ -520,36 +571,48 @@ format_error_result(not_exists, Code, Msg) -> format_error_result(_ErrorAtom, Code, Msg) -> {500, Code, iolist_to_binary(Msg)}. + unauthorized_response() -> json_error(401, 10, <<"You are not authorized to call this command.">>). + invalid_token_response() -> json_error(401, 10, <<"Oauth Token is invalid or expired.">>). + %% outofscope_response() -> %% json_error(401, 11, <<"Token does not grant usage to command required scope.">>). + badrequest_response() -> badrequest_response(<<"400 Bad Request">>). + + badrequest_response(Body) -> json_response(400, misc:json_encode(Body)). + json_format({Code, Result}) -> json_response(Code, misc:json_encode(Result)); json_format({HTMLCode, JSONErrorCode, Message}) -> json_error(HTMLCode, JSONErrorCode, Message). + json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. + %% HTTPCode, JSONCode = integers %% message is binary json_error(HTTPCode, JSONCode, Message) -> - {HTTPCode, ?HEADER(?CT_JSON), - misc:json_encode(#{<<"status">> => <<"error">>, - <<"code">> => JSONCode, - <<"message">> => Message}) - }. + {HTTPCode, + ?HEADER(?CT_JSON), + misc:json_encode(#{ + <<"status">> => <<"error">>, + <<"code">> => JSONCode, + <<"message">> => Message + })}. + log(Call, Args, {Addr, Port}) -> AddrS = misc:ip_to_list({Addr, Port}), @@ -557,66 +620,78 @@ log(Call, Args, {Addr, Port}) -> log(Call, Args, IP) -> ?INFO_MSG("API call ~ts ~p (~p)", [Call, hide_sensitive_args(Args), IP]). -hide_sensitive_args(Args=[_H|_T]) -> + +hide_sensitive_args(Args = [_H | _T]) -> lists:map(fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)}; - ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)}; - (E) -> E end, - Args); + ({<<"newpass">>, NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)}; + (E) -> E + end, + Args); hide_sensitive_args(NonListArgs) -> NonListArgs. + mod_opt_type(default_version) -> econf:either( - econf:int(0, 3), - econf:and_then( - econf:binary(), - fun(Binary) -> - case binary_to_list(Binary) of - F when F >= "24.06" -> - 2; - F when (F > "23.10") and (F < "24.06") -> - 1; - F when F =< "23.10" -> - 0 - end - end)). + econf:int(0, 3), + econf:and_then( + econf:binary(), + fun(Binary) -> + case binary_to_list(Binary) of + F when F >= "24.06" -> + 2; + F when (F > "23.10") and (F < "24.06") -> + 1; + F when F =< "23.10" -> + 0 + end + end)). + -spec mod_options(binary()) -> [{default_version, integer()}]. mod_options(_) -> [{default_version, ?DEFAULT_API_VERSION}]. + mod_doc() -> - #{desc => - [?T("This module provides a ReST interface to call " + #{ + desc => + [?T("This module provides a ReST interface to call " "_`../../developer/ejabberd-api/index.md|ejabberd API`_ " - "commands using JSON data."), "", - ?T("To use this module, in addition to adding it to the 'modules' " - "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " - "_`listen-options.md#request_handlers|request_handlers`_."), "", - ?T("To use a specific API version N, when defining the URL path " - "in the request_handlers, add a vN. " - "For example: '/api/v2: mod_http_api'."), "", - ?T("To run a command, send a POST request to the corresponding " - "URL: 'http://localhost:5280/api/COMMAND-NAME'")], - opts => + "commands using JSON data."), + "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "_`listen-options.md#request_handlers|request_handlers`_."), + "", + ?T("To use a specific API version N, when defining the URL path " + "in the request_handlers, add a vN. " + "For example: '/api/v2: mod_http_api'."), + "", + ?T("To run a command, send a POST request to the corresponding " + "URL: 'http://localhost:5280/api/COMMAND-NAME'")], + opts => [{default_version, - #{value => "integer() | string()", + #{ + value => "integer() | string()", note => "added in 24.12", desc => ?T("What API version to use when none is specified in the URL path. " "If setting an ejabberd version, it will use the latest API " "version that was available in that ejabberd version. " "For example, setting '\"24.06\"' in this option implies '2'. " - "The default value is the latest version.")}}], - example => - ["listen:", - " -", - " port: 5280", - " module: ejabberd_http", - " request_handlers:", - " /api: mod_http_api", - "", - "modules:", - " mod_http_api:", - " default_version: 2"]}. + "The default value is the latest version.") + }}], + example => + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /api: mod_http_api", + "", + "modules:", + " mod_http_api:", + " default_version: 2"] + }. diff --git a/src/mod_http_api_opt.erl b/src/mod_http_api_opt.erl index 326c53e02..0bb1befe2 100644 --- a/src/mod_http_api_opt.erl +++ b/src/mod_http_api_opt.erl @@ -5,9 +5,9 @@ -export([default_version/1]). + -spec default_version(gen_mod:opts() | global | binary()) -> integer(). default_version(Opts) when is_map(Opts) -> gen_mod:get_opt(default_version, Opts); default_version(Host) -> gen_mod:get_module_opt(Host, mod_http_api, default_version). - diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index 3f8db94f5..27e732d27 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -34,8 +34,12 @@ -export([start/2, stop/1, reload/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% request_handlers callbacks -export([process/2]). @@ -47,60 +51,74 @@ -include("logger.hrl"). -include("ejabberd_http.hrl"). + -include_lib("kernel/include/file.hrl"). + -include("translate.hrl"). --record(state, - {host, docroot, accesslog, accesslogfd, - directory_indices, custom_headers, default_content_type, - content_types = [], user_access = none}). +-record(state, { + host, + docroot, + accesslog, + accesslogfd, + directory_indices, + custom_headers, + default_content_type, + content_types = [], + user_access = none + }). %% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data} -define(HTTP_ERR_FILE_NOT_FOUND, - {-1, 404, [], <<"Not found">>}). + {-1, 404, [], <<"Not found">>}). -define(REQUEST_AUTH_HEADERS, - [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]). + [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]). -define(HTTP_ERR_FORBIDDEN, - {-1, 403, [], <<"Forbidden">>}). + {-1, 403, [], <<"Forbidden">>}). -define(HTTP_ERR_REQUEST_AUTH, - {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). + {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). -define(HTTP_ERR_HOST_UNKNOWN, - {-1, 410, [], <<"Host unknown">>}). + {-1, 410, [], <<"Host unknown">>}). -define(DEFAULT_CONTENT_TYPES, - [{<<".css">>, <<"text/css">>}, - {<<".gif">>, <<"image/gif">>}, - {<<".html">>, <<"text/html">>}, - {<<".jar">>, <<"application/java-archive">>}, - {<<".jpeg">>, <<"image/jpeg">>}, - {<<".jpg">>, <<"image/jpeg">>}, - {<<".js">>, <<"text/javascript">>}, - {<<".png">>, <<"image/png">>}, - {<<".svg">>, <<"image/svg+xml">>}, - {<<".txt">>, <<"text/plain">>}, - {<<".xml">>, <<"application/xml">>}, - {<<".xpi">>, <<"application/x-xpinstall">>}, - {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>}]). + [{<<".css">>, <<"text/css">>}, + {<<".gif">>, <<"image/gif">>}, + {<<".html">>, <<"text/html">>}, + {<<".jar">>, <<"application/java-archive">>}, + {<<".jpeg">>, <<"image/jpeg">>}, + {<<".jpg">>, <<"image/jpeg">>}, + {<<".js">>, <<"text/javascript">>}, + {<<".png">>, <<"image/png">>}, + {<<".svg">>, <<"image/svg+xml">>}, + {<<".txt">>, <<"text/plain">>}, + {<<".xml">>, <<"application/xml">>}, + {<<".xpi">>, <<"application/x-xpinstall">>}, + {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>}]). %%==================================================================== %% gen_mod callbacks %%==================================================================== + start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(Host, NewOpts, OldOpts) -> Proc = get_proc_name(Host), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + depends(_Host, _Opts) -> []. + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -111,17 +129,18 @@ depends(_Host, _Opts) -> %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- -init([Host|_]) -> +init([Host | _]) -> Opts = gen_mod:get_module_opts(Host, ?MODULE), try initialize(Host, Opts) of - State -> - process_flag(trap_exit, true), - {ok, State} + State -> + process_flag(trap_exit, true), + {ok, State} catch - throw:Reason -> - {stop, Reason} + throw:Reason -> + {stop, Reason} end. + initialize(Host, Opts) -> DocRoot = mod_http_fileserver_opt:docroot(Opts), AccessLog = mod_http_fileserver_opt:accesslog(Opts), @@ -131,29 +150,32 @@ initialize(Host, Opts) -> DefaultContentType = mod_http_fileserver_opt:default_content_type(Opts), UserAccess0 = mod_http_fileserver_opt:must_authenticate_with(Opts), UserAccess = case UserAccess0 of - [] -> none; - _ -> - maps:from_list(UserAccess0) - end, + [] -> none; + _ -> + maps:from_list(UserAccess0) + end, ContentTypes = build_list_content_types( mod_http_fileserver_opt:content_types(Opts), ?DEFAULT_CONTENT_TYPES), ?DEBUG("Known content types: ~ts", - [str:join([[$*, K, " -> ", V] || {K, V} <- ContentTypes], - <<", ">>)]), - #state{host = Host, - accesslog = AccessLog, - accesslogfd = AccessLogFD, - docroot = DocRoot, - directory_indices = DirectoryIndices, - custom_headers = CustomHeaders, - default_content_type = DefaultContentType, - content_types = ContentTypes, - user_access = UserAccess}. + [str:join([ [$*, K, " -> ", V] || {K, V} <- ContentTypes ], + <<", ">>)]), + #state{ + host = Host, + accesslog = AccessLog, + accesslogfd = AccessLogFD, + docroot = DocRoot, + directory_indices = DirectoryIndices, + custom_headers = CustomHeaders, + default_content_type = DefaultContentType, + content_types = ContentTypes, + user_access = UserAccess + }. --spec build_list_content_types(AdminCTs::[{binary(), binary()|undefined}], - Default::[{binary(), binary()|undefined}]) -> - [{string(), string()|undefined}]. + +-spec build_list_content_types(AdminCTs :: [{binary(), binary() | undefined}], + Default :: [{binary(), binary() | undefined}]) -> + [{string(), string() | undefined}]. %% where CT = {Extension::string(), Value} %% Value = string() | undefined %% @doc Return a unified list without duplicates. @@ -162,25 +184,28 @@ initialize(Host, Opts) -> build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) -> AdminCTs = lists:ukeysort(1, AdminCTsUnsorted), DefaultCTs = lists:ukeysort(1, DefaultCTsUnsorted), - CTsUnfiltered = lists:ukeymerge(1, AdminCTs, - DefaultCTs), - [{Extension, Value} - || {Extension, Value} <- CTsUnfiltered, - Value /= undefined]. + CTsUnfiltered = lists:ukeymerge(1, + AdminCTs, + DefaultCTs), + [ {Extension, Value} + || {Extension, Value} <- CTsUnfiltered, + Value /= undefined ]. + try_open_log(undefined, _Host) -> undefined; try_open_log(FN, _Host) -> FD = try open_log(FN) of - FD1 -> FD1 - catch - throw:{cannot_open_accesslog, FN, Reason} -> - ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]), - undefined - end, + FD1 -> FD1 + catch + throw:{cannot_open_accesslog, FN, Reason} -> + ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]), + undefined + end, ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50), FD. + %%-------------------------------------------------------------------- %% Function: handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | @@ -192,20 +217,26 @@ try_open_log(FN, _Host) -> %%-------------------------------------------------------------------- handle_call({serve, LocalPath, Auth, RHeaders}, _From, State) -> IfModifiedSince = case find_header('If-Modified-Since', RHeaders, bad_date) of - bad_date -> - bad_date; - Val -> - httpd_util:convert_request_date(binary_to_list(Val)) - end, - Reply = serve(LocalPath, Auth, State#state.docroot, State#state.directory_indices, - State#state.custom_headers, - State#state.default_content_type, State#state.content_types, - State#state.user_access, IfModifiedSince), + bad_date -> + bad_date; + Val -> + httpd_util:convert_request_date(binary_to_list(Val)) + end, + Reply = serve(LocalPath, + Auth, + State#state.docroot, + State#state.directory_indices, + State#state.custom_headers, + State#state.default_content_type, + State#state.content_types, + State#state.user_access, + IfModifiedSince), {reply, Reply, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | @@ -220,16 +251,18 @@ handle_cast(reopen_log, State) -> {noreply, State#state{accesslogfd = FD2}}; handle_cast({reload, Host, NewOpts, _OldOpts}, OldState) -> try initialize(Host, NewOpts) of - NewState -> - FD = reopen_log(NewState#state.accesslog, OldState#state.accesslogfd), - {noreply, NewState#state{accesslogfd = FD}} - catch throw:_ -> - {noreply, OldState} + NewState -> + FD = reopen_log(NewState#state.accesslog, OldState#state.accesslogfd), + {noreply, NewState#state{accesslogfd = FD}} + catch + throw:_ -> + {noreply, OldState} end; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | @@ -240,6 +273,7 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() %% Description: This function is called by a gen_server when it is about to @@ -250,12 +284,13 @@ handle_info(Info, State) -> terminate(_Reason, #state{host = Host} = State) -> close_log(State#state.accesslogfd), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50); - true -> - ok + false -> + ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50); + true -> + ok end. + %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed @@ -263,76 +298,92 @@ terminate(_Reason, #state{host = Host} = State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%==================================================================== %% request_handlers callbacks %%==================================================================== --spec process(LocalPath::[binary()], #request{}) -> - {HTTPCode::integer(), [{binary(), binary()}], Page::string()}. + +-spec process(LocalPath :: [binary()], #request{}) -> + {HTTPCode :: integer(), [{binary(), binary()}], Page :: string()}. %% @doc Handle an HTTP request. %% LocalPath is the part of the requested URL path that is "local to the module". %% Returns the page to be sent back to the client and/or HTTP status code. process(LocalPath, #request{host = Host, auth = Auth, headers = RHeaders} = Request) -> ?DEBUG("Requested ~p", [LocalPath]), try - VHost = ejabberd_router:host_of_route(Host), - {FileSize, Code, Headers, Contents} = - gen_server:call(get_proc_name(VHost), - {serve, LocalPath, Auth, RHeaders}), - add_to_log(FileSize, Code, Request#request{host = VHost}), - {Code, Headers, Contents} - catch _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route -> - ?DEBUG("Received an HTTP request with Host: ~ts, " - "but couldn't find the related " - "ejabberd virtual host", [Host]), - {FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN, - add_to_log(FileSize1, Code1, Request#request{host = ejabberd_config:get_myname()}), - {Code1, Headers1, Contents1} + VHost = ejabberd_router:host_of_route(Host), + {FileSize, Code, Headers, Contents} = + gen_server:call(get_proc_name(VHost), + {serve, LocalPath, Auth, RHeaders}), + add_to_log(FileSize, Code, Request#request{host = VHost}), + {Code, Headers, Contents} + catch + _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route -> + ?DEBUG("Received an HTTP request with Host: ~ts, " + "but couldn't find the related " + "ejabberd virtual host", + [Host]), + {FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN, + add_to_log(FileSize1, Code1, Request#request{host = ejabberd_config:get_myname()}), + {Code1, Headers1, Contents1} end. -serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, - ContentTypes, UserAccess, IfModifiedSince) -> + +serve(LocalPath, + Auth, + DocRoot, + DirectoryIndices, + CustomHeaders, + DefaultContentType, + ContentTypes, + UserAccess, + IfModifiedSince) -> CanProceed = case {UserAccess, Auth} of - {none, _} -> true; - {_, {User, Pass}} -> - case maps:find(User, UserAccess) of - {ok, Pass} -> true; - _ -> false - end; - _ -> - false - end, + {none, _} -> true; + {_, {User, Pass}} -> + case maps:find(User, UserAccess) of + {ok, Pass} -> true; + _ -> false + end; + _ -> + false + end, case CanProceed of - false -> - ?HTTP_ERR_REQUEST_AUTH; - true -> - FileName = filename:join(filename:split(DocRoot) ++ LocalPath), - case file:read_file_info(FileName) of - {error, enoent} -> - ?HTTP_ERR_FILE_NOT_FOUND; - {error, enotdir} -> - ?HTTP_ERR_FILE_NOT_FOUND; - {error, eacces} -> - ?HTTP_ERR_FORBIDDEN; - {ok, #file_info{type = directory}} -> serve_index(FileName, - DirectoryIndices, - CustomHeaders, - DefaultContentType, - ContentTypes); - {ok, #file_info{mtime = MTime} = FileInfo} -> - case calendar:local_time_to_universal_time_dst(MTime) of - [IfModifiedSince | _] -> - serve_not_modified(FileInfo, FileName, - CustomHeaders); - _ -> - serve_file(FileInfo, FileName, - CustomHeaders, - DefaultContentType, - ContentTypes) - end - end + false -> + ?HTTP_ERR_REQUEST_AUTH; + true -> + FileName = filename:join(filename:split(DocRoot) ++ LocalPath), + case file:read_file_info(FileName) of + {error, enoent} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, enotdir} -> + ?HTTP_ERR_FILE_NOT_FOUND; + {error, eacces} -> + ?HTTP_ERR_FORBIDDEN; + {ok, #file_info{type = directory}} -> + serve_index(FileName, + DirectoryIndices, + CustomHeaders, + DefaultContentType, + ContentTypes); + {ok, #file_info{mtime = MTime} = FileInfo} -> + case calendar:local_time_to_universal_time_dst(MTime) of + [IfModifiedSince | _] -> + serve_not_modified(FileInfo, + FileName, + CustomHeaders); + _ -> + serve_file(FileInfo, + FileName, + CustomHeaders, + DefaultContentType, + ContentTypes) + end + end end. + %% Troll through the directory indices attempting to find one which %% works, if none can be found, return a 404. serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) -> @@ -340,63 +391,77 @@ serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) -> serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) -> IndexFileName = filename:join([FileName] ++ [Index]), case file:read_file_info(IndexFileName) of - {error, _Error} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes); + {error, _Error} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes); {ok, #file_info{type = directory}} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes); - {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes) + {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes) end. + serve_not_modified(FileInfo, FileName, CustomHeaders) -> ?DEBUG("Delivering not modified: ~ts", [FileName]), - {0, 304, + {0, + 304, ejabberd_http:apply_custom_headers( - [{<<"Server">>, <<"ejabberd">>}, - {<<"Last-Modified">>, last_modified(FileInfo)}], - CustomHeaders), <<>>}. + [{<<"Server">>, <<"ejabberd">>}, + {<<"Last-Modified">>, last_modified(FileInfo)}], + CustomHeaders), + <<>>}. + %% Assume the file exists if we got this far and attempt to read it in %% and serve it up. serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) -> ?DEBUG("Delivering: ~ts", [FileName]), - ContentType = content_type(FileName, DefaultContentType, - ContentTypes), - {FileInfo#file_info.size, 200, + ContentType = content_type(FileName, + DefaultContentType, + ContentTypes), + {FileInfo#file_info.size, + 200, ejabberd_http:apply_custom_headers( - [{<<"Server">>, <<"ejabberd">>}, - {<<"Last-Modified">>, last_modified(FileInfo)}, - {<<"Content-Type">>, ContentType}], - CustomHeaders), + [{<<"Server">>, <<"ejabberd">>}, + {<<"Last-Modified">>, last_modified(FileInfo)}, + {<<"Content-Type">>, ContentType}], + CustomHeaders), {file, FileName}}. + %%---------------------------------------------------------------------- %% Log file %%---------------------------------------------------------------------- + open_log(FN) -> case file:open(FN, [append]) of - {ok, FD} -> - FD; - {error, Reason} -> - throw({cannot_open_accesslog, FN, Reason}) + {ok, FD} -> + FD; + {error, Reason} -> + throw({cannot_open_accesslog, FN, Reason}) end. + close_log(FD) -> file:close(FD). + reopen_log(undefined, undefined) -> ok; reopen_log(FN, FD) -> close_log(FD), open_log(FN). + reopen_log() -> lists:foreach( fun(Host) -> - gen_server:cast(get_proc_name(Host), reopen_log) - end, ejabberd_option:hosts()). + gen_server:cast(get_proc_name(Host), reopen_log) + end, + ejabberd_option:hosts()). + add_to_log(FileSize, Code, Request) -> gen_server:cast(get_proc_name(Request#request.host), - {add_to_log, FileSize, Code, Request}). + {add_to_log, FileSize, Code, Request}). + add_to_log(undefined, _FileSize, _Code, _Request) -> ok; @@ -405,11 +470,11 @@ add_to_log(File, FileSize, Code, Request) -> IP = ip_to_string(element(1, Request#request.ip)), Path = join(Request#request.path, "/"), Query = case stringify_query(Request#request.q) of - <<"">> -> - ""; - String -> - [$? | String] - end, + <<"">> -> + ""; + String -> + [$? | String] + end, UserAgent = find_header('User-Agent', Request#request.headers, "-"), Referer = find_header('Referer', Request#request.headers, "-"), %% Pseudo Combined Apache log format: @@ -420,59 +485,83 @@ add_to_log(File, FileSize, Code, Request) -> %% Missing time zone = (`+' | `-') 4*digit %% Missing protocol version: HTTP/1.1 %% For reference: http://httpd.apache.org/docs/2.2/logs.html - io:format(File, "~ts - - [~p/~p/~p:~p:~p:~p] \"~ts /~ts~ts\" ~p ~p ~p ~p~n", - [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code, - FileSize, Referer, UserAgent]). + io:format(File, + "~ts - - [~p/~p/~p:~p:~p:~p] \"~ts /~ts~ts\" ~p ~p ~p ~p~n", + [IP, + Day, + Month, + Year, + Hour, + Minute, + Second, + Request#request.method, + Path, + Query, + Code, + FileSize, + Referer, + UserAgent]). + stringify_query(Q) -> stringify_query(Q, []). + + stringify_query([], Res) -> join(lists:reverse(Res), "&"); stringify_query([{nokey, _B} | Q], Res) -> stringify_query(Q, Res); stringify_query([{A, B} | Q], Res) -> - stringify_query(Q, [join([A,B], "=") | Res]). + stringify_query(Q, [join([A, B], "=") | Res]). + find_header(Header, Headers, Default) -> case lists:keysearch(Header, 1, Headers) of - {value, {_, Value}} -> Value; - false -> Default + {value, {_, Value}} -> Value; + false -> Default end. + %%---------------------------------------------------------------------- %% Utilities %%---------------------------------------------------------------------- + get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE). + join([], _) -> <<"">>; join([E], _) -> E; join([H | T], Separator) -> - [H2 | T2] = case is_binary(H) of true -> [binary_to_list(I)||I<-[H|T]]; false -> [H | T] end, - Res=lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H2, T2), + [H2 | T2] = case is_binary(H) of true -> [ binary_to_list(I) || I <- [H | T] ]; false -> [H | T] end, + Res = lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H2, T2), case is_binary(H) of true -> list_to_binary(Res); false -> Res end. + content_type(Filename, DefaultContentType, ContentTypes) -> Extension = str:to_lower(filename:extension(Filename)), case lists:keysearch(Extension, 1, ContentTypes) of - {value, {_, ContentType}} -> ContentType; - false -> DefaultContentType + {value, {_, ContentType}} -> ContentType; + false -> DefaultContentType end. + last_modified(FileInfo) -> Then = FileInfo#file_info.mtime, httpd_util:rfc1123_date(Then). + %% Convert IP address tuple to string representation. Accepts either %% IPv4 or IPv6 address tuples. ip_to_string(Address) when size(Address) == 4 -> join(tuple_to_list(Address), "."); ip_to_string(Address) when size(Address) == 8 -> - Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)), + Parts = lists:map(fun(Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)), string:to_lower(lists:flatten(join(Parts, ":"))). + mod_opt_type(accesslog) -> econf:file(write); mod_opt_type(content_types) -> @@ -488,13 +577,14 @@ mod_opt_type(docroot) -> mod_opt_type(must_authenticate_with) -> econf:list( econf:and_then( - econf:and_then( - econf:binary("^[^:]+:[^:]+$"), - econf:binary_sep(":")), - fun([K, V]) -> {K, V} end)). + econf:and_then( + econf:binary("^[^:]+:[^:]+$"), + econf:binary_sep(":")), + fun([K, V]) -> {K, V} end)). + -spec mod_options(binary()) -> [{must_authenticate_with, [{binary(), binary()}]} | - {atom(), any()}]. + {atom(), any()}]. mod_options(_) -> [{accesslog, undefined}, {content_types, []}, @@ -505,79 +595,95 @@ mod_options(_) -> %% Required option docroot]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This simple module serves files from the local disk over HTTP."), opts => [{accesslog, - #{value => ?T("Path"), + #{ + value => ?T("Path"), desc => ?T("File to log accesses using an Apache-like format. " - "No log will be recorded if this option is not specified.")}}, + "No log will be recorded if this option is not specified.") + }}, {docroot, - #{value => ?T("Path"), + #{ + value => ?T("Path"), desc => ?T("Directory to serve the files from. " - "This is a mandatory option.")}}, + "This is a mandatory option.") + }}, {content_types, - #{value => "{Extension: Type}", + #{ + value => "{Extension: Type}", desc => ?T("Specify mappings of extension to content type. " "There are several content types already defined. " "With this option you can add new definitions " "or modify existing ones. The default values are:"), example => - ["content_types:"| - [" " ++ binary_to_list(E) ++ ": " ++ binary_to_list(T) - || {E, T} <- ?DEFAULT_CONTENT_TYPES]]}}, + ["content_types:" | [ " " ++ binary_to_list(E) ++ ": " ++ binary_to_list(T) + || {E, T} <- ?DEFAULT_CONTENT_TYPES ]] + }}, {default_content_type, - #{value => ?T("Type"), + #{ + value => ?T("Type"), desc => ?T("Specify the content type to use for unknown extensions. " - "The default value is 'application/octet-stream'.")}}, + "The default value is 'application/octet-stream'.") + }}, {custom_headers, - #{value => "{Name: Value}", + #{ + value => "{Name: Value}", desc => ?T("Indicate custom HTTP headers to be included in all responses. " - "There are no custom headers by default.")}}, + "There are no custom headers by default.") + }}, {directory_indices, - #{value => "[Index, ...]", + #{ + value => "[Index, ...]", desc => ?T("Indicate one or more directory index files, " "similarly to Apache's 'DirectoryIndex' variable. " "When an HTTP request hits a directory instead of a " "regular file, those directory indices are looked in order, " "and the first one found is returned. " - "The default value is an empty list.")}}, + "The default value is an empty list.") + }}, {must_authenticate_with, - #{value => ?T("[{Username, Hostname}, ...]"), + #{ + value => ?T("[{Username, Hostname}, ...]"), desc => ?T("List of accounts that are allowed to use this service. " - "Default value: '[]'.")}}], + "Default value: '[]'.") + }}], example => [{?T("This example configuration will serve the files from the " - "local directory '/var/www' in the address " - "'http://example.org:5280/pub/content/'. In this example a new " - "content type 'ogg' is defined, 'png' is redefined, and 'jpg' " - "definition is deleted:"), - ["listen:", - " -", - " port: 5280", - " module: ejabberd_http", - " request_handlers:", - " /pub/content: mod_http_fileserver", - "", - "modules:", - " mod_http_fileserver:", - " docroot: /var/www", - " accesslog: /var/log/ejabberd/access.log", - " directory_indices:", - " - index.html", - " - main.htm", - " custom_headers:", - " X-Powered-By: Erlang/OTP", - " X-Fry: \"It's a widely-believed fact!\"", - " content_types:", - " .ogg: audio/ogg", - " .png: image/png", - " default_content_type: text/html"]}]}. + "local directory '/var/www' in the address " + "'http://example.org:5280/pub/content/'. In this example a new " + "content type 'ogg' is defined, 'png' is redefined, and 'jpg' " + "definition is deleted:"), + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /pub/content: mod_http_fileserver", + "", + "modules:", + " mod_http_fileserver:", + " docroot: /var/www", + " accesslog: /var/log/ejabberd/access.log", + " directory_indices:", + " - index.html", + " - main.htm", + " custom_headers:", + " X-Powered-By: Erlang/OTP", + " X-Fry: \"It's a widely-believed fact!\"", + " content_types:", + " .ogg: audio/ogg", + " .png: image/png", + " default_content_type: text/html"]}] + }. diff --git a/src/mod_http_fileserver_opt.erl b/src/mod_http_fileserver_opt.erl index 442ce1d90..a365bdaf3 100644 --- a/src/mod_http_fileserver_opt.erl +++ b/src/mod_http_fileserver_opt.erl @@ -11,45 +11,51 @@ -export([docroot/1]). -export([must_authenticate_with/1]). + -spec accesslog(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). accesslog(Opts) when is_map(Opts) -> gen_mod:get_opt(accesslog, Opts); accesslog(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, accesslog). --spec content_types(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec content_types(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. content_types(Opts) when is_map(Opts) -> gen_mod:get_opt(content_types, Opts); content_types(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, content_types). --spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. custom_headers(Opts) when is_map(Opts) -> gen_mod:get_opt(custom_headers, Opts); custom_headers(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, custom_headers). + -spec default_content_type(gen_mod:opts() | global | binary()) -> binary(). default_content_type(Opts) when is_map(Opts) -> gen_mod:get_opt(default_content_type, Opts); default_content_type(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, default_content_type). + -spec directory_indices(gen_mod:opts() | global | binary()) -> [binary()]. directory_indices(Opts) when is_map(Opts) -> gen_mod:get_opt(directory_indices, Opts); directory_indices(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, directory_indices). + -spec docroot(gen_mod:opts() | global | binary()) -> binary(). docroot(Opts) when is_map(Opts) -> gen_mod:get_opt(docroot, Opts); docroot(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, docroot). --spec must_authenticate_with(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec must_authenticate_with(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. must_authenticate_with(Opts) when is_map(Opts) -> gen_mod:get_opt(must_authenticate_with, Opts); must_authenticate_with(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, must_authenticate_with). - diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index dfa194525..d2605a5ee 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -29,51 +29,51 @@ -behaviour(gen_mod). -protocol({xep, 363, '0.3.0', '15.10', "complete", ""}). --define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. --define(CALL_TIMEOUT, 60000). % 1 minute. --define(SLOT_TIMEOUT, timer:hours(5)). --define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). +-define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. +-define(CALL_TIMEOUT, 60000). % 1 minute. +-define(SLOT_TIMEOUT, timer:hours(5)). +-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). -define(CONTENT_TYPES, - [{<<".avi">>, <<"video/avi">>}, - {<<".bmp">>, <<"image/bmp">>}, - {<<".bz2">>, <<"application/x-bzip2">>}, - {<<".gif">>, <<"image/gif">>}, - {<<".gz">>, <<"application/x-gzip">>}, - {<<".jpeg">>, <<"image/jpeg">>}, - {<<".jpg">>, <<"image/jpeg">>}, - {<<".m4a">>, <<"audio/mp4">>}, - {<<".mp3">>, <<"audio/mpeg">>}, - {<<".mp4">>, <<"video/mp4">>}, - {<<".mpeg">>, <<"video/mpeg">>}, - {<<".mpg">>, <<"video/mpeg">>}, - {<<".ogg">>, <<"application/ogg">>}, - {<<".pdf">>, <<"application/pdf">>}, - {<<".png">>, <<"image/png">>}, - {<<".rtf">>, <<"application/rtf">>}, - {<<".svg">>, <<"image/svg+xml">>}, - {<<".tiff">>, <<"image/tiff">>}, - {<<".txt">>, <<"text/plain">>}, - {<<".wav">>, <<"audio/wav">>}, - {<<".webp">>, <<"image/webp">>}, - {<<".xz">>, <<"application/x-xz">>}, - {<<".zip">>, <<"application/zip">>}]). + [{<<".avi">>, <<"video/avi">>}, + {<<".bmp">>, <<"image/bmp">>}, + {<<".bz2">>, <<"application/x-bzip2">>}, + {<<".gif">>, <<"image/gif">>}, + {<<".gz">>, <<"application/x-gzip">>}, + {<<".jpeg">>, <<"image/jpeg">>}, + {<<".jpg">>, <<"image/jpeg">>}, + {<<".m4a">>, <<"audio/mp4">>}, + {<<".mp3">>, <<"audio/mpeg">>}, + {<<".mp4">>, <<"video/mp4">>}, + {<<".mpeg">>, <<"video/mpeg">>}, + {<<".mpg">>, <<"video/mpeg">>}, + {<<".ogg">>, <<"application/ogg">>}, + {<<".pdf">>, <<"application/pdf">>}, + {<<".png">>, <<"image/png">>}, + {<<".rtf">>, <<"application/rtf">>}, + {<<".svg">>, <<"image/svg+xml">>}, + {<<".tiff">>, <<"image/tiff">>}, + {<<".txt">>, <<"text/plain">>}, + {<<".wav">>, <<"audio/wav">>}, + {<<".webp">>, <<"image/webp">>}, + {<<".xz">>, <<"application/x-xz">>}, + {<<".zip">>, <<"application/zip">>}]). %% gen_mod/supervisor callbacks. -export([start/2, - stop/1, - reload/3, - depends/2, + stop/1, + reload/3, + depends/2, mod_doc/0, - mod_opt_type/1, - mod_options/1]). + mod_opt_type/1, + mod_options/1]). %% gen_server callbacks. -export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% ejabberd_http callback. -export([process/2]). @@ -83,11 +83,13 @@ %% Utility functions. -export([get_proc_name/2, - expand_home/1, - expand_host/2]). + expand_home/1, + expand_host/2]). -include("ejabberd_http.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("translate.hrl"). @@ -96,39 +98,40 @@ %% @indent-begin -record(state, - {server_host = <<>> :: binary(), - hosts = [] :: [binary()], - name = <<>> :: binary(), - access = none :: atom(), - max_size = infinity :: pos_integer() | infinity, - secret_length = 40 :: pos_integer(), - jid_in_url = sha1 :: sha1 | node, - file_mode :: integer() | undefined, - dir_mode :: integer() | undefined, - docroot = <<>> :: binary(), - put_url = <<>> :: binary(), - get_url = <<>> :: binary(), - service_url :: binary() | undefined, - thumbnail = false :: boolean(), - custom_headers = [] :: [{binary(), binary()}], - slots = #{} :: slots(), - external_secret = <<>> :: binary()}). + {server_host = <<>> :: binary(), + hosts = [] :: [binary()], + name = <<>> :: binary(), + access = none :: atom(), + max_size = infinity :: pos_integer() | infinity, + secret_length = 40 :: pos_integer(), + jid_in_url = sha1 :: sha1 | node, + file_mode :: integer() | undefined, + dir_mode :: integer() | undefined, + docroot = <<>> :: binary(), + put_url = <<>> :: binary(), + get_url = <<>> :: binary(), + service_url :: binary() | undefined, + thumbnail = false :: boolean(), + custom_headers = [] :: [{binary(), binary()}], + slots = #{} :: slots(), + external_secret = <<>> :: binary()}). -record(media_info, - {path :: binary(), - type :: atom(), - height :: integer(), - width :: integer()}). + {path :: binary(), + type :: atom(), + height :: integer(), + width :: integer()}). %% @indent-end %% @efmt:on -%% + %% -type state() :: #state{}. -type slot() :: [binary(), ...]. -type slots() :: #{slot() => {pos_integer(), reference()}}. -type media_info() :: #media_info{}. + %%-------------------------------------------------------------------- %% gen_mod/supervisor callbacks. %%-------------------------------------------------------------------- @@ -136,33 +139,38 @@ start(ServerHost, Opts) -> Proc = get_proc_name(ServerHost, ?MODULE), case gen_mod:start_child(?MODULE, ServerHost, Opts, Proc) of - {ok, _} = Ret -> Ret; - {error, {already_started, _}} = Err -> - ?ERROR_MSG("Multiple virtual hosts can't use a single 'put_url' " - "without the @HOST@ keyword", []), - Err; - Err -> - Err + {ok, _} = Ret -> Ret; + {error, {already_started, _}} = Err -> + ?ERROR_MSG("Multiple virtual hosts can't use a single 'put_url' " + "without the @HOST@ keyword", + []), + Err; + Err -> + Err end. + -spec stop(binary()) -> ok | {error, any()}. stop(ServerHost) -> Proc = get_proc_name(ServerHost, ?MODULE), gen_mod:stop_child(Proc). + -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok | {ok, pid()} | {error, term()}. reload(ServerHost, NewOpts, OldOpts) -> NewURL = mod_http_upload_opt:put_url(NewOpts), OldURL = mod_http_upload_opt:put_url(OldOpts), OldProc = get_proc_name(ServerHost, ?MODULE, OldURL), NewProc = get_proc_name(ServerHost, ?MODULE, NewURL), - if OldProc /= NewProc -> - gen_mod:stop_child(OldProc), - start(ServerHost, NewOpts); - true -> - gen_server:cast(NewProc, {reload, NewOpts, OldOpts}) + if + OldProc /= NewProc -> + gen_mod:stop_child(OldProc), + start(ServerHost, NewOpts); + true -> + gen_server:cast(NewProc, {reload, NewOpts, OldOpts}) end. + -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(name) -> econf:binary(); @@ -194,12 +202,12 @@ mod_opt_type(thumbnail) -> econf:and_then( econf:bool(), fun(true) -> - case eimp:supported_formats() of - [] -> econf:fail(eimp_error); - [_|_] -> true - end; - (false) -> - false + case eimp:supported_formats() of + [] -> econf:fail(eimp_error); + [_ | _] -> true + end; + (false) -> + false end); mod_opt_type(external_secret) -> econf:binary(); @@ -210,8 +218,9 @@ mod_opt_type(hosts) -> mod_opt_type(vcard) -> econf:vcard_temp(). + -spec mod_options(binary()) -> [{thumbnail, boolean()} | - {atom(), any()}]. + {atom(), any()}]. mod_options(Host) -> [{host, <<"upload.", Host/binary>>}, {hosts, []}, @@ -232,14 +241,17 @@ mod_options(Host) -> {rm_on_unregister, true}, {thumbnail, false}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module allows for requesting permissions to " "upload a file via HTTP as described in " "https://xmpp.org/extensions/xep-0363.html" "[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."), "", + "another URL from which that file can later be downloaded."), + "", ?T("In order to use this module, it must be enabled " "in 'listen' -> 'ejabberd_http' -> " "_`listen-options.md#request_handlers|request_handlers`_.")], @@ -247,83 +259,104 @@ mod_doc() -> [{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, - #{value => ?T("[Host, ...]"), + #{ + value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix '\"upload.\"'. " - "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + "The keyword '@HOST@' is replaced with the real virtual host name.") + }}, {name, - #{value => ?T("Name"), + #{ + value => ?T("Name"), desc => ?T("A name of the service in the Service Discovery. " "The default value is '\"HTTP File Upload\"'. " - "Please note this will only be displayed by some XMPP clients.")}}, + "Please note this will only be displayed by some XMPP clients.") + }}, {access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option defines the access rule to limit who is " "permitted to use the HTTP upload service. " "The default value is 'local'. If no access rule of " - "that name exists, no user will be allowed to use the service.")}}, + "that name exists, no user will be allowed to use the service.") + }}, {max_size, - #{value => ?T("Size"), + #{ + value => ?T("Size"), desc => ?T("This option limits the acceptable file size. " "Either a number of bytes (larger than zero) or " "'infinity' must be specified. " - "The default value is '104857600'.")}}, + "The default value is '104857600'.") + }}, {secret_length, - #{value => ?T("Length"), + #{ + value => ?T("Length"), desc => ?T("This option defines the length of the random " "string included in the GET and PUT URLs generated " "by 'mod_http_upload'. The minimum length is '8' characters, " "but it is recommended to choose a larger value. " - "The default value is '40'.")}}, + "The default value is '40'.") + }}, {jid_in_url, - #{value => "node | sha1", + #{ + value => "node | sha1", desc => ?T("When this option is set to 'node', the node identifier " "of the user's JID (i.e., the user name) is included in " "the GET and PUT URLs generated by 'mod_http_upload'. " "Otherwise, a SHA-1 hash of the user's bare JID is " - "included instead. The default value is 'sha1'.")}}, + "included instead. The default value is 'sha1'.") + }}, {thumbnail, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This option specifies whether ejabberd should create " "thumbnails of uploaded images. If a thumbnail is created, " "a element that contains the download " "and some metadata is returned with the PUT response. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {file_mode, - #{value => ?T("Permission"), + #{ + value => ?T("Permission"), desc => ?T("This option defines the permission bits of uploaded files. " "The bits are specified as an octal number (see the 'chmod(1)' " "manual page) within double quotes. For example: '\"0644\"'. " "The default is undefined, which means no explicit permissions " - "will be set.")}}, + "will be set.") + }}, {dir_mode, - #{value => ?T("Permission"), + #{ + value => ?T("Permission"), desc => ?T("This option defines the permission bits of the 'docroot' " "directory and any directories created during file uploads. " "The bits are specified as an octal number (see the 'chmod(1)' " "manual page) within double quotes. For example: '\"0755\"'. " "The default is undefined, which means no explicit permissions " - "will be set.")}}, + "will be set.") + }}, {docroot, - #{value => ?T("Path"), + #{ + value => ?T("Path"), desc => ?T("Uploaded files are stored below the directory specified " - "(as an absolute path) with this option. The keyword " + "(as an absolute path) with this option. The keyword " "'@HOME@' is replaced with the home directory of the user " "running ejabberd, and the keyword '@HOST@' with the virtual " - "host name. The default value is '\"@HOME@/upload\"'.")}}, + "host name. The default value is '\"@HOME@/upload\"'.") + }}, {put_url, - #{value => ?T("URL"), + #{ + value => ?T("URL"), desc => ?T("This option specifies the initial part of the PUT URLs " "used for file uploads. The keyword '@HOST@' is replaced " @@ -331,9 +364,11 @@ mod_doc() -> "And '@HOST_URL_ENCODE@' is replaced with the host name encoded for URL, " "useful when your virtual hosts contain non-latin characters. " "NOTE: different virtual hosts cannot use the same PUT URL. " - "The default value is '\"https://@HOST@:5443/upload\"'.")}}, + "The default value is '\"https://@HOST@:5443/upload\"'.") + }}, {get_url, - #{value => ?T("URL"), + #{ + value => ?T("URL"), desc => ?T("This option specifies the initial part of the GET URLs " "used for downloading the files. The default value is 'undefined'. " @@ -343,17 +378,21 @@ mod_doc() -> "are handled by this module, the 'get_url' must match the " "'put_url'. Setting it to a different value only makes " "sense if an external web server or _`mod_http_fileserver`_ " - "is used to serve the uploaded files.")}}, + "is used to serve the uploaded files.") + }}, {service_url, #{desc => ?T("Deprecated.")}}, {custom_headers, - #{value => "{Name: Value}", + #{ + value => "{Name: Value}", desc => ?T("This option specifies additional header fields to be " "included in all HTTP responses. By default no custom " - "headers are included.")}}, + "headers are included.") + }}, {external_secret, - #{value => ?T("Text"), + #{ + value => ?T("Text"), desc => ?T("This option makes it possible to offload all HTTP " "Upload processing to a separate HTTP server. " @@ -361,15 +400,19 @@ mod_doc() -> "secret and behave exactly as described at " "https://modules.prosody.im/mod_http_upload_external.html#implementation" "[Prosody's mod_http_upload_external: Implementation]. " - "There is no default value.")}}, + "There is no default value.") + }}, {rm_on_unregister, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This option specifies whether files uploaded by a user " "should be removed when that user is unregistered. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {vcard, - #{value => ?T("vCard"), + #{ + value => ?T("vCard"), desc => ?T("A custom vCard of the service that will be displayed " "by some XMPP clients in Service Discovery. The value of " @@ -392,7 +435,8 @@ mod_doc() -> " adr:", " -", " work: true", - " street: Elm Street"]}}], + " street: Elm Street"] + }}], example => ["listen:", " -", @@ -405,76 +449,99 @@ mod_doc() -> "modules:", " mod_http_upload:", " docroot: /ejabberd/upload", - " put_url: \"https://@HOST@:5443/upload\""]}. + " put_url: \"https://@HOST@:5443/upload\""] + }. + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. + %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- -spec init(list()) -> {ok, state()}. -init([ServerHost|_]) -> +init([ServerHost | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), Hosts = gen_mod:get_opt_hosts(Opts), case mod_http_upload_opt:rm_on_unregister(Opts) of - true -> - ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, - remove_user, 50); - false -> - ok + true -> + ejabberd_hooks:add(remove_user, + ServerHost, + ?MODULE, + remove_user, + 50); + false -> + ok end, State = init_state(ServerHost, Hosts, Opts), {ok, State}. --spec handle_call(_, {pid(), _}, state()) - -> {reply, {ok, pos_integer(), binary(), - pos_integer() | undefined, - pos_integer() | undefined}, state()} | - {reply, {error, atom()}, state()} | {noreply, state()}. -handle_call({use_slot, Slot, Size}, _From, - #state{file_mode = FileMode, - dir_mode = DirMode, - get_url = GetPrefix, - thumbnail = Thumbnail, - custom_headers = CustomHeaders, - docroot = DocRoot} = State) -> + +-spec handle_call(_, {pid(), _}, state()) -> + {reply, {ok, pos_integer(), + binary(), + pos_integer() | undefined, + pos_integer() | undefined}, + state()} | + {reply, {error, atom()}, state()} | + {noreply, state()}. +handle_call({use_slot, Slot, Size}, + _From, + #state{ + file_mode = FileMode, + dir_mode = DirMode, + get_url = GetPrefix, + thumbnail = Thumbnail, + custom_headers = CustomHeaders, + docroot = DocRoot + } = State) -> case get_slot(Slot, State) of - {ok, {Size, TRef}} -> - misc:cancel_timer(TRef), - NewState = del_slot(Slot, State), - Path = str:join([DocRoot | Slot], <<$/>>), - {reply, - {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders}, - NewState}; - {ok, {_WrongSize, _TRef}} -> - {reply, {error, size_mismatch}, State}; - error -> - {reply, {error, invalid_slot}, State} + {ok, {Size, TRef}} -> + misc:cancel_timer(TRef), + NewState = del_slot(Slot, State), + Path = str:join([DocRoot | Slot], <<$/>>), + {reply, + {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders}, + NewState}; + {ok, {_WrongSize, _TRef}} -> + {reply, {error, size_mismatch}, State}; + error -> + {reply, {error, invalid_slot}, State} end; -handle_call(get_conf, _From, - #state{docroot = DocRoot, - custom_headers = CustomHeaders} = State) -> +handle_call(get_conf, + _From, + #state{ + docroot = DocRoot, + custom_headers = CustomHeaders + } = State) -> {reply, {ok, DocRoot, CustomHeaders}, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + -spec handle_cast(_, state()) -> {noreply, state()}. handle_cast({reload, NewOpts, OldOpts}, - #state{server_host = ServerHost} = State) -> + #state{server_host = ServerHost} = State) -> case {mod_http_upload_opt:rm_on_unregister(NewOpts), - mod_http_upload_opt:rm_on_unregister(OldOpts)} of - {true, false} -> - ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, - remove_user, 50); - {false, true} -> - ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, - remove_user, 50); - _ -> - ok + mod_http_upload_opt:rm_on_unregister(OldOpts)} of + {true, false} -> + ejabberd_hooks:add(remove_user, + ServerHost, + ?MODULE, + remove_user, + 50); + {false, true} -> + ejabberd_hooks:delete(remove_user, + ServerHost, + ?MODULE, + remove_user, + 50); + _ -> + ok end, NewHosts = gen_mod:get_opt_hosts(NewOpts), OldHosts = gen_mod:get_opt_hosts(OldOpts), @@ -485,29 +552,32 @@ handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. + -spec handle_info(timeout | _, state()) -> {noreply, state()}. handle_info({route, #iq{lang = Lang} = Packet}, State) -> try xmpp:decode_els(Packet) of - IQ -> - {Reply, NewState} = case process_iq(IQ, State) of - R when is_record(R, iq) -> - {R, State}; - {R, S} -> - {R, S}; - not_request -> - {none, State} - end, - if Reply /= none -> - ejabberd_router:route(Reply); - true -> - ok - end, - {noreply, NewState} - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Packet, Err), - {noreply, State} + IQ -> + {Reply, NewState} = case process_iq(IQ, State) of + R when is_record(R, iq) -> + {R, State}; + {R, S} -> + {R, S}; + not_request -> + {none, State} + end, + if + Reply /= none -> + ejabberd_router:route(Reply); + true -> + ok + end, + {noreply, NewState} + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + {noreply, State} end; handle_info({timeout, _TRef, Slot}, State) -> NewState = del_slot(Slot, State), @@ -516,149 +586,167 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. terminate(Reason, #state{server_host = ServerHost, hosts = Hosts}) -> ?DEBUG("Stopping HTTP upload process for ~ts: ~p", [ServerHost, Reason]), ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50), lists:foreach(fun ejabberd_router:unregister_route/1, Hosts). + -spec code_change({down, _} | _, state(), _) -> {ok, state()}. code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> ?DEBUG("Updating HTTP upload process for ~ts", [ServerHost]), {ok, State}. + %%-------------------------------------------------------------------- %% ejabberd_http callback. %%-------------------------------------------------------------------- --spec process([binary()], #request{}) - -> {pos_integer(), [{binary(), binary()}], binary()}. +-spec process([binary()], #request{}) -> + {pos_integer(), [{binary(), binary()}], binary()}. process(LocalPath, #request{method = Method, host = Host, ip = IP}) - when length(LocalPath) < 3, - Method == 'PUT' orelse - Method == 'GET' orelse - Method == 'HEAD' -> + when length(LocalPath) < 3, + Method == 'PUT' orelse + Method == 'GET' orelse + Method == 'HEAD' -> ?DEBUG("Rejecting ~ts request from ~ts for ~ts: Too few path components", - [Method, encode_addr(IP), Host]), + [Method, encode_addr(IP), Host]), http_response(404); -process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, - length = Length} = Request0) -> +process(_LocalPath, + #request{ + method = 'PUT', + host = Host, + ip = IP, + length = Length + } = Request0) -> Request = Request0#request{host = redecode_url(Host)}, {Proc, Slot} = parse_http_request(Request), try gen_server:call(Proc, {use_slot, Slot, Length}, ?CALL_TIMEOUT) of - {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} -> - ?DEBUG("Storing file from ~ts for ~ts: ~ts", - [encode_addr(IP), Host, Path]), - case store_file(Path, Request, FileMode, DirMode, - GetPrefix, Slot, Thumbnail) of - ok -> - http_response(201, CustomHeaders); - {ok, Headers, OutData} -> - http_response(201, ejabberd_http:apply_custom_headers(Headers, CustomHeaders), OutData); - {error, closed} -> - ?DEBUG("Cannot store file ~ts from ~ts for ~ts: connection closed", - [Path, encode_addr(IP), Host]), - http_response(404); - {error, Error} -> - ?ERROR_MSG("Cannot store file ~ts from ~ts for ~ts: ~ts", - [Path, encode_addr(IP), Host, format_error(Error)]), - http_response(500) - end; - {error, size_mismatch} -> - ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Unexpected size (~B)", - [lists:last(Slot), encode_addr(IP), Host, Length]), - http_response(413); - {error, invalid_slot} -> - ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Invalid slot", - [lists:last(Slot), encode_addr(IP), Host]), - http_response(403) + {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} -> + ?DEBUG("Storing file from ~ts for ~ts: ~ts", + [encode_addr(IP), Host, Path]), + case store_file(Path, + Request, + FileMode, + DirMode, + GetPrefix, + Slot, + Thumbnail) of + ok -> + http_response(201, CustomHeaders); + {ok, Headers, OutData} -> + http_response(201, ejabberd_http:apply_custom_headers(Headers, CustomHeaders), OutData); + {error, closed} -> + ?DEBUG("Cannot store file ~ts from ~ts for ~ts: connection closed", + [Path, encode_addr(IP), Host]), + http_response(404); + {error, Error} -> + ?ERROR_MSG("Cannot store file ~ts from ~ts for ~ts: ~ts", + [Path, encode_addr(IP), Host, format_error(Error)]), + http_response(500) + end; + {error, size_mismatch} -> + ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Unexpected size (~B)", + [lists:last(Slot), encode_addr(IP), Host, Length]), + http_response(413); + {error, invalid_slot} -> + ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Invalid slot", + [lists:last(Slot), encode_addr(IP), Host]), + http_response(403) catch - exit:{noproc, _} -> - ?WARNING_MSG("Cannot handle PUT request from ~ts for ~ts: " - "Upload not configured for this host", - [encode_addr(IP), Host]), - http_response(404); - _:Error -> - ?ERROR_MSG("Cannot handle PUT request from ~ts for ~ts: ~p", - [encode_addr(IP), Host, Error]), - http_response(500) + exit:{noproc, _} -> + ?WARNING_MSG("Cannot handle PUT request from ~ts for ~ts: " + "Upload not configured for this host", + [encode_addr(IP), Host]), + http_response(404); + _:Error -> + ?ERROR_MSG("Cannot handle PUT request from ~ts for ~ts: ~p", + [encode_addr(IP), Host, Error]), + http_response(500) end; process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request0) - when Method == 'GET'; - Method == 'HEAD' -> + when Method == 'GET'; + Method == 'HEAD' -> Request = Request0#request{host = redecode_url(Host)}, {Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request), try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of - {ok, DocRoot, CustomHeaders} -> - Path = str:join([DocRoot | Slot], <<$/>>), - case file:open(Path, [read]) of - {ok, Fd} -> - file:close(Fd), - ?INFO_MSG("Serving ~ts to ~ts", [Path, encode_addr(IP)]), - ContentType = guess_content_type(FileName), - Headers1 = case ContentType of - <<"image/", _SubType/binary>> -> []; - <<"text/", _SubType/binary>> -> []; - _ -> - [{<<"Content-Disposition">>, - <<"attachment; filename=", - $", FileName/binary, $">>}] - end, - Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], - Headers3 = ejabberd_http:apply_custom_headers(Headers2, CustomHeaders), - http_response(200, Headers3, {file, Path}); - {error, eacces} -> - ?WARNING_MSG("Cannot serve ~ts to ~ts: Permission denied", - [Path, encode_addr(IP)]), - http_response(403); - {error, enoent} -> - ?WARNING_MSG("Cannot serve ~ts to ~ts: No such file", - [Path, encode_addr(IP)]), - http_response(404); - {error, eisdir} -> - ?WARNING_MSG("Cannot serve ~ts to ~ts: Is a directory", - [Path, encode_addr(IP)]), - http_response(404); - {error, Error} -> - ?WARNING_MSG("Cannot serve ~ts to ~ts: ~ts", - [Path, encode_addr(IP), format_error(Error)]), - http_response(500) - end + {ok, DocRoot, CustomHeaders} -> + Path = str:join([DocRoot | Slot], <<$/>>), + case file:open(Path, [read]) of + {ok, Fd} -> + file:close(Fd), + ?INFO_MSG("Serving ~ts to ~ts", [Path, encode_addr(IP)]), + ContentType = guess_content_type(FileName), + Headers1 = case ContentType of + <<"image/", _SubType/binary>> -> []; + <<"text/", _SubType/binary>> -> []; + _ -> + [{<<"Content-Disposition">>, + <<"attachment; filename=", + $", FileName/binary, $">>}] + end, + Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], + Headers3 = ejabberd_http:apply_custom_headers(Headers2, CustomHeaders), + http_response(200, Headers3, {file, Path}); + {error, eacces} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: Permission denied", + [Path, encode_addr(IP)]), + http_response(403); + {error, enoent} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: No such file", + [Path, encode_addr(IP)]), + http_response(404); + {error, eisdir} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: Is a directory", + [Path, encode_addr(IP)]), + http_response(404); + {error, Error} -> + ?WARNING_MSG("Cannot serve ~ts to ~ts: ~ts", + [Path, encode_addr(IP), format_error(Error)]), + http_response(500) + end catch - exit:{noproc, _} -> - ?WARNING_MSG("Cannot handle ~ts request from ~ts for ~ts: " - "Upload not configured for this host", - [Method, encode_addr(IP), Host]), - http_response(404); - _:Error -> - ?ERROR_MSG("Cannot handle ~ts request from ~ts for ~ts: ~p", - [Method, encode_addr(IP), Host, Error]), - http_response(500) + exit:{noproc, _} -> + ?WARNING_MSG("Cannot handle ~ts request from ~ts for ~ts: " + "Upload not configured for this host", + [Method, encode_addr(IP), Host]), + http_response(404); + _:Error -> + ?ERROR_MSG("Cannot handle ~ts request from ~ts for ~ts: ~p", + [Method, encode_addr(IP), Host, Error]), + http_response(500) end; -process(_LocalPath, #request{method = 'OPTIONS', host = Host, - ip = IP} = Request) -> +process(_LocalPath, + #request{ + method = 'OPTIONS', + host = Host, + ip = IP + } = Request) -> ?DEBUG("Responding to OPTIONS request from ~ts for ~ts", - [encode_addr(IP), Host]), + [encode_addr(IP), Host]), {Proc, _Slot} = parse_http_request(Request), try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of - {ok, _DocRoot, CustomHeaders} -> - AllowHeader = {<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}, - http_response(200, ejabberd_http:apply_custom_headers([AllowHeader], CustomHeaders)) + {ok, _DocRoot, CustomHeaders} -> + AllowHeader = {<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}, + http_response(200, ejabberd_http:apply_custom_headers([AllowHeader], CustomHeaders)) catch - exit:{noproc, _} -> - ?WARNING_MSG("Cannot handle OPTIONS request from ~ts for ~ts: " - "Upload not configured for this host", - [encode_addr(IP), Host]), - http_response(404); - _:Error -> - ?ERROR_MSG("Cannot handle OPTIONS request from ~ts for ~ts: ~p", - [encode_addr(IP), Host, Error]), - http_response(500) + exit:{noproc, _} -> + ?WARNING_MSG("Cannot handle OPTIONS request from ~ts for ~ts: " + "Upload not configured for this host", + [encode_addr(IP), Host]), + http_response(404); + _:Error -> + ?ERROR_MSG("Cannot handle OPTIONS request from ~ts for ~ts: ~p", + [encode_addr(IP), Host, Error]), + http_response(500) end; process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> ?DEBUG("Rejecting ~ts request from ~ts for ~ts", - [Method, encode_addr(IP), Host]), + [Method, encode_addr(IP), Host]), http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). + %%-------------------------------------------------------------------- %% State initialization %%-------------------------------------------------------------------- @@ -666,6 +754,7 @@ process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> init_state(ServerHost, Hosts, Opts) -> init_state(#state{server_host = ServerHost, hosts = Hosts}, Opts). + -spec init_state(state(), gen_mod:opts()) -> state(). init_state(#state{server_host = ServerHost, hosts = Hosts} = State, Opts) -> Name = mod_http_upload_opt:name(Opts), @@ -678,9 +767,9 @@ init_state(#state{server_host = ServerHost, hosts = Hosts} = State, Opts) -> DirMode = mod_http_upload_opt:dir_mode(Opts), PutURL = mod_http_upload_opt:put_url(Opts), GetURL = case mod_http_upload_opt:get_url(Opts) of - undefined -> PutURL; - URL -> URL - end, + undefined -> PutURL; + URL -> URL + end, ServiceURL = mod_http_upload_opt:service_url(Opts), Thumbnail = mod_http_upload_opt:thumbnail(Opts), ExternalSecret = mod_http_upload_opt:external_secret(Opts), @@ -688,26 +777,35 @@ init_state(#state{server_host = ServerHost, hosts = Hosts} = State, Opts) -> DocRoot1 = expand_home(str:strip(DocRoot, right, $/)), DocRoot2 = expand_host(DocRoot1, ServerHost), case DirMode of - undefined -> - ok; - Mode -> - file:change_mode(DocRoot2, Mode) + undefined -> + ok; + Mode -> + file:change_mode(DocRoot2, Mode) end, lists:foreach( fun(Host) -> - ejabberd_router:register_route(Host, ServerHost) - end, Hosts), - State#state{server_host = ServerHost, hosts = Hosts, name = Name, - access = Access, max_size = MaxSize, - secret_length = SecretLength, jid_in_url = JIDinURL, - file_mode = FileMode, dir_mode = DirMode, - thumbnail = Thumbnail, - docroot = DocRoot2, - put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), - get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), - service_url = ServiceURL, - external_secret = ExternalSecret, - custom_headers = CustomHeaders}. + ejabberd_router:register_route(Host, ServerHost) + end, + Hosts), + State#state{ + server_host = ServerHost, + hosts = Hosts, + name = Name, + access = Access, + max_size = MaxSize, + secret_length = SecretLength, + jid_in_url = JIDinURL, + file_mode = FileMode, + dir_mode = DirMode, + thumbnail = Thumbnail, + docroot = DocRoot2, + put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), + get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), + service_url = ServiceURL, + external_secret = ExternalSecret, + custom_headers = CustomHeaders + }. + %%-------------------------------------------------------------------- %% Exported utility functions. @@ -717,6 +815,7 @@ get_proc_name(ServerHost, ModuleName) -> PutURL = mod_http_upload_opt:put_url(ServerHost), get_proc_name(ServerHost, ModuleName, PutURL). + -spec get_proc_name(binary(), atom(), binary()) -> atom(). get_proc_name(ServerHost, ModuleName, PutURL) -> %% Once we depend on OTP >= 20.0, we can use binaries with http_uri. @@ -727,52 +826,70 @@ get_proc_name(ServerHost, ModuleName, PutURL) -> ProcPrefix = <>, gen_mod:get_module_proc(ProcPrefix, ModuleName). + -spec expand_home(binary()) -> binary(). expand_home(Input) -> Home = misc:get_home(), misc:expand_keyword(<<"@HOME@">>, Input, Home). + -spec expand_host(binary(), binary()) -> binary(). expand_host(Input, Host) -> misc:expand_keyword(<<"@HOST@">>, Input, Host). + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- %% XMPP request handling. + -spec process_iq(iq(), state()) -> {iq(), state()} | iq() | not_request. process_iq(#iq{type = get, lang = Lang, sub_els = [#disco_info{}]} = IQ, - #state{server_host = ServerHost, name = Name}) -> - AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [], - [ServerHost, ?MODULE, <<"">>, <<"">>]), + #state{server_host = ServerHost, name = Name}) -> + AddInfo = ejabberd_hooks:run_fold(disco_info, + ServerHost, + [], + [ServerHost, ?MODULE, <<"">>, <<"">>]), xmpp:make_iq_result(IQ, iq_disco_info(ServerHost, Lang, Name, AddInfo)); process_iq(#iq{type = get, sub_els = [#disco_items{}]} = IQ, _State) -> xmpp:make_iq_result(IQ, #disco_items{}); process_iq(#iq{type = get, sub_els = [#vcard_temp{}], lang = Lang} = IQ, - #state{server_host = ServerHost}) -> + #state{server_host = ServerHost}) -> VCard = case mod_http_upload_opt:vcard(ServerHost) of - undefined -> - #vcard_temp{fn = <<"ejabberd/mod_http_upload">>, - url = ejabberd_config:get_uri(), - desc = misc:get_descr( - Lang, ?T("ejabberd HTTP Upload service"))}; - V -> - V - end, + undefined -> + #vcard_temp{ + fn = <<"ejabberd/mod_http_upload">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr( + Lang, ?T("ejabberd HTTP Upload service")) + }; + V -> + V + end, xmpp:make_iq_result(IQ, VCard); -process_iq(#iq{type = get, sub_els = [#upload_request{filename = File, - size = Size, - 'content-type' = CType, - xmlns = XMLNS}]} = IQ, - State) -> +process_iq(#iq{ + type = get, + sub_els = [#upload_request{ + filename = File, + size = Size, + 'content-type' = CType, + xmlns = XMLNS + }] + } = IQ, + State) -> process_slot_request(IQ, File, Size, CType, XMLNS, State); -process_iq(#iq{type = get, sub_els = [#upload_request_0{filename = File, - size = Size, - 'content-type' = CType, - xmlns = XMLNS}]} = IQ, - State) -> +process_iq(#iq{ + type = get, + sub_els = [#upload_request_0{ + filename = File, + size = Size, + 'content-type' = CType, + xmlns = XMLNS + }] + } = IQ, + State) -> process_slot_request(IQ, File, Size, CType, XMLNS, State); process_iq(#iq{type = T, lang = Lang} = IQ, _State) when T == get; T == set -> Txt = ?T("No module is handling this query"), @@ -780,121 +897,169 @@ process_iq(#iq{type = T, lang = Lang} = IQ, _State) when T == get; T == set -> process_iq(#iq{}, _State) -> not_request. --spec process_slot_request(iq(), binary(), pos_integer(), binary(), binary(), - state()) -> {iq(), state()} | iq(). + +-spec process_slot_request(iq(), + binary(), + pos_integer(), + binary(), + binary(), + state()) -> {iq(), state()} | iq(). process_slot_request(#iq{lang = Lang, from = From} = IQ, - File, Size, CType, XMLNS, - #state{server_host = ServerHost, - access = Access} = State) -> + File, + Size, + CType, + XMLNS, + #state{ + server_host = ServerHost, + access = Access + } = State) -> case acl:match_rule(ServerHost, Access, From) of - allow -> - ContentType = yield_content_type(CType), - case create_slot(State, From, File, Size, ContentType, XMLNS, - Lang) of - {ok, Slot} -> - Query = make_query_string(Slot, Size, State), - NewState = add_slot(Slot, Size, State), - NewSlot = mk_slot(Slot, State, XMLNS, Query), - {xmpp:make_iq_result(IQ, NewSlot), NewState}; - {ok, PutURL, GetURL} -> - Slot = mk_slot(PutURL, GetURL, XMLNS, <<"">>), - xmpp:make_iq_result(IQ, Slot); - {error, Error} -> - xmpp:make_error(IQ, Error) - end; - deny -> - ?DEBUG("Denying HTTP upload slot request from ~ts", - [jid:encode(From)]), - Txt = ?T("Access denied by service policy"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + allow -> + ContentType = yield_content_type(CType), + case create_slot(State, + From, + File, + Size, + ContentType, + XMLNS, + Lang) of + {ok, Slot} -> + Query = make_query_string(Slot, Size, State), + NewState = add_slot(Slot, Size, State), + NewSlot = mk_slot(Slot, State, XMLNS, Query), + {xmpp:make_iq_result(IQ, NewSlot), NewState}; + {ok, PutURL, GetURL} -> + Slot = mk_slot(PutURL, GetURL, XMLNS, <<"">>), + xmpp:make_iq_result(IQ, Slot); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + deny -> + ?DEBUG("Denying HTTP upload slot request from ~ts", + [jid:encode(From)]), + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. --spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary(), - binary()) - -> {ok, slot()} | {ok, binary(), binary()} | {error, xmpp_element()}. + +-spec create_slot(state(), + jid(), + binary(), + pos_integer(), + binary(), + binary(), + binary()) -> + {ok, slot()} | {ok, binary(), binary()} | {error, xmpp_element()}. create_slot(#state{service_url = undefined, max_size = MaxSize}, - JID, File, Size, _ContentType, XMLNS, Lang) + JID, + File, + Size, + _ContentType, + XMLNS, + Lang) when MaxSize /= infinity, Size > MaxSize -> Text = {?T("File larger than ~w bytes"), [MaxSize]}, ?WARNING_MSG("Rejecting file ~ts from ~ts (too large: ~B bytes)", - [File, jid:encode(JID), Size]), + [File, jid:encode(JID), Size]), Error = xmpp:err_not_acceptable(Text, Lang), Els = xmpp:get_els(Error), - Els1 = [#upload_file_too_large{'max-file-size' = MaxSize, - xmlns = XMLNS} | Els], + Els1 = [#upload_file_too_large{ + 'max-file-size' = MaxSize, + xmlns = XMLNS + } | Els], Error1 = xmpp:set_els(Error, Els1), {error, Error1}; -create_slot(#state{service_url = undefined, - jid_in_url = JIDinURL, - secret_length = SecretLength, - server_host = ServerHost, - docroot = DocRoot}, - JID, File, Size, _ContentType, _XMLNS, Lang) -> +create_slot(#state{ + service_url = undefined, + jid_in_url = JIDinURL, + secret_length = SecretLength, + server_host = ServerHost, + docroot = DocRoot + }, + JID, + File, + Size, + _ContentType, + _XMLNS, + Lang) -> UserStr = make_user_string(JID, JIDinURL), UserDir = <>, - case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow, - [ServerHost, JID, UserDir, Size, Lang]) of - allow -> - RandStr = p1_rand:get_alphanum_string(SecretLength), - FileStr = make_file_string(File), - ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", - [jid:encode(JID), File, Size]), - {ok, [UserStr, RandStr, FileStr]}; - deny -> - {error, xmpp:err_service_unavailable()}; - #stanza_error{} = Error -> - {error, Error} + case ejabberd_hooks:run_fold(http_upload_slot_request, + ServerHost, + allow, + [ServerHost, JID, UserDir, Size, Lang]) of + allow -> + RandStr = p1_rand:get_alphanum_string(SecretLength), + FileStr = make_file_string(File), + ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", + [jid:encode(JID), File, Size]), + {ok, [UserStr, RandStr, FileStr]}; + deny -> + {error, xmpp:err_service_unavailable()}; + #stanza_error{} = Error -> + {error, Error} end; create_slot(#state{service_url = ServiceURL}, - #jid{luser = U, lserver = S} = JID, - File, Size, ContentType, _XMLNS, Lang) -> + #jid{luser = U, lserver = S} = JID, + File, + Size, + ContentType, + _XMLNS, + Lang) -> Options = [{body_format, binary}, {full_result, false}], HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], SizeStr = integer_to_binary(Size), JidStr = jid:encode({U, S, <<"">>}), GetRequest = <> = PutURL, - <<"http", _/binary>> = GetURL] -> - ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", - [jid:encode(JID), File, Size]), - {ok, PutURL, GetURL}; - Lines -> - ?ERROR_MSG("Can't parse data received for ~ts from <~ts>: ~p", - [jid:encode(JID), ServiceURL, Lines]), - Txt = ?T("Failed to parse HTTP response"), - {error, xmpp:err_service_unavailable(Txt, Lang)} - end; - {ok, {402, _Body}} -> - ?WARNING_MSG("Got status code 402 for ~ts from <~ts>", - [jid:encode(JID), ServiceURL]), - {error, xmpp:err_resource_constraint()}; - {ok, {403, _Body}} -> - ?WARNING_MSG("Got status code 403 for ~ts from <~ts>", - [jid:encode(JID), ServiceURL]), - {error, xmpp:err_not_allowed()}; - {ok, {413, _Body}} -> - ?WARNING_MSG("Got status code 413 for ~ts from <~ts>", - [jid:encode(JID), ServiceURL]), - {error, xmpp:err_not_acceptable()}; - {ok, {Code, _Body}} -> - ?ERROR_MSG("Unexpected status code for ~ts from <~ts>: ~B", - [jid:encode(JID), ServiceURL, Code]), - {error, xmpp:err_service_unavailable()}; - {error, Reason} -> - ?ERROR_MSG("Error requesting upload slot for ~ts from <~ts>: ~p", - [jid:encode(JID), ServiceURL, Reason]), - {error, xmpp:err_service_unavailable()} + "?jid=", + (misc:url_encode(JidStr))/binary, + "&name=", + (misc:url_encode(File))/binary, + "&size=", + (misc:url_encode(SizeStr))/binary, + "&content_type=", + (misc:url_encode(ContentType))/binary>>, + case httpc:request(get, + {binary_to_list(GetRequest), []}, + HttpOptions, + Options) of + {ok, {Code, Body}} when Code >= 200, Code =< 299 -> + case binary:split(Body, <<$\n>>, [global, trim]) of + [<<"http", _/binary>> = PutURL, + <<"http", _/binary>> = GetURL] -> + ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", + [jid:encode(JID), File, Size]), + {ok, PutURL, GetURL}; + Lines -> + ?ERROR_MSG("Can't parse data received for ~ts from <~ts>: ~p", + [jid:encode(JID), ServiceURL, Lines]), + Txt = ?T("Failed to parse HTTP response"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + end; + {ok, {402, _Body}} -> + ?WARNING_MSG("Got status code 402 for ~ts from <~ts>", + [jid:encode(JID), ServiceURL]), + {error, xmpp:err_resource_constraint()}; + {ok, {403, _Body}} -> + ?WARNING_MSG("Got status code 403 for ~ts from <~ts>", + [jid:encode(JID), ServiceURL]), + {error, xmpp:err_not_allowed()}; + {ok, {413, _Body}} -> + ?WARNING_MSG("Got status code 413 for ~ts from <~ts>", + [jid:encode(JID), ServiceURL]), + {error, xmpp:err_not_acceptable()}; + {ok, {Code, _Body}} -> + ?ERROR_MSG("Unexpected status code for ~ts from <~ts>: ~B", + [jid:encode(JID), ServiceURL, Code]), + {error, xmpp:err_service_unavailable()}; + {error, Reason} -> + ?ERROR_MSG("Error requesting upload slot for ~ts from <~ts>: ~p", + [jid:encode(JID), ServiceURL, Reason]), + {error, xmpp:err_service_unavailable()} end. + -spec add_slot(slot(), pos_integer(), state()) -> state(). add_slot(Slot, Size, #state{external_secret = <<>>, slots = Slots} = State) -> TRef = erlang:start_timer(?SLOT_TIMEOUT, self(), Slot), @@ -903,17 +1068,20 @@ add_slot(Slot, Size, #state{external_secret = <<>>, slots = Slots} = State) -> add_slot(_Slot, _Size, State) -> State. + -spec get_slot(slot(), state()) -> {ok, {pos_integer(), reference()}} | error. get_slot(Slot, #state{slots = Slots}) -> maps:find(Slot, Slots). + -spec del_slot(slot(), state()) -> state(). del_slot(Slot, #state{slots = Slots} = State) -> NewSlots = maps:remove(Slot, Slots), State#state{slots = NewSlots}. + -spec mk_slot(slot(), state(), binary(), binary()) -> upload_slot(); - (binary(), binary(), binary(), binary()) -> upload_slot(). + (binary(), binary(), binary(), binary()) -> upload_slot(). mk_slot(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS, Query) -> PutURL = str:join([PutPrefix | Slot], <<$/>>), GetURL = str:join([GetPrefix | Slot], <<$/>>), @@ -922,34 +1090,39 @@ mk_slot(PutURL, GetURL, XMLNS, Query) -> PutURL1 = <<(reencode_url(PutURL))/binary, Query/binary>>, GetURL1 = reencode_url(GetURL), case XMLNS of - ?NS_HTTP_UPLOAD_0 -> - #upload_slot_0{get = GetURL1, put = PutURL1, xmlns = XMLNS}; - _ -> - #upload_slot{get = GetURL1, put = PutURL1, xmlns = XMLNS} + ?NS_HTTP_UPLOAD_0 -> + #upload_slot_0{get = GetURL1, put = PutURL1, xmlns = XMLNS}; + _ -> + #upload_slot{get = GetURL1, put = PutURL1, xmlns = XMLNS} end. + reencode_url(UrlString) -> {ok, _, _, Host, _, _, _} = yconf:parse_uri(misc:url_encode(UrlString)), HostDecoded = misc:uri_decode(Host), HostIdna = idna:encode(HostDecoded), re:replace(UrlString, Host, HostIdna, [{return, binary}]). + redecode_url(UrlString) -> {ok, _, _, HostIdna, _, _, _} = yconf:parse_uri(<<"http://", UrlString/binary>>), HostDecoded = idna:decode(HostIdna), Host = misc:uri_quote(HostDecoded), re:replace(UrlString, HostIdna, Host, [{return, binary}]). + -spec make_user_string(jid(), sha1 | node) -> binary(). make_user_string(#jid{luser = U, lserver = S}, sha1) -> str:sha(<>); make_user_string(#jid{luser = U}, node) -> replace_special_chars(U). + -spec make_file_string(binary()) -> binary(). make_file_string(File) -> replace_special_chars(File). + -spec make_query_string(slot(), non_neg_integer(), state()) -> binary(). make_query_string(Slot, Size, #state{external_secret = Key}) when Key /= <<>> -> UrlPath = str:join(Slot, <<$/>>), @@ -960,147 +1133,179 @@ make_query_string(Slot, Size, #state{external_secret = Key}) when Key /= <<>> -> make_query_string(_Slot, _Size, _State) -> <<>>. + -spec replace_special_chars(binary()) -> binary(). replace_special_chars(S) -> - re:replace(S, <<"[^\\p{Xan}_.-]">>, <<$_>>, - [unicode, global, {return, binary}]). + re:replace(S, + <<"[^\\p{Xan}_.-]">>, + <<$_>>, + [unicode, global, {return, binary}]). + -spec yield_content_type(binary()) -> binary(). yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE; yield_content_type(Type) -> Type. --spec encode_addr(inet:ip_address() | {inet:ip_address(), inet:port_number()} | - undefined) -> binary(). + +-spec encode_addr(inet:ip_address() | + {inet:ip_address(), inet:port_number()} | + undefined) -> binary(). encode_addr(IP) -> ejabberd_config:may_hide_data(misc:ip_to_list(IP)). + -spec iq_disco_info(binary(), binary(), binary(), [xdata()]) -> disco_info(). iq_disco_info(Host, Lang, Name, AddInfo) -> Form = case mod_http_upload_opt:max_size(Host) of - infinity -> - AddInfo; - MaxSize -> - lists:foldl( - fun(NS, Acc) -> - Fs = http_upload:encode( - [{'max-file-size', MaxSize}], NS, Lang), - [#xdata{type = result, fields = Fs}|Acc] - end, AddInfo, [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD]) - end, - #disco_info{identities = [#identity{category = <<"store">>, - type = <<"file">>, - name = translate:translate(Lang, Name)}], - features = [?NS_HTTP_UPLOAD, - ?NS_HTTP_UPLOAD_0, - ?NS_HTTP_UPLOAD_OLD, - ?NS_VCARD, - ?NS_DISCO_INFO, - ?NS_DISCO_ITEMS], - xdata = Form}. + infinity -> + AddInfo; + MaxSize -> + lists:foldl( + fun(NS, Acc) -> + Fs = http_upload:encode( + [{'max-file-size', MaxSize}], NS, Lang), + [#xdata{type = result, fields = Fs} | Acc] + end, + AddInfo, + [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD]) + end, + #disco_info{ + identities = [#identity{ + category = <<"store">>, + type = <<"file">>, + name = translate:translate(Lang, Name) + }], + features = [?NS_HTTP_UPLOAD, + ?NS_HTTP_UPLOAD_0, + ?NS_HTTP_UPLOAD_OLD, + ?NS_VCARD, + ?NS_DISCO_INFO, + ?NS_DISCO_ITEMS], + xdata = Form + }. + %% HTTP request handling. + -spec parse_http_request(#request{}) -> {atom(), slot()}. parse_http_request(#request{host = Host0, path = Path}) -> Host = jid:nameprep(Host0), PrefixLength = length(Path) - 3, - {ProcURL, Slot} = if PrefixLength > 0 -> - Prefix = lists:sublist(Path, PrefixLength), - {str:join([Host | Prefix], $/), - lists:nthtail(PrefixLength, Path)}; - true -> - {Host, Path} - end, + {ProcURL, Slot} = if + PrefixLength > 0 -> + Prefix = lists:sublist(Path, PrefixLength), + {str:join([Host | Prefix], $/), + lists:nthtail(PrefixLength, Path)}; + true -> + {Host, Path} + end, {gen_mod:get_module_proc(ProcURL, ?MODULE), Slot}. --spec store_file(binary(), http_request(), - integer() | undefined, - integer() | undefined, - binary(), slot(), boolean()) - -> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}. + +-spec store_file(binary(), + http_request(), + integer() | undefined, + integer() | undefined, + binary(), + slot(), + boolean()) -> + ok | {ok, [{binary(), binary()}], binary()} | {error, term()}. store_file(Path, Request, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> case do_store_file(Path, Request, FileMode, DirMode) of - ok when Thumbnail -> - case read_image(Path) of - {ok, Data, MediaInfo} -> - case convert(Data, MediaInfo) of - {ok, #media_info{path = OutPath} = OutMediaInfo} -> - [UserDir, RandDir | _] = Slot, - FileName = filename:basename(OutPath), - URL = str:join([GetPrefix, UserDir, - RandDir, FileName], <<$/>>), - ThumbEl = thumb_el(OutMediaInfo, URL), - {ok, - [{<<"Content-Type">>, - <<"text/xml; charset=utf-8">>}], - fxml:element_to_binary(ThumbEl)}; - pass -> - ok - end; - pass -> - ok - end; - ok -> - ok; - Err -> - Err + ok when Thumbnail -> + case read_image(Path) of + {ok, Data, MediaInfo} -> + case convert(Data, MediaInfo) of + {ok, #media_info{path = OutPath} = OutMediaInfo} -> + [UserDir, RandDir | _] = Slot, + FileName = filename:basename(OutPath), + URL = str:join([GetPrefix, UserDir, + RandDir, FileName], + <<$/>>), + ThumbEl = thumb_el(OutMediaInfo, URL), + {ok, + [{<<"Content-Type">>, + <<"text/xml; charset=utf-8">>}], + fxml:element_to_binary(ThumbEl)}; + pass -> + ok + end; + pass -> + ok + end; + ok -> + ok; + Err -> + Err end. --spec do_store_file(file:filename_all(), http_request(), - integer() | undefined, - integer() | undefined) - -> ok | {error, term()}. + +-spec do_store_file(file:filename_all(), + http_request(), + integer() | undefined, + integer() | undefined) -> + ok | {error, term()}. do_store_file(Path, Request, FileMode, DirMode) -> try - ok = filelib:ensure_dir(Path), - ok = ejabberd_http:recv_file(Request, Path), - if is_integer(FileMode) -> - ok = file:change_mode(Path, FileMode); - FileMode == undefined -> - ok - end, - if is_integer(DirMode) -> - RandDir = filename:dirname(Path), - UserDir = filename:dirname(RandDir), - ok = file:change_mode(RandDir, DirMode), - ok = file:change_mode(UserDir, DirMode); - DirMode == undefined -> - ok - end + ok = filelib:ensure_dir(Path), + ok = ejabberd_http:recv_file(Request, Path), + if + is_integer(FileMode) -> + ok = file:change_mode(Path, FileMode); + FileMode == undefined -> + ok + end, + if + is_integer(DirMode) -> + RandDir = filename:dirname(Path), + UserDir = filename:dirname(RandDir), + ok = file:change_mode(RandDir, DirMode), + ok = file:change_mode(UserDir, DirMode); + DirMode == undefined -> + ok + end catch - _:{badmatch, {error, Error}} -> - {error, Error} + _:{badmatch, {error, Error}} -> + {error, Error} end. + -spec guess_content_type(binary()) -> binary(). guess_content_type(FileName) -> mod_http_fileserver:content_type(FileName, - ?DEFAULT_CONTENT_TYPE, - ?CONTENT_TYPES). + ?DEFAULT_CONTENT_TYPE, + ?CONTENT_TYPES). --spec http_response(100..599) - -> {pos_integer(), [{binary(), binary()}], binary()}. + +-spec http_response(100..599) -> + {pos_integer(), [{binary(), binary()}], binary()}. http_response(Code) -> http_response(Code, []). --spec http_response(100..599, [{binary(), binary()}]) - -> {pos_integer(), [{binary(), binary()}], binary()}. + +-spec http_response(100..599, [{binary(), binary()}]) -> + {pos_integer(), [{binary(), binary()}], binary()}. http_response(Code, ExtraHeaders) -> Message = <<(code_to_message(Code))/binary, $\n>>, http_response(Code, ExtraHeaders, Message). + -type http_body() :: binary() | {file, file:filename_all()}. --spec http_response(100..599, [{binary(), binary()}], http_body()) - -> {pos_integer(), [{binary(), binary()}], http_body()}. + + +-spec http_response(100..599, [{binary(), binary()}], http_body()) -> + {pos_integer(), [{binary(), binary()}], http_body()}. http_response(Code, ExtraHeaders, Body) -> Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of - true -> - ExtraHeaders; - false -> - [{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders] - end, + true -> + ExtraHeaders; + false -> + [{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders] + end, {Code, Headers, Body}. + -spec code_to_message(100..599) -> binary(). code_to_message(201) -> <<"Upload successful.">>; code_to_message(403) -> <<"Forbidden.">>; @@ -1110,87 +1315,99 @@ code_to_message(413) -> <<"File size doesn't match requested size.">>; code_to_message(500) -> <<"Internal server error.">>; code_to_message(_Code) -> <<"">>. + -spec format_error(atom()) -> string(). format_error(Reason) -> case file:format_error(Reason) of - "unknown POSIX error" -> - case inet:format_error(Reason) of - "unknown POSIX error" -> - atom_to_list(Reason); - Txt -> - Txt - end; - Txt -> - Txt + "unknown POSIX error" -> + case inet:format_error(Reason) of + "unknown POSIX error" -> + atom_to_list(Reason); + Txt -> + Txt + end; + Txt -> + Txt end. + %%-------------------------------------------------------------------- %% Image manipulation stuff. %%-------------------------------------------------------------------- -spec read_image(binary()) -> {ok, binary(), media_info()} | pass. read_image(Path) -> case file:read_file(Path) of - {ok, Data} -> - case eimp:identify(Data) of - {ok, Info} -> - {ok, Data, - #media_info{ - path = Path, - type = proplists:get_value(type, Info), - width = proplists:get_value(width, Info), - height = proplists:get_value(height, Info)}}; - {error, Why} -> - ?DEBUG("Cannot identify type of ~ts: ~ts", - [Path, eimp:format_error(Why)]), - pass - end; - {error, Reason} -> - ?DEBUG("Failed to read file ~ts: ~ts", - [Path, format_error(Reason)]), - pass + {ok, Data} -> + case eimp:identify(Data) of + {ok, Info} -> + {ok, Data, + #media_info{ + path = Path, + type = proplists:get_value(type, Info), + width = proplists:get_value(width, Info), + height = proplists:get_value(height, Info) + }}; + {error, Why} -> + ?DEBUG("Cannot identify type of ~ts: ~ts", + [Path, eimp:format_error(Why)]), + pass + end; + {error, Reason} -> + ?DEBUG("Failed to read file ~ts: ~ts", + [Path, format_error(Reason)]), + pass end. + -spec convert(binary(), media_info()) -> {ok, media_info()} | pass. convert(InData, #media_info{path = Path, type = T, width = W, height = H} = Info) -> - if W * H >= 25000000 -> - ?DEBUG("The image ~ts is more than 25 Mpix", [Path]), - pass; - W =< 300, H =< 300 -> - {ok, Info}; - true -> - Dir = filename:dirname(Path), - Ext = atom_to_binary(T, latin1), - FileName = <<(p1_rand:get_string())/binary, $., Ext/binary>>, - OutPath = filename:join(Dir, FileName), - {W1, H1} = if W > H -> {300, round(H*300/W)}; - H > W -> {round(W*300/H), 300}; - true -> {300, 300} - end, - OutInfo = #media_info{path = OutPath, type = T, width = W1, height = H1}, - case eimp:convert(InData, T, [{scale, {W1, H1}}]) of - {ok, OutData} -> - case file:write_file(OutPath, OutData) of - ok -> - {ok, OutInfo}; - {error, Why} -> - ?ERROR_MSG("Failed to write to ~ts: ~ts", - [OutPath, format_error(Why)]), - pass - end; - {error, Why} -> - ?ERROR_MSG("Failed to convert ~ts to ~ts: ~ts", - [Path, OutPath, eimp:format_error(Why)]), - pass - end + if + W * H >= 25000000 -> + ?DEBUG("The image ~ts is more than 25 Mpix", [Path]), + pass; + W =< 300, H =< 300 -> + {ok, Info}; + true -> + Dir = filename:dirname(Path), + Ext = atom_to_binary(T, latin1), + FileName = <<(p1_rand:get_string())/binary, $., Ext/binary>>, + OutPath = filename:join(Dir, FileName), + {W1, H1} = if + W > H -> {300, round(H * 300 / W)}; + H > W -> {round(W * 300 / H), 300}; + true -> {300, 300} + end, + OutInfo = #media_info{path = OutPath, type = T, width = W1, height = H1}, + case eimp:convert(InData, T, [{scale, {W1, H1}}]) of + {ok, OutData} -> + case file:write_file(OutPath, OutData) of + ok -> + {ok, OutInfo}; + {error, Why} -> + ?ERROR_MSG("Failed to write to ~ts: ~ts", + [OutPath, format_error(Why)]), + pass + end; + {error, Why} -> + ?ERROR_MSG("Failed to convert ~ts to ~ts: ~ts", + [Path, OutPath, eimp:format_error(Why)]), + pass + end end. + -spec thumb_el(media_info(), binary()) -> xmlel(). thumb_el(#media_info{type = T, height = H, width = W}, URI) -> MimeType = <<"image/", (atom_to_binary(T, latin1))/binary>>, - Thumb = #thumbnail{'media-type' = MimeType, uri = URI, - height = H, width = W}, + Thumb = #thumbnail{ + 'media-type' = MimeType, + uri = URI, + height = H, + width = W + }, xmpp:encode(Thumb). + %%-------------------------------------------------------------------- %% Remove user. %%-------------------------------------------------------------------- @@ -1203,12 +1420,12 @@ remove_user(User, Server) -> UserStr = make_user_string(jid:make(User, Server), JIDinURL), UserDir = str:join([DocRoot1, UserStr], <<$/>>), case misc:delete_dir(UserDir) of - ok -> - ?INFO_MSG("Removed HTTP upload directory of ~ts@~ts", [User, Server]); - {error, enoent} -> - ?DEBUG("Found no HTTP upload directory of ~ts@~ts", [User, Server]); - {error, Error} -> - ?ERROR_MSG("Cannot remove HTTP upload directory of ~ts@~ts: ~ts", - [User, Server, format_error(Error)]) + ok -> + ?INFO_MSG("Removed HTTP upload directory of ~ts@~ts", [User, Server]); + {error, enoent} -> + ?DEBUG("Found no HTTP upload directory of ~ts@~ts", [User, Server]); + {error, Error} -> + ?ERROR_MSG("Cannot remove HTTP upload directory of ~ts@~ts: ~ts", + [User, Server, format_error(Error)]) end, ok. diff --git a/src/mod_http_upload_opt.erl b/src/mod_http_upload_opt.erl index 8590a38a1..90d829aab 100644 --- a/src/mod_http_upload_opt.erl +++ b/src/mod_http_upload_opt.erl @@ -22,111 +22,128 @@ -export([thumbnail/1]). -export([vcard/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, access). --spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. custom_headers(Opts) when is_map(Opts) -> gen_mod:get_opt(custom_headers, Opts); custom_headers(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, custom_headers). + -spec dir_mode(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). dir_mode(Opts) when is_map(Opts) -> gen_mod:get_opt(dir_mode, Opts); dir_mode(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, dir_mode). + -spec docroot(gen_mod:opts() | global | binary()) -> binary(). docroot(Opts) when is_map(Opts) -> gen_mod:get_opt(docroot, Opts); docroot(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, docroot). + -spec external_secret(gen_mod:opts() | global | binary()) -> binary(). external_secret(Opts) when is_map(Opts) -> gen_mod:get_opt(external_secret, Opts); external_secret(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, external_secret). + -spec file_mode(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). file_mode(Opts) when is_map(Opts) -> gen_mod:get_opt(file_mode, Opts); file_mode(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, file_mode). + -spec get_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). get_url(Opts) when is_map(Opts) -> gen_mod:get_opt(get_url, Opts); get_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, get_url). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, host). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, hosts). + -spec jid_in_url(gen_mod:opts() | global | binary()) -> 'node' | 'sha1'. jid_in_url(Opts) when is_map(Opts) -> gen_mod:get_opt(jid_in_url, Opts); jid_in_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, jid_in_url). + -spec max_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_size(Opts) when is_map(Opts) -> gen_mod:get_opt(max_size, Opts); max_size(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, max_size). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, name). + -spec put_url(gen_mod:opts() | global | binary()) -> binary(). put_url(Opts) when is_map(Opts) -> gen_mod:get_opt(put_url, Opts); put_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, put_url). + -spec rm_on_unregister(gen_mod:opts() | global | binary()) -> boolean(). rm_on_unregister(Opts) when is_map(Opts) -> gen_mod:get_opt(rm_on_unregister, Opts); rm_on_unregister(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, rm_on_unregister). + -spec secret_length(gen_mod:opts() | global | binary()) -> 1..1114111. secret_length(Opts) when is_map(Opts) -> gen_mod:get_opt(secret_length, Opts); secret_length(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, secret_length). + -spec service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). service_url(Opts) when is_map(Opts) -> gen_mod:get_opt(service_url, Opts); service_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, service_url). + -spec thumbnail(gen_mod:opts() | global | binary()) -> boolean(). thumbnail(Opts) when is_map(Opts) -> gen_mod:get_opt(thumbnail, Opts); thumbnail(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, thumbnail). + -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, vcard). - diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl index 76b99ca05..b8efd1bd6 100644 --- a/src/mod_http_upload_quota.erl +++ b/src/mod_http_upload_quota.erl @@ -34,40 +34,44 @@ %% gen_mod/supervisor callbacks. -export([start/2, - stop/1, - depends/2, + stop/1, + depends/2, mod_doc/0, - mod_opt_type/1, - mod_options/1]). + mod_opt_type/1, + mod_options/1]). %% gen_server callbacks. -export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% ejabberd_hooks callback. -export([handle_slot_request/6]). -include_lib("xmpp/include/jid.hrl"). + -include("logger.hrl"). -include("translate.hrl"). + -include_lib("kernel/include/file.hrl"). --record(state, - {server_host :: binary(), - access_soft_quota :: atom(), - access_hard_quota :: atom(), - max_days :: pos_integer() | infinity, - docroot :: binary(), - disk_usage = #{} :: disk_usage(), - timer :: reference() | undefined}). +-record(state, { + server_host :: binary(), + access_soft_quota :: atom(), + access_hard_quota :: atom(), + max_days :: pos_integer() | infinity, + docroot :: binary(), + disk_usage = #{} :: disk_usage(), + timer :: reference() | undefined + }). -type disk_usage() :: #{{binary(), binary()} => non_neg_integer()}. -type state() :: #state{}. + %%-------------------------------------------------------------------- %% gen_mod/supervisor callbacks. %%-------------------------------------------------------------------- @@ -75,10 +79,12 @@ start(ServerHost, Opts) -> Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), gen_mod:start_child(?MODULE, ServerHost, Opts, Proc). + stop(ServerHost) -> Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), gen_mod:stop_child(Proc). + -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access_soft_quota) -> econf:shaper(); @@ -87,36 +93,45 @@ mod_opt_type(access_hard_quota) -> mod_opt_type(max_days) -> econf:pos_int(infinity). + -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(_) -> [{access_soft_quota, soft_upload_quota}, {access_hard_quota, hard_upload_quota}, {max_days, infinity}]. + mod_doc() -> - #{desc => - [?T("This module adds quota support for mod_http_upload."), "", + #{ + desc => + [?T("This module adds quota support for mod_http_upload."), + "", ?T("This module depends on _`mod_http_upload`_.")], opts => [{max_days, - #{value => ?T("Days"), + #{ + value => ?T("Days"), desc => ?T("If a number larger than zero is specified, " "any files (and directories) older than this " "number of days are removed from the subdirectories " "of the 'docroot' directory, once per day. " - "The default value is 'infinity'.")}}, + "The default value is 'infinity'.") + }}, {access_soft_quota, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option defines which access rule is used " "to specify the \"soft quota\" for the matching JIDs. " "That rule must yield a positive number of megabytes " "for any JID that is supposed to have a quota limit. " "See the description of the 'access_hard_quota' option " - "for details. The default value is 'soft_upload_quota'.")}}, + "for details. The default value is 'soft_upload_quota'.") + }}, {access_hard_quota, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option defines which access rule is used to " "specify the \"hard quota\" for the matching JIDs. " @@ -127,32 +142,36 @@ mod_doc() -> "ejabberd deletes the oldest files uploaded by that " "user until their disk usage equals or falls below " "the specified soft quota (see also option 'access_soft_quota'). " - "The default value is 'hard_upload_quota'.")}}], + "The default value is 'hard_upload_quota'.") + }}], example => - [{?T("Notice it's not necessary to specify the " - "'access_hard_quota' and 'access_soft_quota' options in order " - "to use the quota feature. You can stick to the default names " - "and just specify access rules such as those in this example:"), - ["shaper_rules:", - " soft_upload_quota:", - " 1000: all # MiB", - " hard_upload_quota:", - " 1100: all # MiB", - "", - "modules:", - " mod_http_upload: {}", - " mod_http_upload_quota:", - " max_days: 100"]}]}. + [{?T("Notice it's not necessary to specify the " + "'access_hard_quota' and 'access_soft_quota' options in order " + "to use the quota feature. You can stick to the default names " + "and just specify access rules such as those in this example:"), + ["shaper_rules:", + " soft_upload_quota:", + " 1000: all # MiB", + " hard_upload_quota:", + " 1100: all # MiB", + "", + "modules:", + " mod_http_upload: {}", + " mod_http_upload_quota:", + " max_days: 100"]}] + }. + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> [{mod_http_upload, hard}]. + %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- -spec init(list()) -> {ok, state()}. -init([ServerHost|_]) -> +init([ServerHost | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), AccessSoftQuota = mod_http_upload_quota_opt:access_soft_quota(Opts), @@ -161,225 +180,268 @@ init([ServerHost|_]) -> DocRoot1 = mod_http_upload_opt:docroot(ServerHost), DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)), DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost), - Timer = if MaxDays == infinity -> undefined; - true -> - Timeout = p1_rand:uniform(?TIMEOUT div 2), - erlang:send_after(Timeout, self(), sweep) - end, - ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE, - handle_slot_request, 50), - {ok, #state{server_host = ServerHost, - access_soft_quota = AccessSoftQuota, - access_hard_quota = AccessHardQuota, - max_days = MaxDays, - docroot = DocRoot3, - timer = Timer}}. + Timer = if + MaxDays == infinity -> undefined; + true -> + Timeout = p1_rand:uniform(?TIMEOUT div 2), + erlang:send_after(Timeout, self(), sweep) + end, + ejabberd_hooks:add(http_upload_slot_request, + ServerHost, + ?MODULE, + handle_slot_request, + 50), + {ok, #state{ + server_host = ServerHost, + access_soft_quota = AccessSoftQuota, + access_hard_quota = AccessHardQuota, + max_days = MaxDays, + docroot = DocRoot3, + timer = Timer + }}. + -spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. handle_call(Request, From, State) -> ?ERROR_MSG("Unexpected request from ~p: ~p", [From, Request]), {noreply, State}. + -spec handle_cast(_, state()) -> {noreply, state()}. handle_cast({handle_slot_request, #jid{user = U, server = S} = JID, Path, Size}, - #state{server_host = ServerHost, - access_soft_quota = AccessSoftQuota, - access_hard_quota = AccessHardQuota, - disk_usage = DiskUsage} = State) -> + #state{ + server_host = ServerHost, + access_soft_quota = AccessSoftQuota, + access_hard_quota = AccessHardQuota, + disk_usage = DiskUsage + } = State) -> HardQuota = case ejabberd_shaper:match(ServerHost, AccessHardQuota, JID) of - Hard when is_integer(Hard), Hard > 0 -> - Hard * 1024 * 1024; - _ -> - 0 - end, + Hard when is_integer(Hard), Hard > 0 -> + Hard * 1024 * 1024; + _ -> + 0 + end, SoftQuota = case ejabberd_shaper:match(ServerHost, AccessSoftQuota, JID) of - Soft when is_integer(Soft), Soft > 0 -> - Soft * 1024 * 1024; - _ -> - 0 - end, + Soft when is_integer(Soft), Soft > 0 -> + Soft * 1024 * 1024; + _ -> + 0 + end, OldSize = case maps:find({U, S}, DiskUsage) of - {ok, Value} -> - Value; - error -> - undefined - end, + {ok, Value} -> + Value; + error -> + undefined + end, NewSize = case {HardQuota, SoftQuota} of - {0, 0} -> - ?DEBUG("No quota specified for ~ts", - [jid:encode(JID)]), - undefined; - {0, _} -> - ?WARNING_MSG("No hard quota specified for ~ts", - [jid:encode(JID)]), - enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); - {_, 0} -> - ?WARNING_MSG("No soft quota specified for ~ts", - [jid:encode(JID)]), - enforce_quota(Path, Size, OldSize, HardQuota, HardQuota); - _ when SoftQuota > HardQuota -> - ?WARNING_MSG("Bad quota for ~ts (soft: ~p, hard: ~p)", - [jid:encode(JID), - SoftQuota, HardQuota]), - enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); - _ -> - ?DEBUG("Enforcing quota for ~ts", - [jid:encode(JID)]), - enforce_quota(Path, Size, OldSize, SoftQuota, HardQuota) - end, - NewDiskUsage = if is_integer(NewSize) -> - maps:put({U, S}, NewSize, DiskUsage); - true -> - DiskUsage - end, + {0, 0} -> + ?DEBUG("No quota specified for ~ts", + [jid:encode(JID)]), + undefined; + {0, _} -> + ?WARNING_MSG("No hard quota specified for ~ts", + [jid:encode(JID)]), + enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); + {_, 0} -> + ?WARNING_MSG("No soft quota specified for ~ts", + [jid:encode(JID)]), + enforce_quota(Path, Size, OldSize, HardQuota, HardQuota); + _ when SoftQuota > HardQuota -> + ?WARNING_MSG("Bad quota for ~ts (soft: ~p, hard: ~p)", + [jid:encode(JID), + SoftQuota, + HardQuota]), + enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); + _ -> + ?DEBUG("Enforcing quota for ~ts", + [jid:encode(JID)]), + enforce_quota(Path, Size, OldSize, SoftQuota, HardQuota) + end, + NewDiskUsage = if + is_integer(NewSize) -> + maps:put({U, S}, NewSize, DiskUsage); + true -> + DiskUsage + end, {noreply, State#state{disk_usage = NewDiskUsage}}; handle_cast(Request, State) -> ?ERROR_MSG("Unexpected request: ~p", [Request]), {noreply, State}. + -spec handle_info(_, state()) -> {noreply, state()}. -handle_info(sweep, #state{server_host = ServerHost, - docroot = DocRoot, - max_days = MaxDays} = State) - when is_integer(MaxDays), MaxDays > 0 -> +handle_info(sweep, + #state{ + server_host = ServerHost, + docroot = DocRoot, + max_days = MaxDays + } = State) + when is_integer(MaxDays), MaxDays > 0 -> ?DEBUG("Got 'sweep' message for ~ts", [ServerHost]), Timer = erlang:send_after(?TIMEOUT, self(), sweep), case file:list_dir(DocRoot) of - {ok, Entries} -> - BackThen = secs_since_epoch() - (MaxDays * 86400), - DocRootS = binary_to_list(DocRoot), - PathNames = lists:map(fun(Entry) -> - DocRootS ++ "/" ++ Entry - end, Entries), - UserDirs = lists:filter(fun filelib:is_dir/1, PathNames), - lists:foreach(fun(UserDir) -> - delete_old_files(UserDir, BackThen) - end, UserDirs); - {error, Error} -> - ?ERROR_MSG("Cannot open document root ~ts: ~ts", - [DocRoot, ?FORMAT(Error)]) + {ok, Entries} -> + BackThen = secs_since_epoch() - (MaxDays * 86400), + DocRootS = binary_to_list(DocRoot), + PathNames = lists:map(fun(Entry) -> + DocRootS ++ "/" ++ Entry + end, + Entries), + UserDirs = lists:filter(fun filelib:is_dir/1, PathNames), + lists:foreach(fun(UserDir) -> + delete_old_files(UserDir, BackThen) + end, + UserDirs); + {error, Error} -> + ?ERROR_MSG("Cannot open document root ~ts: ~ts", + [DocRoot, ?FORMAT(Error)]) end, {noreply, State#state{timer = Timer}}; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. terminate(Reason, #state{server_host = ServerHost, timer = Timer}) -> ?DEBUG("Stopping upload quota process for ~ts: ~p", [ServerHost, Reason]), - ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, - handle_slot_request, 50), + ejabberd_hooks:delete(http_upload_slot_request, + ServerHost, + ?MODULE, + handle_slot_request, + 50), misc:cancel_timer(Timer). + -spec code_change({down, _} | _, state(), _) -> {ok, state()}. code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> ?DEBUG("Updating upload quota process for ~ts", [ServerHost]), {ok, State}. + %%-------------------------------------------------------------------- %% ejabberd_hooks callback. %%-------------------------------------------------------------------- --spec handle_slot_request(allow | deny, binary(), jid(), binary(), - non_neg_integer(), binary()) -> allow | deny. +-spec handle_slot_request(allow | deny, + binary(), + jid(), + binary(), + non_neg_integer(), + binary()) -> allow | deny. handle_slot_request(allow, ServerHost, JID, Path, Size, _Lang) -> Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), gen_server:cast(Proc, {handle_slot_request, JID, Path, Size}), allow; handle_slot_request(Acc, _ServerHost, _JID, _Path, _Size, _Lang) -> Acc. + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- --spec enforce_quota(file:filename_all(), non_neg_integer(), - non_neg_integer() | undefined, non_neg_integer(), - non_neg_integer()) - -> non_neg_integer(). +-spec enforce_quota(file:filename_all(), + non_neg_integer(), + non_neg_integer() | undefined, + non_neg_integer(), + non_neg_integer()) -> + non_neg_integer(). enforce_quota(_UserDir, SlotSize, OldSize, _MinSize, MaxSize) - when is_integer(OldSize), OldSize + SlotSize =< MaxSize -> + when is_integer(OldSize), OldSize + SlotSize =< MaxSize -> OldSize + SlotSize; enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) -> Files = lists:sort(fun({_PathA, _SizeA, TimeA}, {_PathB, _SizeB, TimeB}) -> - TimeA > TimeB - end, gather_file_info(UserDir)), + TimeA > TimeB + end, + gather_file_info(UserDir)), {DelFiles, OldSize, NewSize} = - lists:foldl(fun({_Path, Size, _Time}, {[], AccSize, AccSize}) - when AccSize + Size + SlotSize =< MinSize -> - {[], AccSize + Size, AccSize + Size}; - ({Path, Size, _Time}, {[], AccSize, AccSize}) -> - {[Path], AccSize + Size, AccSize}; - ({Path, Size, _Time}, {AccFiles, AccSize, NewSize}) -> - {[Path | AccFiles], AccSize + Size, NewSize} - end, {[], 0, 0}, Files), - if OldSize + SlotSize > MaxSize -> - lists:foreach(fun del_file_and_dir/1, DelFiles), - file:del_dir(UserDir), % In case it's empty, now. - NewSize + SlotSize; - true -> - OldSize + SlotSize + lists:foldl(fun({_Path, Size, _Time}, {[], AccSize, AccSize}) + when AccSize + Size + SlotSize =< MinSize -> + {[], AccSize + Size, AccSize + Size}; + ({Path, Size, _Time}, {[], AccSize, AccSize}) -> + {[Path], AccSize + Size, AccSize}; + ({Path, Size, _Time}, {AccFiles, AccSize, NewSize}) -> + {[Path | AccFiles], AccSize + Size, NewSize} + end, + {[], 0, 0}, + Files), + if + OldSize + SlotSize > MaxSize -> + lists:foreach(fun del_file_and_dir/1, DelFiles), + file:del_dir(UserDir), % In case it's empty, now. + NewSize + SlotSize; + true -> + OldSize + SlotSize end. + -spec delete_old_files(file:filename_all(), integer()) -> ok. delete_old_files(UserDir, CutOff) -> FileInfo = gather_file_info(UserDir), - case [Path || {Path, _Size, Time} <- FileInfo, Time < CutOff] of - [] -> - ok; - OldFiles -> - lists:foreach(fun del_file_and_dir/1, OldFiles), - file:del_dir(UserDir) % In case it's empty, now. + case [ Path || {Path, _Size, Time} <- FileInfo, Time < CutOff ] of + [] -> + ok; + OldFiles -> + lists:foreach(fun del_file_and_dir/1, OldFiles), + file:del_dir(UserDir) % In case it's empty, now. end. --spec gather_file_info(file:filename_all()) - -> [{binary(), non_neg_integer(), non_neg_integer()}]. + +-spec gather_file_info(file:filename_all()) -> + [{binary(), non_neg_integer(), non_neg_integer()}]. gather_file_info(Dir) when is_binary(Dir) -> gather_file_info(binary_to_list(Dir)); gather_file_info(Dir) -> case file:list_dir(Dir) of - {ok, Entries} -> - lists:foldl(fun(Entry, Acc) -> - Path = Dir ++ "/" ++ Entry, - case file:read_file_info(Path, - [{time, posix}]) of - {ok, #file_info{type = directory}} -> - gather_file_info(Path) ++ Acc; - {ok, #file_info{type = regular, - mtime = Time, - size = Size}} -> - [{Path, Size, Time} | Acc]; - {ok, _Info} -> - ?DEBUG("Won't stat(2) non-regular file ~ts", - [Path]), - Acc; - {error, Error} -> - ?ERROR_MSG("Cannot stat(2) ~ts: ~ts", - [Path, ?FORMAT(Error)]), - Acc - end - end, [], Entries); - {error, enoent} -> - ?DEBUG("Directory ~ts doesn't exist", [Dir]), - []; - {error, Error} -> - ?ERROR_MSG("Cannot open directory ~ts: ~ts", [Dir, ?FORMAT(Error)]), - [] + {ok, Entries} -> + lists:foldl(fun(Entry, Acc) -> + Path = Dir ++ "/" ++ Entry, + case file:read_file_info(Path, + [{time, posix}]) of + {ok, #file_info{type = directory}} -> + gather_file_info(Path) ++ Acc; + {ok, #file_info{ + type = regular, + mtime = Time, + size = Size + }} -> + [{Path, Size, Time} | Acc]; + {ok, _Info} -> + ?DEBUG("Won't stat(2) non-regular file ~ts", + [Path]), + Acc; + {error, Error} -> + ?ERROR_MSG("Cannot stat(2) ~ts: ~ts", + [Path, ?FORMAT(Error)]), + Acc + end + end, + [], + Entries); + {error, enoent} -> + ?DEBUG("Directory ~ts doesn't exist", [Dir]), + []; + {error, Error} -> + ?ERROR_MSG("Cannot open directory ~ts: ~ts", [Dir, ?FORMAT(Error)]), + [] end. + -spec del_file_and_dir(file:name_all()) -> ok. del_file_and_dir(File) -> case file:delete(File) of - ok -> - ?INFO_MSG("Removed ~ts", [File]), - Dir = filename:dirname(File), - case file:del_dir(Dir) of - ok -> - ?DEBUG("Removed ~ts", [Dir]); - {error, Error} -> - ?DEBUG("Cannot remove ~ts: ~ts", [Dir, ?FORMAT(Error)]) - end; - {error, Error} -> - ?WARNING_MSG("Cannot remove ~ts: ~ts", [File, ?FORMAT(Error)]) + ok -> + ?INFO_MSG("Removed ~ts", [File]), + Dir = filename:dirname(File), + case file:del_dir(Dir) of + ok -> + ?DEBUG("Removed ~ts", [Dir]); + {error, Error} -> + ?DEBUG("Cannot remove ~ts: ~ts", [Dir, ?FORMAT(Error)]) + end; + {error, Error} -> + ?WARNING_MSG("Cannot remove ~ts: ~ts", [File, ?FORMAT(Error)]) end. + -spec secs_since_epoch() -> non_neg_integer(). secs_since_epoch() -> {MegaSecs, Secs, _MicroSecs} = os:timestamp(), diff --git a/src/mod_http_upload_quota_opt.erl b/src/mod_http_upload_quota_opt.erl index acf739fab..d526168e0 100644 --- a/src/mod_http_upload_quota_opt.erl +++ b/src/mod_http_upload_quota_opt.erl @@ -7,21 +7,23 @@ -export([access_soft_quota/1]). -export([max_days/1]). + -spec access_hard_quota(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. access_hard_quota(Opts) when is_map(Opts) -> gen_mod:get_opt(access_hard_quota, Opts); access_hard_quota(Host) -> gen_mod:get_module_opt(Host, mod_http_upload_quota, access_hard_quota). + -spec access_soft_quota(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. access_soft_quota(Opts) when is_map(Opts) -> gen_mod:get_opt(access_soft_quota, Opts); access_soft_quota(Host) -> gen_mod:get_module_opt(Host, mod_http_upload_quota, access_soft_quota). + -spec max_days(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_days(Opts) when is_map(Opts) -> gen_mod:get_opt(max_days, Opts); max_days(Host) -> gen_mod:get_module_opt(Host, mod_http_upload_quota, max_days). - diff --git a/src/mod_jidprep.erl b/src/mod_jidprep.erl index 3de051156..5c6f6f59b 100644 --- a/src/mod_jidprep.erl +++ b/src/mod_jidprep.erl @@ -41,8 +41,10 @@ -include("logger.hrl"). -include("translate.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- @@ -51,28 +53,35 @@ start(_Host, _Opts) -> {ok, [{iq_handler, ejabberd_local, ?NS_JIDPREP_0, process_iq}, {hook, disco_local_features, disco_local_features, 50}]}. + -spec stop(binary()) -> ok. stop(_Host) -> ok. + -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. + -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access) -> econf:acl(). + -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(_Host) -> [{access, local}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module allows XMPP clients to ask the " "server to normalize a JID as per the rules specified " "in https://tools.ietf.org/html/rfc6122" @@ -81,31 +90,42 @@ mod_doc() -> "or for testing purposes."), opts => [{access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option defines which access rule will " "be used to control who is allowed to use this " - "service. The default value is 'local'.")}}]}. + "service. The default value is 'local'.") + }}] + }. + %%-------------------------------------------------------------------- %% Service discovery. %%-------------------------------------------------------------------- --spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), - binary()) -> mod_disco:features_acc(). +-spec disco_local_features(mod_disco:features_acc(), + jid(), + jid(), + binary(), + binary()) -> mod_disco:features_acc(). disco_local_features(empty, From, To, Node, Lang) -> disco_local_features({result, []}, From, To, Node, Lang); -disco_local_features({result, OtherFeatures} = Acc, From, - #jid{lserver = LServer}, <<"">>, _Lang) -> +disco_local_features({result, OtherFeatures} = Acc, + From, + #jid{lserver = LServer}, + <<"">>, + _Lang) -> Access = mod_jidprep_opt:access(LServer), case acl:match_rule(LServer, Access, From) of - allow -> - {result, [?NS_JIDPREP_0 | OtherFeatures]}; - deny -> - Acc + allow -> + {result, [?NS_JIDPREP_0 | OtherFeatures]}; + deny -> + Acc end; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. + %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- @@ -113,28 +133,36 @@ disco_local_features(Acc, _From, _To, _Node, _Lang) -> process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_iq(#iq{from = From, to = #jid{lserver = LServer}, lang = Lang, - sub_els = [#jidprep{jid = #jid{luser = U, - lserver = S, - lresource = R} = JID}]} = IQ) -> +process_iq(#iq{ + from = From, + to = #jid{lserver = LServer}, + lang = Lang, + sub_els = [#jidprep{ + jid = #jid{ + luser = U, + lserver = S, + lresource = R + } = JID + }] + } = IQ) -> Access = mod_jidprep_opt:access(LServer), case acl:match_rule(LServer, Access, From) of - allow -> - case jid:make(U, S, R) of - #jid{} = Normalized -> - ?DEBUG("Normalized JID for ~ts: ~ts", - [jid:encode(From), jid:encode(JID)]), - xmpp:make_iq_result(IQ, #jidprep{jid = Normalized}); - error -> % Cannot happen. - ?DEBUG("Normalizing JID failed for ~ts: ~ts", - [jid:encode(From), jid:encode(JID)]), - Txt = ?T("JID normalization failed"), - xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)) - end; - deny -> - ?DEBUG("Won't return normalized JID to ~ts: ~ts", - [jid:encode(From), jid:encode(JID)]), - Txt = ?T("JID normalization denied by service policy"), + allow -> + case jid:make(U, S, R) of + #jid{} = Normalized -> + ?DEBUG("Normalized JID for ~ts: ~ts", + [jid:encode(From), jid:encode(JID)]), + xmpp:make_iq_result(IQ, #jidprep{jid = Normalized}); + error -> % Cannot happen. + ?DEBUG("Normalizing JID failed for ~ts: ~ts", + [jid:encode(From), jid:encode(JID)]), + Txt = ?T("JID normalization failed"), + xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)) + end; + deny -> + ?DEBUG("Won't return normalized JID to ~ts: ~ts", + [jid:encode(From), jid:encode(JID)]), + Txt = ?T("JID normalization denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; process_iq(#iq{lang = Lang} = IQ) -> diff --git a/src/mod_jidprep_opt.erl b/src/mod_jidprep_opt.erl index f30f70632..b717bdc49 100644 --- a/src/mod_jidprep_opt.erl +++ b/src/mod_jidprep_opt.erl @@ -5,9 +5,9 @@ -export([access/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_jidprep, access). - diff --git a/src/mod_last.erl b/src/mod_last.erl index ed701ea50..a85e308b3 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -31,14 +31,30 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_local_iq/1, export/1, - process_sm_iq/1, on_presence_update/4, import_info/0, - import/5, import_start/2, store_last_info/4, get_last_info/2, - remove_user/2, mod_opt_type/1, mod_options/1, mod_doc/0, - register_user/2, depends/2, privacy_check_packet/4]). +-export([start/2, + stop/1, + reload/3, + process_local_iq/1, + export/1, + process_sm_iq/1, + on_presence_update/4, + import_info/0, + import/5, + import_start/2, + store_last_info/4, + get_last_info/2, + remove_user/2, + mod_opt_type/1, + mod_options/1, + mod_doc/0, + register_user/2, + depends/2, + privacy_check_packet/4]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_privacy.hrl"). -include("mod_last.hrl"). -include("translate.hrl"). @@ -47,10 +63,11 @@ -type c2s_state() :: ejabberd_c2s:state(). + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), #last_activity{}) -> ok | pass. -callback get_last(binary(), binary()) -> - {ok, {non_neg_integer(), binary()}} | error | {error, any()}. + {ok, {non_neg_integer(), binary()}} | error | {error, any()}. -callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> ok | {error, any()}. -callback remove_user(binary(), binary()) -> any(). -callback use_cache(binary()) -> boolean(). @@ -58,6 +75,7 @@ -optional_callbacks([use_cache/1, cache_nodes/1]). + start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), @@ -69,23 +87,28 @@ start(Host, Opts) -> {hook, remove_user, remove_user, 50}, {hook, unset_presence_hook, on_presence_update, 50}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts). + %%% %%% Uptime of ejabberd node %%% + -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -93,6 +116,7 @@ process_local_iq(#iq{type = set, lang = Lang} = IQ) -> process_local_iq(#iq{type = get} = IQ) -> xmpp:make_iq_result(IQ, #last{seconds = get_node_uptime()}). + -spec get_node_uptime() -> non_neg_integer(). %% @doc Get the uptime of the ejabberd node, expressed in seconds. %% When ejabberd is starting, ejabberd_config:start/0 stores the datetime. @@ -100,10 +124,12 @@ get_node_uptime() -> NodeStart = ejabberd_config:get_node_start(), erlang:monotonic_time(second) - NodeStart. + %%% %%% Serve queries about user last online %%% + -spec process_sm_iq(iq()) -> iq(). process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -112,127 +138,150 @@ process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) -> User = To#jid.luser, Server = To#jid.lserver, {Subscription, _Ask, _Groups} = - ejabberd_hooks:run_fold(roster_get_jid_info, Server, - {none, none, []}, [User, Server, From]), - if (Subscription == both) or (Subscription == from) or - (From#jid.luser == To#jid.luser) and - (From#jid.lserver == To#jid.lserver) -> - Pres = xmpp:set_from_to(#presence{}, To, From), - case ejabberd_hooks:run_fold(privacy_check_packet, - Server, allow, - [To, Pres, out]) of - allow -> get_last_iq(IQ, User, Server); - deny -> xmpp:make_error(IQ, xmpp:err_forbidden()) - end; - true -> - Txt = ?T("Not subscribed"), - xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) + ejabberd_hooks:run_fold(roster_get_jid_info, + Server, + {none, none, []}, + [User, Server, From]), + if + (Subscription == both) or (Subscription == from) or + (From#jid.luser == To#jid.luser) and + (From#jid.lserver == To#jid.lserver) -> + Pres = xmpp:set_from_to(#presence{}, To, From), + case ejabberd_hooks:run_fold(privacy_check_packet, + Server, + allow, + [To, Pres, out]) of + allow -> get_last_iq(IQ, User, Server); + deny -> xmpp:make_error(IQ, xmpp:err_forbidden()) + end; + true -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. + -spec privacy_check_packet(allow | deny, c2s_state(), stanza(), in | out) -> allow | deny | {stop, deny}. -privacy_check_packet(allow, C2SState, - #iq{from = From, to = To, type = T} = IQ, in) +privacy_check_packet(allow, + C2SState, + #iq{from = From, to = To, type = T} = IQ, + in) when T == get; T == set -> case xmpp:has_subtag(IQ, #last{}) of - true -> - #jid{luser = LUser, lserver = LServer} = To, - {Sub, _, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, - {none, none, []}, [LUser, LServer, From]), - if Sub == from; Sub == both -> - Pres = #presence{from = To, to = From}, - case ejabberd_hooks:run_fold( - privacy_check_packet, allow, - [C2SState, Pres, out]) of - allow -> - allow; - deny -> - {stop, deny} - end; - true -> - {stop, deny} - end; - false -> - allow + true -> + #jid{luser = LUser, lserver = LServer} = To, + {Sub, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, + LServer, + {none, none, []}, + [LUser, LServer, From]), + if + Sub == from; Sub == both -> + Pres = #presence{from = To, to = From}, + case ejabberd_hooks:run_fold( + privacy_check_packet, + allow, + [C2SState, Pres, out]) of + allow -> + allow; + deny -> + {stop, deny} + end; + true -> + {stop, deny} + end; + false -> + allow end; privacy_check_packet(Acc, _, _, _) -> Acc. + -spec get_last(binary(), binary()) -> {ok, non_neg_integer(), binary()} | - not_found | {error, any()}. + not_found | + {error, any()}. get_last(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?LAST_CACHE, {LUser, LServer}, - fun() -> Mod:get_last(LUser, LServer) end); - false -> - Mod:get_last(LUser, LServer) - end, + true -> + ets_cache:lookup( + ?LAST_CACHE, + {LUser, LServer}, + fun() -> Mod:get_last(LUser, LServer) end); + false -> + Mod:get_last(LUser, LServer) + end, case Res of - {ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status}; - error -> not_found; - Err -> Err + {ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status}; + error -> not_found; + Err -> Err end. + -spec get_last_iq(iq(), binary(), binary()) -> iq(). get_last_iq(#iq{lang = Lang} = IQ, LUser, LServer) -> case ejabberd_sm:get_user_resources(LUser, LServer) of - [] -> - case get_last(LUser, LServer) of - {error, _Reason} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); - not_found -> - Txt = ?T("No info about last activity found"), - xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); - {ok, TimeStamp, Status} -> - TimeStamp2 = erlang:system_time(second), - Sec = TimeStamp2 - TimeStamp, - xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status}) - end; - _ -> - xmpp:make_iq_result(IQ, #last{seconds = 0}) + [] -> + case get_last(LUser, LServer) of + {error, _Reason} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + not_found -> + Txt = ?T("No info about last activity found"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); + {ok, TimeStamp, Status} -> + TimeStamp2 = erlang:system_time(second), + Sec = TimeStamp2 - TimeStamp, + xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status}) + end; + _ -> + xmpp:make_iq_result(IQ, #last{seconds = 0}) end. + -spec register_user(binary(), binary()) -> any(). register_user(User, Server) -> on_presence_update( - User, - Server, - <<"RegisterResource">>, - <<"Registered but didn't login">>). + User, + Server, + <<"RegisterResource">>, + <<"Registered but didn't login">>). + -spec on_presence_update(binary(), binary(), binary(), binary()) -> any(). on_presence_update(User, Server, _Resource, Status) -> TimeStamp = erlang:system_time(second), store_last_info(User, Server, TimeStamp, Status). + -spec store_last_info(binary(), binary(), non_neg_integer(), binary()) -> any(). store_last_info(User, Server, TimeStamp, Status) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of - true -> - ets_cache:update( - ?LAST_CACHE, {LUser, LServer}, {ok, {TimeStamp, Status}}, - fun() -> - Mod:store_last_info(LUser, LServer, TimeStamp, Status) - end, cache_nodes(Mod, LServer)); - false -> - Mod:store_last_info(LUser, LServer, TimeStamp, Status) + true -> + ets_cache:update( + ?LAST_CACHE, + {LUser, LServer}, + {ok, {TimeStamp, Status}}, + fun() -> + Mod:store_last_info(LUser, LServer, TimeStamp, Status) + end, + cache_nodes(Mod, LServer)); + false -> + Mod:store_last_info(LUser, LServer, TimeStamp, Status) end. + -spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | - not_found. + not_found. get_last_info(LUser, LServer) -> case get_last(LUser, LServer) of - {error, _Reason} -> not_found; - Res -> Res + {error, _Reason} -> not_found; + Res -> Res end. + -spec remove_user(binary(), binary()) -> any(). remove_user(User, Server) -> LUser = jid:nodeprep(User), @@ -241,16 +290,18 @@ remove_user(User, Server) -> Mod:remove_user(LUser, LServer), ets_cache:delete(?LAST_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). + -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?LAST_CACHE, CacheOpts); - false -> - ets_cache:delete(?LAST_CACHE) + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?LAST_CACHE, CacheOpts); + false -> + ets_cache:delete(?LAST_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_last_opt:cache_size(Opts), @@ -258,45 +309,55 @@ cache_opts(Opts) -> LifeTime = mod_last_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(Host); - false -> mod_last_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_last_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + import_info() -> [{<<"last">>, 3}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). + import(LServer, {sql, _}, DBType, <<"last">>, [LUser, TimeStamp, State]) -> TS = case TimeStamp of <<"">> -> 0; _ -> binary_to_integer(TimeStamp) end, - LA = #last_activity{us = {LUser, LServer}, - timestamp = TS, - status = State}, + LA = #last_activity{ + us = {LUser, LServer}, + timestamp = TS, + status = State + }, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, LA). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + depends(_Host, _Opts) -> []. + mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> @@ -308,6 +369,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, @@ -315,8 +377,10 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0012.html" "[XEP-0012: Last Activity]. It can be used " @@ -325,22 +389,33 @@ mod_doc() -> "active on the server, or to query the uptime of the ejabberd server."), opts => [{db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}] + }. diff --git a/src/mod_last_mnesia.erl b/src/mod_last_mnesia.erl index f108101c9..7c6f0b7b6 100644 --- a/src/mod_last_mnesia.erl +++ b/src/mod_last_mnesia.erl @@ -27,50 +27,66 @@ -behaviour(mod_last). %% API --export([init/2, import/2, get_last/2, store_last_info/4, - remove_user/2, use_cache/1]). +-export([init/2, + import/2, + get_last/2, + store_last_info/4, + remove_user/2, + use_cache/1]). -export([need_transform/1, transform/1]). -include("mod_last.hrl"). -include("logger.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, last_activity, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, last_activity)}]). + ejabberd_mnesia:create(?MODULE, + last_activity, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, last_activity)}]). + use_cache(Host) -> case mnesia:table_info(last_activity, storage_type) of - disc_only_copies -> - mod_last_opt:use_cache(Host); - _ -> - false + disc_only_copies -> + mod_last_opt:use_cache(Host); + _ -> + false end. + get_last(LUser, LServer) -> case mnesia:dirty_read(last_activity, {LUser, LServer}) of - [] -> - error; - [#last_activity{timestamp = TimeStamp, - status = Status}] -> - {ok, {TimeStamp, Status}} + [] -> + error; + [#last_activity{ + timestamp = TimeStamp, + status = Status + }] -> + {ok, {TimeStamp, Status}} end. + store_last_info(LUser, LServer, TimeStamp, Status) -> - mnesia:dirty_write(#last_activity{us = {LUser, LServer}, - timestamp = TimeStamp, - status = Status}). + mnesia:dirty_write(#last_activity{ + us = {LUser, LServer}, + timestamp = TimeStamp, + status = Status + }). + remove_user(LUser, LServer) -> US = {LUser, LServer}, mnesia:dirty_delete({last_activity, US}). + import(_LServer, #last_activity{} = LA) -> mnesia:dirty_write(LA). + need_transform({last_activity, {U, S}, _, Status}) when is_list(U) orelse is_list(S) orelse is_list(Status) -> ?INFO_MSG("Mnesia table 'last_activity' will be converted to binary", []), @@ -78,9 +94,12 @@ need_transform({last_activity, {U, S}, _, Status}) need_transform(_) -> false. + transform(#last_activity{us = {U, S}, status = Status} = R) -> - R#last_activity{us = {iolist_to_binary(U), iolist_to_binary(S)}, - status = iolist_to_binary(Status)}. + R#last_activity{ + us = {iolist_to_binary(U), iolist_to_binary(S)}, + status = iolist_to_binary(Status) + }. %%%=================================================================== %%% Internal functions diff --git a/src/mod_last_opt.erl b/src/mod_last_opt.erl index 470ffce5e..3dcbd526f 100644 --- a/src/mod_last_opt.erl +++ b/src/mod_last_opt.erl @@ -9,33 +9,37 @@ -export([db_type/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_last, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_last, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_last, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_last, db_type). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_last, use_cache). - diff --git a/src/mod_last_sql.erl b/src/mod_last_sql.erl index b61300fd2..b4885636f 100644 --- a/src/mod_last_sql.erl +++ b/src/mod_last_sql.erl @@ -26,16 +26,20 @@ -behaviour(mod_last). - %% API --export([init/2, get_last/2, store_last_info/4, remove_user/2, - import/2, export/1]). +-export([init/2, + get_last/2, + store_last_info/4, + remove_user/2, + import/2, + export/1]). -export([sql_schemas/0]). -include("mod_last.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -43,56 +47,69 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"last">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"seconds">>, type = text}, - #sql_column{name = <<"state">>, type = text}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"last">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"seconds">>, type = text}, + #sql_column{name = <<"state">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }] + }]. + get_last(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(seconds)d, @(state)s from last" - " where username=%(LUser)s and %(LServer)H")) of + LServer, + ?SQL("select @(seconds)d, @(state)s from last" + " where username=%(LUser)s and %(LServer)H")) of {selected, []} -> - error; + error; {selected, [{TimeStamp, Status}]} -> {ok, {TimeStamp, Status}}; _Reason -> - {error, db_failure} + {error, db_failure} end. + store_last_info(LUser, LServer, TimeStamp, Status) -> TS = integer_to_binary(TimeStamp), - case ?SQL_UPSERT(LServer, "last", - ["!username=%(LUser)s", + case ?SQL_UPSERT(LServer, + "last", + ["!username=%(LUser)s", "!server_host=%(LServer)s", - "seconds=%(TS)s", - "state=%(Status)s"]) of - ok -> - ok; - _Err -> - {error, db_failure} + "seconds=%(TS)s", + "state=%(Status)s"]) of + ok -> + ok; + _Err -> + {error, db_failure} end. + remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from last where username=%(LUser)s and %(LServer)H")). + export(_Server) -> [{last_activity, - fun(Host, #last_activity{us = {LUser, LServer}, - timestamp = TimeStamp, status = Status}) + fun(Host, + #last_activity{ + us = {LUser, LServer}, + timestamp = TimeStamp, + status = Status + }) when LServer == Host -> TS = integer_to_binary(TimeStamp), [?SQL("delete from last where username=%(LUser)s and %(LServer)H;"), @@ -105,5 +122,6 @@ export(_Server) -> [] end}]. + import(_LServer, _LA) -> pass. diff --git a/src/mod_legacy_auth.erl b/src/mod_legacy_auth.erl index 1fb772d2c..f806de852 100644 --- a/src/mod_legacy_auth.erl +++ b/src/mod_legacy_auth.erl @@ -30,10 +30,12 @@ -export([c2s_unauthenticated_packet/2, c2s_stream_features/2]). -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -type c2s_state() :: ejabberd_c2s:state(). + %%%=================================================================== %%% API %%%=================================================================== @@ -41,124 +43,153 @@ start(_Host, _Opts) -> {ok, [{hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}, {hook, c2s_pre_auth_features, c2s_stream_features, 50}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + mod_options(_) -> []. + mod_doc() -> - #{desc => + #{ + desc => [?T("The module implements " "https://xmpp.org/extensions/xep-0078.html" - "[XEP-0078: Non-SASL Authentication]."), "", + "[XEP-0078: Non-SASL Authentication]."), + "", ?T("NOTE: This type of authentication was obsoleted in " "2008 and you unlikely need this module unless " - "you have something like outdated Jabber bots.")]}. + "you have something like outdated Jabber bots.")] + }. + -spec c2s_unauthenticated_packet(c2s_state(), iq()) -> - c2s_state() | {stop, c2s_state()}. + c2s_state() | {stop, c2s_state()}. c2s_unauthenticated_packet(State, #iq{type = T, sub_els = [_]} = IQ) when T == get; T == set -> try xmpp:try_subtag(IQ, #legacy_auth{}) of - #legacy_auth{} = Auth -> - {stop, authenticate(State, xmpp:set_els(IQ, [Auth]))}; - false -> - State - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Lang = maps:get(lang, State), - Err = xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)), - {stop, ejabberd_c2s:send(State, Err)} + #legacy_auth{} = Auth -> + {stop, authenticate(State, xmpp:set_els(IQ, [Auth]))}; + false -> + State + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Lang = maps:get(lang, State), + Err = xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)), + {stop, ejabberd_c2s:send(State, Err)} end; c2s_unauthenticated_packet(State, _) -> State. + -spec c2s_stream_features([xmpp_element()], binary()) -> [xmpp_element()]. c2s_stream_features(Acc, LServer) -> case gen_mod:is_loaded(LServer, ?MODULE) of - true -> - [#legacy_auth_feature{}|Acc]; - false -> - Acc + true -> + [#legacy_auth_feature{} | Acc]; + false -> + Acc end. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec authenticate(c2s_state(), iq()) -> c2s_state(). authenticate(#{server := Server} = State, - #iq{type = get, sub_els = [#legacy_auth{}]} = IQ) -> + #iq{type = get, sub_els = [#legacy_auth{}]} = IQ) -> LServer = jid:nameprep(Server), Auth = #legacy_auth{username = <<>>, password = <<>>, resource = <<>>}, Res = case ejabberd_auth:plain_password_required(LServer) of - false -> - xmpp:make_iq_result(IQ, Auth#legacy_auth{digest = <<>>}); - true -> - xmpp:make_iq_result(IQ, Auth) - end, + false -> + xmpp:make_iq_result(IQ, Auth#legacy_auth{digest = <<>>}); + true -> + xmpp:make_iq_result(IQ, Auth) + end, ejabberd_c2s:send(State, Res); authenticate(State, - #iq{type = set, lang = Lang, - sub_els = [#legacy_auth{username = U, - resource = R}]} = IQ) + #iq{ + type = set, + lang = Lang, + sub_els = [#legacy_auth{ + username = U, + resource = R + }] + } = IQ) when U == undefined; R == undefined; U == <<"">>; R == <<"">> -> Txt = ?T("Both the username and the resource are required"), Err = xmpp:make_error(IQ, xmpp:err_not_acceptable(Txt, Lang)), ejabberd_c2s:send(State, Err); -authenticate(#{stream_id := StreamID, server := Server, - access := Access, ip := IP} = State, - #iq{type = set, lang = Lang, - sub_els = [#legacy_auth{username = U, - password = P0, - digest = D0, - resource = R}]} = IQ) -> +authenticate(#{ + stream_id := StreamID, + server := Server, + access := Access, + ip := IP + } = State, + #iq{ + type = set, + lang = Lang, + sub_els = [#legacy_auth{ + username = U, + password = P0, + digest = D0, + resource = R + }] + } = IQ) -> P = if is_binary(P0) -> P0; true -> <<>> end, D = if is_binary(D0) -> D0; true -> <<>> end, - DGen = fun (PW) -> str:sha(<>) end, + DGen = fun(PW) -> str:sha(<>) end, JID = jid:make(U, Server, R), case JID /= error andalso - acl:match_rule(JID#jid.lserver, Access, - #{usr => jid:split(JID), ip => IP}) == allow of - true -> - case ejabberd_auth:check_password_with_authmodule( - U, U, JID#jid.lserver, P, D, DGen) of - {true, AuthModule} -> - State1 = State#{sasl_mech => <<"legacy">>}, - State2 = ejabberd_c2s:handle_auth_success( - U, <<"legacy">>, AuthModule, State1), - State3 = State2#{user := U}, - open_session(State3, IQ, R); - _ -> - Err = xmpp:make_error(IQ, xmpp:err_not_authorized()), - process_auth_failure(State, U, Err, 'not-authorized') - end; - false when JID == error -> - Err = xmpp:make_error(IQ, xmpp:err_jid_malformed()), - process_auth_failure(State, U, Err, 'jid-malformed'); - false -> - Txt = ?T("Access denied by service policy"), - Err = xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)), - process_auth_failure(State, U, Err, 'forbidden') + acl:match_rule(JID#jid.lserver, + Access, + #{usr => jid:split(JID), ip => IP}) == allow of + true -> + case ejabberd_auth:check_password_with_authmodule( + U, U, JID#jid.lserver, P, D, DGen) of + {true, AuthModule} -> + State1 = State#{sasl_mech => <<"legacy">>}, + State2 = ejabberd_c2s:handle_auth_success( + U, <<"legacy">>, AuthModule, State1), + State3 = State2#{user := U}, + open_session(State3, IQ, R); + _ -> + Err = xmpp:make_error(IQ, xmpp:err_not_authorized()), + process_auth_failure(State, U, Err, 'not-authorized') + end; + false when JID == error -> + Err = xmpp:make_error(IQ, xmpp:err_jid_malformed()), + process_auth_failure(State, U, Err, 'jid-malformed'); + false -> + Txt = ?T("Access denied by service policy"), + Err = xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)), + process_auth_failure(State, U, Err, 'forbidden') end. + -spec open_session(c2s_state(), iq(), binary()) -> c2s_state(). open_session(State, IQ, R) -> case ejabberd_c2s:bind(R, State) of - {ok, State1} -> - Res = xmpp:make_iq_result(IQ), - ejabberd_c2s:send(State1, Res); - {error, Err, State1} -> - Res = xmpp:make_error(IQ, Err), - ejabberd_c2s:send(State1, Res) + {ok, State1} -> + Res = xmpp:make_iq_result(IQ), + ejabberd_c2s:send(State1, Res); + {error, Err, State1} -> + Res = xmpp:make_error(IQ, Err), + ejabberd_c2s:send(State1, Res) end. + -spec process_auth_failure(c2s_state(), binary(), iq(), atom()) -> c2s_state(). process_auth_failure(State, User, StanzaErr, Reason) -> State1 = ejabberd_c2s:send(State, StanzaErr), @@ -166,6 +197,7 @@ process_auth_failure(State, User, StanzaErr, Reason) -> Text = format_reason(Reason), ejabberd_c2s:handle_auth_failure(User, <<"legacy">>, Text, State2). + -spec format_reason(atom()) -> binary(). format_reason('not-authorized') -> <<"Invalid username or password">>; diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 33f361f47..03e85e68b 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -38,27 +38,53 @@ %% API -export([start/2, stop/1, reload/3, depends/2, mod_doc/0]). --export([sm_receive_packet/1, user_receive_packet/1, user_send_packet/1, - user_send_packet_strip_tag/1, process_iq_v0_2/1, process_iq_v0_3/1, - disco_local_features/5, - disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1, - muc_process_iq/2, muc_filter_message/3, message_is_archived/3, - delete_old_messages/2, get_commands_spec/0, msg_to_el/4, - get_room_config/4, set_room_option/3, offline_message/1, export/1, - mod_options/1, remove_mam_for_user_with_peer/3, remove_mam_for_user/2, - is_empty_for_user/2, is_empty_for_room/3, check_create_room/4, - process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2, select/7, - is_archiving_enabled/2, - get_mam_count/2, - webadmin_menu_hostuser/4, - webadmin_page_hostuser/4, - get_mam_messages/2, webadmin_user/4, - delete_old_messages_batch/5, delete_old_messages_status/1, delete_old_messages_abort/1, - remove_message_from_archive/3]). +-export([sm_receive_packet/1, + user_receive_packet/1, + user_send_packet/1, + user_send_packet_strip_tag/1, + process_iq_v0_2/1, + process_iq_v0_3/1, + disco_local_features/5, + disco_sm_features/5, + remove_user/2, + remove_room/3, + mod_opt_type/1, + muc_process_iq/2, + muc_filter_message/3, + message_is_archived/3, + delete_old_messages/2, + get_commands_spec/0, + msg_to_el/4, + get_room_config/4, + set_room_option/3, + offline_message/1, + export/1, + mod_options/1, + remove_mam_for_user_with_peer/3, + remove_mam_for_user/2, + is_empty_for_user/2, + is_empty_for_room/3, + check_create_room/4, + process_iq/3, + store_mam_message/7, + make_id/0, + wrap_as_mucsub/2, + select/7, + is_archiving_enabled/2, + get_mam_count/2, + webadmin_menu_hostuser/4, + webadmin_page_hostuser/4, + get_mam_messages/2, + webadmin_user/4, + delete_old_messages_batch/5, + delete_old_messages_status/1, + delete_old_messages_abort/1, + remove_message_from_archive/3]). -import(ejabberd_web_admin, [make_command/4, make_command/2]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_commands.hrl"). @@ -73,242 +99,426 @@ -type c2s_state() :: ejabberd_c2s:state(). -type count() :: non_neg_integer() | undefined. + -callback init(binary(), gen_mod:opts()) -> any(). -callback remove_user(binary(), binary()) -> any(). -callback remove_room(binary(), binary(), binary()) -> any(). -callback delete_old_messages(binary() | global, - erlang:timestamp(), - all | chat | groupchat) -> any(). + erlang:timestamp(), + all | chat | groupchat) -> any(). -callback extended_fields(binary()) -> [mam_query:property() | #xdata_field{}]. --callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat, - jid(), binary(), recv | send, integer(), binary(), +-callback store(xmlel(), + binary(), + {binary(), binary()}, + chat | groupchat, + jid(), + binary(), + recv | send, + integer(), + binary(), {true, binary()} | false) -> ok | any(). -callback write_prefs(binary(), binary(), #archive_prefs{}, binary()) -> ok | any(). -callback get_prefs(binary(), binary()) -> {ok, #archive_prefs{}} | error | {error, db_failure}. --callback select(binary(), jid(), jid(), mam_query:result(), - #rsm_set{} | undefined, chat | groupchat) -> - {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | - {error, db_failure}. --callback select(binary(), jid(), jid(), mam_query:result(), - #rsm_set{} | undefined, chat | groupchat, - all | only_count | only_messages) -> - {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | - {error, db_failure}. +-callback select(binary(), + jid(), + jid(), + mam_query:result(), + #rsm_set{} | undefined, + chat | groupchat) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | + {error, db_failure}. +-callback select(binary(), + jid(), + jid(), + mam_query:result(), + #rsm_set{} | undefined, + chat | groupchat, + all | only_count | only_messages) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | + {error, db_failure}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -callback remove_from_archive(binary(), binary(), jid() | none) -> ok | {error, any()}. -callback is_empty_for_user(binary(), binary()) -> boolean(). -callback is_empty_for_room(binary(), binary(), binary()) -> boolean(). --callback select_with_mucsub(binary(), jid(), jid(), mam_query:result(), - #rsm_set{} | undefined, all | only_count | only_messages) -> - {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | - {error, db_failure}. +-callback select_with_mucsub(binary(), + jid(), + jid(), + mam_query:result(), + #rsm_set{} | undefined, + all | only_count | only_messages) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | + {error, db_failure}. --callback delete_old_messages_batch(binary(), erlang:timestamp(), - all | chat | groupchat, - pos_integer()) -> - {ok, non_neg_integer()} | {error, term()}. +-callback delete_old_messages_batch(binary(), + erlang:timestamp(), + all | chat | groupchat, + pos_integer()) -> + {ok, non_neg_integer()} | {error, term()}. --callback delete_old_messages_batch(binary(), erlang:timestamp(), - all | chat | groupchat, - pos_integer(), any()) -> - {ok, any(), non_neg_integer()} | {error, term()}. +-callback delete_old_messages_batch(binary(), + erlang:timestamp(), + all | chat | groupchat, + pos_integer(), + any()) -> + {ok, any(), non_neg_integer()} | {error, term()}. + +-optional_callbacks([use_cache/1, + cache_nodes/1, + select_with_mucsub/6, + select/6, + select/7, + delete_old_messages_batch/5, + delete_old_messages_batch/4]). --optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/6, select/6, select/7, - delete_old_messages_batch/5, delete_old_messages_batch/4]). %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> case mod_mam_opt:db_type(Opts) of - mnesia -> - ?WARNING_MSG("Mnesia backend for ~ts is not recommended: " - "it's limited to 2GB and often gets corrupted " - "when reaching this limit. SQL backend is " - "recommended. Namely, for small servers SQLite " - "is a preferred choice because it's very easy " - "to configure.", [?MODULE]); - _ -> - ok + mnesia -> + ?WARNING_MSG("Mnesia backend for ~ts is not recommended: " + "it's limited to 2GB and often gets corrupted " + "when reaching this limit. SQL backend is " + "recommended. Namely, for small servers SQLite " + "is a preferred choice because it's very easy " + "to configure.", + [?MODULE]); + _ -> + ok end, Mod = gen_mod:db_mod(Opts, ?MODULE), case Mod:init(Host, Opts) of - ok -> - init_cache(Mod, Host, Opts), - register_iq_handlers(Host), - ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, - sm_receive_packet, 50), - ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, - user_receive_packet, 88), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send_packet, 88), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send_packet_strip_tag, 500), - ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, - offline_message, 49), - ejabberd_hooks:add(muc_filter_message, Host, ?MODULE, - muc_filter_message, 50), - ejabberd_hooks:add(muc_process_iq, Host, ?MODULE, - muc_process_iq, 50), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - disco_local_features, 50), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - disco_sm_features, 50), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(get_room_config, Host, ?MODULE, - get_room_config, 50), - ejabberd_hooks:add(set_room_option, Host, ?MODULE, - set_room_option, 50), - ejabberd_hooks:add(store_mam_message, Host, ?MODULE, - store_mam_message, 100), - ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, - webadmin_menu_hostuser, 50), - ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, - webadmin_page_hostuser, 50), - ejabberd_hooks:add(webadmin_user, Host, ?MODULE, - webadmin_user, 50), - case mod_mam_opt:assume_mam_usage(Opts) of - true -> - ejabberd_hooks:add(message_is_archived, Host, ?MODULE, - message_is_archived, 50); - false -> - ok - end, - case mod_mam_opt:clear_archive_on_room_destroy(Opts) of - true -> - ejabberd_hooks:add(remove_room, Host, ?MODULE, - remove_room, 50); - false -> - ejabberd_hooks:add(check_create_room, Host, ?MODULE, - check_create_room, 50) - end, - ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), - ok; - Err -> - Err + ok -> + init_cache(Mod, Host, Opts), + register_iq_handlers(Host), + ejabberd_hooks:add(sm_receive_packet, + Host, + ?MODULE, + sm_receive_packet, + 50), + ejabberd_hooks:add(user_receive_packet, + Host, + ?MODULE, + user_receive_packet, + 88), + ejabberd_hooks:add(user_send_packet, + Host, + ?MODULE, + user_send_packet, + 88), + ejabberd_hooks:add(user_send_packet, + Host, + ?MODULE, + user_send_packet_strip_tag, + 500), + ejabberd_hooks:add(offline_message_hook, + Host, + ?MODULE, + offline_message, + 49), + ejabberd_hooks:add(muc_filter_message, + Host, + ?MODULE, + muc_filter_message, + 50), + ejabberd_hooks:add(muc_process_iq, + Host, + ?MODULE, + muc_process_iq, + 50), + ejabberd_hooks:add(disco_local_features, + Host, + ?MODULE, + disco_local_features, + 50), + ejabberd_hooks:add(disco_sm_features, + Host, + ?MODULE, + disco_sm_features, + 50), + ejabberd_hooks:add(remove_user, + Host, + ?MODULE, + remove_user, + 50), + ejabberd_hooks:add(get_room_config, + Host, + ?MODULE, + get_room_config, + 50), + ejabberd_hooks:add(set_room_option, + Host, + ?MODULE, + set_room_option, + 50), + ejabberd_hooks:add(store_mam_message, + Host, + ?MODULE, + store_mam_message, + 100), + ejabberd_hooks:add(webadmin_menu_hostuser, + Host, + ?MODULE, + webadmin_menu_hostuser, + 50), + ejabberd_hooks:add(webadmin_page_hostuser, + Host, + ?MODULE, + webadmin_page_hostuser, + 50), + ejabberd_hooks:add(webadmin_user, + Host, + ?MODULE, + webadmin_user, + 50), + case mod_mam_opt:assume_mam_usage(Opts) of + true -> + ejabberd_hooks:add(message_is_archived, + Host, + ?MODULE, + message_is_archived, + 50); + false -> + ok + end, + case mod_mam_opt:clear_archive_on_room_destroy(Opts) of + true -> + ejabberd_hooks:add(remove_room, + Host, + ?MODULE, + remove_room, + 50); + false -> + ejabberd_hooks:add(check_create_room, + Host, + ?MODULE, + check_create_room, + 50) + end, + ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()), + ok; + Err -> + Err end. + use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 2) of - true -> Mod:use_cache(Host); - false -> mod_mam_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_mam_opt:use_cache(Host) end. + cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - ets_cache:new(archive_prefs_cache, cache_opts(Opts)); - false -> - ets_cache:delete(archive_prefs_cache) + true -> + ets_cache:new(archive_prefs_cache, cache_opts(Opts)); + false -> + ets_cache:delete(archive_prefs_cache) end. + cache_opts(Opts) -> MaxSize = mod_mam_opt:cache_size(Opts), CacheMissed = mod_mam_opt:cache_missed(Opts), LifeTime = mod_mam_opt:cache_life_time(Opts), [{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}]. + stop(Host) -> unregister_iq_handlers(Host), - ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, - sm_receive_packet, 50), - ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, - user_receive_packet, 88), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send_packet, 88), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send_packet_strip_tag, 500), - ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, - offline_message, 49), - ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE, - muc_filter_message, 50), - ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE, - muc_process_iq, 50), - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, - disco_local_features, 50), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - disco_sm_features, 50), - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(get_room_config, Host, ?MODULE, - get_room_config, 50), - ejabberd_hooks:delete(set_room_option, Host, ?MODULE, - set_room_option, 50), - ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, - store_mam_message, 100), - ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE, - webadmin_menu_hostuser, 50), - ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE, - webadmin_page_hostuser, 50), - ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, - webadmin_user, 50), + ejabberd_hooks:delete(sm_receive_packet, + Host, + ?MODULE, + sm_receive_packet, + 50), + ejabberd_hooks:delete(user_receive_packet, + Host, + ?MODULE, + user_receive_packet, + 88), + ejabberd_hooks:delete(user_send_packet, + Host, + ?MODULE, + user_send_packet, + 88), + ejabberd_hooks:delete(user_send_packet, + Host, + ?MODULE, + user_send_packet_strip_tag, + 500), + ejabberd_hooks:delete(offline_message_hook, + Host, + ?MODULE, + offline_message, + 49), + ejabberd_hooks:delete(muc_filter_message, + Host, + ?MODULE, + muc_filter_message, + 50), + ejabberd_hooks:delete(muc_process_iq, + Host, + ?MODULE, + muc_process_iq, + 50), + ejabberd_hooks:delete(disco_local_features, + Host, + ?MODULE, + disco_local_features, + 50), + ejabberd_hooks:delete(disco_sm_features, + Host, + ?MODULE, + disco_sm_features, + 50), + ejabberd_hooks:delete(remove_user, + Host, + ?MODULE, + remove_user, + 50), + ejabberd_hooks:delete(get_room_config, + Host, + ?MODULE, + get_room_config, + 50), + ejabberd_hooks:delete(set_room_option, + Host, + ?MODULE, + set_room_option, + 50), + ejabberd_hooks:delete(store_mam_message, + Host, + ?MODULE, + store_mam_message, + 100), + ejabberd_hooks:delete(webadmin_menu_hostuser, + Host, + ?MODULE, + webadmin_menu_hostuser, + 50), + ejabberd_hooks:delete(webadmin_page_hostuser, + Host, + ?MODULE, + webadmin_page_hostuser, + 50), + ejabberd_hooks:delete(webadmin_user, + Host, + ?MODULE, + webadmin_user, + 50), case mod_mam_opt:assume_mam_usage(Host) of - true -> - ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, - message_is_archived, 50); - false -> - ok + true -> + ejabberd_hooks:delete(message_is_archived, + Host, + ?MODULE, + message_is_archived, + 50); + false -> + ok end, case mod_mam_opt:clear_archive_on_room_destroy(Host) of - true -> - ejabberd_hooks:delete(remove_room, Host, ?MODULE, - remove_room, 50); - false -> - ejabberd_hooks:delete(check_create_room, Host, ?MODULE, - check_create_room, 50) + true -> + ejabberd_hooks:delete(remove_room, + Host, + ?MODULE, + remove_room, + 50); + false -> + ejabberd_hooks:delete(check_create_room, + Host, + ?MODULE, + check_create_room, + 50) end, ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()). + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts), case {mod_mam_opt:assume_mam_usage(NewOpts), - mod_mam_opt:assume_mam_usage(OldOpts)} of - {true, false} -> - ejabberd_hooks:add(message_is_archived, Host, ?MODULE, - message_is_archived, 50); - {false, true} -> - ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, - message_is_archived, 50); - _ -> - ok + mod_mam_opt:assume_mam_usage(OldOpts)} of + {true, false} -> + ejabberd_hooks:add(message_is_archived, + Host, + ?MODULE, + message_is_archived, + 50); + {false, true} -> + ejabberd_hooks:delete(message_is_archived, + Host, + ?MODULE, + message_is_archived, + 50); + _ -> + ok end. + depends(_Host, _Opts) -> []. + -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP, - ?MODULE, process_iq_v0_2), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP, - ?MODULE, process_iq_v0_2), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_0, - ?MODULE, process_iq_v0_3), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_0, ?MODULE, - process_iq_v0_3), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_1, - ?MODULE, process_iq_v0_3), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_1, - ?MODULE, process_iq_v0_3), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_2, - ?MODULE, process_iq_v0_3), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, - ?MODULE, process_iq_v0_3). + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MAM_TMP, + ?MODULE, + process_iq_v0_2), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MAM_TMP, + ?MODULE, + process_iq_v0_2), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MAM_0, + ?MODULE, + process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MAM_0, + ?MODULE, + process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MAM_1, + ?MODULE, + process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MAM_1, + ?MODULE, + process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MAM_2, + ?MODULE, + process_iq_v0_3), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MAM_2, + ?MODULE, + process_iq_v0_3). + -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> @@ -321,6 +531,7 @@ unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_2), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), @@ -328,13 +539,15 @@ remove_user(User, Server) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), case use_cache(Mod, LServer) of - true -> - ets_cache:delete(archive_prefs_cache, {LUser, LServer}, - cache_nodes(Mod, LServer)); - false -> - ok + true -> + ets_cache:delete(archive_prefs_cache, + {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok end. + -spec remove_room(binary(), binary(), binary()) -> ok. remove_room(LServer, Name, Host) -> LName = jid:nodeprep(Name), @@ -343,126 +556,144 @@ remove_room(LServer, Name, Host) -> Mod:remove_room(LServer, LName, LHost), ok. + -spec remove_mam_for_user(binary(), binary()) -> - {ok, binary()} | {error, binary()}. + {ok, binary()} | {error, binary()}. remove_mam_for_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:remove_from_archive(LUser, LServer, none) of - ok -> - {ok, <<"MAM archive removed">>}; - {error, Bin} when is_binary(Bin) -> - {error, Bin}; - {error, _} -> - {error, <<"Db returned error">>} + ok -> + {ok, <<"MAM archive removed">>}; + {error, Bin} when is_binary(Bin) -> + {error, Bin}; + {error, _} -> + {error, <<"Db returned error">>} end. + -spec remove_mam_for_user_with_peer(binary(), binary(), binary()) -> - {ok, binary()} | {error, binary()}. + {ok, binary()} | {error, binary()}. remove_mam_for_user_with_peer(User, Server, Peer) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), try jid:decode(Peer) of - Jid -> - Mod = get_module_host(LServer), - case Mod:remove_from_archive(LUser, LServer, Jid) of - ok -> - {ok, <<"MAM archive removed">>}; - {error, Bin} when is_binary(Bin) -> - {error, Bin}; - {error, _} -> - {error, <<"Db returned error">>} - end - catch _:_ -> - {error, <<"Invalid peer JID">>} + Jid -> + Mod = get_module_host(LServer), + case Mod:remove_from_archive(LUser, LServer, Jid) of + ok -> + {ok, <<"MAM archive removed">>}; + {error, Bin} when is_binary(Bin) -> + {error, Bin}; + {error, _} -> + {error, <<"Db returned error">>} + end + catch + _:_ -> + {error, <<"Invalid peer JID">>} end. --spec remove_message_from_archive( - User :: binary() | {User :: binary(), Host :: binary()}, - Server :: binary(), StanzaId :: integer()) -> - ok | {error, binary()}. + +-spec remove_message_from_archive(User :: binary() | {User :: binary(), Host :: binary()}, + Server :: binary(), + StanzaId :: integer()) -> + ok | {error, binary()}. remove_message_from_archive(User, Server, StanzaId) when is_binary(User) -> remove_message_from_archive({User, Server}, Server, StanzaId); remove_message_from_archive({_User, _Host} = UserHost, Server, StanzaId) -> Mod = gen_mod:db_mod(Server, ?MODULE), case Mod:remove_from_archive(UserHost, Server, StanzaId) of - ok -> - ok; - {error, Bin} when is_binary(Bin) -> - {error, Bin}; - {error, _} -> - {error, <<"Db returned error">>} + ok -> + ok; + {error, Bin} when is_binary(Bin) -> + {error, Bin}; + {error, _} -> + {error, <<"Db returned error">>} end. + get_module_host(LServer) -> - try gen_mod:db_mod(LServer, ?MODULE) - catch error:{module_not_loaded, ?MODULE, LServer} -> - gen_mod:db_mod(ejabberd_router:host_of_route(LServer), ?MODULE) + try + gen_mod:db_mod(LServer, ?MODULE) + catch + error:{module_not_loaded, ?MODULE, LServer} -> + gen_mod:db_mod(ejabberd_router:host_of_route(LServer), ?MODULE) end. --spec get_room_config([muc_roomconfig:property()], mod_muc_room:state(), - jid(), binary()) -> [muc_roomconfig:property()]. + +-spec get_room_config([muc_roomconfig:property()], + mod_muc_room:state(), + jid(), + binary()) -> [muc_roomconfig:property()]. get_room_config(Fields, RoomState, _From, _Lang) -> Config = RoomState#state.config, Fields ++ [{mam, Config#config.mam}]. --spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary()) - -> {pos_integer(), _}. + +-spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary()) -> + {pos_integer(), _}. set_room_option(_Acc, {mam, Val}, _Lang) -> {#config.mam, Val}; set_room_option(Acc, _Property, _Lang) -> Acc. + -spec sm_receive_packet(stanza()) -> stanza(). sm_receive_packet(#message{to = #jid{lserver = LServer}} = Pkt) -> init_stanza_id(Pkt, LServer); sm_receive_packet(Acc) -> Acc. + -spec user_receive_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. user_receive_packet({#message{from = Peer} = Pkt, #{jid := JID} = C2SState}) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, Pkt1 = case should_archive(Pkt, LServer) of - true -> - case store_msg(Pkt, LUser, LServer, Peer, recv) of - ok -> - mark_stored_msg(Pkt, JID); - _ -> - Pkt - end; - _ -> - Pkt - end, + true -> + case store_msg(Pkt, LUser, LServer, Peer, recv) of + ok -> + mark_stored_msg(Pkt, JID); + _ -> + Pkt + end; + _ -> + Pkt + end, {Pkt1, C2SState}; user_receive_packet(Acc) -> Acc. --spec user_send_packet({stanza(), c2s_state()}) - -> {stanza(), c2s_state()}. + +-spec user_send_packet({stanza(), c2s_state()}) -> + {stanza(), c2s_state()}. user_send_packet({#message{to = Peer} = Pkt, #{jid := JID} = C2SState}) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, Pkt1 = init_stanza_id(Pkt, LServer), Pkt2 = case should_archive(Pkt1, LServer) of - true -> - case store_msg(xmpp:set_from_to(Pkt1, JID, Peer), - LUser, LServer, Peer, send) of - ok -> - mark_stored_msg(Pkt1, JID); - _ -> - Pkt1 - end; - false -> - Pkt1 - end, + true -> + case store_msg(xmpp:set_from_to(Pkt1, JID, Peer), + LUser, + LServer, + Peer, + send) of + ok -> + mark_stored_msg(Pkt1, JID); + _ -> + Pkt1 + end; + false -> + Pkt1 + end, {Pkt2, C2SState}; user_send_packet(Acc) -> Acc. --spec user_send_packet_strip_tag({stanza(), c2s_state()}) - -> {stanza(), c2s_state()}. + +-spec user_send_packet_strip_tag({stanza(), c2s_state()}) -> + {stanza(), c2s_state()}. user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) -> LServer = JID#jid.lserver, Pkt1 = xmpp:del_meta(Pkt, stanza_id), @@ -471,53 +702,60 @@ user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) -> user_send_packet_strip_tag(Acc) -> Acc. + -spec offline_message({any(), message()}) -> {any(), message()}. offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) -> LUser = To#jid.luser, LServer = To#jid.lserver, case should_archive(Pkt, LServer) of - true -> - case store_msg(Pkt, LUser, LServer, Peer, recv) of - ok -> - {archived, mark_stored_msg(Pkt, To)}; - _ -> - Acc - end; - false -> - Acc + true -> + case store_msg(Pkt, LUser, LServer, Peer, recv) of + ok -> + {archived, mark_stored_msg(Pkt, To)}; + _ -> + Acc + end; + false -> + Acc end. --spec muc_filter_message(message(), mod_muc_room:state(), - binary()) -> message(). + +-spec muc_filter_message(message(), + mod_muc_room:state(), + binary()) -> message(). muc_filter_message(#message{meta = #{mam_ignore := true}} = Pkt, _MUCState, _FromNick) -> Pkt; muc_filter_message(#message{from = From} = Pkt, - #state{config = Config, jid = RoomJID} = MUCState, - FromNick) -> + #state{config = Config, jid = RoomJID} = MUCState, + FromNick) -> LServer = RoomJID#jid.lserver, Pkt1 = init_stanza_id(Pkt, LServer), - if Config#config.mam -> - StorePkt = strip_x_jid_tags(Pkt1), - case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of - ok -> - mark_stored_msg(Pkt1, RoomJID); - _ -> - Pkt1 - end; - true -> - Pkt1 + if + Config#config.mam -> + StorePkt = strip_x_jid_tags(Pkt1), + case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of + ok -> + mark_stored_msg(Pkt1, RoomJID); + _ -> + Pkt1 + end; + true -> + Pkt1 end; muc_filter_message(Acc, _MUCState, _FromNick) -> Acc. + -spec make_id() -> integer(). make_id() -> erlang:system_time(microsecond). + -spec get_stanza_id(stanza()) -> integer(). get_stanza_id(#message{meta = #{stanza_id := ID}}) -> ID. + -spec init_stanza_id(stanza(), binary()) -> stanza(). init_stanza_id(#message{meta = #{stanza_id := _ID}} = Pkt, _LServer) -> Pkt; @@ -528,14 +766,16 @@ init_stanza_id(Pkt, LServer) -> Pkt1 = strip_my_stanza_id(Pkt, LServer), xmpp:put_meta(Pkt1, stanza_id, ID). + -spec set_stanza_id(stanza(), jid(), binary()) -> stanza(). set_stanza_id(Pkt, JID, ID) -> BareJID = jid:remove_resource(JID), Archived = #mam_archived{by = BareJID, id = ID}, StanzaID = #stanza_id{by = BareJID, id = ID}, - NewEls = [Archived, StanzaID|xmpp:get_els(Pkt)], + NewEls = [Archived, StanzaID | xmpp:get_els(Pkt)], xmpp:set_els(Pkt, NewEls). + -spec get_origin_id(stanza()) -> binary(). get_origin_id(#message{type = groupchat} = Pkt) -> integer_to_binary(get_stanza_id(Pkt)); @@ -547,91 +787,123 @@ get_origin_id(#message{} = Pkt) -> xmpp:get_id(Pkt) end. + -spec mark_stored_msg(message(), jid()) -> message(). mark_stored_msg(#message{meta = #{stanza_id := ID}} = Pkt, JID) -> Pkt1 = set_stanza_id(Pkt, JID, integer_to_binary(ID)), xmpp:put_meta(Pkt1, mam_archived, true). + % Query archive v0.2 -process_iq_v0_2(#iq{from = #jid{lserver = LServer}, - to = #jid{lserver = LServer}, - type = get, sub_els = [#mam_query{}]} = IQ) -> +process_iq_v0_2(#iq{ + from = #jid{lserver = LServer}, + to = #jid{lserver = LServer}, + type = get, + sub_els = [#mam_query{}] + } = IQ) -> process_iq(LServer, IQ, chat); process_iq_v0_2(IQ) -> process_iq(IQ). + % Query archive v0.3 -process_iq_v0_3(#iq{from = #jid{lserver = LServer}, - to = #jid{lserver = LServer}, - type = set, sub_els = [#mam_query{}]} = IQ) -> +process_iq_v0_3(#iq{ + from = #jid{lserver = LServer}, + to = #jid{lserver = LServer}, + type = set, + sub_els = [#mam_query{}] + } = IQ) -> process_iq(LServer, IQ, chat); -process_iq_v0_3(#iq{from = #jid{lserver = LServer}, - to = #jid{lserver = LServer}, - type = get, sub_els = [#mam_query{}]} = IQ) -> +process_iq_v0_3(#iq{ + from = #jid{lserver = LServer}, + to = #jid{lserver = LServer}, + type = get, + sub_els = [#mam_query{}] + } = IQ) -> process_iq(LServer, IQ); process_iq_v0_3(IQ) -> process_iq(IQ). + -spec muc_process_iq(ignore | iq(), mod_muc_room:state()) -> ignore | iq(). -muc_process_iq(#iq{type = T, lang = Lang, - from = From, - sub_els = [#mam_query{xmlns = NS}]} = IQ, - MUCState) +muc_process_iq(#iq{ + type = T, + lang = Lang, + from = From, + sub_els = [#mam_query{xmlns = NS}] + } = IQ, + MUCState) when (T == set andalso (NS /= ?NS_MAM_TMP)) orelse (T == get andalso NS == ?NS_MAM_TMP) -> case may_enter_room(From, MUCState) of - true -> - LServer = MUCState#state.server_host, - Role = mod_muc_room:get_role(From, MUCState), - process_iq(LServer, IQ, {groupchat, Role, MUCState}); - false -> - Text = ?T("Only members may query archives of this room"), - xmpp:make_error(IQ, xmpp:err_forbidden(Text, Lang)) + true -> + LServer = MUCState#state.server_host, + Role = mod_muc_room:get_role(From, MUCState), + process_iq(LServer, IQ, {groupchat, Role, MUCState}); + false -> + Text = ?T("Only members may query archives of this room"), + xmpp:make_error(IQ, xmpp:err_forbidden(Text, Lang)) end; -muc_process_iq(#iq{type = get, - sub_els = [#mam_query{xmlns = NS}]} = IQ, - MUCState) when NS /= ?NS_MAM_TMP -> +muc_process_iq(#iq{ + type = get, + sub_els = [#mam_query{xmlns = NS}] + } = IQ, + MUCState) when NS /= ?NS_MAM_TMP -> LServer = MUCState#state.server_host, process_iq(LServer, IQ); muc_process_iq(IQ, _MUCState) -> IQ. -parse_query(#mam_query{xmlns = ?NS_MAM_TMP, - start = Start, 'end' = End, - with = With, withtext = Text}, _Lang) -> - {ok, [{start, Start}, {'end', End}, - {with, With}, {withtext, Text}]}; + +parse_query(#mam_query{ + xmlns = ?NS_MAM_TMP, + start = Start, + 'end' = End, + with = With, + withtext = Text + }, + _Lang) -> + {ok, [{start, Start}, + {'end', End}, + {with, With}, + {withtext, Text}]}; parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) -> X = xmpp_util:set_xdata_field( - #xdata_field{var = <<"FORM_TYPE">>, - type = hidden, values = [?NS_MAM_1]}, - Query#mam_query.xdata), + #xdata_field{ + var = <<"FORM_TYPE">>, + type = hidden, + values = [?NS_MAM_1] + }, + Query#mam_query.xdata), {Fields, WithText} = case lists:keytake(<<"{urn:xmpp:fulltext:0}fulltext">>, #xdata_field.var, X#xdata.fields) of - false -> {X#xdata.fields, <<>>}; - {value, #xdata_field{values = [V]}, F} -> {F, V}; - {value, _, F} -> {F, <<>>} - end, - try mam_query:decode(Fields) of - Form -> - if WithText /= <<>> -> - {ok, lists:keystore(withtext, 1, Form, {withtext, WithText})}; - true -> - {ok, Form} - end - catch _:{mam_query, Why} -> - Txt = mam_query:format_error(Why), - {error, xmpp:err_bad_request(Txt, Lang)} + false -> {X#xdata.fields, <<>>}; + {value, #xdata_field{values = [V]}, F} -> {F, V}; + {value, _, F} -> {F, <<>>} + end, + try mam_query:decode(Fields) of + Form -> + if + WithText /= <<>> -> + {ok, lists:keystore(withtext, 1, Form, {withtext, WithText})}; + true -> + {ok, Form} + end + catch + _:{mam_query, Why} -> + Txt = mam_query:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} end; parse_query(#mam_query{}, _Lang) -> {ok, []}. + disco_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; disco_local_features(Acc, _From, _To, <<"">>, _Lang) -> Features = case Acc of - {result, Fs} -> Fs; - empty -> [] - end, + {result, Fs} -> Fs; + empty -> [] + end, {result, [?NS_MESSAGE_RETRACT | Features]}; disco_local_features(empty, _From, _To, _Node, Lang) -> Txt = ?T("No features available"), @@ -639,77 +911,88 @@ disco_local_features(empty, _From, _To, _Node, Lang) -> disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. + disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures}, - #jid{luser = U, lserver = S}, - #jid{luser = U, lserver = S}, <<"">>, _Lang) -> + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, + <<"">>, + _Lang) -> {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0, - ?NS_MESSAGE_RETRACT | - OtherFeatures]}; + ?NS_MESSAGE_RETRACT | OtherFeatures]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec message_is_archived(boolean(), c2s_state(), message()) -> boolean(). message_is_archived(true, _C2SState, _Pkt) -> true; message_is_archived(false, #{lserver := LServer}, Pkt) -> case mod_mam_opt:assume_mam_usage(LServer) of - true -> - is_archived(Pkt, LServer); - false -> - false + true -> + is_archived(Pkt, LServer); + false -> + false end. + %%% %%% Commands %%% %% @format-begin + get_mam_count(User, Host) -> Jid = jid:make(User, Host), {_, _, Count} = select(Host, Jid, Jid, [], #rsm_set{}, chat, only_count), Count. + get_mam_messages(User, Host) -> Jid = jid:make(User, Host), {Messages, _, _} = select(Host, Jid, Jid, [], #rsm_set{}, chat, only_messages), format_user_messages(Messages). + format_user_messages(Messages) -> lists:map(fun({_ID, _IDInt, Fwd}) -> - El = hd(Fwd#forwarded.sub_els), - FPacket = - ejabberd_web_admin:pretty_print_xml( - xmpp:encode(El)), - SFrom = jid:encode(El#message.from), - STo = jid:encode(El#message.to), - Time = format_time(Fwd#forwarded.delay#delay.stamp), - {Time, SFrom, STo, FPacket} + El = hd(Fwd#forwarded.sub_els), + FPacket = + ejabberd_web_admin:pretty_print_xml( + xmpp:encode(El)), + SFrom = jid:encode(El#message.from), + STo = jid:encode(El#message.to), + Time = format_time(Fwd#forwarded.delay#delay.stamp), + {Time, SFrom, STo, FPacket} end, Messages). + format_time(Now) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now), str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second]). + webadmin_user(Acc, User, Server, R) -> - Acc - ++ [make_command(get_mam_count, - R, - [{<<"user">>, User}, {<<"host">>, Server}], - [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/mam/">>}]}])]. + Acc ++ + [make_command(get_mam_count, + R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/mam/">>}]}])]. %% @format-end + %%% %%% Commands: Purge %%% + delete_old_messages_batch(Server, Type, Days, BatchSize, Rate) when Type == <<"chat">>; - Type == <<"groupchat">>; - Type == <<"all">> -> + Type == <<"groupchat">>; + Type == <<"all">> -> CurrentTime = make_id(), Diff = Days * 24 * 60 * 60 * 1000000, TimeStamp = misc:usec_to_now(CurrentTime - Diff), @@ -717,91 +1000,102 @@ delete_old_messages_batch(Server, Type, Days, BatchSize, Rate) when Type == <<"c LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), - case ejabberd_batch:register_task({mam, LServer}, 0, Rate, {LServer, TypeA, TimeStamp, BatchSize, none}, - fun({L, T, St, B, IS} = S) -> - case {erlang:function_exported(Mod, delete_old_messages_batch, 4), - erlang:function_exported(Mod, delete_old_messages_batch, 5)} of - {true, _} -> - case Mod:delete_old_messages_batch(L, St, T, B) of - {ok, Count} -> - {ok, S, Count}; - {error, _} = E -> - E - end; - {_, true} -> - case Mod:delete_old_messages_batch(L, St, T, B, IS) of - {ok, IS2, Count} -> - {ok, {L, St, T, B, IS2}, Count}; - {error, _} = E -> - E - end; - _ -> - {error, not_implemented_for_backend} - end - end) of - ok -> - {ok, ""}; - {error, in_progress} -> - {error, "Operation in progress"} + case ejabberd_batch:register_task({mam, LServer}, + 0, + Rate, + {LServer, TypeA, TimeStamp, BatchSize, none}, + fun({L, T, St, B, IS} = S) -> + case {erlang:function_exported(Mod, delete_old_messages_batch, 4), + erlang:function_exported(Mod, delete_old_messages_batch, 5)} of + {true, _} -> + case Mod:delete_old_messages_batch(L, St, T, B) of + {ok, Count} -> + {ok, S, Count}; + {error, _} = E -> + E + end; + {_, true} -> + case Mod:delete_old_messages_batch(L, St, T, B, IS) of + {ok, IS2, Count} -> + {ok, {L, St, T, B, IS2}, Count}; + {error, _} = E -> + E + end; + _ -> + {error, not_implemented_for_backend} + end + end) of + ok -> + {ok, ""}; + {error, in_progress} -> + {error, "Operation in progress"} end. + + delete_old_messages_status(Server) -> LServer = jid:nameprep(Server), Msg = case ejabberd_batch:task_status({mam, LServer}) of - not_started -> - "Operation not started"; - {failed, Steps, Error} -> - io_lib:format("Operation failed after deleting ~p messages with error ~p", - [Steps, misc:format_val(Error)]); - {aborted, Steps} -> - io_lib:format("Operation was aborted after deleting ~p messages", - [Steps]); - {working, Steps} -> - io_lib:format("Operation in progress, deleted ~p messages", - [Steps]); - {completed, Steps} -> - io_lib:format("Operation was completed after deleting ~p messages", - [Steps]) - end, + not_started -> + "Operation not started"; + {failed, Steps, Error} -> + io_lib:format("Operation failed after deleting ~p messages with error ~p", + [Steps, misc:format_val(Error)]); + {aborted, Steps} -> + io_lib:format("Operation was aborted after deleting ~p messages", + [Steps]); + {working, Steps} -> + io_lib:format("Operation in progress, deleted ~p messages", + [Steps]); + {completed, Steps} -> + io_lib:format("Operation was completed after deleting ~p messages", + [Steps]) + end, lists:flatten(Msg). + delete_old_messages_abort(Server) -> LServer = jid:nameprep(Server), case ejabberd_batch:abort_task({mam, LServer}) of - aborted -> "Operation aborted"; - not_started -> "No task running" + aborted -> "Operation aborted"; + not_started -> "No task running" end. + delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; - TypeBin == <<"groupchat">>; - TypeBin == <<"all">> -> + TypeBin == <<"groupchat">>; + TypeBin == <<"all">> -> CurrentTime = make_id(), Diff = Days * 24 * 60 * 60 * 1000000, TimeStamp = misc:usec_to_now(CurrentTime - Diff), Type = misc:binary_to_atom(TypeBin), DBTypes = lists:usort( - lists:map( - fun(Host) -> - case mod_mam_opt:db_type(Host) of - sql -> {sql, Host}; - Other -> {Other, global} - end - end, ejabberd_option:hosts())), + lists:map( + fun(Host) -> + case mod_mam_opt:db_type(Host) of + sql -> {sql, Host}; + Other -> {Other, global} + end + end, + ejabberd_option:hosts())), Results = lists:map( - fun({DBType, ServerHost}) -> - Mod = gen_mod:db_mod(DBType, ?MODULE), - Mod:delete_old_messages(ServerHost, TimeStamp, Type) - end, DBTypes), + fun({DBType, ServerHost}) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:delete_old_messages(ServerHost, TimeStamp, Type) + end, + DBTypes), case lists:filter(fun(Res) -> Res /= ok end, Results) of - [] -> ok; - [NotOk|_] -> NotOk + [] -> ok; + [NotOk | _] -> NotOk end; delete_old_messages(_TypeBin, _Days) -> unsupported_type. + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + -spec is_empty_for_user(binary(), binary()) -> boolean(). is_empty_for_user(User, Server) -> LUser = jid:nodeprep(User), @@ -809,6 +1103,7 @@ is_empty_for_user(User, Server) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:is_empty_for_user(LUser, LServer). + -spec is_empty_for_room(binary(), binary(), binary()) -> boolean(). is_empty_for_room(LServer, Name, Host) -> LName = jid:nodeprep(Name), @@ -816,101 +1111,124 @@ is_empty_for_room(LServer, Name, Host) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:is_empty_for_room(LServer, LName, LHost). + -spec check_create_room(boolean(), binary(), binary(), binary()) -> boolean(). check_create_room(Acc, ServerHost, RoomID, Host) -> Acc and is_empty_for_room(ServerHost, RoomID, Host). + %%%=================================================================== %%% Internal functions %%%=================================================================== + process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) -> Mod = gen_mod:db_mod(LServer, ?MODULE), CommonFields = [{with, undefined}, - {start, undefined}, - {'end', undefined}], + {start, undefined}, + {'end', undefined}], ExtendedFields = Mod:extended_fields(LServer), Fields = mam_query:encode(CommonFields ++ ExtendedFields), X = xmpp_util:set_xdata_field( - #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]}, - #xdata{type = form, fields = Fields}), + #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]}, + #xdata{type = form, fields = Fields}), xmpp:make_iq_result(IQ, #mam_query{xmlns = NS, xdata = X}). + % Preference setting (both v0.2 & v0.3) -process_iq(#iq{type = set, lang = Lang, - sub_els = [#mam_prefs{default = undefined, xmlns = NS}]} = IQ) -> +process_iq(#iq{ + type = set, + lang = Lang, + sub_els = [#mam_prefs{default = undefined, xmlns = NS}] + } = IQ) -> Why = {missing_attr, <<"default">>, <<"prefs">>, NS}, ErrTxt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(ErrTxt, Lang)); -process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, - to = #jid{lserver = LServer}, - type = set, lang = Lang, - sub_els = [#mam_prefs{xmlns = NS, - default = Default, - always = Always0, - never = Never0}]} = IQ) -> +process_iq(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + to = #jid{lserver = LServer}, + type = set, + lang = Lang, + sub_els = [#mam_prefs{ + xmlns = NS, + default = Default, + always = Always0, + never = Never0 + }] + } = IQ) -> Access = mod_mam_opt:access_preferences(LServer), case acl:match_rule(LServer, Access, jid:make(LUser, LServer)) of - allow -> - Always = lists:usort(get_jids(Always0)), - Never = lists:usort(get_jids(Never0)), - case write_prefs(LUser, LServer, LServer, Default, Always, Never) of - ok -> - NewPrefs = prefs_el(Default, Always, Never, NS), - xmpp:make_iq_result(IQ, NewPrefs); - _Err -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) - end; - deny -> - Txt = ?T("MAM preference modification denied by service policy"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + allow -> + Always = lists:usort(get_jids(Always0)), + Never = lists:usort(get_jids(Never0)), + case write_prefs(LUser, LServer, LServer, Default, Always, Never) of + ok -> + NewPrefs = prefs_el(Default, Always, Never, NS), + xmpp:make_iq_result(IQ, NewPrefs); + _Err -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; + deny -> + Txt = ?T("MAM preference modification denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; -process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, - to = #jid{lserver = LServer}, lang = Lang, - type = get, sub_els = [#mam_prefs{xmlns = NS}]} = IQ) -> +process_iq(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + to = #jid{lserver = LServer}, + lang = Lang, + type = get, + sub_els = [#mam_prefs{xmlns = NS}] + } = IQ) -> case get_prefs(LUser, LServer) of - {ok, Prefs} -> - PrefsEl = prefs_el(Prefs#archive_prefs.default, - Prefs#archive_prefs.always, - Prefs#archive_prefs.never, - NS), - xmpp:make_iq_result(IQ, PrefsEl); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + {ok, Prefs} -> + PrefsEl = prefs_el(Prefs#archive_prefs.default, + Prefs#archive_prefs.always, + Prefs#archive_prefs.never, + NS), + xmpp:make_iq_result(IQ, PrefsEl); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; process_iq(IQ) -> xmpp:make_error(IQ, xmpp:err_not_allowed()). -process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang, - sub_els = [SubEl]} = IQ, MsgType) -> + +process_iq(LServer, + #iq{ + from = #jid{luser = LUser}, + lang = Lang, + sub_els = [SubEl] + } = IQ, + MsgType) -> Ret = case MsgType of - chat -> - maybe_activate_mam(LUser, LServer); - _ -> - ok - end, + chat -> + maybe_activate_mam(LUser, LServer); + _ -> + ok + end, case Ret of - ok -> - case SubEl of - #mam_query{rsm = #rsm_set{index = I}} when is_integer(I) -> - Txt = ?T("Unsupported element"), - xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); - #mam_query{rsm = RSM, flippage = FlipPage, xmlns = NS} -> - case parse_query(SubEl, Lang) of - {ok, Query} -> - NewRSM = limit_max(RSM, NS), - select_and_send(LServer, Query, NewRSM, FlipPage, IQ, MsgType); - {error, Err} -> - xmpp:make_error(IQ, Err) - end - end; - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + ok -> + case SubEl of + #mam_query{rsm = #rsm_set{index = I}} when is_integer(I) -> + Txt = ?T("Unsupported element"), + xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); + #mam_query{rsm = RSM, flippage = FlipPage, xmlns = NS} -> + case parse_query(SubEl, Lang) of + {ok, Query} -> + NewRSM = limit_max(RSM, NS), + select_and_send(LServer, Query, NewRSM, FlipPage, IQ, MsgType); + {error, Err} -> + xmpp:make_error(IQ, Err) + end + end; + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. + -spec should_archive(message(), binary()) -> boolean(). should_archive(#message{type = error}, _LServer) -> false; @@ -918,164 +1236,191 @@ should_archive(#message{type = groupchat}, _LServer) -> false; should_archive(#message{meta = #{from_offline := true}}, _LServer) -> false; -should_archive(#message{body = Body, subject = Subject, - type = Type} = Pkt, LServer) -> +should_archive(#message{ + body = Body, + subject = Subject, + type = Type + } = Pkt, + LServer) -> case is_archived(Pkt, LServer) of - true -> - false; - false -> - case check_store_hint(Pkt) of - store -> - true; - no_store -> - false; - none when Type == headline -> - false; - none -> - case xmpp:get_text(Body) /= <<>> orelse - xmpp:get_text(Subject) /= <<>> of - true -> - true; - _ -> - case misc:unwrap_mucsub_message(Pkt) of - #message{type = groupchat} = Msg -> - should_archive(Msg#message{type = chat}, LServer); - #message{} = Msg -> - should_archive(Msg, LServer); - _ -> - misc:is_mucsub_message(Pkt) - end - end - end + true -> + false; + false -> + case check_store_hint(Pkt) of + store -> + true; + no_store -> + false; + none when Type == headline -> + false; + none -> + case xmpp:get_text(Body) /= <<>> orelse + xmpp:get_text(Subject) /= <<>> of + true -> + true; + _ -> + case misc:unwrap_mucsub_message(Pkt) of + #message{type = groupchat} = Msg -> + should_archive(Msg#message{type = chat}, LServer); + #message{} = Msg -> + should_archive(Msg, LServer); + _ -> + misc:is_mucsub_message(Pkt) + end + end + end end; should_archive(_, _LServer) -> false. + -spec strip_my_stanza_id(stanza(), binary()) -> stanza(). strip_my_stanza_id(Pkt, LServer) -> Els = xmpp:get_els(Pkt), NewEls = lists:filter( - fun(El) -> - Name = xmpp:get_name(El), - NS = xmpp:get_ns(El), - if (Name == <<"archived">> andalso NS == ?NS_MAM_TMP); - (Name == <<"stanza-id">> andalso NS == ?NS_SID_0) -> - try xmpp:decode(El) of - #mam_archived{by = By} -> - By#jid.lserver /= LServer; - #stanza_id{by = By} -> - By#jid.lserver /= LServer - catch _:{xmpp_codec, _} -> - false - end; - true -> - true - end - end, Els), + fun(El) -> + Name = xmpp:get_name(El), + NS = xmpp:get_ns(El), + if + (Name == <<"archived">> andalso NS == ?NS_MAM_TMP); + (Name == <<"stanza-id">> andalso NS == ?NS_SID_0) -> + try xmpp:decode(El) of + #mam_archived{by = By} -> + By#jid.lserver /= LServer; + #stanza_id{by = By} -> + By#jid.lserver /= LServer + catch + _:{xmpp_codec, _} -> + false + end; + true -> + true + end + end, + Els), xmpp:set_els(Pkt, NewEls). + -spec strip_x_jid_tags(stanza()) -> stanza(). strip_x_jid_tags(Pkt) -> Els = xmpp:get_els(Pkt), NewEls = lists:filter( - fun(El) -> - case xmpp:get_name(El) of - <<"x">> -> - NS = xmpp:get_ns(El), - Items = if NS == ?NS_MUC_USER; - NS == ?NS_MUC_ADMIN; - NS == ?NS_MUC_OWNER -> - try xmpp:decode(El) of - #muc_user{items = Is} -> Is; - #muc_admin{items = Is} -> Is; - #muc_owner{items = Is} -> Is - catch _:{xmpp_codec, _} -> - [] - end; - true -> - [] - end, - not lists:any( - fun(#muc_item{jid = JID}) -> - JID /= undefined - end, Items); - _ -> - true - end - end, Els), + fun(El) -> + case xmpp:get_name(El) of + <<"x">> -> + NS = xmpp:get_ns(El), + Items = if + NS == ?NS_MUC_USER; + NS == ?NS_MUC_ADMIN; + NS == ?NS_MUC_OWNER -> + try xmpp:decode(El) of + #muc_user{items = Is} -> Is; + #muc_admin{items = Is} -> Is; + #muc_owner{items = Is} -> Is + catch + _:{xmpp_codec, _} -> + [] + end; + true -> + [] + end, + not lists:any( + fun(#muc_item{jid = JID}) -> + JID /= undefined + end, + Items); + _ -> + true + end + end, + Els), xmpp:set_els(Pkt, NewEls). --spec should_archive_peer(binary(), binary(), - #archive_prefs{}, jid()) -> boolean(). -should_archive_peer(LUser, LServer, - #archive_prefs{default = Default, - always = Always, - never = Never}, - Peer) -> + +-spec should_archive_peer(binary(), + binary(), + #archive_prefs{}, + jid()) -> boolean(). +should_archive_peer(LUser, + LServer, + #archive_prefs{ + default = Default, + always = Always, + never = Never + }, + Peer) -> LPeer = jid:remove_resource(jid:tolower(Peer)), case lists:member(LPeer, Always) of - true -> - true; - false -> - case lists:member(LPeer, Never) of - true -> - false; - false -> - case Default of - always -> true; - never -> false; - roster -> - {Sub, _, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, - LServer, {none, none, []}, - [LUser, LServer, Peer]), - Sub == both orelse Sub == from orelse Sub == to - end - end + true -> + true; + false -> + case lists:member(LPeer, Never) of + true -> + false; + false -> + case Default of + always -> true; + never -> false; + roster -> + {Sub, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, + LServer, + {none, none, []}, + [LUser, LServer, Peer]), + Sub == both orelse Sub == from orelse Sub == to + end + end end. + -spec should_archive_muc(message()) -> boolean(). -should_archive_muc(#message{type = groupchat, - body = Body, subject = Subj} = Pkt) -> +should_archive_muc(#message{ + type = groupchat, + body = Body, + subject = Subj + } = Pkt) -> case check_store_hint(Pkt) of - store -> - true; - no_store -> - false; - none -> - case xmpp:get_text(Body) of - <<"">> -> - case xmpp:get_text(Subj) of - <<"">> -> - false; - _ -> - true - end; - _ -> - true - end + store -> + true; + no_store -> + false; + none -> + case xmpp:get_text(Body) of + <<"">> -> + case xmpp:get_text(Subj) of + <<"">> -> + false; + _ -> + true + end; + _ -> + true + end end; should_archive_muc(_) -> false. + -spec check_store_hint(message()) -> store | no_store | none. check_store_hint(Pkt) -> case has_store_hint(Pkt) of - true -> - store; - false -> - case has_no_store_hint(Pkt) of - true -> - no_store; - false -> - none - end + true -> + store; + false -> + case has_no_store_hint(Pkt) of + true -> + no_store; + false -> + none + end end. + -spec has_store_hint(message()) -> boolean(). has_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'store'}). + -spec has_no_store_hint(message()) -> boolean(). has_no_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'no-store'}) orelse @@ -1083,63 +1428,72 @@ has_no_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'no-permanent-store'}) orelse xmpp:has_subtag(Message, #hint{type = 'no-permanent-storage'}). + -spec is_archived(message(), binary()) -> boolean(). is_archived(Pkt, LServer) -> case xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}) of - #stanza_id{by = #jid{lserver = LServer}} -> - true; - _ -> - false + #stanza_id{by = #jid{lserver = LServer}} -> + true; + _ -> + false end. + -spec may_enter_room(jid(), mod_muc_room:state()) -> boolean(). may_enter_room(From, - #state{config = #config{members_only = false}} = MUCState) -> + #state{config = #config{members_only = false}} = MUCState) -> mod_muc_room:get_affiliation(From, MUCState) /= outcast; may_enter_room(From, MUCState) -> mod_muc_room:is_occupant_or_admin(From, MUCState). --spec store_msg(message(), binary(), binary(), jid(), send | recv) - -> ok | pass | any(). + +-spec store_msg(message(), binary(), binary(), jid(), send | recv) -> + ok | pass | any(). store_msg(Pkt, LUser, LServer, Peer, Dir) -> case get_prefs(LUser, LServer) of - {ok, Prefs} -> - UseMucArchive = mod_mam_opt:user_mucsub_from_muc_archive(LServer), - StoredInMucMam = UseMucArchive andalso xmpp:get_meta(Pkt, in_muc_mam, false), - case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt, StoredInMucMam} of - {true, #message{meta = #{sm_copy := true}}, _} -> - ok; % Already stored. - {true, _, true} -> - ok; % Stored in muc archive. - {true, _, _} -> - case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, - [LUser, LServer, Peer, <<"">>, chat, Dir]) of - #message{} -> ok; - _ -> pass - end; - {false, _, _} -> - pass - end; - {error, _} -> - pass + {ok, Prefs} -> + UseMucArchive = mod_mam_opt:user_mucsub_from_muc_archive(LServer), + StoredInMucMam = UseMucArchive andalso xmpp:get_meta(Pkt, in_muc_mam, false), + case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt, StoredInMucMam} of + {true, #message{meta = #{sm_copy := true}}, _} -> + ok; % Already stored. + {true, _, true} -> + ok; % Stored in muc archive. + {true, _, _} -> + case ejabberd_hooks:run_fold(store_mam_message, + LServer, + Pkt, + [LUser, LServer, Peer, <<"">>, chat, Dir]) of + #message{} -> ok; + _ -> pass + end; + {false, _, _} -> + pass + end; + {error, _} -> + pass end. --spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary()) - -> ok | pass | any(). + +-spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary()) -> + ok | pass | any(). store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> case should_archive_muc(Pkt) of - true -> - {U, S, _} = jid:tolower(RoomJID), - LServer = MUCState#state.server_host, - case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, - [U, S, Peer, Nick, groupchat, recv]) of - #message{} -> ok; - _ -> pass - end; - false -> - pass + true -> + {U, S, _} = jid:tolower(RoomJID), + LServer = MUCState#state.server_host, + case ejabberd_hooks:run_fold(store_mam_message, + LServer, + Pkt, + [U, S, Peer, Nick, groupchat, recv]) of + #message{} -> ok; + _ -> pass + end; + false -> + pass end. + store_mam_message(Pkt, U, S, Peer, Nick, Type, Dir) -> LServer = ejabberd_router:host_of_route(S), US = {U, S}, @@ -1156,384 +1510,470 @@ store_mam_message(Pkt, U, S, Peer, Nick, Type, Dir) -> Mod:store(El, LServer, US, Type, Peer, Nick, Dir, ID, OriginID, Retract), Pkt. + write_prefs(LUser, LServer, Host, Default, Always, Never) -> - Prefs = #archive_prefs{us = {LUser, LServer}, - default = Default, - always = Always, - never = Never}, + Prefs = #archive_prefs{ + us = {LUser, LServer}, + default = Default, + always = Always, + never = Never + }, Mod = gen_mod:db_mod(Host, ?MODULE), case Mod:write_prefs(LUser, LServer, Prefs, Host) of - ok -> - case use_cache(Mod, LServer) of - true -> - ets_cache:delete(archive_prefs_cache, {LUser, LServer}, - cache_nodes(Mod, LServer)); - false -> - ok - end; - _Err -> - {error, db_failure} + ok -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(archive_prefs_cache, + {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end; + _Err -> + {error, db_failure} end. + get_prefs(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of - true -> - ets_cache:lookup(archive_prefs_cache, {LUser, LServer}, - fun() -> Mod:get_prefs(LUser, LServer) end); - false -> - Mod:get_prefs(LUser, LServer) - end, + true -> + ets_cache:lookup(archive_prefs_cache, + {LUser, LServer}, + fun() -> Mod:get_prefs(LUser, LServer) end); + false -> + Mod:get_prefs(LUser, LServer) + end, case Res of - {ok, Prefs} -> - {ok, Prefs}; - {error, _} -> - {error, db_failure}; - error -> - ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), - case ActivateOpt of - true -> - {ok, #archive_prefs{us = {LUser, LServer}, default = never}}; - false -> - Default = mod_mam_opt:default(LServer), - {ok, #archive_prefs{us = {LUser, LServer}, default = Default}} - end + {ok, Prefs} -> + {ok, Prefs}; + {error, _} -> + {error, db_failure}; + error -> + ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), + case ActivateOpt of + true -> + {ok, #archive_prefs{us = {LUser, LServer}, default = never}}; + false -> + Default = mod_mam_opt:default(LServer), + {ok, #archive_prefs{us = {LUser, LServer}, default = Default}} + end end. + prefs_el(Default, Always, Never, NS) -> - #mam_prefs{default = Default, - always = [jid:make(LJ) || LJ <- Always], - never = [jid:make(LJ) || LJ <- Never], - xmlns = NS}. + #mam_prefs{ + default = Default, + always = [ jid:make(LJ) || LJ <- Always ], + never = [ jid:make(LJ) || LJ <- Never ], + xmlns = NS + }. + maybe_activate_mam(LUser, LServer) -> ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), case ActivateOpt of - true -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - Res = case use_cache(Mod, LServer) of - true -> - ets_cache:lookup(archive_prefs_cache, - {LUser, LServer}, - fun() -> - Mod:get_prefs(LUser, LServer) - end); - false -> - Mod:get_prefs(LUser, LServer) - end, - case Res of - {ok, _Prefs} -> - ok; - {error, _} -> - {error, db_failure}; - error -> - Default = mod_mam_opt:default(LServer), - write_prefs(LUser, LServer, LServer, Default, [], []) - end; - false -> - ok + true -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup(archive_prefs_cache, + {LUser, LServer}, + fun() -> + Mod:get_prefs(LUser, LServer) + end); + false -> + Mod:get_prefs(LUser, LServer) + end, + case Res of + {ok, _Prefs} -> + ok; + {error, _} -> + {error, db_failure}; + error -> + Default = mod_mam_opt:default(LServer), + write_prefs(LUser, LServer, LServer, Default, [], []) + end; + false -> + ok end. + select_and_send(LServer, Query, RSM, FlipPage, #iq{from = From, to = To} = IQ, MsgType) -> Ret = case MsgType of - chat -> - select(LServer, From, From, Query, RSM, MsgType); - _ -> - select(LServer, From, To, Query, RSM, MsgType) - end, + chat -> + select(LServer, From, From, Query, RSM, MsgType); + _ -> + select(LServer, From, To, Query, RSM, MsgType) + end, case Ret of - {Msgs, IsComplete, Count} -> - SortedMsgs = lists:keysort(2, Msgs), - SortedMsgs2 = case FlipPage of - true -> lists:reverse(SortedMsgs); - false -> SortedMsgs - end, - send(SortedMsgs2, Count, IsComplete, IQ); - {error, _} -> - Txt = ?T("Database failure"), - Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang), - xmpp:make_error(IQ, Err) + {Msgs, IsComplete, Count} -> + SortedMsgs = lists:keysort(2, Msgs), + SortedMsgs2 = case FlipPage of + true -> lists:reverse(SortedMsgs); + false -> SortedMsgs + end, + send(SortedMsgs2, Count, IsComplete, IQ); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang), + xmpp:make_error(IQ, Err) end. + select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) -> select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, all). -select(_LServer, JidRequestor, JidArchive, Query, RSM, - {groupchat, _Role, #state{config = #config{mam = false}, - history = History}} = MsgType, _Flags) -> + +select(_LServer, + JidRequestor, + JidArchive, + Query, + RSM, + {groupchat, _Role, + #state{ + config = #config{mam = false}, + history = History + }} = MsgType, + _Flags) -> Start = proplists:get_value(start, Query), End = proplists:get_value('end', Query), #lqueue{queue = Q} = History, L = p1_queue:len(Q), Msgs = - lists:flatmap( - fun({Nick, Pkt, _HaveSubject, Now, _Size}) -> - TS = misc:now_to_usec(Now), - case match_interval(Now, Start, End) and - match_rsm(Now, RSM) of - true -> - case msg_to_el(#archive_msg{ - id = integer_to_binary(TS), - type = groupchat, - timestamp = Now, - peer = undefined, - nick = Nick, - packet = Pkt}, - MsgType, JidRequestor, JidArchive) of - {ok, Msg} -> - [{integer_to_binary(TS), TS, Msg}]; - {error, _} -> - [] - end; - false -> - [] - end - end, p1_queue:to_list(Q)), + lists:flatmap( + fun({Nick, Pkt, _HaveSubject, Now, _Size}) -> + TS = misc:now_to_usec(Now), + case match_interval(Now, Start, End) and + match_rsm(Now, RSM) of + true -> + case msg_to_el(#archive_msg{ + id = integer_to_binary(TS), + type = groupchat, + timestamp = Now, + peer = undefined, + nick = Nick, + packet = Pkt + }, + MsgType, + JidRequestor, + JidArchive) of + {ok, Msg} -> + [{integer_to_binary(TS), TS, Msg}]; + {error, _} -> + [] + end; + false -> + [] + end + end, + p1_queue:to_list(Q)), case RSM of - #rsm_set{max = Max, before = Before} when is_binary(Before) -> - {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max), - {NewMsgs, IsComplete, L}; - #rsm_set{max = Max} -> - {NewMsgs, IsComplete} = filter_by_max(Msgs, Max), - {NewMsgs, IsComplete, L}; - _ -> - {Msgs, true, L} + #rsm_set{max = Max, before = Before} when is_binary(Before) -> + {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max), + {NewMsgs, IsComplete, L}; + #rsm_set{max = Max} -> + {NewMsgs, IsComplete} = filter_by_max(Msgs, Max), + {NewMsgs, IsComplete, L}; + _ -> + {Msgs, true, L} end; select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) -> case might_expose_jid(Query, MsgType) of - true -> - {[], true, 0}; - false -> - case {MsgType, mod_mam_opt:user_mucsub_from_muc_archive(LServer)} of - {chat, true} -> - select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); - _ -> - db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) - end + true -> + {[], true, 0}; + false -> + case {MsgType, mod_mam_opt:user_mucsub_from_muc_archive(LServer)} of + {chat, true} -> + select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); + _ -> + db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) + end end. + select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags) -> MucHosts = mod_muc_admin:find_hosts(LServer), Mod = gen_mod:db_mod(LServer, ?MODULE), case proplists:get_value(with, Query) of - #jid{lserver = WithLServer} = MucJid -> - case lists:member(WithLServer, MucHosts) of - true -> - select(LServer, JidRequestor, MucJid, Query, RSM, - {groupchat, member, #state{config = #config{mam = true}}}); - _ -> - db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) - end; - _ -> - case erlang:function_exported(Mod, select_with_mucsub, 6) of - true -> - Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); - false -> - select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) - end + #jid{lserver = WithLServer} = MucJid -> + case lists:member(WithLServer, MucHosts) of + true -> + select(LServer, + JidRequestor, + MucJid, + Query, + RSM, + {groupchat, member, #state{config = #config{mam = true}}}); + _ -> + db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) + end; + _ -> + case erlang:function_exported(Mod, select_with_mucsub, 6) of + true -> + Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); + false -> + select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) + end end. + select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) -> case db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) of - {error, _} = Err -> - Err; - {Entries, All, Count} -> - {Dir, Max} = case RSM of - #rsm_set{max = M, before = V} when is_binary(V) -> - {desc, M}; - #rsm_set{max = M} -> - {asc, M}; - _ -> - {asc, undefined} - end, - SubRooms = case mod_muc_admin:find_hosts(LServer) of - [First|_] -> - case mod_muc:get_subscribed_rooms(First, JidRequestor) of - {ok, L} -> L; - {error, _} -> [] - end; - _ -> - [] - end, - SubRoomJids = [Jid || {Jid, _, _} <- SubRooms], - {E2, A2, C2} = - lists:foldl( - fun(MucJid, {E0, A0, C0}) -> - case select(LServer, JidRequestor, MucJid, Query, RSM, - {groupchat, member, #state{config = #config{mam = true}}}) of - {error, _} -> - {E0, A0, C0}; - {E, A, C} -> - {lists:keymerge(2, E0, wrap_as_mucsub(E, JidRequestor)), - A0 andalso A, C0 + C} - end - end, {Entries, All, Count}, SubRoomJids), - case {Dir, Max} of - {_, undefined} -> - {E2, A2, C2}; - {desc, _} -> - Start = case length(E2) of - Len when Len < Max -> 1; - Len -> Len - Max + 1 - end, - Sub = lists:sublist(E2, Start, Max), - {Sub, if Sub == E2 -> A2; true -> false end, C2}; - _ -> - Sub = lists:sublist(E2, 1, Max), - {Sub, if Sub == E2 -> A2; true -> false end, C2} - end + {error, _} = Err -> + Err; + {Entries, All, Count} -> + {Dir, Max} = case RSM of + #rsm_set{max = M, before = V} when is_binary(V) -> + {desc, M}; + #rsm_set{max = M} -> + {asc, M}; + _ -> + {asc, undefined} + end, + SubRooms = case mod_muc_admin:find_hosts(LServer) of + [First | _] -> + case mod_muc:get_subscribed_rooms(First, JidRequestor) of + {ok, L} -> L; + {error, _} -> [] + end; + _ -> + [] + end, + SubRoomJids = [ Jid || {Jid, _, _} <- SubRooms ], + {E2, A2, C2} = + lists:foldl( + fun(MucJid, {E0, A0, C0}) -> + case select(LServer, + JidRequestor, + MucJid, + Query, + RSM, + {groupchat, member, #state{config = #config{mam = true}}}) of + {error, _} -> + {E0, A0, C0}; + {E, A, C} -> + {lists:keymerge(2, E0, wrap_as_mucsub(E, JidRequestor)), + A0 andalso A, + C0 + C} + end + end, + {Entries, All, Count}, + SubRoomJids), + case {Dir, Max} of + {_, undefined} -> + {E2, A2, C2}; + {desc, _} -> + Start = case length(E2) of + Len when Len < Max -> 1; + Len -> Len - Max + 1 + end, + Sub = lists:sublist(E2, Start, Max), + {Sub, if Sub == E2 -> A2; true -> false end, C2}; + _ -> + Sub = lists:sublist(E2, 1, Max), + {Sub, if Sub == E2 -> A2; true -> false end, C2} + end end. + db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case erlang:function_exported(Mod, select, 7) of - true -> - Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags); - _ -> - Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) + true -> + Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags); + _ -> + Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) end. + wrap_as_mucsub(Messages, #jid{lserver = LServer} = Requester) -> ReqBare = jid:remove_resource(Requester), ReqServer = jid:make(<<>>, LServer, <<>>), - [{T1, T2, wrap_as_mucsub(M, ReqBare, ReqServer)} || {T1, T2, M} <- Messages]. + [ {T1, T2, wrap_as_mucsub(M, ReqBare, ReqServer)} || {T1, T2, M} <- Messages ]. + wrap_as_mucsub(Message, Requester, ReqServer) -> case Message of - #forwarded{delay = #delay{stamp = Stamp, desc = Desc}, - sub_els = [#message{from = From, sub_els = SubEls, subject = Subject} = Msg]} -> - {L1, SubEls2} = case lists:keytake(mam_archived, 1, SubEls) of - {value, Arch, Rest} -> - {[Arch#mam_archived{by = Requester}], Rest}; - _ -> - {[], SubEls} - end, - {Sid, L2, SubEls3} = case lists:keytake(stanza_id, 1, SubEls2) of - {value, #stanza_id{id = Sid0} = SID, Rest2} -> - {Sid0, [SID#stanza_id{by = Requester} | L1], Rest2}; - _ -> - {p1_rand:get_string(), L1, SubEls2} - end, - Msg2 = Msg#message{to = Requester, sub_els = SubEls3}, - Node = case Subject of - [] -> - ?NS_MUCSUB_NODES_MESSAGES; - _ -> - ?NS_MUCSUB_NODES_SUBJECT - end, - #forwarded{delay = #delay{stamp = Stamp, desc = Desc, from = ReqServer}, - sub_els = [ - #message{from = jid:remove_resource(From), to = Requester, - id = Sid, - sub_els = [#ps_event{ - items = #ps_items{ - node = Node, - items = [#ps_item{ - id = Sid, - sub_els = [Msg2] - }]}} | L2]}]}; - _ -> - Message + #forwarded{ + delay = #delay{stamp = Stamp, desc = Desc}, + sub_els = [#message{from = From, sub_els = SubEls, subject = Subject} = Msg] + } -> + {L1, SubEls2} = case lists:keytake(mam_archived, 1, SubEls) of + {value, Arch, Rest} -> + {[Arch#mam_archived{by = Requester}], Rest}; + _ -> + {[], SubEls} + end, + {Sid, L2, SubEls3} = case lists:keytake(stanza_id, 1, SubEls2) of + {value, #stanza_id{id = Sid0} = SID, Rest2} -> + {Sid0, [SID#stanza_id{by = Requester} | L1], Rest2}; + _ -> + {p1_rand:get_string(), L1, SubEls2} + end, + Msg2 = Msg#message{to = Requester, sub_els = SubEls3}, + Node = case Subject of + [] -> + ?NS_MUCSUB_NODES_MESSAGES; + _ -> + ?NS_MUCSUB_NODES_SUBJECT + end, + #forwarded{ + delay = #delay{stamp = Stamp, desc = Desc, from = ReqServer}, + sub_els = [#message{ + from = jid:remove_resource(From), + to = Requester, + id = Sid, + sub_els = [#ps_event{ + items = #ps_items{ + node = Node, + items = [#ps_item{ + id = Sid, + sub_els = [Msg2] + }] + } + } | L2] + }] + }; + _ -> + Message end. -msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick, - peer = Peer, id = ID}, - MsgType, JidRequestor, #jid{lserver = LServer} = JidArchive) -> +msg_to_el(#archive_msg{ + timestamp = TS, + packet = El, + nick = Nick, + peer = Peer, + id = ID + }, + MsgType, + JidRequestor, + #jid{lserver = LServer} = JidArchive) -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of - Pkt1 -> - Pkt2 = case MsgType of - chat -> set_stanza_id(Pkt1, JidArchive, ID); - {groupchat, _, _} -> set_stanza_id(Pkt1, JidArchive, ID); - _ -> Pkt1 - end, - Pkt3 = maybe_update_from_to( - Pkt2, JidRequestor, JidArchive, Peer, MsgType, Nick), - Pkt4 = xmpp:put_meta(Pkt3, archive_nick, Nick), - Delay = #delay{stamp = TS, from = jid:make(LServer)}, - {ok, #forwarded{sub_els = [Pkt4], delay = Delay}} - catch _:{xmpp_codec, Why} -> - ?ERROR_MSG("Failed to decode raw element ~p from message " - "archive of user ~ts: ~ts", - [El, jid:encode(JidArchive), xmpp:format_error(Why)]), - {error, invalid_xml} + Pkt1 -> + Pkt2 = case MsgType of + chat -> set_stanza_id(Pkt1, JidArchive, ID); + {groupchat, _, _} -> set_stanza_id(Pkt1, JidArchive, ID); + _ -> Pkt1 + end, + Pkt3 = maybe_update_from_to( + Pkt2, JidRequestor, JidArchive, Peer, MsgType, Nick), + Pkt4 = xmpp:put_meta(Pkt3, archive_nick, Nick), + Delay = #delay{stamp = TS, from = jid:make(LServer)}, + {ok, #forwarded{sub_els = [Pkt4], delay = Delay}} + catch + _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode raw element ~p from message " + "archive of user ~ts: ~ts", + [El, jid:encode(JidArchive), xmpp:format_error(Why)]), + {error, invalid_xml} end. -maybe_update_from_to(#message{sub_els = Els} = Pkt, JidRequestor, JidArchive, - Peer, {groupchat, Role, - #state{config = #config{anonymous = Anon}}}, - Nick) -> + +maybe_update_from_to(#message{sub_els = Els} = Pkt, + JidRequestor, + JidArchive, + Peer, + {groupchat, Role, + #state{config = #config{anonymous = Anon}}}, + Nick) -> ExposeJID = case {Peer, JidRequestor} of - {undefined, _JidRequestor} -> - false; - {{U, S, _R}, #jid{luser = U, lserver = S}} -> - true; - {_Peer, _JidRequestor} when not Anon; Role == moderator -> - true; - {_Peer, _JidRequestor} -> - false - end, + {undefined, _JidRequestor} -> + false; + {{U, S, _R}, #jid{luser = U, lserver = S}} -> + true; + {_Peer, _JidRequestor} when not Anon; Role == moderator -> + true; + {_Peer, _JidRequestor} -> + false + end, Items = case ExposeJID of - true -> - [#muc_user{items = [#muc_item{jid = Peer}]}]; - false -> - [] - end, - Pkt#message{from = jid:replace_resource(JidArchive, Nick), - to = undefined, - sub_els = Items ++ Els}; + true -> + [#muc_user{items = [#muc_item{jid = Peer}]}]; + false -> + [] + end, + Pkt#message{ + from = jid:replace_resource(JidArchive, Nick), + to = undefined, + sub_els = Items ++ Els + }; maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, _MsgType, _Nick) -> Pkt. + -spec send([{binary(), integer(), xmlel()}], - count(), boolean(), iq()) -> iq() | ignore. -send(Msgs, Count, IsComplete, - #iq{from = From, to = To, - sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) -> + count(), + boolean(), + iq()) -> iq() | ignore. +send(Msgs, + Count, + IsComplete, + #iq{ + from = From, + to = To, + sub_els = [#mam_query{id = QID, xmlns = NS}] + } = IQ) -> Hint = #hint{type = 'no-store'}, Els = lists:map( - fun({ID, _IDInt, El}) -> - #message{from = To, - to = From, - sub_els = [#mam_result{xmlns = NS, - id = ID, - queryid = QID, - sub_els = [El]}]} - end, Msgs), + fun({ID, _IDInt, El}) -> + #message{ + from = To, + to = From, + sub_els = [#mam_result{ + xmlns = NS, + id = ID, + queryid = QID, + sub_els = [El] + }] + } + end, + Msgs), RSMOut = make_rsm_out(Msgs, Count), - Result = if NS == ?NS_MAM_TMP -> - #mam_query{xmlns = NS, id = QID, rsm = RSMOut}; - NS == ?NS_MAM_0 -> - #mam_fin{xmlns = NS, id = QID, rsm = RSMOut, - complete = IsComplete}; - true -> - #mam_fin{xmlns = NS, rsm = RSMOut, complete = IsComplete} - end, - if NS /= ?NS_MAM_0 -> - lists:foreach( - fun(El) -> - ejabberd_router:route(El) - end, Els), - xmpp:make_iq_result(IQ, Result); - true -> - ejabberd_router:route(xmpp:make_iq_result(IQ)), - lists:foreach( - fun(El) -> - ejabberd_router:route(El) - end, Els), - ejabberd_router:route( - #message{from = To, to = From, sub_els = [Result, Hint]}), - ignore + Result = if + NS == ?NS_MAM_TMP -> + #mam_query{xmlns = NS, id = QID, rsm = RSMOut}; + NS == ?NS_MAM_0 -> + #mam_fin{ + xmlns = NS, + id = QID, + rsm = RSMOut, + complete = IsComplete + }; + true -> + #mam_fin{xmlns = NS, rsm = RSMOut, complete = IsComplete} + end, + if + NS /= ?NS_MAM_0 -> + lists:foreach( + fun(El) -> + ejabberd_router:route(El) + end, + Els), + xmpp:make_iq_result(IQ, Result); + true -> + ejabberd_router:route(xmpp:make_iq_result(IQ)), + lists:foreach( + fun(El) -> + ejabberd_router:route(El) + end, + Els), + ejabberd_router:route( + #message{from = To, to = From, sub_els = [Result, Hint]}), + ignore end. + -spec make_rsm_out([{binary(), integer(), xmlel()}], count()) -> rsm_set(). make_rsm_out([], Count) -> #rsm_set{count = Count}; -make_rsm_out([{FirstID, _, _}|_] = Msgs, Count) -> +make_rsm_out([{FirstID, _, _} | _] = Msgs, Count) -> {LastID, _, _} = lists:last(Msgs), #rsm_set{first = #rsm_first{data = FirstID}, last = LastID, count = Count}. + filter_by_max(Msgs, undefined) -> {Msgs, true}; filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> @@ -1541,9 +1981,10 @@ filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> filter_by_max(_Msgs, _Junk) -> {[], true}. + -spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined. limit_max(RSM, ?NS_MAM_TMP) -> - RSM; % XEP-0313 v0.2 doesn't require clients to support RSM. + RSM; % XEP-0313 v0.2 doesn't require clients to support RSM. limit_max(undefined, _NS) -> #rsm_set{max = ?DEF_PAGE_SIZE}; limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) -> @@ -1553,11 +1994,13 @@ limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE -> limit_max(RSM, _NS) -> RSM. + match_interval(Now, Start, undefined) -> Now >= Start; match_interval(Now, Start, End) -> (Now >= Start) and (Now =< End). + match_rsm(Now, #rsm_set{'after' = ID}) when is_binary(ID), ID /= <<"">> -> Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), Now > Now1; @@ -1567,17 +2010,20 @@ match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> -> match_rsm(_Now, _) -> true. + might_expose_jid(Query, - {groupchat, Role, #state{config = #config{anonymous = true}}}) + {groupchat, Role, #state{config = #config{anonymous = true}}}) when Role /= moderator -> proplists:is_defined(with, Query); might_expose_jid(_Query, _MsgType) -> false. + get_jids(undefined) -> []; get_jids(Js) -> - [jid:tolower(jid:remove_resource(J)) || J <- Js]. + [ jid:tolower(jid:remove_resource(J)) || J <- Js ]. + is_archiving_enabled(LUser, LServer) -> case gen_mod:is_loaded(LServer, mod_mam) of @@ -1592,140 +2038,182 @@ is_archiving_enabled(LUser, LServer) -> false end. -get_commands_spec() -> - [ - #ejabberd_commands{name = get_mam_count, tags = [mam], - desc = "Get number of MAM messages in a local user archive", - module = ?MODULE, function = get_mam_count, - note = "added in 24.10", - policy = user, - args = [], - result_example = 5, - result_desc = "Number", - result = {value, integer}}, - #ejabberd_commands{name = get_mam_messages, - tags = [internal, mam], - desc = "Get the mam messages", - policy = user, - module = mod_mam, function = get_mam_messages, - args = [], - result = {archive, {list, {messages, {tuple, [{time, string}, - {from, string}, - {to, string}, - {packet, string} - ]}}}}}, - #ejabberd_commands{name = delete_old_mam_messages, tags = [mam, purge], - desc = "Delete MAM messages older than DAYS", - longdesc = "Valid message TYPEs: " - "`chat`, `groupchat`, `all`.", - module = ?MODULE, function = delete_old_messages, - args_desc = ["Type of messages to delete (`chat`, `groupchat`, `all`)", - "Days to keep messages"], - args_example = [<<"all">>, 31], - args = [{type, binary}, {days, integer}], - result = {res, rescode}}, - #ejabberd_commands{name = delete_old_mam_messages_batch, tags = [mam, purge], - desc = "Delete MAM messages older than DAYS", - note = "added in 22.05", - longdesc = "Valid message TYPEs: " - "`chat`, `groupchat`, `all`.", - module = ?MODULE, function = delete_old_messages_batch, - args_desc = ["Name of host where messages should be deleted", - "Type of messages to delete (`chat`, `groupchat`, `all`)", - "Days to keep messages", - "Number of messages to delete per batch", - "Desired rate of messages to delete per minute"], - args_example = [<<"localhost">>, <<"all">>, 31, 1000, 10000], - args = [{host, binary}, {type, binary}, {days, integer}, {batch_size, integer}, {rate, integer}], - result = {res, restuple}, - result_desc = "Result tuple", - result_example = {ok, <<"Removal of 5000 messages in progress">>}}, - #ejabberd_commands{name = delete_old_mam_messages_status, tags = [mam, purge], - desc = "Status of delete old MAM messages operation", - note = "added in 22.05", - module = ?MODULE, function = delete_old_messages_status, - args_desc = ["Name of host where messages should be deleted"], - args_example = [<<"localhost">>], - args = [{host, binary}], - result = {status, string}, - result_desc = "Status test", - result_example = "Operation in progress, delete 5000 messages"}, - #ejabberd_commands{name = abort_delete_old_mam_messages, tags = [mam, purge], - desc = "Abort currently running delete old MAM messages operation", - note = "added in 22.05", - module = ?MODULE, function = delete_old_messages_abort, - args_desc = ["Name of host where operation should be aborted"], - args_example = [<<"localhost">>], - args = [{host, binary}], - result = {status, string}, - result_desc = "Status text", - result_example = "Operation aborted"}, - #ejabberd_commands{name = remove_mam_for_user, tags = [mam], - desc = "Remove mam archive for user", - module = ?MODULE, function = remove_mam_for_user, - args = [{user, binary}, {host, binary}], - args_rename = [{server, host}], - args_desc = ["Username", "Server"], - args_example = [<<"bob">>, <<"example.com">>], - result = {res, restuple}, - result_desc = "Result tuple", - result_example = {ok, <<"MAM archive removed">>}}, - #ejabberd_commands{name = remove_mam_for_user_with_peer, tags = [mam], - desc = "Remove mam archive for user with peer", - module = ?MODULE, function = remove_mam_for_user_with_peer, - args = [{user, binary}, {host, binary}, {with, binary}], - args_rename = [{server, host}], - args_desc = ["Username", "Server", "Peer"], - args_example = [<<"bob">>, <<"example.com">>, <<"anne@example.com">>], - result = {res, restuple}, - result_desc = "Result tuple", - result_example = {ok, <<"MAM archive removed">>}} - ]. +get_commands_spec() -> + [#ejabberd_commands{ + name = get_mam_count, + tags = [mam], + desc = "Get number of MAM messages in a local user archive", + module = ?MODULE, + function = get_mam_count, + note = "added in 24.10", + policy = user, + args = [], + result_example = 5, + result_desc = "Number", + result = {value, integer} + }, + #ejabberd_commands{ + name = get_mam_messages, + tags = [internal, mam], + desc = "Get the mam messages", + policy = user, + module = mod_mam, + function = get_mam_messages, + args = [], + result = {archive, {list, {messages, {tuple, [{time, string}, + {from, string}, + {to, string}, + {packet, string}]}}}} + }, + + #ejabberd_commands{ + name = delete_old_mam_messages, + tags = [mam, purge], + desc = "Delete MAM messages older than DAYS", + longdesc = "Valid message TYPEs: " + "`chat`, `groupchat`, `all`.", + module = ?MODULE, + function = delete_old_messages, + args_desc = ["Type of messages to delete (`chat`, `groupchat`, `all`)", + "Days to keep messages"], + args_example = [<<"all">>, 31], + args = [{type, binary}, {days, integer}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = delete_old_mam_messages_batch, + tags = [mam, purge], + desc = "Delete MAM messages older than DAYS", + note = "added in 22.05", + longdesc = "Valid message TYPEs: " + "`chat`, `groupchat`, `all`.", + module = ?MODULE, + function = delete_old_messages_batch, + args_desc = ["Name of host where messages should be deleted", + "Type of messages to delete (`chat`, `groupchat`, `all`)", + "Days to keep messages", + "Number of messages to delete per batch", + "Desired rate of messages to delete per minute"], + args_example = [<<"localhost">>, <<"all">>, 31, 1000, 10000], + args = [{host, binary}, {type, binary}, {days, integer}, {batch_size, integer}, {rate, integer}], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"Removal of 5000 messages in progress">>} + }, + #ejabberd_commands{ + name = delete_old_mam_messages_status, + tags = [mam, purge], + desc = "Status of delete old MAM messages operation", + note = "added in 22.05", + module = ?MODULE, + function = delete_old_messages_status, + args_desc = ["Name of host where messages should be deleted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status test", + result_example = "Operation in progress, delete 5000 messages" + }, + #ejabberd_commands{ + name = abort_delete_old_mam_messages, + tags = [mam, purge], + desc = "Abort currently running delete old MAM messages operation", + note = "added in 22.05", + module = ?MODULE, + function = delete_old_messages_abort, + args_desc = ["Name of host where operation should be aborted"], + args_example = [<<"localhost">>], + args = [{host, binary}], + result = {status, string}, + result_desc = "Status text", + result_example = "Operation aborted" + }, + #ejabberd_commands{ + name = remove_mam_for_user, + tags = [mam], + desc = "Remove mam archive for user", + module = ?MODULE, + function = remove_mam_for_user, + args = [{user, binary}, {host, binary}], + args_rename = [{server, host}], + args_desc = ["Username", "Server"], + args_example = [<<"bob">>, <<"example.com">>], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"MAM archive removed">>} + }, + #ejabberd_commands{ + name = remove_mam_for_user_with_peer, + tags = [mam], + desc = "Remove mam archive for user with peer", + module = ?MODULE, + function = remove_mam_for_user_with_peer, + args = [{user, binary}, {host, binary}, {with, binary}], + args_rename = [{server, host}], + args_desc = ["Username", "Server", "Peer"], + args_example = [<<"bob">>, <<"example.com">>, <<"anne@example.com">>], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"MAM archive removed">>} + }]. %%% %%% WebAdmin %%% + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"mam">>, <<"MAM">>}, - {<<"mam-archive">>, <<"MAM Archive">>}]. + {<<"mam-archive">>, <<"MAM Archive">>}]. -webadmin_page_hostuser(_, Host, U, - #request{us = _US, path = [<<"mam">>]} = R) -> - Res = ?H1GL(<<"MAM">>, <<"modules/#mod_mam">>, <<"mod_mam">>) - ++ [make_command(get_mam_count, - R, - [{<<"user">>, U}, {<<"host">>, Host}], - [{result_links, - [{value, arg_host, 5, <<"user/", U/binary, "/mam-archive/">>}]}]), - make_command(remove_mam_for_user, - R, - [{<<"user">>, U}, {<<"host">>, Host}], - [{style, danger}]), - make_command(remove_mam_for_user_with_peer, - R, - [{<<"user">>, U}, {<<"host">>, Host}], - [{style, danger}])], + +webadmin_page_hostuser(_, + Host, + U, + #request{us = _US, path = [<<"mam">>]} = R) -> + Res = ?H1GL(<<"MAM">>, <<"modules/#mod_mam">>, <<"mod_mam">>) ++ + [make_command(get_mam_count, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{result_links, + [{value, arg_host, 5, <<"user/", U/binary, "/mam-archive/">>}]}]), + make_command(remove_mam_for_user, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{style, danger}]), + make_command(remove_mam_for_user_with_peer, + R, + [{<<"user">>, U}, {<<"host">>, Host}], + [{style, danger}])], {stop, Res}; -webadmin_page_hostuser(_, Host, U, - #request{us = _US, path = [<<"mam-archive">> | RPath], - lang = Lang} = R) -> +webadmin_page_hostuser(_, + Host, + U, + #request{ + us = _US, + path = [<<"mam-archive">> | RPath], + lang = Lang + } = R) -> PageTitle = str:translate_and_format(Lang, ?T("~ts's MAM Archive"), [jid:encode({U, Host, <<"">>})]), Head = ?H1GL(PageTitle, <<"modules/#mod_mam">>, <<"mod_mam">>), - Res = make_command(get_mam_messages, R, [{<<"user">>, U}, - {<<"host">>, Host}], - [{table_options, {10, RPath}}, - {result_links, [{packet, paragraph, 1, <<"">>}]}]), + Res = make_command(get_mam_messages, + R, + [{<<"user">>, U}, + {<<"host">>, Host}], + [{table_options, {10, RPath}}, + {result_links, [{packet, paragraph, 1, <<"">>}]}]), {stop, Head ++ [Res]}; webadmin_page_hostuser(Acc, _, _, _) -> Acc. + %%% %%% Documentation %%% + mod_opt_type(compress_xml) -> econf:bool(); mod_opt_type(assume_mam_usage) -> @@ -1751,6 +2239,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{assume_mam_usage, false}, {default, never}, @@ -1765,27 +2254,33 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module implements " - "https://xmpp.org/extensions/xep-0313.html" - "[XEP-0313: Message Archive Management] and " - "https://xmpp.org/extensions/xep-0441.html" - "[XEP-0441: Message Archive Management Preferences]. " - "Compatible XMPP clients can use it to store their " - "chat history on the server."), "", + "https://xmpp.org/extensions/xep-0313.html" + "[XEP-0313: Message Archive Management] and " + "https://xmpp.org/extensions/xep-0441.html" + "[XEP-0441: Message Archive Management Preferences]. " + "Compatible XMPP clients can use it to store their " + "chat history on the server."), + "", ?T("NOTE: Mnesia backend for mod_mam is not recommended: it's limited " - "to 2GB and often gets corrupted when reaching this limit. " - "SQL backend is recommended. Namely, for small servers SQLite " - "is a preferred choice because it's very easy to configure.")], + "to 2GB and often gets corrupted when reaching this limit. " + "SQL backend is recommended. Namely, for small servers SQLite " + "is a preferred choice because it's very easy to configure.")], opts => [{access_preferences, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => - ?T("This access rule defines who is allowed to modify the " - "MAM preferences. The default value is 'all'.")}}, + ?T("This access rule defines who is allowed to modify the " + "MAM preferences. The default value is 'all'.") + }}, {assume_mam_usage, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This option determines how ejabberd's " "stream management code (see _`mod_stream_mgmt`_) " @@ -1796,9 +2291,11 @@ mod_doc() -> "MAM archive if this option is set to 'true'. In " "this case, ejabberd assumes those messages will " "be retrieved from the archive. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {default, - #{value => "always | never | roster", + #{ + value => "always | never | roster", desc => ?T("The option defines default policy for chat history. " "When 'always' is set every chat message is stored. " @@ -1806,54 +2303,74 @@ mod_doc() -> "user's roster is stored. And 'never' fully disables " "chat history. Note that a client can change its " "policy via protocol commands. " - "The default value is 'never'.")}}, + "The default value is 'never'.") + }}, {request_activates_archiving, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If the value is 'true', no messages are stored " "for a user until their client issue a MAM request, " "regardless of the value of the 'default' option. " "Once the server received a request, that user's " "messages are archived as usual. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {compress_xml, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("When enabled, new messages added to archives are " "compressed using a custom compression algorithm. " "This feature works only with SQL backends. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {clear_archive_on_room_destroy, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Whether to destroy message archive of a room " "(see _`mod_muc`_) when it gets destroyed. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}, {user_mucsub_from_muc_archive, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("When this option is disabled, for each individual " - "subscriber a separate mucsub message is stored. With this " - "option enabled, when a user fetches archive virtual " - "mucsub, messages are generated from muc archives. " - "The default value is 'false'.")}}]}. + "subscriber a separate mucsub message is stored. With this " + "option enabled, when a user fetches archive virtual " + "mucsub, messages are generated from muc archives. " + "The default value is 'false'.") + }}] + }. diff --git a/src/mod_mam_mnesia.erl b/src/mod_mam_mnesia.erl index 4f59fa1fc..a99dbf912 100644 --- a/src/mod_mam_mnesia.erl +++ b/src/mod_mam_mnesia.erl @@ -27,127 +27,151 @@ -behaviour(mod_mam). %% API --export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, - extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/6, +-export([init/2, + remove_user/2, + remove_room/3, + delete_old_messages/3, + extended_fields/1, + store/10, + write_prefs/4, + get_prefs/2, + select/6, remove_from_archive/3, - is_empty_for_user/2, is_empty_for_room/3, delete_old_messages_batch/5, + is_empty_for_user/2, + is_empty_for_room/3, + delete_old_messages_batch/5, transform/1]). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("mod_mam.hrl"). -define(BIN_GREATER_THAN(A, B), - ((A > B andalso byte_size(A) == byte_size(B)) - orelse byte_size(A) > byte_size(B))). + ((A > B andalso byte_size(A) == byte_size(B)) orelse + byte_size(A) > byte_size(B))). -define(BIN_LESS_THAN(A, B), - ((A < B andalso byte_size(A) == byte_size(B)) - orelse byte_size(A) < byte_size(B))). + ((A < B andalso byte_size(A) == byte_size(B)) orelse + byte_size(A) < byte_size(B))). + +-define(TABLE_SIZE_LIMIT, 2000000000). % A bit less than 2 GiB. --define(TABLE_SIZE_LIMIT, 2000000000). % A bit less than 2 GiB. %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> try - {atomic, _} = ejabberd_mnesia:create( - ?MODULE, archive_msg, - [{disc_only_copies, [node()]}, - {type, bag}, - {attributes, record_info(fields, archive_msg)}]), - {atomic, _} = ejabberd_mnesia:create( - ?MODULE, archive_prefs, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, archive_prefs)}]), - ok - catch _:{badmatch, _} -> - {error, db_failure} + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, + archive_msg, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, archive_msg)}]), + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, + archive_prefs, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, archive_prefs)}]), + ok + catch + _:{badmatch, _} -> + {error, db_failure} end. + remove_user(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> - mnesia:delete({archive_msg, US}), - mnesia:delete({archive_prefs, US}) - end, + F = fun() -> + mnesia:delete({archive_msg, US}), + mnesia:delete({archive_prefs, US}) + end, mnesia:transaction(F). + remove_room(_LServer, LName, LHost) -> remove_user(LName, LHost). + remove_from_archive(LUser, LHost, Key) when is_binary(LUser) -> remove_from_archive({LUser, LHost}, LHost, Key); remove_from_archive(US, _LServer, none) -> - case mnesia:transaction(fun () -> mnesia:delete({archive_msg, US}) end) of - {atomic, _} -> ok; - {aborted, Reason} -> {error, Reason} + case mnesia:transaction(fun() -> mnesia:delete({archive_msg, US}) end) of + {atomic, _} -> ok; + {aborted, Reason} -> {error, Reason} end; remove_from_archive(US, _LServer, #jid{} = WithJid) -> Peer = jid:remove_resource(jid:split(WithJid)), - F = fun () -> - Msgs = mnesia:select( - archive_msg, - ets:fun2ms( - fun(#archive_msg{us = US1, bare_peer = Peer1} = Msg) - when US1 == US, Peer1 == Peer -> Msg - end)), - lists:foreach(fun mnesia:delete_object/1, Msgs) - end, + F = fun() -> + Msgs = mnesia:select( + archive_msg, + ets:fun2ms( + fun(#archive_msg{us = US1, bare_peer = Peer1} = Msg) + when US1 == US, Peer1 == Peer -> Msg + end)), + lists:foreach(fun mnesia:delete_object/1, Msgs) + end, case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> {error, Reason} + {atomic, _} -> ok; + {aborted, Reason} -> {error, Reason} end; remove_from_archive(US, _LServer, StanzaId) -> Timestamp = misc:usec_to_now(StanzaId), - F = fun () -> - Msgs = mnesia:select( - archive_msg, - ets:fun2ms( - fun(#archive_msg{us = US1, timestamp = Timestamp1} = Msg) - when US1 == US, Timestamp1 == Timestamp -> Msg - end)), - lists:foreach(fun mnesia:delete_object/1, Msgs) - end, + F = fun() -> + Msgs = mnesia:select( + archive_msg, + ets:fun2ms( + fun(#archive_msg{us = US1, timestamp = Timestamp1} = Msg) + when US1 == US, Timestamp1 == Timestamp -> Msg + end)), + lists:foreach(fun mnesia:delete_object/1, Msgs) + end, case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> {error, Reason} + {atomic, _} -> ok; + {aborted, Reason} -> {error, Reason} end. + delete_old_messages(global, TimeStamp, Type) -> mnesia:change_table_copy_type(archive_msg, node(), disc_copies), Result = delete_old_user_messages(mnesia:dirty_first(archive_msg), TimeStamp, Type), mnesia:change_table_copy_type(archive_msg, node(), disc_only_copies), Result. + delete_old_user_messages('$end_of_table', _TimeStamp, _Type) -> ok; delete_old_user_messages(User, TimeStamp, Type) -> F = fun() -> - Msgs = mnesia:read(archive_msg, User), - Keep = lists:filter( - fun(#archive_msg{timestamp = MsgTS, - type = MsgType}) -> - MsgTS >= TimeStamp orelse (Type /= all andalso - Type /= MsgType) - end, Msgs), - if length(Keep) < length(Msgs) -> - mnesia:delete({archive_msg, User}), - lists:foreach(fun(Msg) -> mnesia:write(Msg) end, Keep); - true -> - ok - end - end, + Msgs = mnesia:read(archive_msg, User), + Keep = lists:filter( + fun(#archive_msg{ + timestamp = MsgTS, + type = MsgType + }) -> + MsgTS >= TimeStamp orelse (Type /= all andalso + Type /= MsgType) + end, + Msgs), + if + length(Keep) < length(Msgs) -> + mnesia:delete({archive_msg, User}), + lists:foreach(fun(Msg) -> mnesia:write(Msg) end, Keep); + true -> + ok + end + end, NextRecord = mnesia:dirty_next(archive_msg, User), case mnesia:transaction(F) of - {atomic, ok} -> - delete_old_user_messages(NextRecord, TimeStamp, Type); - {aborted, Err} -> - ?ERROR_MSG("Cannot delete old MAM messages: ~ts", [Err]), - Err + {atomic, ok} -> + delete_old_user_messages(NextRecord, TimeStamp, Type); + {aborted, Err} -> + ?ERROR_MSG("Cannot delete old MAM messages: ~ts", [Err]), + Err end. + delete_batch('$end_of_table', _LServer, _TS, _Type, Num) -> {Num, '$end_of_table'}; delete_batch(LastUS, _LServer, _TS, _Type, 0) -> @@ -158,50 +182,64 @@ delete_batch({_, LServer2} = LastUS, LServer, TS, Type, Num) when LServer /= LSe delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Num); delete_batch(LastUS, LServer, TS, Type, Num) -> Left = - lists:foldl( - fun(_, 0) -> - 0; - (#archive_msg{timestamp = TS2, type = Type2} = O, Num2) when TS2 < TS, (Type == all orelse Type == Type2) -> - mnesia:delete_object(O), - Num2 - 1; - (_, Num2) -> - Num2 - end, Num, mnesia:wread({archive_msg, LastUS})), + lists:foldl( + fun(_, 0) -> + 0; + (#archive_msg{timestamp = TS2, type = Type2} = O, Num2) when TS2 < TS, (Type == all orelse Type == Type2) -> + mnesia:delete_object(O), + Num2 - 1; + (_, Num2) -> + Num2 + end, + Num, + mnesia:wread({archive_msg, LastUS})), case Left of - 0 -> {0, LastUS}; - _ -> delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Left) + 0 -> {0, LastUS}; + _ -> delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Left) end. + delete_old_messages_batch(LServer, TimeStamp, Type, Batch, LastUS) -> R = mnesia:transaction( - fun() -> - {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Type, Batch), - {Batch - Num, NextUS} - end), + fun() -> + {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Type, Batch), + {Batch - Num, NextUS} + end), case R of - {atomic, {Num, State}} -> - {ok, State, Num}; - {aborted, Err} -> - {error, Err} + {atomic, {Num, State}} -> + {ok, State, Num}; + {aborted, Err} -> + {error, Err} end. + extended_fields(_) -> []. -store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS, - OriginID, Retract) -> + +store(Pkt, + _, + {LUser, LServer}, + Type, + Peer, + Nick, + _Dir, + TS, + OriginID, + Retract) -> case Retract of {true, RID} -> mnesia:transaction( - fun () -> + fun() -> {PUser, PServer, _} = jid:tolower(Peer), Msgs = mnesia:select( archive_msg, ets:fun2ms( fun(#archive_msg{ - us = US1, - bare_peer = Peer1, - origin_id = OriginID1} = Msg) + us = US1, + bare_peer = Peer1, + origin_id = OriginID1 + } = Msg) when US1 == {LUser, LServer}, Peer1 == {PUser, PServer, <<>>}, OriginID1 == RID -> Msg @@ -211,81 +249,96 @@ store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS, false -> ok end, case {mnesia:table_info(archive_msg, disc_only_copies), - mnesia:table_info(archive_msg, memory)} of - {[_|_], TableSize} when TableSize > ?TABLE_SIZE_LIMIT -> - ?ERROR_MSG("MAM archives too large, won't store message for ~ts@~ts", - [LUser, LServer]), - {error, overflow}; - _ -> - LPeer = {PUser, PServer, _} = jid:tolower(Peer), - F = fun() -> - mnesia:write( - #archive_msg{us = {LUser, LServer}, - id = integer_to_binary(TS), - timestamp = misc:usec_to_now(TS), - peer = LPeer, - bare_peer = {PUser, PServer, <<>>}, - type = Type, - nick = Nick, - packet = Pkt, - origin_id = OriginID}) - end, - case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, Err} -> - ?ERROR_MSG("Cannot add message to MAM archive of ~ts@~ts: ~ts", - [LUser, LServer, Err]), - Err - end + mnesia:table_info(archive_msg, memory)} of + {[_ | _], TableSize} when TableSize > ?TABLE_SIZE_LIMIT -> + ?ERROR_MSG("MAM archives too large, won't store message for ~ts@~ts", + [LUser, LServer]), + {error, overflow}; + _ -> + LPeer = {PUser, PServer, _} = jid:tolower(Peer), + F = fun() -> + mnesia:write( + #archive_msg{ + us = {LUser, LServer}, + id = integer_to_binary(TS), + timestamp = misc:usec_to_now(TS), + peer = LPeer, + bare_peer = {PUser, PServer, <<>>}, + type = Type, + nick = Nick, + packet = Pkt, + origin_id = OriginID + }) + end, + case mnesia:transaction(F) of + {atomic, ok} -> + ok; + {aborted, Err} -> + ?ERROR_MSG("Cannot add message to MAM archive of ~ts@~ts: ~ts", + [LUser, LServer, Err]), + Err + end end. + write_prefs(_LUser, _LServer, Prefs, _ServerHost) -> mnesia:dirty_write(Prefs). + get_prefs(LUser, LServer) -> case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of - [Prefs] -> - {ok, Prefs}; - _ -> - error + [Prefs] -> + {ok, Prefs}; + _ -> + error end. -select(_LServer, JidRequestor, + +select(_LServer, + JidRequestor, #jid{luser = LUser, lserver = LServer} = JidArchive, - Query, RSM, MsgType) -> + Query, + RSM, + MsgType) -> Start = proplists:get_value(start, Query), End = proplists:get_value('end', Query), With = proplists:get_value(with, Query), - LWith = if With /= undefined -> jid:tolower(With); - true -> undefined - end, + LWith = if + With /= undefined -> jid:tolower(With); + true -> undefined + end, MS = make_matchspec(LUser, LServer, Start, End, LWith), Msgs = mnesia:dirty_select(archive_msg, MS), SortedMsgs = lists:keysort(#archive_msg.timestamp, Msgs), {FilteredMsgs, IsComplete} = filter_by_rsm(SortedMsgs, RSM), Count = length(Msgs), Result = {lists:flatmap( - fun(Msg) -> - case mod_mam:msg_to_el( - Msg, MsgType, JidRequestor, JidArchive) of - {ok, El} -> - [{Msg#archive_msg.id, - binary_to_integer(Msg#archive_msg.id), - El}]; - {error, _} -> - [] - end - end, FilteredMsgs), IsComplete, Count}, + fun(Msg) -> + case mod_mam:msg_to_el( + Msg, MsgType, JidRequestor, JidArchive) of + {ok, El} -> + [{Msg#archive_msg.id, + binary_to_integer(Msg#archive_msg.id), + El}]; + {error, _} -> + [] + end + end, + FilteredMsgs), + IsComplete, + Count}, erlang:garbage_collect(), Result. + is_empty_for_user(LUser, LServer) -> mnesia:dirty_read(archive_msg, {LUser, LServer}) == []. + is_empty_for_room(_LServer, LName, LHost) -> is_empty_for_user(LName, LHost). + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -294,59 +347,74 @@ make_matchspec(LUser, LServer, Start, undefined, With) -> make_matchspec(LUser, LServer, Start, [], With); make_matchspec(LUser, LServer, Start, End, {_, _, <<>>} = With) -> ets:fun2ms( - fun(#archive_msg{timestamp = TS, - us = US, - bare_peer = BPeer} = Msg) - when Start =< TS, End >= TS, - US == {LUser, LServer}, - BPeer == With -> - Msg + fun(#archive_msg{ + timestamp = TS, + us = US, + bare_peer = BPeer + } = Msg) + when Start =< TS, + End >= TS, + US == {LUser, LServer}, + BPeer == With -> + Msg end); make_matchspec(LUser, LServer, Start, End, {_, _, _} = With) -> ets:fun2ms( - fun(#archive_msg{timestamp = TS, - us = US, - peer = Peer} = Msg) - when Start =< TS, End >= TS, - US == {LUser, LServer}, - Peer == With -> - Msg + fun(#archive_msg{ + timestamp = TS, + us = US, + peer = Peer + } = Msg) + when Start =< TS, + End >= TS, + US == {LUser, LServer}, + Peer == With -> + Msg end); make_matchspec(LUser, LServer, Start, End, undefined) -> ets:fun2ms( - fun(#archive_msg{timestamp = TS, - us = US, - peer = Peer} = Msg) - when Start =< TS, End >= TS, - US == {LUser, LServer} -> - Msg + fun(#archive_msg{ + timestamp = TS, + us = US, + peer = Peer + } = Msg) + when Start =< TS, + End >= TS, + US == {LUser, LServer} -> + Msg end). + filter_by_rsm(Msgs, undefined) -> {Msgs, true}; filter_by_rsm(_Msgs, #rsm_set{max = Max}) when Max < 0 -> {[], true}; filter_by_rsm(Msgs, #rsm_set{max = Max, before = Before, 'after' = After}) -> - NewMsgs = if is_binary(After), After /= <<"">> -> - lists:filter( - fun(#archive_msg{id = I}) -> - ?BIN_GREATER_THAN(I, After) - end, Msgs); - is_binary(Before), Before /= <<"">> -> - lists:foldl( - fun(#archive_msg{id = I} = Msg, Acc) - when ?BIN_LESS_THAN(I, Before) -> - [Msg|Acc]; - (_, Acc) -> - Acc - end, [], Msgs); - is_binary(Before), Before == <<"">> -> - lists:reverse(Msgs); - true -> - Msgs - end, + NewMsgs = if + is_binary(After), After /= <<"">> -> + lists:filter( + fun(#archive_msg{id = I}) -> + ?BIN_GREATER_THAN(I, After) + end, + Msgs); + is_binary(Before), Before /= <<"">> -> + lists:foldl( + fun(#archive_msg{id = I} = Msg, Acc) + when ?BIN_LESS_THAN(I, Before) -> + [Msg | Acc]; + (_, Acc) -> + Acc + end, + [], + Msgs); + is_binary(Before), Before == <<"">> -> + lists:reverse(Msgs); + true -> + Msgs + end, filter_by_max(NewMsgs, Max). + filter_by_max(Msgs, undefined) -> {Msgs, true}; filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> @@ -354,17 +422,19 @@ filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> filter_by_max(_Msgs, _Junk) -> {[], true}. + transform({archive_msg, US, ID, Timestamp, Peer, BarePeer, - Packet, Nick, Type}) -> + Packet, Nick, Type}) -> #archive_msg{ - us = US, - id = ID, - timestamp = Timestamp, - peer = Peer, - bare_peer = BarePeer, - packet = Packet, - nick = Nick, - type = Type, - origin_id = <<"">>}; + us = US, + id = ID, + timestamp = Timestamp, + peer = Peer, + bare_peer = BarePeer, + packet = Packet, + nick = Nick, + type = Type, + origin_id = <<"">> + }; transform(Other) -> Other. diff --git a/src/mod_mam_opt.erl b/src/mod_mam_opt.erl index d8d970a13..82f11e9d1 100644 --- a/src/mod_mam_opt.erl +++ b/src/mod_mam_opt.erl @@ -16,75 +16,86 @@ -export([use_cache/1]). -export([user_mucsub_from_muc_archive/1]). + -spec access_preferences(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_preferences(Opts) when is_map(Opts) -> gen_mod:get_opt(access_preferences, Opts); access_preferences(Host) -> gen_mod:get_module_opt(Host, mod_mam, access_preferences). + -spec assume_mam_usage(gen_mod:opts() | global | binary()) -> boolean(). assume_mam_usage(Opts) when is_map(Opts) -> gen_mod:get_opt(assume_mam_usage, Opts); assume_mam_usage(Host) -> gen_mod:get_module_opt(Host, mod_mam, assume_mam_usage). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_mam, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_mam, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_mam, cache_size). + -spec clear_archive_on_room_destroy(gen_mod:opts() | global | binary()) -> boolean(). clear_archive_on_room_destroy(Opts) when is_map(Opts) -> gen_mod:get_opt(clear_archive_on_room_destroy, Opts); clear_archive_on_room_destroy(Host) -> gen_mod:get_module_opt(Host, mod_mam, clear_archive_on_room_destroy). + -spec compress_xml(gen_mod:opts() | global | binary()) -> boolean(). compress_xml(Opts) when is_map(Opts) -> gen_mod:get_opt(compress_xml, Opts); compress_xml(Host) -> gen_mod:get_module_opt(Host, mod_mam, compress_xml). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mam, db_type). + -spec default(gen_mod:opts() | global | binary()) -> 'always' | 'never' | 'roster'. default(Opts) when is_map(Opts) -> gen_mod:get_opt(default, Opts); default(Host) -> gen_mod:get_module_opt(Host, mod_mam, default). + -spec request_activates_archiving(gen_mod:opts() | global | binary()) -> boolean(). request_activates_archiving(Opts) when is_map(Opts) -> gen_mod:get_opt(request_activates_archiving, Opts); request_activates_archiving(Host) -> gen_mod:get_module_opt(Host, mod_mam, request_activates_archiving). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_mam, use_cache). + -spec user_mucsub_from_muc_archive(gen_mod:opts() | global | binary()) -> boolean(). user_mucsub_from_muc_archive(Opts) when is_map(Opts) -> gen_mod:get_opt(user_mucsub_from_muc_archive, Opts); user_mucsub_from_muc_archive(Host) -> gen_mod:get_module_opt(Host, mod_mam, user_mucsub_from_muc_archive). - diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl index 8a1d8e02f..fe727cb3a 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -24,23 +24,36 @@ -module(mod_mam_sql). - -behaviour(mod_mam). %% API --export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, - extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3, - is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6, - delete_old_messages_batch/4, count_messages_to_delete/3]). +-export([init/2, + remove_user/2, + remove_room/3, + delete_old_messages/3, + extended_fields/1, + store/10, + write_prefs/4, + get_prefs/2, + select/7, + export/1, + remove_from_archive/3, + is_empty_for_user/2, + is_empty_for_room/3, + select_with_mucsub/6, + delete_old_messages_batch/4, + count_messages_to_delete/3]). -export([sql_schemas/0]). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_mam.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("mod_muc_room.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -48,108 +61,136 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 2, - tables = - [#sql_table{ - name = <<"archive">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"timestamp">>, type = bigint}, - #sql_column{name = <<"peer">>, type = text}, - #sql_column{name = <<"bare_peer">>, type = text}, - #sql_column{name = <<"xml">>, type = {text, big}}, - #sql_column{name = <<"txt">>, type = {text, big}}, - #sql_column{name = <<"id">>, type = bigserial}, - #sql_column{name = <<"kind">>, type = {text, 10}}, - #sql_column{name = <<"nick">>, type = text}, - #sql_column{name = <<"origin_id">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"timestamp">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"origin_id">>]} - ], - post_create = - fun(#sql_schema_info{db_type = mysql}) -> - [<<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>]; - (_) -> - [] - end}, - #sql_table{ - name = <<"archive_prefs">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"def">>, type = text}, - #sql_column{name = <<"always">>, type = text}, - #sql_column{name = <<"never">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}], - update = - [{add_column, <<"archive">>, <<"origin_id">>}, - {create_index, <<"archive">>, - [<<"server_host">>, <<"username">>, <<"origin_id">>]} - ]}, + version = 2, + tables = + [#sql_table{ + name = <<"archive">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = {text, 10}}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"origin_id">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"origin_id">>] + }], + post_create = + fun(#sql_schema_info{db_type = mysql}) -> + [<<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>]; + (_) -> + [] + end + }, + #sql_table{ + name = <<"archive_prefs">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"def">>, type = text}, + #sql_column{name = <<"always">>, type = text}, + #sql_column{name = <<"never">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }], + update = + [{add_column, <<"archive">>, <<"origin_id">>}, + {create_index, <<"archive">>, + [<<"server_host">>, <<"username">>, <<"origin_id">>]}] + }, #sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"archive">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"timestamp">>, type = bigint}, - #sql_column{name = <<"peer">>, type = text}, - #sql_column{name = <<"bare_peer">>, type = text}, - #sql_column{name = <<"xml">>, type = {text, big}}, - #sql_column{name = <<"txt">>, type = {text, big}}, - #sql_column{name = <<"id">>, type = bigserial}, - #sql_column{name = <<"kind">>, type = {text, 10}}, - #sql_column{name = <<"nick">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"peer">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"timestamp">>]} - ], - post_create = - fun(#sql_schema_info{db_type = mysql}) -> - ejabberd_sql:sql_query_t( - <<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>); - (_) -> - ok - end}, - #sql_table{ - name = <<"archive_prefs">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"def">>, type = text}, - #sql_column{name = <<"always">>, type = text}, - #sql_column{name = <<"never">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"archive">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"peer">>, type = text}, + #sql_column{name = <<"bare_peer">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"txt">>, type = {text, big}}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{name = <<"kind">>, type = {text, 10}}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"timestamp">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"peer">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"timestamp">>] + }], + post_create = + fun(#sql_schema_info{db_type = mysql}) -> + ejabberd_sql:sql_query_t( + <<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>); + (_) -> + ok + end + }, + #sql_table{ + name = <<"archive_prefs">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"def">>, type = text}, + #sql_column{name = <<"always">>, type = text}, + #sql_column{name = <<"never">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }] + }]. + remove_user(LUser, LServer) -> ejabberd_sql:sql_query( @@ -159,108 +200,113 @@ remove_user(LUser, LServer) -> LServer, ?SQL("delete from archive_prefs where username=%(LUser)s and %(LServer)H")). + remove_room(LServer, LName, LHost) -> LUser = jid:encode({LName, LHost, <<>>}), remove_user(LUser, LServer). + remove_from_archive({LUser, LHost}, LServer, Key) -> remove_from_archive(jid:encode({LUser, LHost, <<>>}), LServer, Key); remove_from_archive(LUser, LServer, none) -> case ejabberd_sql:sql_query(LServer, - ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")) of - {error, Reason} -> {error, Reason}; - _ -> ok + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")) of + {error, Reason} -> {error, Reason}; + _ -> ok end; remove_from_archive(LUser, LServer, #jid{} = WithJid) -> Peer = jid:encode(jid:remove_resource(WithJid)), case ejabberd_sql:sql_query(LServer, - ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and bare_peer=%(Peer)s")) of - {error, Reason} -> {error, Reason}; - _ -> ok + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and bare_peer=%(Peer)s")) of + {error, Reason} -> {error, Reason}; + _ -> ok end; remove_from_archive(LUser, LServer, StanzaId) -> case ejabberd_sql:sql_query(LServer, - ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and timestamp=%(StanzaId)d")) of - {error, Reason} -> {error, Reason}; - _ -> ok + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and timestamp=%(StanzaId)d")) of + {error, Reason} -> {error, Reason}; + _ -> ok end. + count_messages_to_delete(ServerHost, TimeStamp, Type) -> TS = misc:now_to_usec(TimeStamp), Res = - case Type of - all -> - ejabberd_sql:sql_query( - ServerHost, - ?SQL("select count(*) from archive" - " where timestamp < %(TS)d and %(ServerHost)H")); - _ -> - SType = misc:atom_to_binary(Type), - ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(count(*))d from archive" - " where timestamp < %(TS)d" - " and kind=%(SType)s" - " and %(ServerHost)H")) - end, + case Type of + all -> + ejabberd_sql:sql_query( + ServerHost, + ?SQL("select count(*) from archive" + " where timestamp < %(TS)d and %(ServerHost)H")); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + ?SQL("select @(count(*))d from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H")) + end, case Res of - {selected, [Count]} -> - {ok, Count}; - _ -> - error + {selected, [Count]} -> + {ok, Count}; + _ -> + error end. + delete_old_messages_batch(ServerHost, TimeStamp, Type, Batch) -> TS = misc:now_to_usec(TimeStamp), Res = - case Type of - all -> - ejabberd_sql:sql_query( - ServerHost, - fun(sqlite, _) -> - ejabberd_sql:sql_query_t( - ?SQL("delete from archive where rowid in " - "(select rowid from archive where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d)")); - (mssql, _) -> - ejabberd_sql:sql_query_t( - ?SQL("delete top(%(Batch)d)§ from archive" - " where timestamp < %(TS)d and %(ServerHost)H")); - (_, _) -> - ejabberd_sql:sql_query_t( - ?SQL("delete from archive" - " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d")) - end); - _ -> - SType = misc:atom_to_binary(Type), - ejabberd_sql:sql_query( - ServerHost, - fun(sqlite,_)-> - ejabberd_sql:sql_query_t( - ?SQL("delete from archive where rowid in (" - " select rowid from archive where timestamp < %(TS)d" - " and kind=%(SType)s" - " and %(ServerHost)H limit %(Batch)d)")); - (mssql, _)-> - ejabberd_sql:sql_query_t( - ?SQL("delete top(%(Batch)d) from archive" - " where timestamp < %(TS)d" - " and kind=%(SType)s" - " and %(ServerHost)H")); - (_,_)-> - ejabberd_sql:sql_query_t( - ?SQL("delete from archive" - " where timestamp < %(TS)d" - " and kind=%(SType)s" - " and %(ServerHost)H limit %(Batch)d")) - end) - end, + case Type of + all -> + ejabberd_sql:sql_query( + ServerHost, + fun(sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive where rowid in " + "(select rowid from archive where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d)")); + (mssql, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete top(%(Batch)d)§ from archive" + " where timestamp < %(TS)d and %(ServerHost)H")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive" + " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d")) + end); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + fun(sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive where rowid in (" + " select rowid from archive where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H limit %(Batch)d)")); + (mssql, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete top(%(Batch)d) from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H limit %(Batch)d")) + end) + end, case Res of - {updated, Count} -> - {ok, Count}; - {error, _} = Error -> - Error + {updated, Count} -> + {ok, Count}; + {error, _} = Error -> + Error end. + delete_old_messages(ServerHost, TimeStamp, Type) -> TS = misc:now_to_usec(TimeStamp), case Type of @@ -280,42 +326,54 @@ delete_old_messages(ServerHost, TimeStamp, Type) -> end, ok. + extended_fields(LServer) -> case ejabberd_option:sql_type(LServer) of - mysql -> - [{withtext, <<"">>}, - #xdata_field{var = <<"{urn:xmpp:fulltext:0}fulltext">>, - type = 'text-single', - label = <<"Search the text">>, - values = []}]; - _ -> - [] + mysql -> + [{withtext, <<"">>}, + #xdata_field{ + var = <<"{urn:xmpp:fulltext:0}fulltext">>, + type = 'text-single', + label = <<"Search the text">>, + values = [] + }]; + _ -> + [] end. -store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS, - OriginID, Retract) -> + +store(Pkt, + LServer, + {LUser, LHost}, + Type, + Peer, + Nick, + _Dir, + TS, + OriginID, + Retract) -> SUser = case Type of - chat -> LUser; - groupchat -> jid:encode({LUser, LHost, <<>>}) - end, + chat -> LUser; + groupchat -> jid:encode({LUser, LHost, <<>>}) + end, BarePeer = jid:encode( - jid:tolower( - jid:remove_resource(Peer))), + jid:tolower( + jid:remove_resource(Peer))), LPeer = jid:encode( - jid:tolower(Peer)), + jid:tolower(Peer)), Body = fxml:get_subtag_cdata(Pkt, <<"body">>), SType = misc:atom_to_binary(Type), SqlType = ejabberd_option:sql_type(LServer), XML = case mod_mam_opt:compress_xml(LServer) of - true -> - J1 = case Type of - chat -> jid:encode({LUser, LHost, <<>>}); - groupchat -> SUser - end, - xml_compress:encode(Pkt, J1, LPeer); - _ -> - fxml:element_to_binary(Pkt) - end, + true -> + J1 = case Type of + chat -> jid:encode({LUser, LHost, <<>>}); + groupchat -> SUser + end, + xml_compress:encode(Pkt, J1, LPeer); + _ -> + fxml:element_to_binary(Pkt) + end, case Retract of {true, RID} -> ejabberd_sql:sql_query( @@ -328,119 +386,151 @@ store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS, false -> ok end, case SqlType of - mssql -> case ejabberd_sql:sql_query( - LServer, - ?SQL_INSERT( - "archive", - ["username=%(SUser)s", - "server_host=%(LServer)s", - "timestamp=%(TS)d", - "peer=%(LPeer)s", - "bare_peer=%(BarePeer)s", - "xml=N%(XML)s", - "txt=N%(Body)s", - "kind=%(SType)s", - "nick=%(Nick)s", - "origin_id=%(OriginID)s"])) of - {updated, _} -> - ok; - Err -> - Err - end; - _ -> case ejabberd_sql:sql_query( - LServer, - ?SQL_INSERT( - "archive", - ["username=%(SUser)s", - "server_host=%(LServer)s", - "timestamp=%(TS)d", - "peer=%(LPeer)s", - "bare_peer=%(BarePeer)s", - "xml=%(XML)s", - "txt=%(Body)s", - "kind=%(SType)s", - "nick=%(Nick)s", - "origin_id=%(OriginID)s"])) of - {updated, _} -> - ok; - Err -> - Err - end + mssql -> + case ejabberd_sql:sql_query( + LServer, + ?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=N%(XML)s", + "txt=N%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])) of + {updated, _} -> + ok; + Err -> + Err + end; + _ -> + case ejabberd_sql:sql_query( + LServer, + ?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=%(XML)s", + "txt=%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])) of + {updated, _} -> + ok; + Err -> + Err + end end. -write_prefs(LUser, _LServer, #archive_prefs{default = Default, - never = Never, - always = Always}, - ServerHost) -> + +write_prefs(LUser, + _LServer, + #archive_prefs{ + default = Default, + never = Never, + always = Always + }, + ServerHost) -> SDefault = erlang:atom_to_binary(Default, utf8), SAlways = misc:term_to_expr(Always), SNever = misc:term_to_expr(Never), case ?SQL_UPSERT( - ServerHost, - "archive_prefs", - ["!username=%(LUser)s", - "!server_host=%(ServerHost)s", - "def=%(SDefault)s", - "always=%(SAlways)s", - "never=%(SNever)s"]) of - ok -> - ok; - Err -> - Err + ServerHost, + "archive_prefs", + ["!username=%(LUser)s", + "!server_host=%(ServerHost)s", + "def=%(SDefault)s", + "always=%(SAlways)s", + "never=%(SNever)s"]) of + ok -> + ok; + Err -> + Err end. + get_prefs(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs" + LServer, + ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs" " where username=%(LUser)s and %(LServer)H")) of - {selected, [{SDefault, SAlways, SNever}]} -> - Default = erlang:binary_to_existing_atom(SDefault, utf8), - Always = ejabberd_sql:decode_term(SAlways), - Never = ejabberd_sql:decode_term(SNever), - {ok, #archive_prefs{us = {LUser, LServer}, - default = Default, - always = Always, - never = Never}}; - _ -> - error + {selected, [{SDefault, SAlways, SNever}]} -> + Default = erlang:binary_to_existing_atom(SDefault, utf8), + Always = ejabberd_sql:decode_term(SAlways), + Never = ejabberd_sql:decode_term(SNever), + {ok, #archive_prefs{ + us = {LUser, LServer}, + default = Default, + always = Always, + never = Never + }}; + _ -> + error end. -select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, - MAMQuery, RSM, MsgType, Flags) -> + +select(LServer, + JidRequestor, + #jid{luser = LUser} = JidArchive, + MAMQuery, + RSM, + MsgType, + Flags) -> User = case MsgType of - chat -> LUser; - _ -> jid:encode(JidArchive) - end, + chat -> LUser; + _ -> jid:encode(JidArchive) + end, {Query, CountQuery} = make_sql_query(User, LServer, MAMQuery, RSM, none), do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery, Flags). --spec select_with_mucsub(binary(), jid(), jid(), mam_query:result(), - #rsm_set{} | undefined, all | only_count | only_messages) -> - {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()} | - {error, db_failure}. -select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, - MAMQuery, RSM, Flags) -> + +-spec select_with_mucsub(binary(), + jid(), + jid(), + mam_query:result(), + #rsm_set{} | undefined, + all | only_count | only_messages) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()} | + {error, db_failure}. +select_with_mucsub(LServer, + JidRequestor, + #jid{luser = LUser} = JidArchive, + MAMQuery, + RSM, + Flags) -> Extra = case gen_mod:db_mod(LServer, mod_muc) of - mod_muc_sql -> - subscribers_table; - _ -> - SubRooms = case mod_muc_admin:find_hosts(LServer) of - [First|_] -> - case mod_muc:get_subscribed_rooms(First, JidRequestor) of - {ok, L} -> L; - {error, _} -> [] - end; - _ -> - [] - end, - [jid:encode(Jid) || {Jid, _, _} <- SubRooms] - end, + mod_muc_sql -> + subscribers_table; + _ -> + SubRooms = case mod_muc_admin:find_hosts(LServer) of + [First | _] -> + case mod_muc:get_subscribed_rooms(First, JidRequestor) of + {ok, L} -> L; + {error, _} -> [] + end; + _ -> + [] + end, + [ jid:encode(Jid) || {Jid, _, _} <- SubRooms ] + end, {Query, CountQuery} = make_sql_query(LUser, LServer, MAMQuery, RSM, Extra), do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery, Flags). -do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM, - MsgType, Query, CountQuery, Flags) -> + +do_select_query(LServer, + JidRequestor, + #jid{luser = LUser} = JidArchive, + RSM, + MsgType, + Query, + CountQuery, + Flags) -> % TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a % reasonable limit on how many stanzas may be pushed to a client in one % request. If a query returns a number of stanzas greater than this limit @@ -448,142 +538,184 @@ do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM, % return a policy-violation error to the client." We currently don't do this % for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer. QRes = case Flags of - all -> - {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)}; - only_messages -> - {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}}; - only_count -> - {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)} - end, + all -> + {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)}; + only_messages -> + {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}}; + only_count -> + {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)} + end, case QRes of - {{selected, _, Res}, {selected, _, [[Count]]}} -> - {Max, Direction, _} = get_max_direction_id(RSM), - {Res1, IsComplete} = - if Max >= 0 andalso Max /= undefined andalso length(Res) > Max -> - if Direction == before -> - {lists:nthtail(1, Res), false}; - true -> - {lists:sublist(Res, Max), false} - end; - true -> - {Res, true} - end, - MucState = #state{config = #config{anonymous = true}}, - JidArchiveS = jid:encode(jid:remove_resource(JidArchive)), - {lists:flatmap( - fun([TS, XML, PeerBin, Kind, Nick]) -> - case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick, - MsgType, JidRequestor, JidArchive) of - {ok, El} -> - [{TS, binary_to_integer(TS), El}]; - {error, _} -> - [] - end; - ([User, TS, XML, PeerBin, Kind, Nick]) when User == LUser -> - case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick, - MsgType, JidRequestor, JidArchive) of - {ok, El} -> - [{TS, binary_to_integer(TS), El}]; - {error, _} -> - [] - end; - ([User, TS, XML, PeerBin, Kind, Nick]) -> - case make_archive_el(User, TS, XML, PeerBin, Kind, Nick, - {groupchat, member, MucState}, JidRequestor, - jid:decode(User)) of - {ok, El} -> - mod_mam:wrap_as_mucsub([{TS, binary_to_integer(TS), El}], - JidRequestor); - {error, _} -> - [] - end - end, Res1), IsComplete, binary_to_integer(Count)}; - _ -> - {[], false, 0} + {{selected, _, Res}, {selected, _, [[Count]]}} -> + {Max, Direction, _} = get_max_direction_id(RSM), + {Res1, IsComplete} = + if + Max >= 0 andalso Max /= undefined andalso length(Res) > Max -> + if + Direction == before -> + {lists:nthtail(1, Res), false}; + true -> + {lists:sublist(Res, Max), false} + end; + true -> + {Res, true} + end, + MucState = #state{config = #config{anonymous = true}}, + JidArchiveS = jid:encode(jid:remove_resource(JidArchive)), + {lists:flatmap( + fun([TS, XML, PeerBin, Kind, Nick]) -> + case make_archive_el(JidArchiveS, + TS, + XML, + PeerBin, + Kind, + Nick, + MsgType, + JidRequestor, + JidArchive) of + {ok, El} -> + [{TS, binary_to_integer(TS), El}]; + {error, _} -> + [] + end; + ([User, TS, XML, PeerBin, Kind, Nick]) when User == LUser -> + case make_archive_el(JidArchiveS, + TS, + XML, + PeerBin, + Kind, + Nick, + MsgType, + JidRequestor, + JidArchive) of + {ok, El} -> + [{TS, binary_to_integer(TS), El}]; + {error, _} -> + [] + end; + ([User, TS, XML, PeerBin, Kind, Nick]) -> + case make_archive_el(User, + TS, + XML, + PeerBin, + Kind, + Nick, + {groupchat, member, MucState}, + JidRequestor, + jid:decode(User)) of + {ok, El} -> + mod_mam:wrap_as_mucsub([{TS, binary_to_integer(TS), El}], + JidRequestor); + {error, _} -> + [] + end + end, + Res1), + IsComplete, + binary_to_integer(Count)}; + _ -> + {[], false, 0} end. + export(_Server) -> [{archive_prefs, - fun(Host, #archive_prefs{us = + fun(Host, + #archive_prefs{ + us = {LUser, LServer}, - default = Default, - always = Always, - never = Never}) - when LServer == Host -> - SDefault = erlang:atom_to_binary(Default, utf8), - SAlways = misc:term_to_expr(Always), - SNever = misc:term_to_expr(Never), - [?SQL_INSERT( - "archive_prefs", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "def=%(SDefault)s", - "always=%(SAlways)s", - "never=%(SNever)s"])]; - (_Host, _R) -> + default = Default, + always = Always, + never = Never + }) + when LServer == Host -> + SDefault = erlang:atom_to_binary(Default, utf8), + SAlways = misc:term_to_expr(Always), + SNever = misc:term_to_expr(Never), + [?SQL_INSERT( + "archive_prefs", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "def=%(SDefault)s", + "always=%(SAlways)s", + "never=%(SNever)s"])]; + (_Host, _R) -> [] end}, {archive_msg, - fun([Host | HostTail], #archive_msg{us ={LUser, LServer}, - id = _ID, timestamp = TS, peer = Peer, - type = Type, nick = Nick, packet = Pkt, origin_id = OriginID}) - when (LServer == Host) or ([LServer] == HostTail) -> - TStmp = misc:now_to_usec(TS), - SUser = case Type of - chat -> LUser; - groupchat -> jid:encode({LUser, LServer, <<>>}) - end, - BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))), - LPeer = jid:encode(jid:tolower(Peer)), - XML = fxml:element_to_binary(Pkt), - Body = fxml:get_subtag_cdata(Pkt, <<"body">>), - SType = misc:atom_to_binary(Type), - SqlType = ejabberd_option:sql_type(Host), - case SqlType of - mssql -> [?SQL_INSERT( - "archive", - ["username=%(SUser)s", - "server_host=%(LServer)s", - "timestamp=%(TStmp)d", - "peer=%(LPeer)s", - "bare_peer=%(BarePeer)s", - "xml=N%(XML)s", - "txt=N%(Body)s", - "kind=%(SType)s", - "nick=%(Nick)s", - "origin_id=%(OriginID)s"])]; - _ -> [?SQL_INSERT( - "archive", - ["username=%(SUser)s", - "server_host=%(LServer)s", - "timestamp=%(TStmp)d", - "peer=%(LPeer)s", - "bare_peer=%(BarePeer)s", - "xml=%(XML)s", - "txt=%(Body)s", - "kind=%(SType)s", - "nick=%(Nick)s", - "origin_id=%(OriginID)s"])] - end; + fun([Host | HostTail], + #archive_msg{ + us = {LUser, LServer}, + id = _ID, + timestamp = TS, + peer = Peer, + type = Type, + nick = Nick, + packet = Pkt, + origin_id = OriginID + }) + when (LServer == Host) or ([LServer] == HostTail) -> + TStmp = misc:now_to_usec(TS), + SUser = case Type of + chat -> LUser; + groupchat -> jid:encode({LUser, LServer, <<>>}) + end, + BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))), + LPeer = jid:encode(jid:tolower(Peer)), + XML = fxml:element_to_binary(Pkt), + Body = fxml:get_subtag_cdata(Pkt, <<"body">>), + SType = misc:atom_to_binary(Type), + SqlType = ejabberd_option:sql_type(Host), + case SqlType of + mssql -> + [?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TStmp)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=N%(XML)s", + "txt=N%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])]; + _ -> + [?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TStmp)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=%(XML)s", + "txt=%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s", + "origin_id=%(OriginID)s"])] + end; (_Host, _R) -> [] end}]. + is_empty_for_user(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(1)d from archive" - " where username=%(LUser)s and %(LServer)H limit 1")) of - {selected, [{1}]} -> - false; - _ -> - true + LServer, + ?SQL("select @(1)d from archive" + " where username=%(LUser)s and %(LServer)H limit 1")) of + {selected, [{1}]} -> + false; + _ -> + true end. + is_empty_for_room(LServer, LName, LHost) -> LUser = jid:encode({LName, LHost, <<>>}), is_empty_for_user(LUser, LServer). + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -595,179 +727,223 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> {Max, Direction, ID} = get_max_direction_id(RSM), ODBCType = ejabberd_option:sql_type(LServer), ToString = fun(S) -> ejabberd_sql:to_string_literal(ODBCType, S) end, - LimitClause = if is_integer(Max), Max >= 0, ODBCType /= mssql -> - [<<" limit ">>, integer_to_binary(Max+1)]; - true -> - [] - end, - TopClause = if is_integer(Max), Max >= 0, ODBCType == mssql -> - [<<" TOP ">>, integer_to_binary(Max+1)]; - true -> - [] - end, - SubOrderClause = if LimitClause /= []; TopClause /= [] -> - <<" ORDER BY timestamp DESC ">>; - true -> - [] - end, - WithTextClause = if is_binary(WithText), WithText /= <<>> -> - [<<" and match (txt) against (">>, - ToString(WithText), <<")">>]; - true -> - [] - end, + LimitClause = if + is_integer(Max), Max >= 0, ODBCType /= mssql -> + [<<" limit ">>, integer_to_binary(Max + 1)]; + true -> + [] + end, + TopClause = if + is_integer(Max), Max >= 0, ODBCType == mssql -> + [<<" TOP ">>, integer_to_binary(Max + 1)]; + true -> + [] + end, + SubOrderClause = if + LimitClause /= []; TopClause /= [] -> + <<" ORDER BY timestamp DESC ">>; + true -> + [] + end, + WithTextClause = if + is_binary(WithText), WithText /= <<>> -> + [<<" and match (txt) against (">>, + ToString(WithText), + <<")">>]; + true -> + [] + end, WithClause = case catch jid:tolower(With) of - {_, _, <<>>} -> - [<<" and bare_peer=">>, - ToString(jid:encode(With))]; - {_, _, _} -> - [<<" and peer=">>, - ToString(jid:encode(With))]; - _ -> - [] - end, + {_, _, <<>>} -> + [<<" and bare_peer=">>, + ToString(jid:encode(With))]; + {_, _, _} -> + [<<" and peer=">>, + ToString(jid:encode(With))]; + _ -> + [] + end, PageClause = case catch binary_to_integer(ID) of - I when is_integer(I), I >= 0 -> - case Direction of - before -> - [<<" AND timestamp < ">>, ID]; - 'after' -> - [<<" AND timestamp > ">>, ID]; - _ -> - [] - end; - _ -> - [] - end, + I when is_integer(I), I >= 0 -> + case Direction of + before -> + [<<" AND timestamp < ">>, ID]; + 'after' -> + [<<" AND timestamp > ">>, ID]; + _ -> + [] + end; + _ -> + [] + end, StartClause = case Start of - {_, _, _} -> - [<<" and timestamp >= ">>, - integer_to_binary(misc:now_to_usec(Start))]; - _ -> - [] - end, + {_, _, _} -> + [<<" and timestamp >= ">>, + integer_to_binary(misc:now_to_usec(Start))]; + _ -> + [] + end, EndClause = case End of - {_, _, _} -> - [<<" and timestamp <= ">>, - integer_to_binary(misc:now_to_usec(End))]; - _ -> - [] - end, + {_, _, _} -> + [<<" and timestamp <= ">>, + integer_to_binary(misc:now_to_usec(End))]; + _ -> + [] + end, SUser = ToString(User), SServer = ToString(LServer), HostMatch = case ejabberd_sql:use_new_schema() of - true -> - [<<" and server_host=", SServer/binary>>]; - _ -> - <<"">> - end, + true -> + [<<" and server_host=", SServer/binary>>]; + _ -> + <<"">> + end, {UserSel, UserWhere} = case ExtraUsernames of - Users when is_list(Users) -> - EscUsers = [ToString(U) || U <- [User | Users]], - {<<" username,">>, - [<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]}; - subscribers_table -> - SJid = ToString(jid:encode({User, LServer, <<>>})), - RoomName = case ODBCType of - sqlite -> - <<"room || '@' || host">>; - _ -> - <<"concat(room, '@', host)">> - end, - {<<" username,">>, - [<<" (username = ">>, SUser, - <<" or username in (select ">>, RoomName, - <<" from muc_room_subscribers where jid=">>, SJid, HostMatch, <<"))">>]}; - _ -> - {<<>>, [<<" username=">>, SUser]} - end, + Users when is_list(Users) -> + EscUsers = [ ToString(U) || U <- [User | Users] ], + {<<" username,">>, + [<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]}; + subscribers_table -> + SJid = ToString(jid:encode({User, LServer, <<>>})), + RoomName = case ODBCType of + sqlite -> + <<"room || '@' || host">>; + _ -> + <<"concat(room, '@', host)">> + end, + {<<" username,">>, + [<<" (username = ">>, + SUser, + <<" or username in (select ">>, + RoomName, + <<" from muc_room_subscribers where jid=">>, + SJid, + HostMatch, + <<"))">>]}; + _ -> + {<<>>, [<<" username=">>, SUser]} + end, - Query = [<<"SELECT ">>, TopClause, UserSel, - <<" timestamp, xml, peer, kind, nick" - " FROM archive WHERE">>, UserWhere, HostMatch, - WithClause, WithTextClause, - StartClause, EndClause, PageClause], + Query = [<<"SELECT ">>, + TopClause, + UserSel, + <<" timestamp, xml, peer, kind, nick" + " FROM archive WHERE">>, + UserWhere, + HostMatch, + WithClause, + WithTextClause, + StartClause, + EndClause, + PageClause], QueryPage = - case Direction of - before -> - % ID can be empty because of - % XEP-0059: Result Set Management - % 2.5 Requesting the Last Page in a Result Set - [<<"SELECT">>, UserSel, <<" timestamp, xml, peer, kind, nick FROM (">>, - Query, SubOrderClause, - LimitClause, <<") AS t ORDER BY timestamp ASC;">>]; - _ -> - [Query, <<" ORDER BY timestamp ASC ">>, - LimitClause, <<";">>] - end, + case Direction of + before -> + % ID can be empty because of + % XEP-0059: Result Set Management + % 2.5 Requesting the Last Page in a Result Set + [<<"SELECT">>, + UserSel, + <<" timestamp, xml, peer, kind, nick FROM (">>, + Query, + SubOrderClause, + LimitClause, + <<") AS t ORDER BY timestamp ASC;">>]; + _ -> + [Query, + <<" ORDER BY timestamp ASC ">>, + LimitClause, + <<";">>] + end, {QueryPage, - [<<"SELECT COUNT(*) FROM archive WHERE ">>, UserWhere, - HostMatch, WithClause, WithTextClause, - StartClause, EndClause, <<";">>]}. + [<<"SELECT COUNT(*) FROM archive WHERE ">>, + UserWhere, + HostMatch, + WithClause, + WithTextClause, + StartClause, + EndClause, + <<";">>]}. + -spec get_max_direction_id(rsm_set() | undefined) -> - {integer() | undefined, - before | 'after' | undefined, - binary()}. + {integer() | undefined, + before | 'after' | undefined, + binary()}. get_max_direction_id(RSM) -> case RSM of - #rsm_set{max = Max, before = Before} when is_binary(Before) -> - {Max, before, Before}; - #rsm_set{max = Max, 'after' = After} when is_binary(After) -> - {Max, 'after', After}; - #rsm_set{max = Max} -> - {Max, undefined, <<>>}; - _ -> - {undefined, undefined, <<>>} + #rsm_set{max = Max, before = Before} when is_binary(Before) -> + {Max, before, Before}; + #rsm_set{max = Max, 'after' = After} when is_binary(After) -> + {Max, 'after', After}; + #rsm_set{max = Max} -> + {Max, undefined, <<>>}; + _ -> + {undefined, undefined, <<>>} end. --spec make_archive_el(binary(), binary(), binary(), binary(), binary(), - binary(), _, jid(), jid()) -> - {ok, xmpp_element()} | {error, invalid_jid | - invalid_timestamp | - invalid_xml}. + +-spec make_archive_el(binary(), + binary(), + binary(), + binary(), + binary(), + binary(), + _, + jid(), + jid()) -> + {ok, xmpp_element()} | + {error, invalid_jid | + invalid_timestamp | + invalid_xml}. make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchive) -> case xml_compress:decode(XML, User, Peer) of - #xmlel{} = El -> - try binary_to_integer(TS) of - TSInt -> - try jid:decode(Peer) of - PeerJID -> - Now = misc:usec_to_now(TSInt), - PeerLJID = jid:tolower(PeerJID), - T = case Kind of - <<"">> -> chat; - null -> chat; - _ -> misc:binary_to_atom(Kind) - end, - mod_mam:msg_to_el( - #archive_msg{timestamp = Now, - id = TS, - packet = El, - type = T, - nick = Nick, - peer = PeerLJID}, - MsgType, JidRequestor, JidArchive) - catch _:{bad_jid, _} -> - ?ERROR_MSG("Malformed 'peer' field with value " - "'~ts' detected for user ~ts in table " - "'archive': invalid JID", - [Peer, jid:encode(JidArchive)]), - {error, invalid_jid} - end - catch _:_ -> - ?ERROR_MSG("Malformed 'timestamp' field with value '~ts' " - "detected for user ~ts in table 'archive': " - "not an integer", - [TS, jid:encode(JidArchive)]), - {error, invalid_timestamp} - end; - {error, {_, Reason}} -> - ?ERROR_MSG("Malformed 'xml' field with value '~ts' detected " - "for user ~ts in table 'archive': ~ts", - [XML, jid:encode(JidArchive), Reason]), - {error, invalid_xml} + #xmlel{} = El -> + try binary_to_integer(TS) of + TSInt -> + try jid:decode(Peer) of + PeerJID -> + Now = misc:usec_to_now(TSInt), + PeerLJID = jid:tolower(PeerJID), + T = case Kind of + <<"">> -> chat; + null -> chat; + _ -> misc:binary_to_atom(Kind) + end, + mod_mam:msg_to_el( + #archive_msg{ + timestamp = Now, + id = TS, + packet = El, + type = T, + nick = Nick, + peer = PeerLJID + }, + MsgType, + JidRequestor, + JidArchive) + catch + _:{bad_jid, _} -> + ?ERROR_MSG("Malformed 'peer' field with value " + "'~ts' detected for user ~ts in table " + "'archive': invalid JID", + [Peer, jid:encode(JidArchive)]), + {error, invalid_jid} + end + catch + _:_ -> + ?ERROR_MSG("Malformed 'timestamp' field with value '~ts' " + "detected for user ~ts in table 'archive': " + "not an integer", + [TS, jid:encode(JidArchive)]), + {error, invalid_timestamp} + end; + {error, {_, Reason}} -> + ?ERROR_MSG("Malformed 'xml' field with value '~ts' detected " + "for user ~ts in table 'archive': ~ts", + [XML, jid:encode(JidArchive), Reason]), + {error, invalid_xml} end. diff --git a/src/mod_matrix_gw.erl b/src/mod_matrix_gw.erl index 54edafcf2..aa1c00f96 100644 --- a/src/mod_matrix_gw.erl +++ b/src/mod_matrix_gw.erl @@ -34,24 +34,43 @@ -behaviour(?GEN_SERVER). -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process/2, - start_link/1, - procname/1, - init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3, - depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). --export([parse_auth/1, encode_canonical_json/1, +-export([start/2, + stop/1, + reload/3, + process/2, + start_link/1, + procname/1, + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + depends/2, + mod_opt_type/1, + mod_options/1, + mod_doc/0]). +-export([parse_auth/1, + encode_canonical_json/1, is_canonical_json/1, get_id_domain_exn/1, - base64_decode/1, base64_encode/1, - prune_event/2, get_event_id/2, content_hash/1, - sign_event/3, sign_pruned_event/2, sign_json/2, - send_request/8, s2s_out_bounce_packet/2, user_receive_packet/1, - process_disco_info/1, - process_disco_items/1, + base64_decode/1, + base64_encode/1, + prune_event/2, + get_event_id/2, + content_hash/1, + sign_event/3, + sign_pruned_event/2, + sign_json/2, + send_request/8, + s2s_out_bounce_packet/2, + user_receive_packet/1, + process_disco_info/1, + process_disco_items/1, route/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("translate.hrl"). @@ -65,6 +84,7 @@ {<<"Access-Control-Allow-Methods">>, <<"GET, POST, PUT, DELETE, OPTIONS">>}, {<<"Access-Control-Allow-Headers">>, <<"X-Requested-With, Content-Type, Authorization">>}]). + process([<<"key">>, <<"v2">>, <<"server">> | _], #request{method = 'GET', host = _Host} = _Request) -> Host = ejabberd_config:get_myname(), @@ -73,22 +93,30 @@ process([<<"key">>, <<"v2">>, <<"server">> | _], ServerName = mod_matrix_gw_opt:matrix_domain(Host), TS = erlang:system_time(millisecond) + timer:hours(24 * 7), {PubKey, _PrivKey} = mod_matrix_gw_opt:key(Host), - JSON = #{<<"old_verify_keys">> => #{}, + JSON = #{ + <<"old_verify_keys">> => #{}, <<"server_name">> => ServerName, <<"valid_until_ts">> => TS, <<"verify_keys">> => #{ - KeyID => #{ - <<"key">> => base64_encode(PubKey) - } - }}, + KeyID => #{ + <<"key">> => base64_encode(PubKey) + } + } + }, SJSON = sign_json(Host, JSON), - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(SJSON)}; process([<<"federation">>, <<"v1">>, <<"version">>], #request{method = 'GET', host = _Host} = _Request) -> - JSON = #{<<"server">> => #{<<"name">> => <<"ejabberd/mod_matrix_gw">>, - <<"version">> => <<"0.1">>}}, - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + JSON = #{ + <<"server">> => #{ + <<"name">> => <<"ejabberd/mod_matrix_gw">>, + <<"version">> => <<"0.1">> + } + }, + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(JSON)}; process([<<"federation">>, <<"v1">>, <<"query">>, <<"profile">>], #request{method = 'GET', host = _Host} = Request) -> @@ -119,11 +147,15 @@ process([<<"federation">>, <<"v1">>, <<"user">>, <<"devices">>, UserID], #request{method = 'GET', host = _Host} = Request) -> case preprocess_federation_request(Request) of {ok, _JSON, _Origin} -> - Res = #{<<"devices">> => - [#{<<"device_id">> => <<"ejabberd/mod_matrix_gw">>, - <<"keys">> => []}], + Res = #{ + <<"devices">> => + [#{ + <<"device_id">> => <<"ejabberd/mod_matrix_gw">>, + <<"keys">> => [] + }], <<"stream_id">> => 1, - <<"user_id">> => UserID}, + <<"user_id">> => UserID + }, {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {result, HTTPResult} -> HTTPResult @@ -134,7 +166,8 @@ process([<<"federation">>, <<"v1">>, <<"user">>, <<"keys">>, <<"query">>], {ok, #{<<"device_keys">> := DeviceKeys}, _Origin} -> DeviceKeys2 = maps:map(fun(_Key, _) -> #{} end, DeviceKeys), Res = #{<<"device_keys">> => DeviceKeys2}, - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; @@ -144,13 +177,16 @@ process([<<"federation">>, <<"v1">>, <<"user">>, <<"keys">>, <<"query">>], process([<<"federation">>, <<"v2">>, <<"invite">>, RoomID, EventID], #request{method = 'PUT', host = _Host} = Request) -> case preprocess_federation_request(Request) of - {ok, #{<<"event">> := #{%<<"origin">> := Origin, + {ok, #{ + <<"event">> := #{ %<<"origin">> := Origin, <<"content">> := Content, <<"room_id">> := RoomID, <<"sender">> := Sender, - <<"state_key">> := UserID} = Event, - <<"room_version">> := RoomVer} = JSON, - Origin} -> + <<"state_key">> := UserID + } = Event, + <<"room_version">> := RoomVer + } = JSON, + Origin} -> case mod_matrix_gw_room:binary_to_room_version(RoomVer) of #room_version{} = RoomVersion -> %% TODO: check type and userid @@ -193,9 +229,11 @@ process([<<"federation">>, <<"v2">>, <<"invite">>, RoomID, EventID], process([<<"federation">>, <<"v1">>, <<"send">>, _TxnID], #request{method = 'PUT', host = _Host} = Request) -> case preprocess_federation_request(Request, false) of - {ok, #{<<"origin">> := Origin, - <<"pdus">> := PDUs} = JSON, - Origin} -> + {ok, #{ + <<"origin">> := Origin, + <<"pdus">> := PDUs + } = JSON, + Origin} -> ?DEBUG("send request ~p~n", [JSON]), Host = ejabberd_config:get_myname(), Res = lists:map( @@ -206,9 +244,11 @@ process([<<"federation">>, <<"v1">>, <<"send">>, _TxnID], {get_event_id(PDU, mod_matrix_gw_room:binary_to_room_version(<<"9">>)), #{<<"error">> => Error}} end - end, PDUs), + end, + PDUs), ?DEBUG("send res ~p~n", [Res]), - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(maps:from_list(Res))}; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; @@ -218,9 +258,11 @@ process([<<"federation">>, <<"v1">>, <<"send">>, _TxnID], process([<<"federation">>, <<"v1">>, <<"get_missing_events">>, RoomID], #request{method = 'POST', host = _Host} = Request) -> case preprocess_federation_request(Request, false) of - {ok, #{<<"earliest_events">> := EarliestEvents, - <<"latest_events">> := LatestEvents} = JSON, - Origin} -> + {ok, #{ + <<"earliest_events">> := EarliestEvents, + <<"latest_events">> := LatestEvents + } = JSON, + Origin} -> ?DEBUG("get_missing_events request ~p~n", [JSON]), Limit = maps:get(<<"limit">>, JSON, 10), MinDepth = maps:get(<<"min_depth">>, JSON, 0), @@ -229,7 +271,8 @@ process([<<"federation">>, <<"v1">>, <<"get_missing_events">>, RoomID], Host, Origin, RoomID, EarliestEvents, LatestEvents, Limit, MinDepth), ?DEBUG("get_missing_events res ~p~n", [PDUs]), Res = #{<<"events">> => PDUs}, - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {ok, _JSON, _Origin} -> {400, [], <<"400 Bad Request: invalid format">>}; @@ -255,14 +298,18 @@ process([<<"federation">>, <<"v1">>, <<"backfill">>, RoomID], _ -> [] end - end, LatestEvents), + end, + LatestEvents), PDUs = PDUs2 ++ PDUs1, ?DEBUG("backfill res ~p~n", [PDUs]), MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), - Res = #{<<"origin">> => MatrixServer, + Res = #{ + <<"origin">> => MatrixServer, <<"origin_server_ts">> => erlang:system_time(millisecond), - <<"pdus">> => PDUs}, - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + <<"pdus">> => PDUs + }, + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {result, HTTPResult} -> HTTPResult @@ -279,10 +326,13 @@ process([<<"federation">>, <<"v1">>, <<"state_ids">>, RoomID], Host = ejabberd_config:get_myname(), case mod_matrix_gw_room:get_state_ids(Host, Origin, RoomID, EventID) of {ok, AuthChain, PDUs} -> - Res = #{<<"auth_chain_ids">> => AuthChain, - <<"pdu_ids">> => PDUs}, + Res = #{ + <<"auth_chain_ids">> => AuthChain, + <<"pdu_ids">> => PDUs + }, ?DEBUG("get_state_ids res ~p~n", [Res]), - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {error, room_not_found} -> {400, [], <<"400 Bad Request: room not found">>}; @@ -314,17 +364,22 @@ process([<<"federation">>, <<"v1">>, <<"event">>, EventID], end; (_, Acc) -> Acc - end, undefined, mod_matrix_gw_room:get_rooms_list()), + end, + undefined, + mod_matrix_gw_room:get_rooms_list()), ?DEBUG("get_event res ~p~n", [PDU]), case PDU of undefined -> {400, [], <<"400 Bad Request: event not found">>}; _ -> MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), - Res = #{<<"origin">> => MatrixServer, + Res = #{ + <<"origin">> => MatrixServer, <<"origin_server_ts">> => erlang:system_time(millisecond), - <<"pdus">> => [PDU]}, - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + <<"pdus">> => [PDU] + }, + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)} end; {result, HTTPResult} -> @@ -339,29 +394,42 @@ process([<<"federation">>, <<"v1">>, <<"make_join">>, RoomID, UserID], Origin -> case mod_matrix_gw_room:make_join(Host, RoomID, UserID, Params) of {error, room_not_found} -> - Res = #{<<"errcode">> => <<"M_NOT_FOUND">>, - <<"error">> => <<"Unknown room">>}, - {404, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + Res = #{ + <<"errcode">> => <<"M_NOT_FOUND">>, + <<"error">> => <<"Unknown room">> + }, + {404, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {error, not_invited} -> - Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, - <<"error">> => <<"You are not invited to this room">>}, - {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + Res = #{ + <<"errcode">> => <<"M_FORBIDDEN">>, + <<"error">> => <<"You are not invited to this room">> + }, + {403, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {error, {incompatible_version, Ver}} -> - Res = #{<<"errcode">> => <<"M_INCOMPATIBLE_ROOM_VERSION">>, + Res = #{ + <<"errcode">> => <<"M_INCOMPATIBLE_ROOM_VERSION">>, <<"error">> => <<"Your homeserver does not support the features required to join this room">>, - <<"room_version">> => Ver}, - {400, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + <<"room_version">> => Ver + }, + {400, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {ok, Res} -> - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)} end; _ -> - Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, - <<"error">> => <<"User not from origin">>}, - {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + Res = #{ + <<"errcode">> => <<"M_FORBIDDEN">>, + <<"error">> => <<"User not from origin">> + }, + {403, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)} end; {result, HTTPResult} -> @@ -370,36 +438,49 @@ process([<<"federation">>, <<"v1">>, <<"make_join">>, RoomID, UserID], process([<<"federation">>, <<"v2">>, <<"send_join">>, RoomID, EventID], #request{method = 'PUT', host = _Host} = Request) -> case preprocess_federation_request(Request) of - {ok, #{<<"content">> := #{<<"membership">> := <<"join">>}, + {ok, #{ + <<"content">> := #{<<"membership">> := <<"join">>}, %<<"origin">> := Origin, <<"room_id">> := RoomID, <<"sender">> := Sender, <<"state_key">> := Sender, - <<"type">> := <<"m.room.member">>} = JSON, Origin} -> + <<"type">> := <<"m.room.member">> + } = JSON, + Origin} -> Host = ejabberd_config:get_myname(), case get_id_domain_exn(Sender) of Origin -> case mod_matrix_gw_room:send_join(Host, Origin, RoomID, EventID, JSON) of {error, Error} when is_binary(Error) -> - Res = #{<<"errcode">> => <<"M_BAD_REQUEST">>, - <<"error">> => Error}, - {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + Res = #{ + <<"errcode">> => <<"M_BAD_REQUEST">>, + <<"error">> => Error + }, + {403, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {ok, Res} -> ?DEBUG("send_join res: ~p~n", [Res]), - {200, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + {200, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)} end; _ -> - Res = #{<<"errcode">> => <<"M_FORBIDDEN">>, - <<"error">> => <<"User not from origin">>}, - {403, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + Res = #{ + <<"errcode">> => <<"M_FORBIDDEN">>, + <<"error">> => <<"User not from origin">> + }, + {403, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)} end; {ok, _JSON, _Origin} -> - Res = #{<<"errcode">> => <<"M_BAD_REQUEST">>, - <<"error">> => <<"Invalid event format">>}, - {400, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], + Res = #{ + <<"errcode">> => <<"M_BAD_REQUEST">>, + <<"error">> => <<"Invalid event format">> + }, + {400, + [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}], misc:json_encode(Res)}; {result, HTTPResult} -> HTTPResult @@ -415,17 +496,21 @@ process(Path, Request) -> ?DEBUG("matrix 404: ~p~n~p~n", [Path, Request]), ejabberd_web:error(not_found). + preprocess_federation_request(Request) -> preprocess_federation_request(Request, true). + preprocess_federation_request(Request, DoSignCheck) -> ?DEBUG("matrix federation: ~p~n", [Request]), case proplists:get_value('Authorization', Request#request.headers) of Auth when is_binary(Auth) -> case parse_auth(Auth) of - #{<<"origin">> := MatrixServer, + #{ + <<"origin">> := MatrixServer, <<"key">> := _, - <<"sig">> := _} = AuthParams -> + <<"sig">> := _ + } = AuthParams -> ?DEBUG("auth ~p~n", [AuthParams]), if Request#request.length =< ?MAX_REQUEST_SIZE -> @@ -451,8 +536,10 @@ preprocess_federation_request(Request, DoSignCheck) -> JSON -> Host = ejabberd_config:get_myname(), case mod_matrix_gw_s2s:check_auth( - Host, MatrixServer, - AuthParams, JSON, + Host, + MatrixServer, + AuthParams, + JSON, Request2) of true -> ?DEBUG("auth ok~n", []), @@ -472,47 +559,61 @@ preprocess_federation_request(Request, DoSignCheck) -> {result, {400, [], <<"400 Bad Request: no 'Authorization' header">>}} end. -recv_data(#request{length = Len, data = Trail, - sockmod = SockMod, socket = Socket} = Request) -> + +recv_data(#request{ + length = Len, + data = Trail, + sockmod = SockMod, + socket = Socket + } = Request) -> NewLen = Len - byte_size(Trail), - if NewLen > 0 -> - case SockMod:recv(Socket, NewLen, 60000) of - {ok, Data} -> + if + NewLen > 0 -> + case SockMod:recv(Socket, NewLen, 60000) of + {ok, Data} -> Request#request{data = <>}; - {error, _} -> Request - end; - true -> - Request + {error, _} -> Request + end; + true -> + Request end. --record(state, - {host :: binary(), - server_host :: binary()}). +-record(state, { + host :: binary(), + server_host :: binary() + }). -type state() :: #state{}. + start(Host, _Opts) -> case mod_matrix_gw_sup:start(Host) of - {ok, _} -> + {ok, _} -> {ok, [{hook, s2s_out_bounce_packet, s2s_out_bounce_packet, 50}, {hook, user_receive_packet, user_receive_packet, 50}]}; - Err -> - Err + Err -> + Err end. + stop(Host) -> Proc = mod_matrix_gw_sup:procname(Host), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). + reload(_Host, _NewOpts, _OldOpts) -> ok. + start_link(Host) -> Proc = procname(Host), - ?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host], - ejabberd_config:fsm_limit_opts([])). + ?GEN_SERVER:start_link({local, Proc}, + ?MODULE, + [Host], + ejabberd_config:fsm_limit_opts([])). + -spec init(list()) -> {ok, state()}. init([Host]) -> @@ -523,34 +624,45 @@ init([Host]) -> Opts = gen_mod:get_module_opts(Host, ?MODULE), MyHost = gen_mod:get_opt(host, Opts), register_routes(Host, [MyHost]), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, + MyHost, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, + MyHost, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items), {ok, #state{server_host = Host, host = MyHost}}. + -spec handle_call(term(), {pid(), term()}, state()) -> - {reply, ok | {ok, pid()} | {error, any()}, state()} | - {stop, normal, ok, state()}. + {reply, ok | {ok, pid()} | {error, any()}, state()} | + {stop, normal, ok, state()}. handle_call(stop, _From, State) -> {stop, normal, ok, State}. + -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -spec terminate(term(), state()) -> any(). terminate(_Reason, #state{host = Host}) -> unregister_routes([Host]), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS). + -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -559,25 +671,31 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. register_routes(ServerHost, Hosts) -> lists:foreach( fun(Host) -> - ejabberd_router:register_route( - Host, ServerHost, {apply, ?MODULE, route}) - end, Hosts). + ejabberd_router:register_route( + Host, ServerHost, {apply, ?MODULE, route}) + end, + Hosts). + unregister_routes(Hosts) -> lists:foreach( fun(Host) -> - ejabberd_router:unregister_route(Host) - end, Hosts). + ejabberd_router:unregister_route(Host) + end, + Hosts). + procname(Host) -> binary_to_atom( <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary>>, utf8). + parse_auth(<<"X-Matrix ", S/binary>>) -> parse_auth1(S, <<>>, []); parse_auth(_) -> error. + parse_auth1(<<$=, Cs/binary>>, S, Ts) -> parse_auth2(Cs, S, <<>>, Ts); parse_auth1(<<$,, Cs/binary>>, <<>>, Ts) -> parse_auth1(Cs, [], Ts); @@ -586,12 +704,14 @@ parse_auth1(<>, S, Ts) -> parse_auth1(Cs, <>, Ts); parse_auth1(<<>>, <<>>, T) -> maps:from_list(T); parse_auth1(<<>>, _S, _T) -> error. + parse_auth2(<<$", Cs/binary>>, Key, Val, Ts) -> parse_auth3(Cs, Key, Val, Ts); parse_auth2(<>, Key, Val, Ts) -> parse_auth4(Cs, Key, <>, Ts); parse_auth2(<<>>, _, _, _) -> error. + parse_auth3(<<$", Cs/binary>>, Key, Val, Ts) -> parse_auth4(Cs, Key, Val, Ts); parse_auth3(<<$\\, C, Cs/binary>>, Key, Val, Ts) -> @@ -600,6 +720,7 @@ parse_auth3(<>, Key, Val, Ts) -> parse_auth3(Cs, Key, <>, Ts); parse_auth3(<<>>, _, _, _) -> error. + parse_auth4(<<$,, Cs/binary>>, Key, Val, Ts) -> parse_auth1(Cs, <<>>, [{Key, Val} | Ts]); parse_auth4(<<$\s, Cs/binary>>, Key, Val, Ts) -> @@ -609,21 +730,40 @@ parse_auth4(<>, Key, Val, Ts) -> parse_auth4(<<>>, Key, Val, Ts) -> parse_auth1(<<>>, <<>>, [{Key, Val} | Ts]). + prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, RoomVersion) -> Keys = case RoomVersion#room_version.updated_redaction_rules of false -> - [<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>, - <<"state_key">>, <<"content">>, <<"hashes">>, - <<"signatures">>, <<"depth">>, <<"prev_events">>, - <<"prev_state">>, <<"auth_events">>, <<"origin">>, - <<"origin_server_ts">>, <<"membership">>]; + [<<"event_id">>, + <<"type">>, + <<"room_id">>, + <<"sender">>, + <<"state_key">>, + <<"content">>, + <<"hashes">>, + <<"signatures">>, + <<"depth">>, + <<"prev_events">>, + <<"prev_state">>, + <<"auth_events">>, + <<"origin">>, + <<"origin_server_ts">>, + <<"membership">>]; true -> - [<<"event_id">>, <<"type">>, <<"room_id">>, <<"sender">>, - <<"state_key">>, <<"content">>, <<"hashes">>, - <<"signatures">>, <<"depth">>, <<"prev_events">>, - <<"auth_events">>, <<"origin_server_ts">>] + [<<"event_id">>, + <<"type">>, + <<"room_id">>, + <<"sender">>, + <<"state_key">>, + <<"content">>, + <<"hashes">>, + <<"signatures">>, + <<"depth">>, + <<"prev_events">>, + <<"auth_events">>, + <<"origin_server_ts">>] end, Keys2 = case {RoomVersion#room_version.hydra, Type} of @@ -650,10 +790,14 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, C3; true -> case Content of - #{<<"third_party_invite">> := - #{<<"signed">> := InvSign}} -> - C3#{<<"third_party_invite">> => - #{<<"signed">> => InvSign}}; + #{ + <<"third_party_invite">> := + #{<<"signed">> := InvSign} + } -> + C3#{ + <<"third_party_invite">> => + #{<<"signed">> => InvSign} + }; _ -> C3 end @@ -676,15 +820,27 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, case RoomVersion#room_version.updated_redaction_rules of false -> maps:with( - [<<"ban">>, <<"events">>, <<"events_default">>, - <<"kick">>, <<"redact">>, <<"state_default">>, - <<"users">>, <<"users_default">>], Content); + [<<"ban">>, + <<"events">>, + <<"events_default">>, + <<"kick">>, + <<"redact">>, + <<"state_default">>, + <<"users">>, + <<"users_default">>], + Content); true -> maps:with( - [<<"ban">>, <<"events">>, <<"events_default">>, + [<<"ban">>, + <<"events">>, + <<"events_default">>, <<"invite">>, - <<"kick">>, <<"redact">>, <<"state_default">>, - <<"users">>, <<"users_default">>], Content) + <<"kick">>, + <<"redact">>, + <<"state_default">>, + <<"users">>, + <<"users_default">>], + Content) end; <<"m.room.history_visibility">> -> maps:with([<<"history_visibility">>], Content); @@ -701,41 +857,53 @@ prune_event(#{<<"type">> := Type, <<"content">> := Content} = Event, end, Event2#{<<"content">> := Content2}. + reference_hash(PrunedEvent) -> Event2 = maps:without([<<"signatures">>, <<"age_ts">>, <<"unsigned">>], PrunedEvent), S = encode_canonical_json(Event2), crypto:hash(sha256, S). + content_hash(Event) -> - Event2 = maps:without([<<"signatures">>, <<"age_ts">>, <<"unsigned">>, - <<"hashes">>, <<"outlier">>, <<"destinations">>], + Event2 = maps:without([<<"signatures">>, + <<"age_ts">>, + <<"unsigned">>, + <<"hashes">>, + <<"outlier">>, + <<"destinations">>], Event), S = encode_canonical_json(Event2), crypto:hash(sha256, S). + get_event_id(Event, RoomVersion) -> PrunedEvent = prune_event(Event, RoomVersion), get_pruned_event_id(PrunedEvent). + get_pruned_event_id(PrunedEvent) -> B = base64url_encode(reference_hash(PrunedEvent)), <<$$, B/binary>>. + encode_canonical_json(JSON) -> JSON2 = sort_json(JSON), misc:json_encode(JSON2). + sort_json(#{} = Map) -> Map2 = maps:map(fun(_K, V) -> sort_json(V) - end, Map), + end, + Map), {lists:sort(maps:to_list(Map2))}; sort_json(List) when is_list(List) -> lists:map(fun sort_json/1, List); sort_json(JSON) -> JSON. + is_canonical_json(N) when is_integer(N), -16#1FFFFFFFFFFFFF =< N, N =< 16#1FFFFFFFFFFFFF -> @@ -752,7 +920,9 @@ is_canonical_json(Map) when is_map(Map) -> is_canonical_json(V); (_K, _V, false) -> false - end, true, Map); + end, + true, + Map); is_canonical_json(List) when is_list(List) -> lists:all(fun is_canonical_json/1, List); is_canonical_json(_) -> @@ -769,16 +939,19 @@ base64_decode(B) -> end, base64:decode(Fixed). + base64_encode(B) -> D = base64:encode(B), K = binary:longest_common_suffix([D, <<"==">>]), binary:part(D, 0, size(D) - K). + base64url_encode(B) -> D = base64_encode(B), D1 = binary:replace(D, <<"+">>, <<"-">>, [global]), binary:replace(D1, <<"/">>, <<"_">>, [global]). + sign_event(Host, Event, RoomVersion) -> PrunedEvent = prune_event(Event, RoomVersion), case sign_pruned_event(Host, PrunedEvent) of @@ -786,10 +959,12 @@ sign_event(Host, Event, RoomVersion) -> Event#{<<"signatures">> => Signatures} end. + sign_pruned_event(Host, PrunedEvent) -> Event2 = maps:without([<<"age_ts">>, <<"unsigned">>], PrunedEvent), sign_json(Host, Event2). + sign_json(Host, JSON) -> Signatures = maps:get(<<"signatures">>, JSON, #{}), JSON2 = maps:without([<<"signatures">>, <<"unsigned">>], JSON), @@ -803,18 +978,24 @@ sign_json(Host, JSON) -> Signatures2 = Signatures#{SignatureName => #{KeyID => Sig64}}, JSON#{<<"signatures">> => Signatures2}. --spec send_request( - binary(), - get | post | put, - binary(), - [binary()], - [{binary(), binary()}], - none | #{atom() | binary() => misc:json_value()}, - [any()], - [any()]) -> {ok, any()} | {error, any()}. -send_request(Host, Method, MatrixServer, Path, Query, JSON, - HTTPOptions, Options) -> +-spec send_request(binary(), + get | post | put, + binary(), + [binary()], + [{binary(), binary()}], + none | #{atom() | binary() => misc:json_value()}, + [any()], + [any()]) -> {ok, any()} | {error, any()}. + +send_request(Host, + Method, + MatrixServer, + Path, + Query, + JSON, + HTTPOptions, + Options) -> URI1 = iolist_to_binary( lists:map(fun(P) -> [$/, misc:uri_quote(P)] end, Path)), URI = @@ -825,14 +1006,19 @@ send_request(Host, Method, MatrixServer, Path, Query, JSON, lists:map( fun({K, V}) -> iolist_to_binary( - [misc:uri_quote(K), $=, + [misc:uri_quote(K), + $=, misc:uri_quote(V)]) - end, Query), $&), + end, + Query), + $&), <> end, {MHost, MPort} = mod_matrix_gw_s2s:get_matrix_host_port(Host, MatrixServer), - URL = <<"https://", MHost/binary, - ":", (integer_to_binary(MPort))/binary, + URL = <<"https://", + MHost/binary, + ":", + (integer_to_binary(MPort))/binary, URI/binary>>, SMethod = case Method of @@ -863,9 +1049,11 @@ send_request(Host, Method, MatrixServer, Path, Query, JSON, ?DEBUG("httpc request res ~p", [HTTPRes]), HTTPRes. + make_auth_header(Host, MatrixServer, Method, URI, Content) -> Origin = mod_matrix_gw_opt:matrix_domain(Host), - JSON = #{<<"method">> => Method, + JSON = #{ + <<"method">> => Method, <<"uri">> => URI, <<"origin">> => Origin, <<"destination">> => MatrixServer @@ -883,12 +1071,14 @@ make_auth_header(Host, MatrixServer, Method, URI, Content) -> "\",sig=\"", Sig/binary, "\",", "destination=\"", MatrixServer/binary, "\"">>. + get_id_domain_exn(B) -> case binary:split(B, <<":">>) of [_, Tail] -> Tail; _ -> error({invalid_id, B}) end. + s2s_out_bounce_packet(S2SState, Pkt) -> #{server_host := Host} = S2SState, case mod_matrix_gw_opt:matrix_id_as_jid(Host) of @@ -904,6 +1094,7 @@ s2s_out_bounce_packet(S2SState, Pkt) -> {stop, ignore} end. + user_receive_packet({Pkt, C2SState} = Acc) -> #{lserver := Host} = C2SState, case mod_matrix_gw_opt:matrix_id_as_jid(Host) of @@ -928,40 +1119,58 @@ user_receive_packet({Pkt, C2SState} = Acc) -> end end. + -spec route(stanza()) -> ok. route(#iq{to = #jid{luser = <<"">>, lresource = <<"">>}} = IQ) -> ejabberd_router:process_iq(IQ); route(Pkt) -> mod_matrix_gw_room:route(Pkt). + -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_info(#iq{type = get, - sub_els = [#disco_info{node = <<"">>}]} = IQ) -> +process_disco_info(#iq{ + type = get, + sub_els = [#disco_info{node = <<"">>}] + } = IQ) -> Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC], - Identity = #identity{category = <<"gateway">>, - type = <<"matrix">>}, + Identity = #identity{ + category = <<"gateway">>, + type = <<"matrix">> + }, xmpp:make_iq_result( - IQ, #disco_info{features = Features, - identities = [Identity]}); -process_disco_info(#iq{type = get, lang = Lang, - sub_els = [#disco_info{}]} = IQ) -> + IQ, + #disco_info{ + features = Features, + identities = [Identity] + }); +process_disco_info(#iq{ + type = get, + lang = Lang, + sub_els = [#disco_info{}] + } = IQ) -> xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); process_disco_info(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_items(#iq{type = get, - sub_els = [#disco_items{node = <<>>}]} = IQ) -> +process_disco_items(#iq{ + type = get, + sub_els = [#disco_items{node = <<>>}] + } = IQ) -> xmpp:make_iq_result(IQ, #disco_items{}); -process_disco_items(#iq{type = get, lang = Lang, - sub_els = [#disco_items{}]} = IQ) -> +process_disco_items(#iq{ + type = get, + lang = Lang, + sub_els = [#disco_items{}] + } = IQ) -> xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); process_disco_items(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), @@ -971,6 +1180,7 @@ process_disco_items(#iq{lang = Lang} = IQ) -> depends(_Host, _Opts) -> []. + mod_opt_type(host) -> econf:host(); mod_opt_type(matrix_domain) -> @@ -979,7 +1189,7 @@ mod_opt_type(key_name) -> econf:binary(); mod_opt_type(key) -> fun(Key) -> - Key1 = (yconf:binary())(Key), + Key1 = (yconf:binary())(Key), Key2 = base64_decode(Key1), crypto:generate_key(eddsa, ed25519, Key2) end; @@ -990,6 +1200,7 @@ mod_opt_type(notary_servers) -> mod_opt_type(leave_timeout) -> econf:non_neg_int(). + -spec mod_options(binary()) -> [{key, {binary(), binary()}} | {atom(), any()}]. @@ -1002,8 +1213,10 @@ mod_options(Host) -> {notary_servers, []}, {leave_timeout, 0}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("https://matrix.org/[Matrix] gateway. "), ?T("Supports room versions 9, 10 and 11 since ejabberd 25.03; " "room versions 4 and higher since ejabberd 25.07; " @@ -1012,62 +1225,77 @@ mod_doc() -> ?T("This module is available since ejabberd 24.02.")], note => "improved in 25.08", example => - ["listen:", - " -", - " port: 8448", - " module: ejabberd_http", - " tls: true", - " request_handlers:", - " \"/_matrix\": mod_matrix_gw", - "", - "modules:", - " mod_matrix_gw:", - " key_name: \"key1\"", - " key: \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"", - " matrix_id_as_jid: true"], + ["listen:", + " -", + " port: 8448", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " \"/_matrix\": mod_matrix_gw", + "", + "modules:", + " mod_matrix_gw:", + " key_name: \"key1\"", + " key: \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"", + " matrix_id_as_jid: true"], opts => [{matrix_domain, - #{value => ?T("Domain"), + #{ + value => ?T("Domain"), desc => ?T("Specify a domain in the Matrix federation. " "The keyword '@HOST@' is replaced with the hostname. " - "The default value is '@HOST@'.")}}, - {host, - #{value => ?T("Host"), + "The default value is '@HOST@'.") + }}, + {host, + #{ + value => ?T("Host"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'host' option is not specified, the Jabber ID will be " - "the hostname of the virtual host with the prefix '\"matrix.\"'. " - "The keyword '@HOST@' is replaced with the real virtual host name.")}}, - {key_name, - #{value => "string()", + "the hostname of the virtual host with the prefix '\"matrix.\"'. " + "The keyword '@HOST@' is replaced with the real virtual host name.") + }}, + {key_name, + #{ + value => "string()", desc => - ?T("Name of the matrix signing key.")}}, - {key, - #{value => "string()", + ?T("Name of the matrix signing key.") + }}, + {key, + #{ + value => "string()", desc => - ?T("Value of the matrix signing key, in base64.")}}, - {matrix_id_as_jid, - #{value => "true | false", + ?T("Value of the matrix signing key, in base64.") + }}, + {matrix_id_as_jid, + #{ + value => "true | false", desc => ?T("If set to 'true', all packets failing to be delivered via an XMPP " - "server-to-server connection will then be routed to the Matrix gateway " - "by translating a Jabber ID 'user@matrixdomain.tld' to a Matrix user " - "identifier '@user:matrixdomain.tld'. When set to 'false', messages " - "must be explicitly sent to the matrix gateway service Jabber ID to be " - "routed to a remote Matrix server. In this case, to send a message to " - "Matrix user '@user:matrixdomain.tld', the client must send a message " - "to the JID 'user%matrixdomain.tld@matrix.myxmppdomain.tld', where " - "'matrix.myxmppdomain.tld' is the JID of the gateway service as set by the " - "'host' option. The default is 'false'.")}}, - {notary_servers, - #{value => "[Server, ...]", + "server-to-server connection will then be routed to the Matrix gateway " + "by translating a Jabber ID 'user@matrixdomain.tld' to a Matrix user " + "identifier '@user:matrixdomain.tld'. When set to 'false', messages " + "must be explicitly sent to the matrix gateway service Jabber ID to be " + "routed to a remote Matrix server. In this case, to send a message to " + "Matrix user '@user:matrixdomain.tld', the client must send a message " + "to the JID 'user%matrixdomain.tld@matrix.myxmppdomain.tld', where " + "'matrix.myxmppdomain.tld' is the JID of the gateway service as set by the " + "'host' option. The default is 'false'.") + }}, + {notary_servers, + #{ + value => "[Server, ...]", desc => - ?T("A list of notary servers.")}}, - {leave_timeout, - #{value => "integer()", + ?T("A list of notary servers.") + }}, + {leave_timeout, + #{ + value => "integer()", desc => - ?T("Delay in seconds between a user leaving a MUC room and sending 'leave' Matrix event.")}} - ] + ?T("Delay in seconds between a user leaving a MUC room and sending 'leave' Matrix event.") + }}] }. + + -endif. diff --git a/src/mod_matrix_gw_opt.erl b/src/mod_matrix_gw_opt.erl index 974aafd48..f8802104b 100644 --- a/src/mod_matrix_gw_opt.erl +++ b/src/mod_matrix_gw_opt.erl @@ -11,45 +11,51 @@ -export([matrix_id_as_jid/1]). -export([notary_servers/1]). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, host). --spec key(gen_mod:opts() | global | binary()) -> {binary(),binary()}. + +-spec key(gen_mod:opts() | global | binary()) -> {binary(), binary()}. key(Opts) when is_map(Opts) -> gen_mod:get_opt(key, Opts); key(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, key). + -spec key_name(gen_mod:opts() | global | binary()) -> binary(). key_name(Opts) when is_map(Opts) -> gen_mod:get_opt(key_name, Opts); key_name(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, key_name). + -spec leave_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). leave_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(leave_timeout, Opts); leave_timeout(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, leave_timeout). + -spec matrix_domain(gen_mod:opts() | global | binary()) -> binary(). matrix_domain(Opts) when is_map(Opts) -> gen_mod:get_opt(matrix_domain, Opts); matrix_domain(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_domain). + -spec matrix_id_as_jid(gen_mod:opts() | global | binary()) -> boolean(). matrix_id_as_jid(Opts) when is_map(Opts) -> gen_mod:get_opt(matrix_id_as_jid, Opts); matrix_id_as_jid(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, matrix_id_as_jid). + -spec notary_servers(gen_mod:opts() | global | binary()) -> [binary()]. notary_servers(Opts) when is_map(Opts) -> gen_mod:get_opt(notary_servers, Opts); notary_servers(Host) -> gen_mod:get_module_opt(Host, mod_matrix_gw, notary_servers). - diff --git a/src/mod_matrix_gw_room.erl b/src/mod_matrix_gw_room.erl index 65babf7e5..a7e98dbc8 100644 --- a/src/mod_matrix_gw_room.erl +++ b/src/mod_matrix_gw_room.erl @@ -28,16 +28,25 @@ -behaviour(gen_statem). %% API --export([start_link/2, supervisor/1, create_db/0, - get_room_pid/2, join_direct/5, process_pdu/3, - get_missing_events/7, get_state_ids/4, - get_rooms_list/0, get_event/3, - make_join/4, send_join/5, - create_new_room/3, room_add_event/3, +-export([start_link/2, + supervisor/1, + create_db/0, + get_room_pid/2, + join_direct/5, + process_pdu/3, + get_missing_events/7, + get_state_ids/4, + get_rooms_list/0, + get_event/3, + make_join/4, + send_join/5, + create_new_room/3, + room_add_event/3, binary_to_room_version/1, parse_user_id/1, send_muc_invite/7, - escape/1, unescape/1, + escape/1, + unescape/1, route/1]). %% gen_statem callbacks @@ -45,81 +54,92 @@ -export([handle_event/4]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("mod_matrix_gw.hrl"). --record(matrix_room, - {room_id :: binary(), - pid :: pid()}). +-record(matrix_room, { + room_id :: binary(), + pid :: pid() + }). --record(matrix_direct, - {local_remote, - room_id :: binary()}). +-record(matrix_direct, { + local_remote, + room_id :: binary() + }). --record(event, - {id :: binary(), - room_version :: #room_version{}, - room_id :: binary(), - type :: binary(), - state_key :: binary() | undefined, - depth :: integer(), - auth_events :: [binary()], - sender :: binary(), - prev_events :: [binary()], - origin_server_ts :: integer(), - json :: #{atom() | binary() => misc:json_value()}, - state_map}). +-record(event, { + id :: binary(), + room_version :: #room_version{}, + room_id :: binary(), + type :: binary(), + state_key :: binary() | undefined, + depth :: integer(), + auth_events :: [binary()], + sender :: binary(), + prev_events :: [binary()], + origin_server_ts :: integer(), + json :: #{atom() | binary() => misc:json_value()}, + state_map + }). --record(direct, - {local_user :: jid() | undefined, - remote_user :: binary() | undefined, - client_state}). +-record(direct, { + local_user :: jid() | undefined, + remote_user :: binary() | undefined, + client_state + }). --record(multi_user, - {join_ts :: integer(), - room_jid :: jid()}). +-record(multi_user, { + join_ts :: integer(), + room_jid :: jid() + }). --record(multi, - {users :: #{{binary(), binary()} => - ({online, #{binary() => #multi_user{}}} | - {offline, reference()})}}). +-record(multi, { + users :: #{ + {binary(), binary()} => + ({online, #{binary() => #multi_user{}}} | + {offline, reference()}) + } + }). --record(data, - {host :: binary(), - kind :: #direct{} | #multi{} | undefined, - room_id :: binary(), - room_jid :: jid(), - room_version :: #room_version{}, - via :: binary | undefined, - events = #{}, - latest_events = sets:new([{version, 2}]), - nonlatest_events = sets:new([{version, 2}]), - event_queue = treap:empty(), - outgoing_txns = #{}}). +-record(data, { + host :: binary(), + kind :: #direct{} | #multi{} | undefined, + room_id :: binary(), + room_jid :: jid(), + room_version :: #room_version{}, + via :: binary | undefined, + events = #{}, + latest_events = sets:new([{version, 2}]), + nonlatest_events = sets:new([{version, 2}]), + event_queue = treap:empty(), + outgoing_txns = #{} + }). --define(ROOM_CREATE, <<"m.room.create">>). --define(ROOM_MEMBER, <<"m.room.member">>). --define(ROOM_JOIN_RULES, <<"m.room.join_rules">>). --define(ROOM_POWER_LEVELS, <<"m.room.power_levels">>). --define(ROOM_3PI, <<"m.room.third_party_invite">>). --define(ROOM_MESSAGE, <<"m.room.message">>). +-define(ROOM_CREATE, <<"m.room.create">>). +-define(ROOM_MEMBER, <<"m.room.member">>). +-define(ROOM_JOIN_RULES, <<"m.room.join_rules">>). +-define(ROOM_POWER_LEVELS, <<"m.room.power_levels">>). +-define(ROOM_3PI, <<"m.room.third_party_invite">>). +-define(ROOM_MESSAGE, <<"m.room.message">>). -define(ROOM_HISTORY_VISIBILITY, <<"m.room.history_visibility">>). --define(ROOM_TOPIC, <<"m.room.topic">>). --define(ROOM_ALIASES, <<"m.room.aliases">>). +-define(ROOM_TOPIC, <<"m.room.topic">>). +-define(ROOM_ALIASES, <<"m.room.aliases">>). -define(CREATOR_PL, (1 bsl 53)). --define(MAX_DEPTH, 16#7FFFFFFFFFFFFFFF). +-define(MAX_DEPTH, 16#7FFFFFFFFFFFFFFF). -define(MAX_TXN_RETRIES, 5). --define(MATRIX_ROOM_ALIAS_CACHE, matrix_room_alias_cache). +-define(MATRIX_ROOM_ALIAS_CACHE, matrix_room_alias_cache). -define(MATRIX_ROOM_ALIAS_CACHE_ERROR_TIMEOUT, 60000). %%%=================================================================== %%% API %%%=================================================================== + %%-------------------------------------------------------------------- %% @doc %% Creates a gen_statem process which calls Module:init/1 to @@ -129,31 +149,37 @@ %% @end %%-------------------------------------------------------------------- -spec start_link(binary(), binary()) -> - {ok, Pid :: pid()} | - ignore | - {error, Error :: term()}. + {ok, Pid :: pid()} | + ignore | + {error, Error :: term()}. start_link(Host, RoomID) -> - gen_statem:start_link(?MODULE, [Host, RoomID], + gen_statem:start_link(?MODULE, + [Host, RoomID], ejabberd_config:fsm_limit_opts([])). + -spec supervisor(binary()) -> atom(). supervisor(Host) -> gen_mod:get_module_proc(Host, mod_matrix_gw_room_sup). + create_db() -> ejabberd_mnesia:create( - ?MODULE, matrix_room, + ?MODULE, + matrix_room, [{ram_copies, [node()]}, {type, set}, {attributes, record_info(fields, matrix_room)}]), ejabberd_mnesia:create( - ?MODULE, matrix_direct, + ?MODULE, + matrix_direct, [{ram_copies, [node()]}, {type, set}, {attributes, record_info(fields, matrix_direct)}]), ets_cache:new(?MATRIX_ROOM_ALIAS_CACHE), ok. + get_room_pid(Host, RoomID) -> case get_existing_room_pid(Host, RoomID) of {error, not_found} -> @@ -166,6 +192,7 @@ get_room_pid(Host, RoomID) -> {ok, Pid} end. + get_existing_room_pid(_Host, RoomID) -> case mnesia:dirty_read(matrix_room, RoomID) of [] -> @@ -174,6 +201,7 @@ get_existing_room_pid(_Host, RoomID) -> {ok, Pid} end. + join_direct(Host, MatrixServer, RoomID, Sender, UserID) -> case get_room_pid(Host, RoomID) of {ok, Pid} -> @@ -182,8 +210,12 @@ join_direct(Host, MatrixServer, RoomID, Sender, UserID) -> Error end. -route(#presence{from = From, to = #jid{luser = <>} = To, - type = Type} = Packet) + +route(#presence{ + from = From, + to = #jid{luser = <>} = To, + type = Type + } = Packet) when C == $!; C == $# -> Host = ejabberd_config:get_myname(), @@ -220,10 +252,13 @@ route(#presence{from = From, to = #jid{luser = <>} = To, ejabberd_router:route_error(Packet, Err), ok end; -route(#message{from = From, to = #jid{luser = <>} = To, - type = groupchat, - body = Body, - id = MsgID}) +route(#message{ + from = From, + to = #jid{luser = <>} = To, + type = groupchat, + body = Body, + id = MsgID + }) when C == $!; C == $# -> Host = ejabberd_config:get_myname(), @@ -240,12 +275,16 @@ route(#message{from = From, to = #jid{luser = <>} = To, case get_existing_room_pid(Host, RoomID) of {ok, Pid} -> JSON = - #{<<"content">> => - #{<<"body">> => Text, + #{ + <<"content">> => + #{ + <<"body">> => Text, <<"msgtype">> => <<"m.text">>, - <<"net.process-one.xmpp-id">> => MsgID}, + <<"net.process-one.xmpp-id">> => MsgID + }, <<"sender">> => UserID, - <<"type">> => ?ROOM_MESSAGE}, + <<"type">> => ?ROOM_MESSAGE + }, gen_statem:cast(Pid, {add_event, JSON}), ok; _ -> @@ -279,11 +318,15 @@ route(#message{from = From, to = To, body = Body} = _Pkt) -> FromMatrixID = <<$@, (From#jid.luser)/binary, $:, MatrixServer/binary>>, JSON = - #{<<"content">> => - #{<<"body">> => Text, - <<"msgtype">> => <<"m.text">>}, + #{ + <<"content">> => + #{ + <<"body">> => Text, + <<"msgtype">> => <<"m.text">> + }, <<"sender">> => FromMatrixID, - <<"type">> => ?ROOM_MESSAGE}, + <<"type">> => ?ROOM_MESSAGE + }, gen_statem:cast(Pid, {add_event, JSON}), ok; {error, _} -> @@ -298,31 +341,42 @@ route(#message{from = From, to = To, body = Body} = _Pkt) -> MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), FromMatrixID = <<$@, (From#jid.luser)/binary, $:, MatrixServer/binary>>, - gen_statem:cast(Pid, {create, MatrixServer, RoomID, - FromMatrixID, ToMatrixID}), + gen_statem:cast(Pid, + {create, MatrixServer, RoomID, + FromMatrixID, ToMatrixID}), JSONs = - [#{<<"content">> => - #{<<"creator">> => FromMatrixID, - <<"room_version">> => <<"9">>}, + [#{ + <<"content">> => + #{ + <<"creator">> => FromMatrixID, + <<"room_version">> => <<"9">> + }, <<"sender">> => FromMatrixID, <<"state_key">> => <<"">>, - <<"type">> => ?ROOM_CREATE}, - #{<<"content">> => + <<"type">> => ?ROOM_CREATE + }, + #{ + <<"content">> => #{<<"membership">> => <<"join">>}, <<"sender">> => FromMatrixID, <<"state_key">> => FromMatrixID, - <<"type">> => ?ROOM_MEMBER}, - #{<<"content">> => - #{<<"ban">> => 50, + <<"type">> => ?ROOM_MEMBER + }, + #{ + <<"content">> => + #{ + <<"ban">> => 50, <<"events">> => - #{<<"m.room.avatar">> => 50, + #{ + <<"m.room.avatar">> => 50, <<"m.room.canonical_alias">> => 50, <<"m.room.encryption">> => 100, <<"m.room.history_visibility">> => 100, <<"m.room.name">> => 50, <<"m.room.power_levels">> => 100, <<"m.room.server_acl">> => 100, - <<"m.room.tombstone">> => 100}, + <<"m.room.tombstone">> => 100 + }, <<"events_default">> => 0, <<"historical">> => 100, <<"invite">> => 0, @@ -330,39 +384,57 @@ route(#message{from = From, to = To, body = Body} = _Pkt) -> <<"redact">> => 50, <<"state_default">> => 50, <<"users">> => - #{FromMatrixID => 100, - ToMatrixID => 100}, - <<"users_default">> => 0}, + #{ + FromMatrixID => 100, + ToMatrixID => 100 + }, + <<"users_default">> => 0 + }, <<"sender">> => FromMatrixID, <<"state_key">> => <<"">>, - <<"type">> => ?ROOM_POWER_LEVELS}, - #{<<"content">> => #{<<"join_rule">> => <<"invite">>}, + <<"type">> => ?ROOM_POWER_LEVELS + }, + #{ + <<"content">> => #{<<"join_rule">> => <<"invite">>}, <<"sender">> => FromMatrixID, <<"state_key">> => <<"">>, - <<"type">> => ?ROOM_JOIN_RULES}, - #{<<"content">> => #{<<"history_visibility">> => <<"shared">>}, + <<"type">> => ?ROOM_JOIN_RULES + }, + #{ + <<"content">> => #{<<"history_visibility">> => <<"shared">>}, <<"sender">> => FromMatrixID, <<"state_key">> => <<"">>, - <<"type">> => ?ROOM_HISTORY_VISIBILITY}, - #{<<"content">> => #{<<"guest_access">> => <<"can_join">>}, + <<"type">> => ?ROOM_HISTORY_VISIBILITY + }, + #{ + <<"content">> => #{<<"guest_access">> => <<"can_join">>}, <<"sender">> => FromMatrixID, <<"state_key">> => <<"">>, - <<"type">> => <<"m.room.guest_access">>}, - #{<<"content">> => - #{<<"is_direct">> => true, - <<"membership">> => <<"invite">>}, + <<"type">> => <<"m.room.guest_access">> + }, + #{ + <<"content">> => + #{ + <<"is_direct">> => true, + <<"membership">> => <<"invite">> + }, <<"sender">> => FromMatrixID, <<"state_key">> => ToMatrixID, - <<"type">> => ?ROOM_MEMBER}, - #{<<"content">> => - #{<<"body">> => Text, - <<"msgtype">> => <<"m.text">>}, + <<"type">> => ?ROOM_MEMBER + }, + #{ + <<"content">> => + #{ + <<"body">> => Text, + <<"msgtype">> => <<"m.text">> + }, <<"sender">> => FromMatrixID, - <<"type">> => ?ROOM_MESSAGE} - ], + <<"type">> => ?ROOM_MESSAGE + }], lists:foreach(fun(JSON) -> gen_statem:cast(Pid, {add_event, JSON}) - end, JSONs), + end, + JSONs), ok; {error, _} -> %%TODO @@ -384,10 +456,14 @@ route(#iq{type = Type, lang = Lang, sub_els = [_]} = IQ0) -> {error, xmpp:err_not_allowed()}; {get, #disco_info{node = <<>>}} -> {result, - #disco_info{identities = - [#identity{category = <<"conference">>, - type = <<"text">>}], - features = [?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS]}}; + #disco_info{ + identities = + [#identity{ + category = <<"conference">>, + type = <<"text">> + }], + features = [?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS] + }}; {get, #disco_info{node = _}} -> {error, xmpp:err_item_not_found()}; {get, #disco_items{node = <<>>}} -> @@ -403,27 +479,31 @@ route(#iq{type = Type, lang = Lang, sub_els = [_]} = IQ0) -> {error, Error} -> ejabberd_router:route(xmpp:make_error(IQ, Error)) end - catch _:{xmpp_codec, Why} -> + catch + _:{xmpp_codec, Why} -> ErrTxt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(ErrTxt, Lang), - ejabberd_router:route_error(IQ0, Err), - ok + Err = xmpp:err_bad_request(ErrTxt, Lang), + ejabberd_router:route_error(IQ0, Err), + ok end; route(_) -> ok. + get_missing_events(Host, Origin, RoomID, EarliestEvents, LatestEvents, Limit, MinDepth) -> case get_existing_room_pid(Host, RoomID) of {ok, Pid} -> Events = gen_statem:call( - Pid, {get_missing_events, Origin, EarliestEvents, LatestEvents, - Limit, MinDepth}), - [E#event.json || E <- Events]; + Pid, + {get_missing_events, Origin, EarliestEvents, LatestEvents, + Limit, MinDepth}), + [ E#event.json || E <- Events ]; {error, _} -> %%TODO [] end. + get_state_ids(Host, Origin, RoomID, EventID) -> case get_existing_room_pid(Host, RoomID) of {ok, Pid} -> @@ -434,9 +514,11 @@ get_state_ids(Host, Origin, RoomID, EventID) -> {error, room_not_found} end. + get_rooms_list() -> mnesia:dirty_all_keys(matrix_room). + get_event(Host, RoomID, EventID) -> case get_existing_room_pid(Host, RoomID) of {ok, Pid} -> @@ -446,6 +528,7 @@ get_event(Host, RoomID, EventID) -> {error, room_not_found} end. + make_join(Host, RoomID, UserID, Params) -> case get_existing_room_pid(Host, RoomID) of {ok, Pid} -> @@ -454,6 +537,7 @@ make_join(Host, RoomID, UserID, Params) -> {error, room_not_found} end. + send_join(Host, Origin, RoomID, EventID, JSON) -> case process_pdu(Host, Origin, JSON) of {ok, EventID} -> @@ -464,10 +548,12 @@ send_join(Host, Origin, RoomID, EventID, JSON) -> StateMapJSON = lists:map(fun(EID) -> {ok, E} = get_event(Host, RoomID, EID), E end, StateMap), MyOrigin = mod_matrix_gw_opt:matrix_domain(Host), - Res = #{<<"event">> => EventJSON, + Res = #{ + <<"event">> => EventJSON, <<"state">> => StateMapJSON, <<"auth_chain">> => AuthChainJSON, - <<"origin">> => MyOrigin}, + <<"origin">> => MyOrigin + }, {ok, Res}; {ok, _} -> {error, <<"Bad event id">>}; @@ -475,18 +561,21 @@ send_join(Host, Origin, RoomID, EventID, JSON) -> Error end. + create_new_room(Host, XMPPID, MatrixID) -> RoomID = new_room_id(), case get_room_pid(Host, RoomID) of {ok, Pid} -> MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), - gen_statem:cast(Pid, {create, MatrixServer, RoomID, - XMPPID, MatrixID}), + gen_statem:cast(Pid, + {create, MatrixServer, RoomID, + XMPPID, MatrixID}), {ok, RoomID}; {error, _} = Error -> Error end. + room_add_event(Host, RoomID, Event) -> case get_existing_room_pid(Host, RoomID) of {ok, Pid} -> @@ -495,10 +584,12 @@ room_add_event(Host, RoomID, Event) -> {error, room_not_found} end. + %%%=================================================================== %%% gen_statem callbacks %%%=================================================================== + %%-------------------------------------------------------------------- %% @private %% @doc @@ -513,13 +604,18 @@ init([Host, RoomID]) -> {ok, RID} = room_id_to_xmpp(RoomID), RoomJID = jid:make(RID, ServiceHost), mnesia:dirty_write( - #matrix_room{room_id = RoomID, - pid = self()}), + #matrix_room{ + room_id = RoomID, + pid = self() + }), {ok, state_name, - #data{host = Host, + #data{ + host = Host, room_id = RoomID, room_jid = RoomJID, - room_version = binary_to_room_version(<<"9">>)}}. + room_version = binary_to_room_version(<<"9">>) + }}. + %%-------------------------------------------------------------------- %% @private @@ -528,10 +624,11 @@ init([Host, RoomID]) -> %% this function is called for every event a gen_statem receives. %% @end %%-------------------------------------------------------------------- --spec handle_event( - gen_statem:event_type(), Msg :: term(), - State :: term(), Data :: term()) -> - gen_statem:handle_event_result(). +-spec handle_event(gen_statem:event_type(), + Msg :: term(), + State :: term(), + Data :: term()) -> + gen_statem:handle_event_result(). handle_event({call, From}, get_room_version, _State, Data) -> {keep_state, Data, [{reply, From, Data#data.room_version}]}; handle_event({call, From}, get_latest_events, _State, Data) -> @@ -546,7 +643,8 @@ handle_event({call, From}, {partition_missed_events, EventIDs}, _State, Data) -> Res = lists:partition( fun(EventID) -> maps:is_key(EventID, Data#data.events) - end, EventIDs), + end, + EventIDs), {keep_state, Data, [{reply, From, Res}]}; handle_event({call, From}, {partition_events_with_statemap, EventIDs}, _State, Data) -> Res = lists:partition( @@ -556,7 +654,8 @@ handle_event({call, From}, {partition_events_with_statemap, EventIDs}, _State, D {ok, _} -> true; error -> false end - end, EventIDs), + end, + EventIDs), {keep_state, Data, [{reply, From, Res}]}; handle_event({call, From}, {auth_and_store_external_events, EventList}, _State, Data) -> try @@ -565,23 +664,27 @@ handle_event({call, From}, {auth_and_store_external_events, EventList}, _State, catch Class:Reason:ST -> ?INFO_MSG("failed auth_and_store_external_events: ~p", [{Class, Reason, ST}]), - {keep_state, Data, [{reply, From, {error, Reason}}, - {next_event, internal, update_client}]} + {keep_state, Data, + [{reply, From, {error, Reason}}, + {next_event, internal, update_client}]} end; handle_event({call, From}, {resolve_auth_store_event, Event}, _State, Data) -> try Data2 = do_resolve_auth_store_event(Event, Data), - {keep_state, Data2, [{reply, From, ok}, - {next_event, internal, update_client}]} + {keep_state, Data2, + [{reply, From, ok}, + {next_event, internal, update_client}]} catch Class:Reason:ST -> ?INFO_MSG("failed resolve_auth_store_event: ~p", [{Class, Reason, ST}]), - {keep_state, Data, [{reply, From, {error, Reason}}, - {next_event, internal, update_client}]} + {keep_state, Data, + [{reply, From, {error, Reason}}, + {next_event, internal, update_client}]} end; handle_event({call, From}, {get_missing_events, Origin, EarliestEvents, LatestEvents, Limit, MinDepth}, - _State, Data) -> + _State, + Data) -> try PDUs = do_get_missing_events(Origin, EarliestEvents, LatestEvents, Limit, MinDepth, Data), {keep_state_and_data, [{reply, From, PDUs}]} @@ -592,7 +695,8 @@ handle_event({call, From}, end; handle_event({call, From}, {get_state_ids, Origin, EventID}, - _State, Data) -> + _State, + Data) -> try Reply = do_get_state_ids(Origin, EventID, Data), {keep_state_and_data, [{reply, From, Reply}]} @@ -603,7 +707,8 @@ handle_event({call, From}, end; handle_event({call, From}, {get_event, EventID}, - _State, Data) -> + _State, + Data) -> try Reply = case maps:find(EventID, Data#data.events) of @@ -620,23 +725,28 @@ handle_event({call, From}, end; handle_event({call, From}, {make_join, UserID, Params}, - _State, Data) -> + _State, + Data) -> try Ver = (Data#data.room_version)#room_version.id, Reply = case lists:member({<<"ver">>, Ver}, Params) of true -> - JSON = #{<<"content">> => + JSON = #{ + <<"content">> => #{<<"membership">> => <<"join">>}, <<"sender">> => UserID, <<"state_key">> => UserID, - <<"type">> => ?ROOM_MEMBER}, + <<"type">> => ?ROOM_MEMBER + }, {JSON2, _} = fill_event(JSON, Data), Event = json_to_event(JSON2, Data#data.room_version), case check_event_auth(Event, Data) of true -> - Res = #{<<"event">> => JSON2, - <<"room_version">> => Ver}, + Res = #{ + <<"event">> => JSON2, + <<"room_version">> => Ver + }, {ok, Res}; false -> {error, not_invited} @@ -657,14 +767,22 @@ handle_event(cast, {join_direct, MatrixServer, RoomID, Sender, UserID}, State, D case user_id_to_jid(UserID, Data) of #jid{lserver = Host} = UserJID -> mnesia:dirty_write( - #matrix_direct{local_remote = {{UserJID#jid.luser, UserJID#jid.lserver}, Sender}, - room_id = RoomID}), + #matrix_direct{ + local_remote = {{UserJID#jid.luser, UserJID#jid.lserver}, Sender}, + room_id = RoomID + }), MakeJoinRes = mod_matrix_gw:send_request( - Host, get, MatrixServer, - [<<"_matrix">>, <<"federation">>, <<"v1">>, <<"make_join">>, - RoomID, UserID], - [{<<"ver">>, V} || V <- supported_versions()], + Host, + get, + MatrixServer, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"make_join">>, + RoomID, + UserID], + [ {<<"ver">>, V} || V <- supported_versions() ], none, [{timeout, 5000}], [{sync, true}, @@ -673,8 +791,10 @@ handle_event(cast, {join_direct, MatrixServer, RoomID, Sender, UserID}, State, D case MakeJoinRes of {ok, {{_, 200, _}, _Headers, Body}} -> try misc:json_decode(Body) of - #{<<"event">> := Event, - <<"room_version">> := SRoomVersion} -> + #{ + <<"event">> := Event, + <<"room_version">> := SRoomVersion + } -> case binary_to_room_version(SRoomVersion) of false -> ?DEBUG("unsupported room version on make_join: ~p", [MakeJoinRes]), @@ -682,22 +802,33 @@ handle_event(cast, {join_direct, MatrixServer, RoomID, Sender, UserID}, State, D #room_version{} = RoomVersion -> Origin = mod_matrix_gw_opt:matrix_domain(Host), Event2 = - Event#{<<"origin">> => Origin, - <<"origin_server_ts">> => - erlang:system_time(millisecond)}, + Event#{ + <<"origin">> => Origin, + <<"origin_server_ts">> => + erlang:system_time(millisecond) + }, CHash = mod_matrix_gw:content_hash(Event2), Event3 = - Event2#{<<"hashes">> => - #{<<"sha256">> => - mod_matrix_gw:base64_encode(CHash)}}, + Event2#{ + <<"hashes">> => + #{ + <<"sha256">> => + mod_matrix_gw:base64_encode(CHash) + } + }, Event4 = mod_matrix_gw:sign_event(Host, Event3, RoomVersion), EventID = mod_matrix_gw:get_event_id(Event4, RoomVersion), SendJoinRes = mod_matrix_gw:send_request( - Data#data.host, put, MatrixServer, - [<<"_matrix">>, <<"federation">>, - <<"v2">>, <<"send_join">>, - RoomID, EventID], + Data#data.host, + put, + MatrixServer, + [<<"_matrix">>, + <<"federation">>, + <<"v2">>, + <<"send_join">>, + RoomID, + EventID], [], Event4, [{connect_timeout, 5000}, @@ -706,11 +837,16 @@ handle_event(cast, {join_direct, MatrixServer, RoomID, Sender, UserID}, State, D {body_format, binary}]), ?DEBUG("send_join ~p~n", [SendJoinRes]), process_send_join_res( - MatrixServer, SendJoinRes, RoomVersion, + MatrixServer, + SendJoinRes, + RoomVersion, Data#data{ - kind = #direct{local_user = UserJID, - remote_user = Sender}, - room_version = RoomVersion}) + kind = #direct{ + local_user = UserJID, + remote_user = Sender + }, + room_version = RoomVersion + }) end; _JSON -> ?DEBUG("received bad JSON on make_join: ~p", [MakeJoinRes]), @@ -738,11 +874,13 @@ handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> case user_id_from_jid(UserJID, Host) of {ok, UserID} -> JoinTS = erlang:system_time(millisecond), - JSON = #{<<"content">> => + JSON = #{ + <<"content">> => #{<<"membership">> => <<"join">>}, <<"sender">> => UserID, <<"state_key">> => UserID, - <<"type">> => ?ROOM_MEMBER}, + <<"type">> => ?ROOM_MEMBER + }, Users = Kind#multi.users, Resources = case Users of @@ -758,11 +896,19 @@ handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> kind = Kind#multi{ users = - Users#{{LUser, LServer} => - {online, - Resources#{LResource => - #multi_user{join_ts = JoinTS, - room_jid = RoomJID}}}}}}, + Users#{ + {LUser, LServer} => + {online, + Resources#{ + LResource => + #multi_user{ + join_ts = JoinTS, + room_jid = RoomJID + } + }} + } + } + }, {keep_state, Data2, [{next_event, cast, {add_event, JSON}}]}; error -> ?INFO_MSG("bad join user id: ~p", [UserJID]), @@ -780,10 +926,16 @@ handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> MatrixServer when is_binary(MatrixServer) -> MakeJoinRes = mod_matrix_gw:send_request( - Host, get, MatrixServer, - [<<"_matrix">>, <<"federation">>, <<"v1">>, <<"make_join">>, - RoomID, UserID], - [{<<"ver">>, V} || V <- supported_versions()], + Host, + get, + MatrixServer, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"make_join">>, + RoomID, + UserID], + [ {<<"ver">>, V} || V <- supported_versions() ], none, [{timeout, 5000}], [{sync, true}, @@ -792,8 +944,10 @@ handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> case MakeJoinRes of {ok, {{_, 200, _}, _Headers, Body}} -> try misc:json_decode(Body) of - #{<<"event">> := Event, - <<"room_version">> := SRoomVersion} -> + #{ + <<"event">> := Event, + <<"room_version">> := SRoomVersion + } -> case binary_to_room_version(SRoomVersion) of false -> ?DEBUG("unsupported room version on make_join: ~p", [MakeJoinRes]), @@ -802,21 +956,32 @@ handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> JoinTS = erlang:system_time(millisecond), Origin = mod_matrix_gw_opt:matrix_domain(Host), Event2 = - Event#{<<"origin">> => Origin, - <<"origin_server_ts">> => JoinTS}, + Event#{ + <<"origin">> => Origin, + <<"origin_server_ts">> => JoinTS + }, CHash = mod_matrix_gw:content_hash(Event2), Event3 = - Event2#{<<"hashes">> => - #{<<"sha256">> => - mod_matrix_gw:base64_encode(CHash)}}, + Event2#{ + <<"hashes">> => + #{ + <<"sha256">> => + mod_matrix_gw:base64_encode(CHash) + } + }, Event4 = mod_matrix_gw:sign_event(Host, Event3, RoomVersion), EventID = mod_matrix_gw:get_event_id(Event4, RoomVersion), SendJoinRes = mod_matrix_gw:send_request( - Data#data.host, put, MatrixServer, - [<<"_matrix">>, <<"federation">>, - <<"v2">>, <<"send_join">>, - RoomID, EventID], + Data#data.host, + put, + MatrixServer, + [<<"_matrix">>, + <<"federation">>, + <<"v2">>, + <<"send_join">>, + RoomID, + EventID], [], Event4, [{connect_timeout, 5000}, @@ -826,15 +991,26 @@ handle_event(cast, {join, UserJID, Packet, Via}, _State, Data) -> RoomJID = jid:remove_resource(xmpp:get_to(Packet)), ?DEBUG("send_join ~p~n", [SendJoinRes]), process_send_join_res( - MatrixServer, SendJoinRes, RoomVersion, + MatrixServer, + SendJoinRes, + RoomVersion, Data#data{ kind = - #multi{users = - #{{LUser, LServer} => - {online, - #{LResource => #multi_user{join_ts = JoinTS, - room_jid = RoomJID}}}}}, - room_version = RoomVersion}) + #multi{ + users = + #{ + {LUser, LServer} => + {online, + #{ + LResource => #multi_user{ + join_ts = JoinTS, + room_jid = RoomJID + } + }} + } + }, + room_version = RoomVersion + }) end; _JSON -> ?DEBUG("received bad JSON on make_join: ~p", [MakeJoinRes]), @@ -887,7 +1063,8 @@ handle_event(cast, {leave, UserJID}, _State, Data) -> if Resources2 == #{} -> LeaveTimeout = mod_matrix_gw_opt:leave_timeout(Host) * 1000, - TimerRef = erlang:start_timer(LeaveTimeout, self(), + TimerRef = erlang:start_timer(LeaveTimeout, + self(), {leave, LUser, LServer}), Users2 = Users#{{LUser, LServer} => {offline, TimerRef}}, Kind = (Data#data.kind)#multi{users = Users2}, @@ -907,11 +1084,18 @@ handle_event(cast, {create, _MatrixServer, RoomID, LocalUserID, RemoteUserID}, _ case user_id_to_jid(LocalUserID, Data) of #jid{lserver = Host} = UserJID -> mnesia:dirty_write( - #matrix_direct{local_remote = {{UserJID#jid.luser, UserJID#jid.lserver}, RemoteUserID}, - room_id = RoomID}), + #matrix_direct{ + local_remote = {{UserJID#jid.luser, UserJID#jid.lserver}, RemoteUserID}, + room_id = RoomID + }), {keep_state, - Data#data{kind = #direct{local_user = UserJID, - remote_user = RemoteUserID}}, []}; + Data#data{ + kind = #direct{ + local_user = UserJID, + remote_user = RemoteUserID + } + }, + []}; UserJID -> ?INFO_MSG("bad create user id: ~p", [{LocalUserID, UserJID}]), {stop, normal} @@ -928,8 +1112,9 @@ handle_event(cast, {add_event, JSON}, _State, Data) -> handle_event({call, From}, {add_event, JSON}, _State, Data) -> try {Data2, Event} = add_event(JSON, Data), - {keep_state, Data2, [{reply, From, {ok, Event#event.id}}, - {next_event, internal, update_client}]} + {keep_state, Data2, + [{reply, From, {ok, Event#event.id}}, + {next_event, internal, update_client}]} catch Class:Reason:ST -> ?INFO_MSG("failed add_event: ~p", [{Class, Reason, ST}]), @@ -938,7 +1123,9 @@ handle_event({call, From}, {add_event, JSON}, _State, Data) -> handle_event(cast, Msg, State, Data) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {next_state, State, Data, []}; -handle_event(internal, update_client, _State, +handle_event(internal, + update_client, + _State, #data{kind = #direct{local_user = JID}} = Data) -> try case update_client(Data) of @@ -949,11 +1136,13 @@ handle_event(internal, update_client, _State, Host = Data#data.host, MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), LocalUserID = <<$@, (JID#jid.luser)/binary, $:, MatrixServer/binary>>, - JSON = #{<<"content">> => + JSON = #{ + <<"content">> => #{<<"membership">> => <<"leave">>}, <<"sender">> => LocalUserID, <<"state_key">> => LocalUserID, - <<"type">> => ?ROOM_MEMBER}, + <<"type">> => ?ROOM_MEMBER + }, {keep_state, Data2, [{next_event, cast, {add_event, JSON}}]}; stop -> {stop, normal} @@ -963,7 +1152,9 @@ handle_event(internal, update_client, _State, ?INFO_MSG("failed update_client: ~p", [{Class, Reason, ST}]), {keep_state_and_data, []} end; -handle_event(internal, update_client, _State, +handle_event(internal, + update_client, + _State, #data{kind = #multi{}} = Data) -> try case update_client(Data) of @@ -994,8 +1185,10 @@ handle_event(info, {send_txn_res, RequestID, TxnID, Server, Res}, _State, Data) Data2 = case Queue of [] -> - Data#data{outgoing_txns = - maps:remove(Server, Data#data.outgoing_txns)}; + Data#data{ + outgoing_txns = + maps:remove(Server, Data#data.outgoing_txns) + }; _ -> send_new_txn(lists:reverse(Queue), Server, Data) end, @@ -1023,11 +1216,13 @@ handle_event(info, {timeout, TimerRef, {leave, LUser, LServer}}, State, Data) -> Kind = (Data#data.kind)#multi{users = Users2}, Data2 = Data#data{kind = Kind}, {ok, UserID} = user_id_from_jid(jid:make(LUser, LServer), Host), - JSON = #{<<"content">> => + JSON = #{ + <<"content">> => #{<<"membership">> => <<"leave">>}, <<"sender">> => UserID, <<"state_key">> => UserID, - <<"type">> => ?ROOM_MEMBER}, + <<"type">> => ?ROOM_MEMBER + }, {keep_state, Data2, [{next_event, cast, {add_event, JSON}}]}; _ -> {next_state, State, Data, []} @@ -1036,6 +1231,7 @@ handle_event(info, Info, State, Data) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {next_state, State, Data, []}. + %%-------------------------------------------------------------------- %% @private %% @doc @@ -1046,57 +1242,71 @@ handle_event(info, Info, State, Data) -> %% @end %%-------------------------------------------------------------------- -spec terminate(Reason :: term(), State :: term(), Data :: term()) -> - any(). + any(). terminate(Reason, _State, Data) -> mnesia:dirty_delete_object( - #matrix_room{room_id = Data#data.room_id, - pid = self()}), + #matrix_room{ + room_id = Data#data.room_id, + pid = self() + }), %% TODO: wait for messages case Data#data.kind of - #direct{local_user = #jid{} = LocalUserJID, - remote_user = RemoteUser} -> + #direct{ + local_user = #jid{} = LocalUserJID, + remote_user = RemoteUser + } -> mnesia:dirty_delete_object( - #matrix_direct{local_remote = {{LocalUserJID#jid.luser, LocalUserJID#jid.lserver}, - RemoteUser}, - room_id = Data#data.room_id}); + #matrix_direct{ + local_remote = {{LocalUserJID#jid.luser, LocalUserJID#jid.lserver}, + RemoteUser}, + room_id = Data#data.room_id + }); _ -> ok end, ?INFO_MSG("terminated ~p: ~p", [Data#data.room_id, Reason]), ok. + %%-------------------------------------------------------------------- %% @private %% @doc %% Convert process state when code is changed %% @end %%-------------------------------------------------------------------- --spec code_change( - OldVsn :: term() | {down,term()}, - State :: term(), Data :: term(), Extra :: term()) -> - {ok, NewState :: term(), NewData :: term()}. +-spec code_change(OldVsn :: term() | {down, term()}, + State :: term(), + Data :: term(), + Extra :: term()) -> + {ok, NewState :: term(), NewData :: term()}. code_change(_OldVsn, State, Data, _Extra) -> {ok, State, Data}. + callback_mode() -> handle_event_function. + %%%=================================================================== %%% Internal functions %%%=================================================================== + get_event_exn(EventID, Data) -> maps:get(EventID, Data#data.events). + process_send_join_res(MatrixServer, SendJoinRes, RoomVersion, Data) -> case SendJoinRes of {ok, {{_, 200, _}, _Headers, Body}} -> try case misc:json_decode(Body) of - #{<<"auth_chain">> := JSONAuthChain, + #{ + <<"auth_chain">> := JSONAuthChain, <<"event">> := JSONEvent, - <<"state">> := JSONState} = JSON when is_list(JSONAuthChain), - is_list(JSONState) -> + <<"state">> := JSONState + } = JSON when is_list(JSONAuthChain), + is_list(JSONState) -> AuthChain = lists:map(fun(J) -> json_to_event(J, RoomVersion) end, JSONAuthChain), @@ -1117,36 +1327,51 @@ process_send_join_res(MatrixServer, SendJoinRes, RoomVersion, Data) -> {ok, _} -> ok; {error, Error} -> error(Error) end - end, [Event] ++ AuthChain ++ State) + end, + [Event] ++ AuthChain ++ State) end, CreateEvents = lists:filter( - fun(#event{type = ?ROOM_CREATE, - state_key = <<"">>}) -> true; + fun(#event{ + type = ?ROOM_CREATE, + state_key = <<"">> + }) -> true; (_) -> false - end, State), + end, + State), RoomVersionID = RoomVersion#room_version.id, case CreateEvents of [#event{ - id = CreateEventID, - json = - #{<<"content">> := - #{<<"room_version">> := - RoomVersionID}}} = + id = CreateEventID, + json = + #{ + <<"content">> := + #{ + <<"room_version">> := + RoomVersionID + } + } + } = CreateEvent] -> ?DEBUG("create event: ~p~n", [CreateEvent]), AuthCreateEvents = lists:filtermap( - fun(#event{id = ID, - type = ?ROOM_CREATE, - state_key = <<"">>}) -> + fun(#event{ + id = ID, + type = ?ROOM_CREATE, + state_key = <<"">> + }) -> {true, ID}; (_) -> false - end, AuthChain), + end, + AuthChain), case AuthCreateEvents of [CreateEventID] -> Data2 = process_send_join_res2( - MatrixServer, AuthChain, Event, State, + MatrixServer, + AuthChain, + Event, + State, Data), {keep_state, Data2, []}; _ -> @@ -1171,13 +1396,16 @@ process_send_join_res(MatrixServer, SendJoinRes, RoomVersion, Data) -> {stop, normal, Data} end. + process_send_join_res2(MatrixServer, AuthChain, Event, State, Data) -> Data2 = do_auth_and_store_external_events(AuthChain ++ State, Data), StateMap = lists:foldl( fun(E, Acc) -> Acc#{{E#event.type, E#event.state_key} => E#event.id} - end, #{}, State), + end, + #{}, + State), StateMap2 = case Event#event.state_key of undefined -> @@ -1194,17 +1422,24 @@ process_send_join_res2(MatrixServer, AuthChain, Event, State, Data) -> error({event_auth_error, Event2#event.id}) end, MissingEventsQuery = - #{<<"earliest_events">> => [], + #{ + <<"earliest_events">> => [], <<"latest_events">> => [Event#event.id], - <<"limit">> => 10}, + <<"limit">> => 10 + }, Host = Data3#data.host, Pid = self(), RoomID = Data3#data.room_id, RoomVersion = Data3#data.room_version, mod_matrix_gw:send_request( - Host, post, MatrixServer, - [<<"_matrix">>, <<"federation">>, <<"v1">>, - <<"get_missing_events">>, RoomID], + Host, + post, + MatrixServer, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"get_missing_events">>, + RoomID], [], MissingEventsQuery, [{timeout, 60000}], @@ -1214,12 +1449,17 @@ process_send_join_res2(MatrixServer, AuthChain, Event, State, Data) -> fun({_, Res}) -> spawn(fun() -> process_missing_events_res( - Host, MatrixServer, Pid, RoomID, RoomVersion, + Host, + MatrixServer, + Pid, + RoomID, + RoomVersion, {ok, Res}) end) end}]), Data3. + do_auth_and_store_external_events(EventList, Data) -> Events = maps:from_list(lists:map(fun(E) -> {E#event.id, E} end, EventList)), @@ -1236,12 +1476,16 @@ do_auth_and_store_external_events(EventList, Data) -> false -> error({event_auth_error, E}) end - end, Data, SortedEvents), + end, + Data, + SortedEvents), Data2. + auth_and_store_external_events(Pid, EventList) -> gen_statem:call(Pid, {auth_and_store_external_events, EventList}). + statemap_find(Key, StateMap, Data) -> case maps:find(Key, StateMap) of {ok, #event{}} = Res -> @@ -1252,6 +1496,7 @@ statemap_find(Key, StateMap, Data) -> error end. + check_event_auth(Event, Data) -> StateMap = maps:from_list( @@ -1259,9 +1504,11 @@ check_event_auth(Event, Data) -> fun(EID) -> E = get_event_exn(EID, Data), {{E#event.type, E#event.state_key}, E} - end, Event#event.auth_events)), + end, + Event#event.auth_events)), check_event_auth(Event, StateMap, Data). + check_event_auth(Event, StateMap, Data) -> RoomVersion = Data#data.room_version, case Event#event.type of @@ -1288,8 +1535,10 @@ check_event_auth(Event, StateMap, Data) -> case RoomVersion#room_version.implicit_room_creator of false -> case Event#event.json of - #{<<"content">> := - #{<<"creator">> := _}} -> + #{ + <<"content">> := + #{<<"creator">> := _} + } -> true; _ -> false @@ -1298,18 +1547,23 @@ check_event_auth(Event, StateMap, Data) -> case RoomVersion#room_version.hydra of true -> case Event#event.json of - #{<<"content">> := - #{<<"additional_creators">> := Creators}} when is_list(Creators) -> + #{ + <<"content">> := + #{<<"additional_creators">> := Creators} + } when is_list(Creators) -> lists:foreach( fun(C) -> case check_user_id(C) of true -> ok; false -> error(not_allowed) end - end, Creators), + end, + Creators), true; - #{<<"content">> := - #{<<"additional_creators">> := _}} -> + #{ + <<"content">> := + #{<<"additional_creators">> := _} + } -> false; _ -> true @@ -1330,8 +1584,10 @@ check_event_auth(Event, StateMap, Data) -> case Event#event.type of ?ROOM_MEMBER -> case Event#event.json of - #{<<"content">> := - #{<<"membership">> := Membership}} -> + #{ + <<"content">> := + #{<<"membership">> := Membership} + } -> %% TODO: join_authorised_via_users_server case Membership of <<"join">> -> @@ -1371,9 +1627,14 @@ check_event_auth(Event, StateMap, Data) -> Sender = Event#event.sender, case maps:find({?ROOM_MEMBER, Sender}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> case Event#event.type of ?ROOM_3PI -> SenderLevel = get_user_power_level(Event#event.sender, StateMap, Data), @@ -1408,6 +1669,7 @@ check_event_auth(Event, StateMap, Data) -> end end. + check_event_auth_join(Event, StateMap, Data) -> RoomVersion = Data#data.room_version, StateKey = Event#event.state_key, @@ -1426,27 +1688,45 @@ check_event_auth_join(Event, StateMap, Data) -> JoinRule = case maps:find({?ROOM_JOIN_RULES, <<"">>}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"join_rule">> := JR}}}} -> + json = #{ + <<"content">> := + #{<<"join_rule">> := JR} + } + }} -> JR; _ -> <<"invite">> end, case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"ban">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"ban">> + } + } + }} -> false; {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> true; {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - SenderMembership}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + SenderMembership + } + } + }} -> case {JoinRule, SenderMembership} of {<<"public">>, _} -> true; {<<"invite">>, <<"invite">>} -> true; @@ -1474,6 +1754,7 @@ check_event_auth_join(Event, StateMap, Data) -> end end. + check_event_auth_invite(Event, StateMap, Data) -> StateKey = Event#event.state_key, case Event#event.json of @@ -1483,19 +1764,34 @@ check_event_auth_invite(Event, StateMap, Data) -> _ -> case maps:find({?ROOM_MEMBER, Event#event.sender}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"ban">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"ban">> + } + } + }} -> false; {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> false; _ -> UserLevel = get_user_power_level(Event#event.sender, StateMap, Data), @@ -1512,12 +1808,16 @@ check_event_auth_invite(Event, StateMap, Data) -> end end. + check_event_auth_leave(Event, StateMap, Data) -> StateKey = Event#event.state_key, case maps:find({?ROOM_MEMBER, Event#event.sender}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := SenderMembership}}}} -> + json = #{ + <<"content">> := + #{<<"membership">> := SenderMembership} + } + }} -> case Event#event.sender of StateKey -> case SenderMembership of @@ -1533,9 +1833,14 @@ check_event_auth_leave(Event, StateMap, Data) -> CheckBan = case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"ban">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"ban">> + } + } + }} -> BanLevel = case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of {ok, #event{json = #{<<"content">> := #{<<"ban">> := S}}}} -> @@ -1567,12 +1872,16 @@ check_event_auth_leave(Event, StateMap, Data) -> false end. + check_event_auth_ban(Event, StateMap, Data) -> StateKey = Event#event.state_key, case maps:find({?ROOM_MEMBER, Event#event.sender}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := SenderMembership}}}} -> + json = #{ + <<"content">> := + #{<<"membership">> := SenderMembership} + } + }} -> case SenderMembership of <<"join">> -> SenderLevel = get_user_power_level(Event#event.sender, StateMap, Data), @@ -1591,6 +1900,7 @@ check_event_auth_ban(Event, StateMap, Data) -> false end. + check_event_auth_knock(Event, StateMap, Data) -> StateKey = Event#event.state_key, case Event#event.sender of @@ -1598,8 +1908,11 @@ check_event_auth_knock(Event, StateMap, Data) -> JoinRule = case maps:find({?ROOM_JOIN_RULES, <<"">>}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"join_rule">> := JR}}}} -> + json = #{ + <<"content">> := + #{<<"join_rule">> := JR} + } + }} -> JR; _ -> <<"invite">> @@ -1617,14 +1930,24 @@ check_event_auth_knock(Event, StateMap, Data) -> true -> case maps:find({?ROOM_MEMBER, StateKey}, StateMap) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"ban">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"ban">> + } + } + }} -> false; {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> false; _ -> true @@ -1636,6 +1959,7 @@ check_event_auth_knock(Event, StateMap, Data) -> false end. + check_event_power_level(Event, StateMap, Data) -> PLContent = case maps:find({?ROOM_POWER_LEVELS, <<"">>}, StateMap) of @@ -1657,6 +1981,7 @@ check_event_power_level(Event, StateMap, Data) -> false end. + get_event_power_level(Type, StateKey, PL) -> case {StateKey, PL} of {_, #{<<"events">> := #{Type := Level}}} -> @@ -1671,6 +1996,7 @@ get_event_power_level(Type, StateKey, PL) -> 50 end. + get_user_power_level(User, StateMap, Data) -> RoomVersion = Data#data.room_version, PL = @@ -1683,16 +2009,19 @@ get_user_power_level(User, StateMap, Data) -> RoomVersion#room_version.implicit_room_creator, statemap_find({?ROOM_CREATE, <<"">>}, StateMap, Data)} of {false, false, - {ok, #event{json = #{<<"content">> := #{<<"creator">> := User}}}}} -> + {ok, #event{json = #{<<"content">> := #{<<"creator">> := User}}}}} -> true; {false, true, {ok, #event{sender = User}}} -> true; {true, _, {ok, #event{sender = User}}} -> true; {true, _, - {ok, #event{ - json = #{<<"content">> := - #{<<"additional_creators">> := Creators}}}}} + {ok, #event{ + json = #{ + <<"content">> := + #{<<"additional_creators">> := Creators} + } + }}} when is_list(Creators) -> lists:member(User, Creators); _ -> @@ -1711,6 +2040,7 @@ get_user_power_level(User, StateMap, Data) -> 0 end. + check_event_auth_power_levels(Event, StateMap, Data) -> try case Event#event.json of @@ -1723,8 +2053,10 @@ check_event_auth_power_levels(Event, StateMap, Data) -> {ok, #event{sender = C} = E} -> Creators = case E#event.json of - #{<<"content">> := - #{<<"additional_creators">> := ACs}} -> + #{ + <<"content">> := + #{<<"additional_creators">> := ACs} + } -> [C | ACs]; _ -> [C] @@ -1756,8 +2088,13 @@ check_event_auth_power_levels(Event, StateMap, Data) -> _ -> ok end end, - [<<"users_default">>, <<"events_default">>, <<"state_default">>, - <<"ban">>, <<"redact">>, <<"kick">>, <<"invite">>]), + [<<"users_default">>, + <<"events_default">>, + <<"state_default">>, + <<"ban">>, + <<"redact">>, + <<"kick">>, + <<"invite">>]), lists:foreach( fun(Key) -> NewMap = maps:get(Key, NewPL, #{}), @@ -1767,7 +2104,9 @@ check_event_auth_power_levels(Event, StateMap, Data) -> is_integer(V) -> ok; true -> error(not_allowed) end - end, [], NewMap) + end, + [], + NewMap) end, CheckKeys); false -> @@ -1779,11 +2118,15 @@ check_event_auth_power_levels(Event, StateMap, Data) -> true -> ok; false -> error(not_allowed) end - end, ok, Users), + end, + ok, + Users), StateKey = Event#event.state_key, case StateMap of - #{{?ROOM_POWER_LEVELS, StateKey} := - #event{json = #{<<"content">> := OldPL}}} -> + #{ + {?ROOM_POWER_LEVELS, StateKey} := + #event{json = #{<<"content">> := OldPL}} + } -> UserLevel = get_user_power_level(Event#event.sender, StateMap, Data), lists:foreach( fun(Field) -> @@ -1793,8 +2136,13 @@ check_event_auth_power_levels(Event, StateMap, Data) -> false -> error(not_allowed) end end, - [<<"users_default">>, <<"events_default">>, <<"state_default">>, - <<"ban">>, <<"redact">>, <<"kick">>, <<"invite">>]), + [<<"users_default">>, + <<"events_default">>, + <<"state_default">>, + <<"ban">>, + <<"redact">>, + <<"kick">>, + <<"invite">>]), lists:foreach( fun(Key) -> OldMap = maps:get(Key, OldPL, #{}), @@ -1812,7 +2160,9 @@ check_event_auth_power_levels(Event, StateMap, Data) -> true -> ok; false -> error(not_allowed) end - end, [], maps:merge(OldMap, NewMap)) + end, + [], + maps:merge(OldMap, NewMap)) end, CheckKeys), true; @@ -1827,6 +2177,7 @@ check_event_auth_power_levels(Event, StateMap, Data) -> false end. + check_event_auth_power_levels_aux(Field, OldDict, NewDict, UserLevel, UserID) -> UserLevel2 = case UserID of @@ -1850,6 +2201,7 @@ check_event_auth_power_levels_aux(Field, OldDict, NewDict, UserLevel, UserID) -> end end. + check_user_id(S) -> case S of <<$@, Parts/binary>> -> @@ -1861,6 +2213,7 @@ check_user_id(S) -> false end. + parse_user_id(Str) -> case Str of <<$@, Parts/binary>> -> @@ -1872,9 +2225,11 @@ parse_user_id(Str) -> error end. + get_int(I) when is_integer(I) -> I; get_int(S) when is_binary(S) -> binary_to_integer(S). + fill_event(JSON, Data) -> Host = Data#data.host, MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), @@ -1884,7 +2239,8 @@ fill_event(JSON, Data) -> [0 | lists:map( fun(EID) -> (maps:get(EID, Data#data.events))#event.depth - end, PrevEvents)]), + end, + PrevEvents)]), Depth2 = min(Depth + 1, ?MAX_DEPTH), ?DEBUG("fill ~p", [{PrevEvents, Data#data.events}]), StateMaps = @@ -1898,7 +2254,8 @@ fill_event(JSON, Data) -> _ -> error({missed_prev_event, EID}) end - end, PrevEvents), + end, + PrevEvents), StateMap = resolve_state_maps(StateMaps, Data), AuthEvents = lists:usort( @@ -1911,22 +2268,29 @@ fill_event(JSON, Data) -> end, compute_event_auth_keys(JSON, Data#data.room_version))), ?DEBUG("auth_events ~p", [{AuthEvents, compute_event_auth_keys(JSON, Data#data.room_version)}]), - {JSON#{<<"auth_events">> => AuthEvents, - <<"depth">> => Depth2, - <<"origin">> => MatrixServer, - <<"origin_server_ts">> => erlang:system_time(millisecond), - <<"prev_events">> => PrevEvents, - <<"room_id">> => Data#data.room_id}, + {JSON#{ + <<"auth_events">> => AuthEvents, + <<"depth">> => Depth2, + <<"origin">> => MatrixServer, + <<"origin_server_ts">> => erlang:system_time(millisecond), + <<"prev_events">> => PrevEvents, + <<"room_id">> => Data#data.room_id + }, StateMap}. + add_event(JSON, Data) -> Host = Data#data.host, {Msg, StateMap} = fill_event(JSON, Data), CHash = mod_matrix_gw:content_hash(Msg), Msg2 = - Msg#{<<"hashes">> => - #{<<"sha256">> => - mod_matrix_gw:base64_encode(CHash)}}, + Msg#{ + <<"hashes">> => + #{ + <<"sha256">> => + mod_matrix_gw:base64_encode(CHash) + } + }, Msg3 = mod_matrix_gw:sign_event(Host, Msg2, Data#data.room_version), Event = json_to_event(Msg3, Data#data.room_version), StateMap2 = @@ -1965,10 +2329,12 @@ store_event(Event, Data) -> _ -> SeenEvents = Event#event.prev_events ++ Event#event.auth_events, LatestEs = - lists:foldl(fun(E, Acc) -> sets:del_element(E, Acc) end, Data2#data.latest_events, + lists:foldl(fun(E, Acc) -> sets:del_element(E, Acc) end, + Data2#data.latest_events, SeenEvents), NonLatestEs = - lists:foldl(fun(E, Acc) -> sets:add_element(E, Acc) end, Data2#data.nonlatest_events, + lists:foldl(fun(E, Acc) -> sets:add_element(E, Acc) end, + Data2#data.nonlatest_events, SeenEvents), LatestEs2 = case maps:is_key(Event#event.id, NonLatestEs) of @@ -1985,13 +2351,17 @@ store_event(Event, Data) -> Event#event.id, {erlang:monotonic_time(micro_seconds), erlang:unique_integer([monotonic])}, - [], Data2#data.event_queue), - Data2#data{events = Events#{Event#event.id => Event}, - latest_events = LatestEvents, - nonlatest_events = NonLatestEvents, - event_queue = EventQueue} + [], + Data2#data.event_queue), + Data2#data{ + events = Events#{Event#event.id => Event}, + latest_events = LatestEvents, + nonlatest_events = NonLatestEvents, + event_queue = EventQueue + } end. + simple_toposort(Events) -> {Res, _Used} = lists:foldl( @@ -2003,9 +2373,12 @@ simple_toposort(Events) -> true -> Acc end - end, {[], #{}}, maps:values(Events)), + end, + {[], #{}}, + maps:values(Events)), lists:reverse(Res). + simple_toposort_dfs(EventID, {Res, Used}, Events) -> case maps:find(EventID, Events) of error -> @@ -2024,12 +2397,15 @@ simple_toposort_dfs(EventID, {Res, Used}, Events) -> black -> Acc end - end, {Res, Used2}, Event#event.auth_events), + end, + {Res, Used2}, + Event#event.auth_events), Used9 = Used8#{EventID => black}, Res9 = [EventID | Res8], {Res9, Used9} end. + check_event_sig_and_hash(Host, Event) -> case check_event_signature(Host, Event) of true -> @@ -2046,30 +2422,39 @@ check_event_sig_and_hash(Host, Event) -> {error, {invalid_signature, Event#event.id}} end. + get_room_version(Pid) -> gen_statem:call(Pid, get_room_version). + partition_missed_events(Pid, EventIDs) -> gen_statem:call(Pid, {partition_missed_events, EventIDs}). + partition_events_with_statemap(Pid, EventIDs) -> gen_statem:call(Pid, {partition_events_with_statemap, EventIDs}). + get_latest_events(Pid) -> gen_statem:call(Pid, get_latest_events). + check_event_signature(Host, Event) -> PrunedEvent = mod_matrix_gw:prune_event(Event#event.json, Event#event.room_version), - mod_matrix_gw_s2s:check_signature(Host, PrunedEvent, + mod_matrix_gw_s2s:check_signature(Host, + PrunedEvent, Event#event.room_version). + find_event(Pid, EventID) -> gen_statem:call(Pid, {find_event, EventID}). + resolve_auth_store_event(Pid, Event) -> gen_statem:call(Pid, {resolve_auth_store_event, Event}). + process_pdu(Host, Origin, PDU) -> %% TODO: error handling #{<<"room_id">> := RoomID} = PDU, @@ -2081,7 +2466,7 @@ process_pdu(Host, Origin, PDU) -> true -> ?DEBUG("process pdu: ~p~n", [PDU]), {SeenEvents, MissedEvents} = - partition_missed_events(Pid, Event#event.prev_events), + partition_missed_events(Pid, Event#event.prev_events), ?DEBUG("seen/missed: ~p~n", [{SeenEvents, MissedEvents}]), case MissedEvents of [] -> @@ -2092,24 +2477,37 @@ process_pdu(Host, Origin, PDU) -> lists:foldl( fun(E, Acc) -> Acc#{E => []} - end, LatestEvents, SeenEvents), + end, + LatestEvents, + SeenEvents), ?DEBUG("earliest ~p~n", [EarliestEvents]), MissingEventsQuery = - #{<<"earliest_events">> => maps:keys(EarliestEvents), + #{ + <<"earliest_events">> => maps:keys(EarliestEvents), <<"latest_events">> => [Event#event.id], - <<"limit">> => 10}, + <<"limit">> => 10 + }, MissingEventsRes = mod_matrix_gw:send_request( - Host, post, Origin, - [<<"_matrix">>, <<"federation">>, <<"v1">>, - <<"get_missing_events">>, RoomID], + Host, + post, + Origin, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"get_missing_events">>, + RoomID], [], MissingEventsQuery, [{timeout, 60000}], [{sync, true}, {body_format, binary}]), ?DEBUG("missing res ~p~n", [MissingEventsRes]), - process_missing_events_res(Host, Origin, Pid, RoomID, RoomVersion, + process_missing_events_res(Host, + Origin, + Pid, + RoomID, + RoomVersion, MissingEventsRes), ok end, @@ -2122,7 +2520,12 @@ process_pdu(Host, Origin, PDU) -> {error, <<"Room doesn't exist">>} end. -process_missing_events_res(Host, Origin, Pid, RoomID, RoomVersion, + +process_missing_events_res(Host, + Origin, + Pid, + RoomID, + RoomVersion, {ok, {{_, 200, _}, _Headers, Body}}) -> try case misc:json_decode(Body) of @@ -2137,6 +2540,7 @@ process_missing_events_res(Host, Origin, Pid, RoomID, RoomVersion, process_missing_events_res(_Host, _Origin, _Pid, _RoomID, _RoomVersion, _) -> ok. + process_missing_events(Host, Origin, Pid, RoomID, RoomVersion, JSONEvents) -> Events = lists:map(fun(J) -> json_to_event(J, RoomVersion) end, JSONEvents), SortedEvents = lists:keysort(#event.depth, Events), @@ -2156,8 +2560,12 @@ process_missing_events(Host, Origin, Pid, RoomID, RoomVersion, JSONEvents) -> end, case ShouldProcess of true -> - fetch_prev_statemaps(Host, Origin, Pid, - RoomID, RoomVersion, Event), + fetch_prev_statemaps(Host, + Origin, + Pid, + RoomID, + RoomVersion, + Event), resolve_auth_store_event(Pid, Event), ok; false -> @@ -2166,9 +2574,11 @@ process_missing_events(Host, Origin, Pid, RoomID, RoomVersion, JSONEvents) -> {error, Reason} -> error(Reason) end - end, SortedEvents), + end, + SortedEvents), ok. + fetch_prev_statemaps(Host, Origin, Pid, RoomID, RoomVersion, Event) -> ?DEBUG("fetch_prev_statemaps ~p~n", [Event#event.id]), {SeenEvents, MissedEvents} = @@ -2185,7 +2595,9 @@ fetch_prev_statemaps(Host, Origin, Pid, RoomID, RoomVersion, Event) -> lists:foldl( fun(E, Acc) -> Acc#{{E#event.type, E#event.state_key} => E#event.id} - end, #{}, State), + end, + #{}, + State), auth_and_store_external_events( Pid, [MissedEvent#event{state_map = StateMap}]), ok; @@ -2196,14 +2608,20 @@ fetch_prev_statemaps(Host, Origin, Pid, RoomID, RoomVersion, Event) -> {error, Error} -> error(Error) end - end, MissedEvents). + end, + MissedEvents). + request_room_state(Host, Origin, _Pid, RoomID, RoomVersion, Event) -> Res = mod_matrix_gw:send_request( - Host, get, Origin, - [<<"_matrix">>, <<"federation">>, - <<"v1">>, <<"state">>, + Host, + get, + Origin, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"state">>, RoomID], [{<<"event_id">>, Event#event.id}], none, @@ -2215,9 +2633,11 @@ request_room_state(Host, Origin, _Pid, RoomID, RoomVersion, Event) -> {ok, {{_, 200, _}, _Headers, Body}} -> try case misc:json_decode(Body) of - #{<<"auth_chain">> := JSONAuthChain, - <<"pdus">> := JSONState} = _JSON when is_list(JSONAuthChain), - is_list(JSONState) -> + #{ + <<"auth_chain">> := JSONAuthChain, + <<"pdus">> := JSONState + } = _JSON when is_list(JSONAuthChain), + is_list(JSONState) -> AuthChain = lists:map(fun(J) -> json_to_event(J, RoomVersion) end, JSONAuthChain), @@ -2241,10 +2661,11 @@ request_room_state(Host, Origin, _Pid, RoomID, RoomVersion, Event) -> end; {error, Error} -> error(Error) end - end, AuthChain ++ State), + end, + AuthChain ++ State), ?DEBUG("req state ~p~n", - [{[E#event.id || E <- AuthChain], - [E#event.id || E <- State]}]), + [{[ E#event.id || E <- AuthChain ], + [ E#event.id || E <- State ]}]), {ok, AuthChain, State} end catch @@ -2258,12 +2679,17 @@ request_room_state(Host, Origin, _Pid, RoomID, RoomVersion, Event) -> {error, Reason} end. + request_event(Host, Origin, _Pid, RoomID, RoomVersion, EventID) -> Res = mod_matrix_gw:send_request( - Host, get, Origin, - [<<"_matrix">>, <<"federation">>, - <<"v1">>, <<"event">>, + Host, + get, + Origin, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"event">>, EventID], [], none, @@ -2299,6 +2725,7 @@ request_event(Host, Origin, _Pid, RoomID, RoomVersion, EventID) -> {error, Reason} end. + get_event_prev_state_map(Event, Data) -> StateMaps = lists:map( @@ -2311,9 +2738,11 @@ get_event_prev_state_map(Event, Data) -> _ -> error({missed_prev_event, EID}) end - end, Event#event.prev_events), + end, + Event#event.prev_events), resolve_state_maps(StateMaps, Data). + do_resolve_auth_store_event(Event, Data) -> StateMap = get_event_prev_state_map(Event, Data), StateMap2 = @@ -2332,6 +2761,7 @@ do_resolve_auth_store_event(Event, Data) -> error({event_auth_error, Event2#event.id}) end. + resolve_state_maps([], _Data) -> #{}; resolve_state_maps([StateMap], _Data) -> @@ -2354,7 +2784,7 @@ resolve_state_maps(StateMaps, Data) -> AuthDiff = calculate_auth_diff(StateMaps, Data), ?DEBUG("auth diff ~p~n", [AuthDiff]), FullConflictedSet = - maps:from_list([{E, []} || E <- AuthDiff ++ Conflicted]), + maps:from_list([ {E, []} || E <- AuthDiff ++ Conflicted ]), ?DEBUG("fcs ~p~n", [FullConflictedSet]), %% TODO: test PowerEvents = @@ -2362,7 +2792,8 @@ resolve_state_maps(StateMaps, Data) -> fun(EventID) -> Event = maps:get(EventID, Data#data.events), is_power_event(Event) - end, maps:keys(FullConflictedSet)), + end, + maps:keys(FullConflictedSet)), SortedPowerEvents = lexicographic_toposort(PowerEvents, FullConflictedSet, Data), ?DEBUG("spe ~p~n", [SortedPowerEvents]), StateMap = @@ -2374,7 +2805,7 @@ resolve_state_maps(StateMaps, Data) -> Unconflicted, iterative_auth_checks(SortedPowerEvents, #{}, Data)) end, - PowerEventsSet = maps:from_list([{E, []} || E <- SortedPowerEvents]), + PowerEventsSet = maps:from_list([ {E, []} || E <- SortedPowerEvents ]), OtherEvents = lists:filter(fun(E) -> not maps:is_key(E, PowerEventsSet) end, maps:keys(FullConflictedSet)), PLID = maps:get({?ROOM_POWER_LEVELS, <<"">>}, StateMap, undefined), @@ -2386,6 +2817,7 @@ resolve_state_maps(StateMaps, Data) -> Resolved end. + calculate_conflict(StateMaps) -> Keys = lists:usort(lists:flatmap(fun maps:keys/1, StateMaps)), lists:foldl( @@ -2394,7 +2826,8 @@ calculate_conflict(StateMaps) -> lists:usort( lists:map(fun(StateMap) -> maps:find(Key, StateMap) - end, StateMaps)), + end, + StateMaps)), case EventIDs of [{ok, EventID}] -> {Unconflicted#{Key => EventID}, Conflicted}; @@ -2403,22 +2836,26 @@ calculate_conflict(StateMaps) -> lists:flatmap( fun(error) -> []; ({ok, EventID}) -> [EventID] - end, EventIDs), + end, + EventIDs), {Unconflicted, Conflicted#{Key => EventIDs2}} end - end, {#{}, #{}}, Keys). + end, + {#{}, #{}}, + Keys). + calculate_conflicted_subgraph([], _Data) -> []; calculate_conflicted_subgraph(Events, Data) -> MinDepth = lists:min( - [(maps:get(EID, Data#data.events))#event.depth || EID <- Events]), + [ (maps:get(EID, Data#data.events))#event.depth || EID <- Events ]), AuthEvents = lists:append( - [(maps:get(EID, Data#data.events))#event.auth_events || EID <- Events]), + [ (maps:get(EID, Data#data.events))#event.auth_events || EID <- Events ]), Used0 = - maps:from_list([{E, true} || E <- Events]), + maps:from_list([ {E, true} || E <- Events ]), {Res, _Used} = lists:foldl( fun(EID, {_Res, Used} = Acc) -> @@ -2428,9 +2865,12 @@ calculate_conflicted_subgraph(Events, Data) -> true -> Acc end - end, {Events, Used0}, AuthEvents), + end, + {Events, Used0}, + AuthEvents), Res. + calculate_conflicted_subgraph_dfs(EventID, {Res, Used}, MinDepth, Data) -> case maps:find(EventID, Data#data.events) of error -> @@ -2459,7 +2899,9 @@ calculate_conflicted_subgraph_dfs(EventID, {Res, Used}, MinDepth, Data) -> _ -> {Res4, Used4, false} end - end, {Res, Used2, false}, Event#event.auth_events), + end, + {Res, Used2, false}, + Event#event.auth_events), Used9 = Used8#{EventID => Reachable}, Res9 = case Reachable of @@ -2490,12 +2932,16 @@ calculate_auth_diff(StateMaps, Data) -> end, Set2 = Set band bnot (1 bsl K), gb_trees:enter({Depth, EID}, Set2, Q2) - end, Q, StateMap) - end, gb_trees:empty(), + end, + Q, + StateMap) + end, + gb_trees:empty(), lists:zip(lists:seq(0, N - 1), StateMaps)), Count = lists:sum(gb_trees:values(Queue)), calculate_auth_diff_bfs(Queue, Count, [], Data). + calculate_auth_diff_bfs(_Queue, 0, Res, _Data) -> Res; calculate_auth_diff_bfs(Queue, Count, Res, Data) -> @@ -2513,6 +2959,7 @@ calculate_auth_diff_bfs(Queue, Count, Res, Data) -> calculate_auth_diff_bfs2(Event#event.auth_events, Set, Queue2, Count - Set, Res2, Data) end. + calculate_auth_diff_bfs2([], _Set, Queue, Count, Res, Data) -> calculate_auth_diff_bfs(Queue, Count, Res, Data); calculate_auth_diff_bfs2([EID | Events], Set, Queue, Count, Res, Data) -> @@ -2527,19 +2974,29 @@ calculate_auth_diff_bfs2([EID | Events], Set, Queue, Count, Res, Data) -> calculate_auth_diff_bfs2(Events, Set, Queue2, Count - Set2 + Set3, Res, Data) end. + is_power_event(#event{type = ?ROOM_POWER_LEVELS, state_key = <<"">>}) -> true; is_power_event(#event{type = ?ROOM_JOIN_RULES, state_key = <<"">>}) -> true; -is_power_event(#event{type = ?ROOM_MEMBER, state_key = StateKey, sender = Sender, - json = #{<<"content">> := #{<<"membership">> := <<"leave">>}}}) -> +is_power_event(#event{ + type = ?ROOM_MEMBER, + state_key = StateKey, + sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"leave">>}} + }) -> StateKey /= Sender; -is_power_event(#event{type = ?ROOM_MEMBER, state_key = StateKey, sender = Sender, - json = #{<<"content">> := #{<<"membership">> := <<"ban">>}}}) -> +is_power_event(#event{ + type = ?ROOM_MEMBER, + state_key = StateKey, + sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"ban">>}} + }) -> StateKey /= Sender; is_power_event(_) -> false. + lexicographic_toposort(EventIDs, EventSet, Data) -> {Used, Rev} = lists:foldl( @@ -2555,7 +3012,9 @@ lexicographic_toposort(EventIDs, EventSet, Data) -> false -> Acc end - end, {#{}, #{}}, EventIDs), + end, + {#{}, #{}}, + EventIDs), ?DEBUG("rev ~p~n", [Rev]), OutgoingCnt = maps:fold( @@ -2569,8 +3028,12 @@ lexicographic_toposort(EventIDs, EventSet, Data) -> false -> Acc2 end - end, Acc, maps:get(EventID, Rev, [])) - end, maps:map(fun(_, _) -> 0 end, Used), Used), + end, + Acc, + maps:get(EventID, Rev, [])) + end, + maps:map(fun(_, _) -> 0 end, Used), + Used), Current = maps:fold( fun(EventID, 0, Acc) -> @@ -2579,10 +3042,13 @@ lexicographic_toposort(EventIDs, EventSet, Data) -> gb_trees:enter({-PowerLevel, Event#event.origin_server_ts, EventID}, [], Acc); (_, _, Acc) -> Acc - end, gb_trees:empty(), OutgoingCnt), + end, + gb_trees:empty(), + OutgoingCnt), OutgoingCnt2 = maps:filter(fun(_, 0) -> false; (_, _) -> true end, OutgoingCnt), lexicographic_toposort_loop(Current, OutgoingCnt2, Rev, [], Data). + lexicographic_toposort_prepare(EventID, Used, Rev, EventSet, Data) -> Event = maps:get(EventID, Data#data.events), Used2 = Used#{EventID => []}, @@ -2592,7 +3058,9 @@ lexicographic_toposort_prepare(EventID, Used, Rev, EventSet, Data) -> true -> Rev4 = maps:update_with( EID, - fun(Es) -> [EventID | Es] end, [EventID], Rev3), + fun(Es) -> [EventID | Es] end, + [EventID], + Rev3), case maps:is_key(EID, Used3) of false -> lexicographic_toposort_prepare(EID, Used3, Rev4, EventSet, Data); @@ -2602,7 +3070,10 @@ lexicographic_toposort_prepare(EventID, Used, Rev, EventSet, Data) -> false -> Acc end - end, {Used2, Rev}, Event#event.auth_events). + end, + {Used2, Rev}, + Event#event.auth_events). + lexicographic_toposort_loop(Current, OutgoingCnt, Rev, Res, Data) -> %?DEBUG("toposort ~p", [{gb_trees:to_list(Current), OutgoingCnt, Res}]), @@ -2634,10 +3105,13 @@ lexicographic_toposort_loop(Current, OutgoingCnt, Rev, Res, Data) -> false -> Acc end - end, {OutgoingCnt, Current2}, maps:get(EventID, Rev, [])), + end, + {OutgoingCnt, Current2}, + maps:get(EventID, Rev, [])), lexicographic_toposort_loop(Current3, OutgoingCnt2, Rev, [EventID | Res], Data) end. + get_sender_power_level(EventID, Data) -> RoomVersion = Data#data.room_version, Event = maps:get(EventID, Data#data.events), @@ -2658,16 +3132,19 @@ get_sender_power_level(EventID, Data) -> 100; {_, false, undefined} -> 0; - {_, _, + {_, + _, #event{json = #{<<"content">> := #{<<"users">> := #{Sender := Level}}}}} -> get_int(Level); - {_, _, + {_, + _, #event{json = #{<<"content">> := #{<<"users_default">> := Level}}}} -> get_int(Level); _ -> 0 end. + iterative_auth_checks(Events, StateMap, Data) -> lists:foldl( fun(EventID, StateMap2) -> @@ -2682,7 +3159,9 @@ iterative_auth_checks(Events, StateMap, Data) -> false -> SM#{{E#event.type, E#event.state_key} => E#event.id} end - end, StateMap2, Event#event.auth_events), + end, + StateMap2, + Event#event.auth_events), %% TODO: not optimal StateMap4 = maps:map(fun(_, EID) -> maps:get(EID, Data#data.events) end, StateMap3), @@ -2692,7 +3171,10 @@ iterative_auth_checks(Events, StateMap, Data) -> false -> StateMap2 end - end, StateMap, Events). + end, + StateMap, + Events). + mainline_sort(OtherEvents, PLID, Data) -> IdxMap = mainline_sort_init(PLID, -1, #{}, Data), @@ -2702,9 +3184,12 @@ mainline_sort(OtherEvents, PLID, Data) -> Event = maps:get(EventID, Data#data.events), {Idx, IMap2} = mainline_sort_find(EventID, IMap, Data), {[{Idx, Event#event.origin_server_ts, EventID} | Events], IMap2} - end, {[], IdxMap}, OtherEvents), + end, + {[], IdxMap}, + OtherEvents), lists:map(fun({_, _, EID}) -> EID end, lists:sort(OtherEvents2)). + mainline_sort_init(undefined, _Idx, IdxMap, _Data) -> IdxMap; mainline_sort_init(PLID, Idx, IdxMap, Data) when is_binary(PLID) -> @@ -2712,6 +3197,7 @@ mainline_sort_init(PLID, Idx, IdxMap, Data) when is_binary(PLID) -> PLID2 = find_power_level_event(PLID, Data), mainline_sort_init(PLID2, Idx - 1, IdxMap2, Data). + mainline_sort_find(undefined, IdxMap, _Data) -> {0, IdxMap}; mainline_sort_find(EventID, IdxMap, Data) -> @@ -2724,6 +3210,7 @@ mainline_sort_find(EventID, IdxMap, Data) -> {Idx, IdxMap3} end. + find_power_level_event(EventID, Data) -> Event = maps:get(EventID, Data#data.events), lists:foldl( @@ -2735,7 +3222,10 @@ find_power_level_event(EventID, Data) -> end; (_, PLID) -> PLID - end, undefined, Event#event.auth_events). + end, + undefined, + Event#event.auth_events). + find_create_event(EventID, Data) -> Event = maps:get(EventID, Data#data.events), @@ -2748,7 +3238,10 @@ find_create_event(EventID, Data) -> end; (_, Create) -> Create - end, undefined, Event#event.auth_events). + end, + undefined, + Event#event.auth_events). + is_creator(EventID, User, Data) -> case find_create_event(EventID, Data) of @@ -2760,22 +3253,36 @@ is_creator(EventID, User, Data) -> RoomVersion#room_version.implicit_room_creator, CreateEvent} of {false, false, - #event{type = ?ROOM_CREATE, state_key = <<"">>, - json = #{<<"content">> := - #{<<"creator">> := User}}}} -> + #event{ + type = ?ROOM_CREATE, + state_key = <<"">>, + json = #{ + <<"content">> := + #{<<"creator">> := User} + } + }} -> true; {false, true, - #event{type = ?ROOM_CREATE, state_key = <<"">>, - sender = User}} -> + #event{ + type = ?ROOM_CREATE, + state_key = <<"">>, + sender = User + }} -> true; {true, _, - #event{type = ?ROOM_CREATE, state_key = <<"">>, - sender = User}} -> + #event{ + type = ?ROOM_CREATE, + state_key = <<"">>, + sender = User + }} -> true; {true, _, - #event{ - json = #{<<"content">> := - #{<<"additional_creators">> := Creators}}}} + #event{ + json = #{ + <<"content">> := + #{<<"additional_creators">> := Creators} + } + }} when is_list(Creators) -> lists:member(User, Creators); _ -> @@ -2785,168 +3292,191 @@ is_creator(EventID, User, Data) -> binary_to_room_version(<<"4">>) -> - #room_version{id = <<"4">>, - enforce_key_validity = false, - special_case_aliases_auth = true, - strict_canonicaljson = false, - limit_notifications_power_levels = false, - knock_join_rule = false, - restricted_join_rule = false, - restricted_join_rule_fix = false, - knock_restricted_join_rule = false, - enforce_int_power_levels = false, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"4">>, + enforce_key_validity = false, + special_case_aliases_auth = true, + strict_canonicaljson = false, + limit_notifications_power_levels = false, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"5">>) -> - #room_version{id = <<"5">>, - enforce_key_validity = true, - special_case_aliases_auth = true, - strict_canonicaljson = false, - limit_notifications_power_levels = false, - knock_join_rule = false, - restricted_join_rule = false, - restricted_join_rule_fix = false, - knock_restricted_join_rule = false, - enforce_int_power_levels = false, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"5">>, + enforce_key_validity = true, + special_case_aliases_auth = true, + strict_canonicaljson = false, + limit_notifications_power_levels = false, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"6">>) -> - #room_version{id = <<"6">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = false, - restricted_join_rule = false, - restricted_join_rule_fix = false, - knock_restricted_join_rule = false, - enforce_int_power_levels = false, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"6">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = false, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"7">>) -> - #room_version{id = <<"7">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = false, - restricted_join_rule_fix = false, - knock_restricted_join_rule = false, - enforce_int_power_levels = false, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"7">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = false, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"8">>) -> - #room_version{id = <<"8">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = true, - restricted_join_rule_fix = false, - knock_restricted_join_rule = false, - enforce_int_power_levels = false, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"8">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = false, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"9">>) -> - #room_version{id = <<"9">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = true, - restricted_join_rule_fix = true, - knock_restricted_join_rule = false, - enforce_int_power_levels = false, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"9">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = false, + enforce_int_power_levels = false, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"10">>) -> - #room_version{id = <<"10">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = true, - restricted_join_rule_fix = true, - knock_restricted_join_rule = true, - enforce_int_power_levels = true, - implicit_room_creator = false, - updated_redaction_rules = false, - hydra = false - }; + #room_version{ + id = <<"10">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = false, + updated_redaction_rules = false, + hydra = false + }; binary_to_room_version(<<"11">>) -> - #room_version{id = <<"11">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = true, - restricted_join_rule_fix = true, - knock_restricted_join_rule = true, - enforce_int_power_levels = true, - implicit_room_creator = true, - updated_redaction_rules = true, - hydra = false - }; + #room_version{ + id = <<"11">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = true, + updated_redaction_rules = true, + hydra = false + }; binary_to_room_version(<<"org.matrix.hydra.11">>) -> - #room_version{id = <<"org.matrix.hydra.11">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = true, - restricted_join_rule_fix = true, - knock_restricted_join_rule = true, - enforce_int_power_levels = true, - implicit_room_creator = true, - updated_redaction_rules = true, - hydra = true - }; + #room_version{ + id = <<"org.matrix.hydra.11">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = true, + updated_redaction_rules = true, + hydra = true + }; binary_to_room_version(<<"12">>) -> - #room_version{id = <<"12">>, - enforce_key_validity = true, - special_case_aliases_auth = false, - strict_canonicaljson = true, - limit_notifications_power_levels = true, - knock_join_rule = true, - restricted_join_rule = true, - restricted_join_rule_fix = true, - knock_restricted_join_rule = true, - enforce_int_power_levels = true, - implicit_room_creator = true, - updated_redaction_rules = true, - hydra = true - }; + #room_version{ + id = <<"12">>, + enforce_key_validity = true, + special_case_aliases_auth = false, + strict_canonicaljson = true, + limit_notifications_power_levels = true, + knock_join_rule = true, + restricted_join_rule = true, + restricted_join_rule_fix = true, + knock_restricted_join_rule = true, + enforce_int_power_levels = true, + implicit_room_creator = true, + updated_redaction_rules = true, + hydra = true + }; binary_to_room_version(_) -> false. -supported_versions() -> - [<<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"8">>, <<"9">>, - <<"10">>, <<"11">>, <<"org.matrix.hydra.11">>, <<"12">>]. -json_to_event(#{<<"type">> := Type, +supported_versions() -> + [<<"4">>, + <<"5">>, + <<"6">>, + <<"7">>, + <<"8">>, + <<"9">>, + <<"10">>, + <<"11">>, + <<"org.matrix.hydra.11">>, + <<"12">>]. + + +json_to_event(#{ + <<"type">> := Type, <<"depth">> := Depth, <<"auth_events">> := AuthEvents0, <<"sender">> := Sender, <<"prev_events">> := PrevEvents, - <<"origin_server_ts">> := OriginServerTS} = JSON, RoomVersion) + <<"origin_server_ts">> := OriginServerTS + } = JSON, + RoomVersion) when is_binary(Type), is_integer(Depth), is_list(AuthEvents0) -> @@ -2987,17 +3517,20 @@ json_to_event(#{<<"type">> := Type, false -> ok end, - #event{id = EventID, - room_version = RoomVersion, - room_id = RoomID, - type = Type, - state_key = StateKey, - depth = Depth, - auth_events = AuthEvents, - sender = Sender, - prev_events = PrevEvents, - origin_server_ts = OriginServerTS, - json = JSON}. + #event{ + id = EventID, + room_version = RoomVersion, + room_id = RoomID, + type = Type, + state_key = StateKey, + depth = Depth, + auth_events = AuthEvents, + sender = Sender, + prev_events = PrevEvents, + origin_server_ts = OriginServerTS, + json = JSON + }. + check_event_content_hash(Event) -> JSON = Event#event.json, @@ -3009,16 +3542,19 @@ check_event_content_hash(Event) -> false end. + notify_event(Event, Data) -> Data2 = notify_event_matrix(Event, Data), notify_event_xmpp(Event, Data2). -notify_event_matrix( - #event{type = ?ROOM_MEMBER, - state_key = StateKey, - sender = Sender, - json = #{<<"content">> := #{<<"membership">> := <<"invite">>}}} = Event, - #data{kind = #direct{}} = Data) -> + +notify_event_matrix(#event{ + type = ?ROOM_MEMBER, + state_key = StateKey, + sender = Sender, + json = #{<<"content">> := #{<<"membership">> := <<"invite">>}} + } = Event, + #data{kind = #direct{}} = Data) -> Host = Data#data.host, MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), case mod_matrix_gw:get_id_domain_exn(StateKey) of @@ -3026,7 +3562,8 @@ notify_event_matrix( Data; RemoteServer -> StrippedState = - maps:with([{?ROOM_CREATE, <<"">>}, {?ROOM_JOIN_RULES, <<"">>}, + maps:with([{?ROOM_CREATE, <<"">>}, + {?ROOM_JOIN_RULES, <<"">>}, {?ROOM_MEMBER, Sender}], Event#event.state_map), StrippedState2 = @@ -3035,16 +3572,24 @@ notify_event_matrix( E = maps:get(EID, Data#data.events), maps:with([<<"sender">>, <<"type">>, <<"state_key">>, <<"content">>], E#event.json) - end, StrippedState), - JSON = #{<<"event">> => Event#event.json, + end, + StrippedState), + JSON = #{ + <<"event">> => Event#event.json, <<"room_version">> => (Event#event.room_version)#room_version.id, - <<"invite_room_state">> => maps:values(StrippedState2)}, + <<"invite_room_state">> => maps:values(StrippedState2) + }, InviteRes = mod_matrix_gw:send_request( - Data#data.host, put, RemoteServer, - [<<"_matrix">>, <<"federation">>, - <<"v2">>, <<"invite">>, - Data#data.room_id, Event#event.id], + Data#data.host, + put, + RemoteServer, + [<<"_matrix">>, + <<"federation">>, + <<"v2">>, + <<"invite">>, + Data#data.room_id, + Event#event.id], [], JSON, [{timeout, 5000}], @@ -3082,8 +3627,10 @@ notify_event_matrix(#event{sender = Sender} = Event, Queue2 = [Event | Queue], DataAcc#data{ outgoing_txns = - maps:put(Server, {T, Queue2}, - DataAcc#data.outgoing_txns)}; + maps:put(Server, + {T, Queue2}, + DataAcc#data.outgoing_txns) + }; _ -> send_new_txn([Event], Server, DataAcc) end; @@ -3091,16 +3638,25 @@ notify_event_matrix(#event{sender = Sender} = Event, Data end end - end, Data, RemoteServers); + end, + Data, + RemoteServers); error -> Data end. -notify_event_xmpp( - #event{type = ?ROOM_MESSAGE, sender = Sender, - json = #{<<"content">> := #{<<"msgtype">> := <<"m.text">>, - <<"body">> := Body}}}, - #data{kind = #direct{local_user = UserJID}} = Data) -> + +notify_event_xmpp(#event{ + type = ?ROOM_MESSAGE, + sender = Sender, + json = #{ + <<"content">> := #{ + <<"msgtype">> := <<"m.text">>, + <<"body">> := Body + } + } + }, + #data{kind = #direct{local_user = UserJID}} = Data) -> case user_id_to_jid(Sender, Data) of #jid{} = SenderJID -> LSenderJID = jid:tolower(SenderJID), @@ -3110,34 +3666,47 @@ notify_event_xmpp( Data; _ -> RoomID = Data#data.room_id, - Msg = #message{from = SenderJID, - to = UserJID, - type = chat, - body = [#text{data = Body}], - sub_els = [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, <<"p1:matrix">>}, - {<<"room_id">>, RoomID}]}] - }, + Msg = #message{ + from = SenderJID, + to = UserJID, + type = chat, + body = [#text{data = Body}], + sub_els = [#xmlel{ + name = <<"x">>, + attrs = [{<<"xmlns">>, <<"p1:matrix">>}, + {<<"room_id">>, RoomID}] + }] + }, ejabberd_router:route(Msg), Data end; error -> Data end; -notify_event_xmpp( - #event{type = ?ROOM_MESSAGE, sender = Sender, - json = #{<<"content">> := #{<<"msgtype">> := <<"m.text">>, - <<"body">> := Body} = Content, - <<"origin_server_ts">> := OriginTS}}, - #data{kind = #multi{users = Users}} = Data) -> +notify_event_xmpp(#event{ + type = ?ROOM_MESSAGE, + sender = Sender, + json = #{ + <<"content">> := #{ + <<"msgtype">> := <<"m.text">>, + <<"body">> := Body + } = Content, + <<"origin_server_ts">> := OriginTS + } + }, + #data{kind = #multi{users = Users}} = Data) -> case Sender of <<$@, SenderUser/binary>> -> ?DEBUG("notify xmpp ~p", [Users]), maps:fold( fun({LUser, LServer}, {online, Resources}, ok) -> maps:fold( - fun(LResource, #multi_user{join_ts = JoinTS, - room_jid = RoomJID}, ok) + fun(LResource, + #multi_user{ + join_ts = JoinTS, + room_jid = RoomJID + }, + ok) when JoinTS =< OriginTS -> From = jid:replace_resource(RoomJID, SenderUser), UserJID = jid:make(LUser, LServer, LResource), @@ -3149,38 +3718,51 @@ notify_event_xmpp( _ -> <<"">> end, - Msg = #message{id = MsgID, - from = From, - to = UserJID, - type = groupchat, - body = [#text{data = Body}] - }, + Msg = #message{ + id = MsgID, + from = From, + to = UserJID, + type = groupchat, + body = [#text{data = Body}] + }, TimeStamp = misc:usec_to_now(OriginTS * 1000), TSMsg = misc:add_delay_info( Msg, Data#data.room_jid, TimeStamp), ejabberd_router:route(TSMsg); (_, _, _) -> ok - end, ok, Resources); + end, + ok, + Resources); (_, _, ok) -> ok - end, ok, Users), + end, + ok, + Users), Data; _ -> Data end; -notify_event_xmpp( - #event{type = ?ROOM_MEMBER, sender = Sender, - json = #{<<"content">> := #{<<"membership">> := <<"join">>}, - <<"origin_server_ts">> := OriginTS}} = Event, - #data{kind = #multi{users = Users}} = Data) -> +notify_event_xmpp(#event{ + type = ?ROOM_MEMBER, + sender = Sender, + json = #{ + <<"content">> := #{<<"membership">> := <<"join">>}, + <<"origin_server_ts">> := OriginTS + } + } = Event, + #data{kind = #multi{users = Users}} = Data) -> case user_id_to_jid(Sender, Data) of #jid{} = SenderJID -> <<$@, SenderUser/binary>> = Sender, maps:fold( fun({LUser, LServer}, {online, Resources}, ok) -> maps:fold( - fun(LResource, #multi_user{join_ts = JoinTS, - room_jid = RoomJID}, ok) + fun(LResource, + #multi_user{ + join_ts = JoinTS, + room_jid = RoomJID + }, + ok) when JoinTS =< OriginTS -> From = jid:replace_resource(RoomJID, SenderUser), IsSelfPresence = @@ -3201,12 +3783,14 @@ notify_event_xmpp( false -> [] end, Pres = #presence{ - from = From, - to = UserJID, - type = available, - sub_els = [#muc_user{items = [Item], - status_codes = Status}] - }, + from = From, + to = UserJID, + type = available, + sub_els = [#muc_user{ + items = [Item], + status_codes = Status + }] + }, ejabberd_router:route(Pres), case IsSelfPresence of true -> @@ -3224,29 +3808,36 @@ notify_event_xmpp( end, Subject = #message{ - from = RoomJID, - to = UserJID, - type = groupchat, - subject = [#text{data = Topic}] - }, + from = RoomJID, + to = UserJID, + type = groupchat, + subject = [#text{data = Topic}] + }, ejabberd_router:route(Subject); false -> ok end; (_, _, _) -> ok - end, ok, Resources); + end, + ok, + Resources); (_, _, ok) -> ok - end, ok, Users), + end, + ok, + Users), Data; error -> Data end; -notify_event_xmpp( - #event{type = ?ROOM_MEMBER, - state_key = StateKey, - json = #{<<"content">> := #{<<"membership">> := Membership}, - <<"origin_server_ts">> := OriginTS}}, - #data{kind = #multi{users = Users}} = Data) +notify_event_xmpp(#event{ + type = ?ROOM_MEMBER, + state_key = StateKey, + json = #{ + <<"content">> := #{<<"membership">> := Membership}, + <<"origin_server_ts">> := OriginTS + } + }, + #data{kind = #multi{users = Users}} = Data) when Membership == <<"leave">>; Membership == <<"ban">> -> case StateKey of @@ -3254,24 +3845,35 @@ notify_event_xmpp( maps:fold( fun({LUser, LServer}, {online, Resources}, ok) -> maps:fold( - fun(LResource, #multi_user{join_ts = JoinTS, - room_jid = RoomJID}, ok) + fun(LResource, + #multi_user{ + join_ts = JoinTS, + room_jid = RoomJID + }, + ok) when JoinTS =< OriginTS -> From = jid:replace_resource(RoomJID, RUser), UserJID = jid:make(LUser, LServer, LResource), - Item = #muc_item{affiliation = none, - role = none}, - Pres = #presence{from = From, - to = UserJID, - type = unavailable, - sub_els = [#muc_user{items = [Item]}] - }, + Item = #muc_item{ + affiliation = none, + role = none + }, + Pres = #presence{ + from = From, + to = UserJID, + type = unavailable, + sub_els = [#muc_user{items = [Item]}] + }, ejabberd_router:route(Pres); (_, _, _) -> ok - end, ok, Resources); + end, + ok, + Resources); (_, _, ok) -> ok - end, ok, Users), + end, + ok, + Users), case user_id_to_jid(StateKey, Data) of #jid{} = RJID -> US = {RJID#jid.luser, RJID#jid.lserver}, @@ -3281,13 +3883,17 @@ notify_event_xmpp( maps:fold( fun(_, #multi_user{join_ts = TS}, Acc) -> max(Acc, TS) - end, 0, Resources), + end, + 0, + Resources), if JoinTS =< OriginTS -> Users2 = maps:remove(US, Users), Data#data{ kind = (Data#data.kind)#multi{ - users = Users2}}; + users = Users2 + } + }; true -> Data end; @@ -3303,24 +3909,29 @@ notify_event_xmpp( notify_event_xmpp(_Event, Data) -> Data. + send_initial_presences(JID, RoomJID, Event, Data) -> ?DEBUG("send_initial_presences ~p", [{JID, Event}]), maps:fold( fun({?ROOM_MEMBER, _}, EID, ok) -> case maps:find(EID, Data#data.events) of {ok, #event{ - sender = <<$@, SenderUser/binary>> = Sender, - json = #{<<"content">> := - #{<<"membership">> := <<"join">>}}}} -> + sender = <<$@, SenderUser/binary>> = Sender, + json = #{ + <<"content">> := + #{<<"membership">> := <<"join">>} + } + }} -> From = jid:replace_resource(RoomJID, SenderUser), Item = get_user_muc_item( Sender, Event#event.state_map, Data), - Pres = #presence{from = From, - to = JID, - type = available, - sub_els = [#muc_user{items = [Item]}] - }, + Pres = #presence{ + from = From, + to = JID, + type = available, + sub_els = [#muc_user{items = [Item]}] + }, ejabberd_router:route(Pres), ok; _ -> @@ -3328,7 +3939,10 @@ send_initial_presences(JID, RoomJID, Event, Data) -> end; (_, _, ok) -> ok - end, ok, Event#event.state_map). + end, + ok, + Event#event.state_map). + get_user_muc_item(User, StateMap, Data) -> SenderLevel = get_user_power_level(User, StateMap, Data), @@ -3340,11 +3954,15 @@ get_user_muc_item(User, StateMap, Data) -> end, if SenderLevel >= BanLevel -> - #muc_item{affiliation = admin, - role = moderator}; + #muc_item{ + affiliation = admin, + role = moderator + }; true -> - #muc_item{affiliation = member, - role = participant} + #muc_item{ + affiliation = member, + role = participant + } end. @@ -3352,6 +3970,7 @@ send_new_txn(Events, Server, Data) -> TxnID = p1_rand:get_string(), send_txn(TxnID, Events, Server, 1, [], Data). + send_txn(TxnID, Events, Server, Count, Queue, Data) -> ?DEBUG("send txn ~p~n", [{TxnID, Server}]), Host = Data#data.host, @@ -3359,10 +3978,12 @@ send_txn(TxnID, Events, Server, Count, Queue, Data) -> PDUs = lists:map(fun(E) -> E#event.json end, Events), Body = - #{<<"origin">> => Origin, + #{ + <<"origin">> => Origin, <<"origin_server_ts">> => erlang:system_time(millisecond), - <<"pdus">> => PDUs}, + <<"pdus">> => PDUs + }, Self = self(), Receiver = fun({RequestID, Res}) -> @@ -3371,23 +3992,31 @@ send_txn(TxnID, Events, Server, Count, Queue, Data) -> end, {ok, RequestID} = mod_matrix_gw:send_request( - Host, put, Server, - [<<"_matrix">>, <<"federation">>, - <<"v1">>, <<"send">>, + Host, + put, + Server, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"send">>, TxnID], [], Body, [{timeout, 5000}], [{sync, false}, {receiver, Receiver}]), - Data#data{outgoing_txns = - maps:put(Server, {{RequestID, TxnID, Events, Count}, Queue}, - Data#data.outgoing_txns)}. + Data#data{ + outgoing_txns = + maps:put(Server, + {{RequestID, TxnID, Events, Count}, Queue}, + Data#data.outgoing_txns) + }. + do_get_missing_events(Origin, EarliestEvents, LatestEvents, Limit, MinDepth, Data) -> case is_server_joined(Origin, Data) of true -> - Visited = maps:from_list([{E, []} || E <- EarliestEvents]), + Visited = maps:from_list([ {E, []} || E <- EarliestEvents ]), Queue = queue:from_list(LatestEvents), Limit2 = min(max(Limit, 0), 20), do_get_missing_events_bfs(Queue, Visited, Limit2, MinDepth, [], Data); @@ -3395,6 +4024,7 @@ do_get_missing_events(Origin, EarliestEvents, LatestEvents, Limit, MinDepth, Dat [] end. + do_get_missing_events_bfs(_Queue, _Visited, 0, _MinDepth, Res, _Data) -> Res; do_get_missing_events_bfs(Queue, Visited, Limit, MinDepth, Res, Data) -> @@ -3411,6 +4041,7 @@ do_get_missing_events_bfs(Queue, Visited, Limit, MinDepth, Res, Data) -> Res end. + do_get_missing_events_bfs2(_PrevEvents, _Queue, _Visited, 0, _MinDepth, Res, _Data) -> Res; do_get_missing_events_bfs2([], Queue, Visited, Limit, MinDepth, Res, Data) -> @@ -3432,6 +4063,7 @@ do_get_missing_events_bfs2([EventID | PrevEvents], Queue, Visited, Limit, MinDep end end. + do_get_state_ids(Origin, EventID, Data) -> case is_server_joined(Origin, Data) of true -> @@ -3448,6 +4080,7 @@ do_get_state_ids(Origin, EventID, Data) -> {error, not_allowed} end. + do_get_state_ids_dfs([], _Visited, Res, _Data) -> Res; do_get_state_ids_dfs([EventID | Queue], Visited, Res, Data) -> @@ -3478,9 +4111,14 @@ is_server_joined(Server, Data) -> Server -> case maps:find(EID, Data#data.events) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> throw(found); _ -> ok @@ -3490,18 +4128,23 @@ is_server_joined(Server, Data) -> end; (_, _, ok) -> ok - end, ok, Event#event.state_map), + end, + ok, + Event#event.state_map), ok; _ -> ok end - end, ok, Data#data.latest_events), + end, + ok, + Data#data.latest_events), false catch throw:found -> true end. + get_remote_servers(Data) -> Servers = maps:fold( @@ -3513,22 +4156,32 @@ get_remote_servers(Data) -> Server = mod_matrix_gw:get_id_domain_exn(UserID), case maps:find(EID, Data#data.events) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> maps:put(Server, [], Acc2); _ -> Acc2 end; (_, _, Acc2) -> Acc2 - end, Acc, Event#event.state_map); + end, + Acc, + Event#event.state_map); _ -> Acc end - end, #{}, Data#data.latest_events), + end, + #{}, + Data#data.latest_events), maps:keys(Servers). + get_joined_users(Data) -> Users = maps:fold( @@ -3539,22 +4192,32 @@ get_joined_users(Data) -> fun({?ROOM_MEMBER, UserID}, EID, Acc2) -> case maps:find(EID, Data#data.events) of {ok, #event{ - json = #{<<"content">> := - #{<<"membership">> := - <<"join">>}}}} -> + json = #{ + <<"content">> := + #{ + <<"membership">> := + <<"join">> + } + } + }} -> maps:put(UserID, [], Acc2); _ -> Acc2 end; (_, _, Acc2) -> Acc2 - end, Acc, Event#event.state_map); + end, + Acc, + Event#event.state_map); _ -> Acc end - end, #{}, Data#data.latest_events), + end, + #{}, + Data#data.latest_events), maps:keys(Users). + user_id_to_jid(Str, #data{} = Data) -> user_id_to_jid(Str, Data#data.host); user_id_to_jid(Str, Host) when is_binary(Host) -> @@ -3571,6 +4234,7 @@ user_id_to_jid(Str, Host) when is_binary(Host) -> error end. + user_id_from_jid(#jid{luser = U, lserver = Host}, Host) -> ServerName = mod_matrix_gw_opt:matrix_domain(Host), {ok, <<$@, U/binary, $:, ServerName/binary>>}; @@ -3584,21 +4248,25 @@ user_id_from_jid(JID, _Host) -> error end. + new_room_id() -> Host = ejabberd_config:get_myname(), Letters = <<"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ">>, N = size(Letters), - S = << <<(binary:at(Letters, X rem N))>> || - <> <= crypto:strong_rand_bytes(18)>>, + S = << <<(binary:at(Letters, X rem N))>> + || <> <= crypto:strong_rand_bytes(18) >>, MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), <<$!, S/binary, $:, MatrixServer/binary>>. + compute_event_auth_keys(#{<<"type">> := ?ROOM_CREATE}, _RoomVersion) -> []; -compute_event_auth_keys(#{<<"type">> := ?ROOM_MEMBER, +compute_event_auth_keys(#{ + <<"type">> := ?ROOM_MEMBER, <<"sender">> := Sender, <<"content">> := #{<<"membership">> := Membership} = Content, - <<"state_key">> := StateKey}, + <<"state_key">> := StateKey + }, RoomVersion) -> Common1 = [{?ROOM_POWER_LEVELS, <<"">>}, {?ROOM_MEMBER, Sender}, @@ -3643,9 +4311,13 @@ compute_event_auth_keys(#{<<"type">> := _, <<"sender">> := Sender}, RoomVersion) end. -update_client(#data{kind = #direct{client_state = undefined, - local_user = JID, - remote_user = RemoteUserID}} = Data) -> +update_client(#data{ + kind = #direct{ + client_state = undefined, + local_user = JID, + remote_user = RemoteUserID + } + } = Data) -> Host = Data#data.host, MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), LocalUserID = <<$@, (JID#jid.luser)/binary, $:, MatrixServer/binary>>, @@ -3657,19 +4329,23 @@ update_client(#data{kind = #direct{client_state = undefined, {ok, Data#data{kind = (Data#data.kind)#direct{client_state = established}}}; [_] -> {leave, unknown_remote_user, - Data#data{kind = (Data#data.kind)#direct{client_state = leave}}}; + Data#data{kind = (Data#data.kind)#direct{client_state = leave}}}; [] -> {ok, Data}; _ -> {leave, too_many_users, - Data#data{kind = (Data#data.kind)#direct{client_state = leave}}} + Data#data{kind = (Data#data.kind)#direct{client_state = leave}}} end; false -> {ok, Data} end; -update_client(#data{kind = #direct{client_state = established, - local_user = JID, - remote_user = RemoteUserID}} = Data) -> +update_client(#data{ + kind = #direct{ + client_state = established, + local_user = JID, + remote_user = RemoteUserID + } + } = Data) -> Host = Data#data.host, MatrixServer = mod_matrix_gw_opt:matrix_domain(Host), LocalUserID = <<$@, (JID#jid.luser)/binary, $:, MatrixServer/binary>>, @@ -3704,11 +4380,16 @@ send_muc_invite(Host, Origin, RoomID, Sender, UserID, Event, IRS) -> ServiceHost = mod_matrix_gw_opt:host(Host), Alias = lists:foldl( - fun(#{<<"type">> := <<"m.room.canonical_alias">>, - <<"content">> := #{<<"alias">> := A}}, _) + fun(#{ + <<"type">> := <<"m.room.canonical_alias">>, + <<"content">> := #{<<"alias">> := A} + }, + _) when is_binary(A) -> A; (_, Acc) -> Acc - end, none, IRS), + end, + none, + IRS), {ok, EscRoomID} = case Alias of <<$#, Parts/binary>> -> @@ -3731,18 +4412,20 @@ send_muc_invite(Host, Origin, RoomID, Sender, UserID, Event, IRS) -> Invite = #muc_invite{to = undefined, from = SenderJID}, XUser = #muc_user{invites = [Invite]}, Msg = #message{ - from = RoomJID, - to = UserJID, - sub_els = [XUser] - }, + from = RoomJID, + to = UserJID, + sub_els = [XUser] + }, ejabberd_router:route(Msg); _ -> ok end. + room_id_to_xmpp(RoomID) -> room_id_to_xmpp(RoomID, undefined). + room_id_to_xmpp(RoomID, Origin) -> case RoomID of <<$!, Parts/binary>> -> @@ -3768,6 +4451,7 @@ room_id_to_xmpp(RoomID, Origin) -> error end. + room_id_from_xmpp(Host, RID) -> case RID of <<$!, Parts/binary>> -> @@ -3805,15 +4489,22 @@ room_id_from_xmpp(Host, RID) -> error end. + resolve_alias(Host, Origin, Alias) -> ets_cache:lookup( - ?MATRIX_ROOM_ALIAS_CACHE, Alias, + ?MATRIX_ROOM_ALIAS_CACHE, + Alias, fun() -> Res = mod_matrix_gw:send_request( - Host, get, Origin, - [<<"_matrix">>, <<"federation">>, - <<"v1">>, <<"query">>, <<"directory">>], + Host, + get, + Origin, + [<<"_matrix">>, + <<"federation">>, + <<"v1">>, + <<"query">>, + <<"directory">>], [{<<"room_alias">>, Alias}], none, [{timeout, 5000}], @@ -3842,29 +4533,32 @@ resolve_alias(Host, Origin, Alias) -> escape(S) -> escape(S, <<>>). + escape(<<>>, Res) -> Res; escape(<>, Res) -> Res2 = case C of $\s -> <>; - $" -> <>; - $% -> <>; - $& -> <>; - $' -> <>; - $/ -> <>; - $: -> <>; - $< -> <>; - $> -> <>; - $@ -> <>; + $" -> <>; + $% -> <>; + $& -> <>; + $' -> <>; + $/ -> <>; + $: -> <>; + $< -> <>; + $> -> <>; + $@ -> <>; $\\ -> <>; _ -> <> end, escape(S, Res2). + unescape(S) -> unescape(S, <<>>). + unescape(<<>>, Res) -> Res; unescape(<<"\\20", S/binary>>, Res) -> unescape(S, <>); unescape(<<"\\22", S/binary>>, Res) -> unescape(S, <>); @@ -3879,4 +4573,5 @@ unescape(<<"\\40", S/binary>>, Res) -> unescape(S, <>); unescape(<<"\\5c", S/binary>>, Res) -> unescape(S, <>); unescape(<>, Res) -> unescape(S, <>). + -endif. diff --git a/src/mod_matrix_gw_s2s.erl b/src/mod_matrix_gw_s2s.erl index 241b78ef6..32573844f 100644 --- a/src/mod_matrix_gw_s2s.erl +++ b/src/mod_matrix_gw_s2s.erl @@ -27,8 +27,12 @@ -behaviour(gen_statem). %% API --export([start_link/2, supervisor/1, create_db/0, - get_connection/2, check_auth/5, check_signature/3, +-export([start_link/2, + supervisor/1, + create_db/0, + get_connection/2, + check_auth/5, + check_signature/3, get_matrix_host_port/2]). %% gen_statem callbacks @@ -37,28 +41,34 @@ -include("logger.hrl"). -include("ejabberd_http.hrl"). + -include_lib("kernel/include/inet.hrl"). + -include("mod_matrix_gw.hrl"). --record(matrix_s2s, - {to :: binary(), - pid :: pid()}). +-record(matrix_s2s, { + to :: binary(), + pid :: pid() + }). --record(pending, - {request_id :: any(), - servers :: [binary()], - key_queue = []}). +-record(pending, { + request_id :: any(), + servers :: [binary()], + key_queue = [] + }). --record(wait, - {timer_ref :: reference(), - last :: integer()}). +-record(wait, { + timer_ref :: reference(), + last :: integer() + }). --record(data, - {host :: binary(), - matrix_server :: binary(), - matrix_host_port :: {binary(), integer()} | undefined, - keys = #{}, - state :: #pending{} | #wait{}}). +-record(data, { + host :: binary(), + matrix_server :: binary(), + matrix_host_port :: {binary(), integer()} | undefined, + keys = #{}, + state :: #pending{} | #wait{} + }). -define(KEYS_REQUEST_TIMEOUT, 600000). @@ -66,6 +76,7 @@ %%% API %%%=================================================================== + %%-------------------------------------------------------------------- %% @doc %% Creates a gen_statem process which calls Module:init/1 to @@ -75,37 +86,43 @@ %% @end %%-------------------------------------------------------------------- -spec start_link(binary(), binary()) -> - {ok, Pid :: pid()} | - ignore | - {error, Error :: term()}. + {ok, Pid :: pid()} | + ignore | + {error, Error :: term()}. start_link(Host, MatrixServer) -> - gen_statem:start_link(?MODULE, [Host, MatrixServer], + gen_statem:start_link(?MODULE, + [Host, MatrixServer], ejabberd_config:fsm_limit_opts([])). + -spec supervisor(binary()) -> atom(). supervisor(Host) -> gen_mod:get_module_proc(Host, mod_matrix_gw_s2s_sup). + create_db() -> ejabberd_mnesia:create( - ?MODULE, matrix_s2s, + ?MODULE, + matrix_s2s, [{ram_copies, [node()]}, {type, set}, {attributes, record_info(fields, matrix_s2s)}]), ok. + get_connection(Host, MatrixServer) -> case mnesia:dirty_read(matrix_s2s, MatrixServer) of - [] -> - case supervisor:start_child(supervisor(Host), - [Host, MatrixServer]) of - {ok, undefined} -> {error, ignored}; - Res -> Res - end; + [] -> + case supervisor:start_child(supervisor(Host), + [Host, MatrixServer]) of + {ok, undefined} -> {error, ignored}; + Res -> Res + end; [#matrix_s2s{pid = Pid}] -> {ok, Pid} end. + get_key(Host, MatrixServer, KeyID) -> case mod_matrix_gw_opt:matrix_domain(Host) of MatrixServer -> @@ -120,6 +137,7 @@ get_key(Host, MatrixServer, KeyID) -> end end. + get_matrix_host_port(Host, MatrixServer) -> case mod_matrix_gw_opt:matrix_domain(Host) of MatrixServer -> @@ -144,6 +162,7 @@ get_matrix_host_port(Host, MatrixServer) -> % Error % end. + check_auth(Host, MatrixServer, AuthParams, Content, Request) -> case get_connection(Host, MatrixServer) of {ok, S2SPid} -> @@ -153,13 +172,14 @@ check_auth(Host, MatrixServer, AuthParams, Content, Request) -> %% TODO: check ValidUntil Destination = mod_matrix_gw_opt:matrix_domain(Host), #{<<"sig">> := Sig} = AuthParams, - JSON = #{<<"method">> => atom_to_binary(Request#request.method, latin1), + JSON = #{ + <<"method">> => atom_to_binary(Request#request.method, latin1), <<"uri">> => Request#request.raw_path, <<"origin">> => MatrixServer, <<"destination">> => Destination, <<"signatures">> => #{ - MatrixServer => #{KeyID => Sig} - } + MatrixServer => #{KeyID => Sig} + } }, JSON2 = case Content of @@ -181,11 +201,14 @@ check_auth(Host, MatrixServer, AuthParams, Content, Request) -> false end. + check_signature(Host, JSON, RoomVersion) -> case JSON of - #{<<"sender">> := Sender, + #{ + <<"sender">> := Sender, <<"signatures">> := Sigs, - <<"origin_server_ts">> := OriginServerTS} -> + <<"origin_server_ts">> := OriginServerTS + } -> MatrixServer = mod_matrix_gw:get_id_domain_exn(Sender), case Sigs of #{MatrixServer := #{} = KeySig} -> @@ -224,6 +247,7 @@ check_signature(Host, JSON, RoomVersion) -> %%% gen_statem callbacks %%%=================================================================== + %%-------------------------------------------------------------------- %% @private %% @doc @@ -235,14 +259,19 @@ check_signature(Host, JSON, RoomVersion) -> -spec init(Args :: term()) -> gen_statem:init_result(term()). init([Host, MatrixServer]) -> mnesia:dirty_write( - #matrix_s2s{to = MatrixServer, - pid = self()}), + #matrix_s2s{ + to = MatrixServer, + pid = self() + }), {ok, state_name, - request_keys( - MatrixServer, - #data{host = Host, + request_keys( + MatrixServer, + #data{ + host = Host, matrix_server = MatrixServer, - state = #wait{timer_ref = make_ref(), last = 0}})}. + state = #wait{timer_ref = make_ref(), last = 0} + })}. + %%-------------------------------------------------------------------- %% @private @@ -251,10 +280,11 @@ init([Host, MatrixServer]) -> %% this function is called for every event a gen_statem receives. %% @end %%-------------------------------------------------------------------- --spec handle_event( - gen_statem:event_type(), Msg :: term(), - State :: term(), Data :: term()) -> - gen_statem:handle_event_result(). +-spec handle_event(gen_statem:event_type(), + Msg :: term(), + State :: term(), + Data :: term()) -> + gen_statem:handle_event_result(). %handle_event({call, From}, _Msg, State, Data) -> % {next_state, State, Data, [{reply, From, ok}]}. handle_event({call, From}, get_matrix_host_port, _State, Data) -> @@ -275,7 +305,8 @@ handle_event({call, From}, {get_key, KeyID}, State, Data) -> #pending{key_queue = KeyQueue} = St -> KeyQueue2 = [{From, KeyID} | KeyQueue], {next_state, State, - Data#data{state = St#pending{key_queue = KeyQueue2}}, []}; + Data#data{state = St#pending{key_queue = KeyQueue2}}, + []}; #wait{timer_ref = TimerRef, last = Last} -> TS = erlang:system_time(millisecond), if @@ -284,7 +315,8 @@ handle_event({call, From}, {get_key, KeyID}, State, Data) -> #pending{key_queue = KeyQueue} = St = Data2#data.state, KeyQueue2 = [{From, KeyID} | KeyQueue], {next_state, State, - Data2#data{state = St#pending{key_queue = KeyQueue2}}, []}; + Data2#data{state = St#pending{key_queue = KeyQueue2}}, + []}; true -> Timeout = case erlang:read_timer(TimerRef) of @@ -297,16 +329,26 @@ handle_event({call, From}, {get_key, KeyID}, State, Data) -> end, TRef = erlang:start_timer(Timeout, self(), []), {next_state, State, - Data#data{state = #wait{timer_ref = TRef, - last = Last}}, - [{reply, From, error}]} + Data#data{ + state = #wait{ + timer_ref = TRef, + last = Last + } + }, + [{reply, From, error}]} end end end; -handle_event(cast, {key_reply, RequestID, HTTPResult}, State, - #data{state = #pending{request_id = RequestID, - servers = Servers, - key_queue = KeyQueue} = St} = Data) -> +handle_event(cast, + {key_reply, RequestID, HTTPResult}, + State, + #data{ + state = #pending{ + request_id = RequestID, + servers = Servers, + key_queue = KeyQueue + } = St + } = Data) -> TS = erlang:system_time(millisecond), Res = case HTTPResult of @@ -326,11 +368,15 @@ handle_event(cast, {key_reply, RequestID, HTTPResult}, State, ?DEBUG("key ~p~n", [VerifyKey]), ?DEBUG("check ~p~n", [catch check_signature( - JSON, Data#data.matrix_server, - KeyID, VerifyKey)]), + JSON, + Data#data.matrix_server, + KeyID, + VerifyKey)]), true = check_signature( - JSON, Data#data.matrix_server, - KeyID, VerifyKey), + JSON, + Data#data.matrix_server, + KeyID, + VerifyKey), #{<<"valid_until_ts">> := ValidUntil} = JSON, ValidUntil2 = min(ValidUntil, @@ -346,14 +392,17 @@ handle_event(cast, {key_reply, RequestID, HTTPResult}, State, OldKeys = maps:filtermap( fun(_KID, - #{<<"key">> := SK, - <<"expired_ts">> := Exp}) + #{ + <<"key">> := SK, + <<"expired_ts">> := Exp + }) when is_integer(Exp), is_binary(SK) -> {true, {mod_matrix_gw:base64_decode(SK), Exp}}; (_, _) -> false - end, OldKeysJSON), + end, + OldKeysJSON), NewKeys = maps:filtermap( fun(_KID, @@ -362,7 +411,8 @@ handle_event(cast, {key_reply, RequestID, HTTPResult}, State, {true, {mod_matrix_gw:base64_decode(SK), ValidUntil2}}; (_, _) -> false - end, VerifyKeys), + end, + VerifyKeys), {ok, maps:merge(OldKeys, NewKeys), ValidUntil2} catch _:_ -> @@ -393,16 +443,23 @@ handle_event(cast, {key_reply, RequestID, HTTPResult}, State, end, KeyQueue), TimerRef = erlang:start_timer(max(ValidTS - TS, ?KEYS_REQUEST_TIMEOUT), - self(), []), - Data2 = Data#data{keys = Keys, - state = #wait{timer_ref = TimerRef, - last = TS}}, + self(), + []), + Data2 = Data#data{ + keys = Keys, + state = #wait{ + timer_ref = TimerRef, + last = TS + } + }, ?DEBUG("KEYS ~p~n", [{Keys, Data2}]), {next_state, State, Data2, Replies}; {error, Data2} -> {next_state, State, Data2, []} end; -handle_event(info, {timeout, TimerRef, []}, State, +handle_event(info, + {timeout, TimerRef, []}, + State, #data{state = #wait{timer_ref = TimerRef}} = Data) -> Data2 = request_keys(Data#data.matrix_server, Data), {next_state, State, Data2, []}; @@ -413,6 +470,7 @@ handle_event(info, Info, State, Data) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {next_state, State, Data, []}. + %%-------------------------------------------------------------------- %% @private %% @doc @@ -423,34 +481,41 @@ handle_event(info, Info, State, Data) -> %% @end %%-------------------------------------------------------------------- -spec terminate(Reason :: term(), State :: term(), Data :: term()) -> - any(). + any(). terminate(_Reason, _State, Data) -> mnesia:dirty_delete_object( - #matrix_s2s{to = Data#data.matrix_server, - pid = self()}), + #matrix_s2s{ + to = Data#data.matrix_server, + pid = self() + }), %% TODO: wait for messages ok. + %%-------------------------------------------------------------------- %% @private %% @doc %% Convert process state when code is changed %% @end %%-------------------------------------------------------------------- --spec code_change( - OldVsn :: term() | {down,term()}, - State :: term(), Data :: term(), Extra :: term()) -> - {ok, NewState :: term(), NewData :: term()}. +-spec code_change(OldVsn :: term() | {down, term()}, + State :: term(), + Data :: term(), + Extra :: term()) -> + {ok, NewState :: term(), NewData :: term()}. code_change(_OldVsn, State, Data, _Extra) -> {ok, State, Data}. + callback_mode() -> handle_event_function. + %%%=================================================================== %%% Internal functions %%%=================================================================== + do_get_matrix_host_port(MatrixServer) -> case binary:split(MatrixServer, <<":">>) of [Addr] -> @@ -460,7 +525,8 @@ do_get_matrix_host_port(MatrixServer) -> _ -> URL = <<"https://", Addr/binary, "/.well-known/matrix/server">>, HTTPRes = - httpc:request(get, {URL, []}, + httpc:request(get, + {URL, []}, [{timeout, 5000}], [{sync, true}, {body_format, binary}]), @@ -492,7 +558,7 @@ do_get_matrix_host_port(MatrixServer) -> case inet_res:getbyname(SRVName, srv, 5000) of {ok, HostEntry} -> {hostent, _Name, _Aliases, _AddrType, _Len, - HAddrList} = HostEntry, + HAddrList} = HostEntry, case h_addr_list_to_host_ports(HAddrList) of {ok, [{Host, Port} | _]} -> {list_to_binary(Host), Port}; @@ -515,27 +581,31 @@ do_get_matrix_host_port(MatrixServer) -> end end. + %% Copied from xmpp_stream_out.erl -type host_port() :: {inet:hostname(), inet:port_number()}. -type h_addr_list() :: [{integer(), integer(), inet:port_number(), string()}]. --spec h_addr_list_to_host_ports(h_addr_list()) -> {ok, [host_port(),...]} | - {error, nxdomain}. + + +-spec h_addr_list_to_host_ports(h_addr_list()) -> {ok, [host_port(), ...]} | + {error, nxdomain}. h_addr_list_to_host_ports(AddrList) -> PrioHostPorts = lists:flatmap( - fun({Priority, Weight, Port, Host}) -> - N = case Weight of - 0 -> 0; - _ -> (Weight + 1) * p1_rand:uniform() - end, - [{Priority * 65536 - N, Host, Port}]; - (_) -> - [] - end, AddrList), - HostPorts = [{Host, Port} - || {_Priority, Host, Port} <- lists:usort(PrioHostPorts)], + fun({Priority, Weight, Port, Host}) -> + N = case Weight of + 0 -> 0; + _ -> (Weight + 1) * p1_rand:uniform() + end, + [{Priority * 65536 - N, Host, Port}]; + (_) -> + [] + end, + AddrList), + HostPorts = [ {Host, Port} + || {_Priority, Host, Port} <- lists:usort(PrioHostPorts) ], case HostPorts of - [] -> {error, nxdomain}; - _ -> {ok, HostPorts} + [] -> {error, nxdomain}; + _ -> {ok, HostPorts} end. @@ -553,22 +623,29 @@ check_signature(JSON, SignatureName, KeyID, VerifyKey) -> false end. + request_keys(Via, Data) -> {MHost, MPort} = do_get_matrix_host_port(Via), URL = case Data#data.matrix_server of Via -> - <<"https://", MHost/binary, - ":", (integer_to_binary(MPort))/binary, + <<"https://", + MHost/binary, + ":", + (integer_to_binary(MPort))/binary, "/_matrix/key/v2/server">>; MatrixServer -> - <<"https://", MHost/binary, - ":", (integer_to_binary(MPort))/binary, - "/_matrix/key/v2/query/", MatrixServer/binary>> + <<"https://", + MHost/binary, + ":", + (integer_to_binary(MPort))/binary, + "/_matrix/key/v2/query/", + MatrixServer/binary>> end, Self = self(), {ok, RequestID} = - httpc:request(get, {URL, []}, + httpc:request(get, + {URL, []}, [{timeout, 5000}], [{sync, false}, {receiver, @@ -588,8 +665,13 @@ request_keys(Via, Data) -> #wait{timer_ref = TimerRef} -> erlang:cancel_timer(TimerRef), NotaryServers = mod_matrix_gw_opt:notary_servers(Data#data.host), - Data#data{state = #pending{request_id = RequestID, - servers = NotaryServers}} + Data#data{ + state = #pending{ + request_id = RequestID, + servers = NotaryServers + } + } end. + -endif. diff --git a/src/mod_matrix_gw_sup.erl b/src/mod_matrix_gw_sup.erl index f08f36e68..c2d2af18b 100644 --- a/src/mod_matrix_gw_sup.erl +++ b/src/mod_matrix_gw_sup.erl @@ -28,26 +28,32 @@ %% Supervisor callbacks -export([init/1]). + %%%=================================================================== %%% API functions %%%=================================================================== start(Host) -> - Spec = #{id => procname(Host), - start => {?MODULE, start_link, [Host]}, - restart => permanent, - shutdown => infinity, - type => supervisor, - modules => [?MODULE]}, + Spec = #{ + id => procname(Host), + start => {?MODULE, start_link, [Host]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [?MODULE] + }, supervisor:start_child(ejabberd_gen_mod_sup, Spec). + start_link(Host) -> Proc = procname(Host), supervisor:start_link({local, Proc}, ?MODULE, [Host]). + -spec procname(binary()) -> atom(). procname(Host) -> gen_mod:get_module_proc(Host, ?MODULE). + %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== @@ -55,23 +61,31 @@ init([Host]) -> S2SName = mod_matrix_gw_s2s:supervisor(Host), RoomName = mod_matrix_gw_room:supervisor(Host), Specs = - [#{id => S2SName, + [#{ + id => S2SName, start => {ejabberd_tmp_sup, start_link, [S2SName, mod_matrix_gw_s2s]}, restart => permanent, shutdown => infinity, type => supervisor, - modules => [ejabberd_tmp_sup]}, - #{id => RoomName, + modules => [ejabberd_tmp_sup] + }, + #{ + id => RoomName, start => {ejabberd_tmp_sup, start_link, [RoomName, mod_matrix_gw_room]}, restart => permanent, shutdown => infinity, type => supervisor, - modules => [ejabberd_tmp_sup]}, - #{id => mod_matrix_gw:procname(Host), + modules => [ejabberd_tmp_sup] + }, + #{ + id => mod_matrix_gw:procname(Host), start => {mod_matrix_gw, start_link, [Host]}, restart => permanent, shutdown => timer:minutes(1), type => worker, - modules => [mod_matrix_gw]}], + modules => [mod_matrix_gw] + }], {ok, {{one_for_one, 10, 1}, Specs}}. + + -endif. diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl index 77b690867..66a13fb3c 100644 --- a/src/mod_metrics.erl +++ b/src/mod_metrics.erl @@ -29,18 +29,24 @@ -behaviour(gen_mod). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -export([start/2, stop/1, mod_opt_type/1, mod_options/1, depends/2, reload/3]). -export([push/2, mod_doc/0]). -export([offline_message_hook/1, - sm_register_connection_hook/3, sm_remove_connection_hook/3, - user_send_packet/1, user_receive_packet/1, - s2s_send_packet/1, s2s_receive_packet/1, - remove_user/2, register_user/2]). + sm_register_connection_hook/3, + sm_remove_connection_hook/3, + user_send_packet/1, + user_receive_packet/1, + s2s_send_packet/1, + s2s_receive_packet/1, + remove_user/2, + register_user/2]). --define(SOCKET_NAME, mod_metrics_udp_socket). +-define(SOCKET_NAME, mod_metrics_udp_socket). -define(SOCKET_REGISTER_RETRIES, 10). -type probe() :: atom() | {atom(), integer()}. @@ -49,6 +55,7 @@ %% API %%==================================================================== + start(_Host, _Opts) -> {ok, [{hook, offline_message_hook, offline_message_hook, 20}, {hook, sm_register_connection_hook, sm_register_connection_hook, 20}, @@ -60,15 +67,19 @@ start(_Host, _Opts) -> {hook, remove_user, remove_user, 20}, {hook, register_user, register_user, 20}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + %%==================================================================== %% Hooks handlers %%==================================================================== @@ -77,46 +88,55 @@ offline_message_hook({_Action, #message{to = #jid{lserver = LServer}}} = Acc) -> push(LServer, offline_message), Acc. + -spec sm_register_connection_hook(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). -sm_register_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> +sm_register_connection_hook(_SID, #jid{lserver = LServer}, _Info) -> push(LServer, sm_register_connection). + -spec sm_remove_connection_hook(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). -sm_remove_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> +sm_remove_connection_hook(_SID, #jid{lserver = LServer}, _Info) -> push(LServer, sm_remove_connection). + -spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_send_packet({Packet, #{jid := #jid{lserver = LServer}} = C2SState}) -> push(LServer, user_send_packet), {Packet, C2SState}. + -spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_receive_packet({Packet, #{jid := #jid{lserver = LServer}} = C2SState}) -> push(LServer, user_receive_packet), {Packet, C2SState}. + -spec s2s_send_packet(stanza()) -> stanza(). s2s_send_packet(Packet) -> #jid{lserver = LServer} = xmpp:get_from(Packet), push(LServer, s2s_send_packet), Packet. + -spec s2s_receive_packet({stanza(), ejabberd_s2s_in:state()}) -> - {stanza(), ejabberd_s2s_in:state()}. + {stanza(), ejabberd_s2s_in:state()}. s2s_receive_packet({Packet, S2SState}) -> To = xmpp:get_to(Packet), LServer = ejabberd_router:host_of_route(To#jid.lserver), push(LServer, s2s_receive_packet), {Packet, S2SState}. + -spec remove_user(binary(), binary()) -> any(). remove_user(_User, Server) -> push(jid:nameprep(Server), remove_user). + -spec register_user(binary(), binary()) -> any(). register_user(_User, Server) -> push(jid:nameprep(Server), register_user). + %%==================================================================== %% metrics push handler %%==================================================================== @@ -126,88 +146,117 @@ push(Host, Probe) -> Port = mod_metrics_opt:port(Host), send_metrics(Host, Probe, IP, Port). + -spec send_metrics(binary(), probe(), inet:ip4_address(), inet:port_number()) -> - ok | {error, not_owner | inet:posix()}. + ok | {error, not_owner | inet:posix()}. send_metrics(Host, Probe, Peer, Port) -> % our default metrics handler is https://github.com/processone/grapherl % grapherl metrics are named first with service domain, then nodename % and name of the data itself, followed by type timestamp and value % example => process-one.net/xmpp-1.user_receive_packet:c/1441784958:1 [_, FQDN] = binary:split(misc:atom_to_binary(node()), <<"@">>), - [Node|_] = binary:split(FQDN, <<".">>), + [Node | _] = binary:split(FQDN, <<".">>), BaseId = <>, TS = integer_to_binary(erlang:system_time(second)), case get_socket(?SOCKET_REGISTER_RETRIES) of - {ok, Socket} -> - case Probe of - {Key, Val} -> - BVal = integer_to_binary(Val), - Data = <>, - gen_udp:send(Socket, Peer, Port, Data); - Key -> - Data = <>, - gen_udp:send(Socket, Peer, Port, Data) - end; - Err -> - Err + {ok, Socket} -> + case Probe of + {Key, Val} -> + BVal = integer_to_binary(Val), + Data = <>, + gen_udp:send(Socket, Peer, Port, Data); + Key -> + Data = <>, + gen_udp:send(Socket, Peer, Port, Data) + end; + Err -> + Err end. + -spec get_socket(integer()) -> {ok, gen_udp:socket()} | {error, inet:posix()}. get_socket(N) -> case whereis(?SOCKET_NAME) of - undefined -> - case gen_udp:open(0) of - {ok, Socket} -> - try register(?SOCKET_NAME, Socket) of - true -> {ok, Socket} - catch _:badarg when N > 1 -> - gen_udp:close(Socket), - get_socket(N-1) - end; - {error, Reason} = Err -> - ?ERROR_MSG("Can not open udp socket to grapherl: ~ts", - [inet:format_error(Reason)]), - Err - end; - Socket -> - {ok, Socket} + undefined -> + case gen_udp:open(0) of + {ok, Socket} -> + try register(?SOCKET_NAME, Socket) of + true -> {ok, Socket} + catch + _:badarg when N > 1 -> + gen_udp:close(Socket), + get_socket(N - 1) + end; + {error, Reason} = Err -> + ?ERROR_MSG("Can not open udp socket to grapherl: ~ts", + [inet:format_error(Reason)]), + Err + end; + Socket -> + {ok, Socket} end. + mod_opt_type(ip) -> econf:ipv4(); mod_opt_type(port) -> econf:port(). + mod_options(_) -> - [{ip, {127,0,0,1}}, {port, 11111}]. + [{ip, {127, 0, 0, 1}}, {port, 11111}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module sends events to external backend " "(by now only https://github.com/processone/grapherl" - "[grapherl] is supported). Supported events are:"), "", - "- sm_register_connection", "", - "- sm_remove_connection", "", - "- user_send_packet", "", - "- user_receive_packet", "", - "- s2s_send_packet", "", - "- s2s_receive_packet", "", - "- register_user", "", - "- remove_user", "", - "- offline_message", "", + "[grapherl] is supported). Supported events are:"), + "", + "- sm_register_connection", + "", + "- sm_remove_connection", + "", + "- user_send_packet", + "", + "- user_receive_packet", + "", + "- s2s_send_packet", + "", + "- s2s_receive_packet", + "", + "- register_user", + "", + "- remove_user", + "", + "- offline_message", + "", ?T("When enabled, every call to these hooks triggers " "a counter event to be sent to the external backend.")], opts => [{ip, - #{value => ?T("IPv4Address"), + #{ + value => ?T("IPv4Address"), desc => ?T("IPv4 address where the backend is located. " - "The default value is '127.0.0.1'.")}}, + "The default value is '127.0.0.1'.") + }}, {port, - #{value => ?T("Port"), + #{ + value => ?T("Port"), desc => ?T("An internet port number at which the backend " "is listening for incoming connections/packets. " - "The default value is '11111'.")}}]}. + "The default value is '11111'.") + }}] + }. diff --git a/src/mod_metrics_opt.erl b/src/mod_metrics_opt.erl index 22b656775..43011917d 100644 --- a/src/mod_metrics_opt.erl +++ b/src/mod_metrics_opt.erl @@ -6,15 +6,16 @@ -export([ip/1]). -export([port/1]). --spec ip(gen_mod:opts() | global | binary()) -> {127,0,0,1} | inet:ip4_address(). + +-spec ip(gen_mod:opts() | global | binary()) -> {127, 0, 0, 1} | inet:ip4_address(). ip(Opts) when is_map(Opts) -> gen_mod:get_opt(ip, Opts); ip(Host) -> gen_mod:get_module_opt(Host, mod_metrics, ip). + -spec port(gen_mod:opts() | global | binary()) -> 1..1114111. port(Opts) when is_map(Opts) -> gen_mod:get_opt(port, Opts); port(Host) -> gen_mod:get_module_opt(Host, mod_metrics, port). - diff --git a/src/mod_mix.erl b/src/mod_mix.erl index ced8ada75..ffdefe1b7 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -32,36 +32,49 @@ -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% Hooks -export([process_disco_info/1, - process_disco_items/1, - process_mix_core/1, - process_mam_query/1, - process_pubsub_query/1]). + process_disco_items/1, + process_mix_core/1, + process_mam_query/1, + process_pubsub_query/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). --callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. --callback set_channel(binary(), binary(), binary(), - jid:jid(), boolean(), binary()) -> - ok | {error, db_failure}. --callback get_channels(binary(), binary()) -> - {ok, [binary()]} | {error, db_failure}. --callback get_channel(binary(), binary(), binary()) -> - {ok, {jid(), boolean(), binary()}} | - {error, notfound | db_failure}. --callback set_participant(binary(), binary(), binary(), jid(), binary(), binary()) -> - ok | {error, db_failure}. --callback get_participant(binary(), binary(), binary(), jid()) -> - {ok, {binary(), binary()}} | {error, notfound | db_failure}. --record(state, {hosts :: [binary()], - server_host :: binary()}). +-callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. +-callback set_channel(binary(), + binary(), + binary(), + jid:jid(), + boolean(), + binary()) -> + ok | {error, db_failure}. +-callback get_channels(binary(), binary()) -> + {ok, [binary()]} | {error, db_failure}. +-callback get_channel(binary(), binary(), binary()) -> + {ok, {jid(), boolean(), binary()}} | + {error, notfound | db_failure}. +-callback set_participant(binary(), binary(), binary(), jid(), binary(), binary()) -> + ok | {error, db_failure}. +-callback get_participant(binary(), binary(), binary(), jid()) -> + {ok, {binary(), binary()}} | {error, notfound | db_failure}. + +-record(state, { + hosts :: [binary()], + server_host :: binary() + }). + %%%=================================================================== %%% API @@ -69,16 +82,20 @@ start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + depends(_Host, _Opts) -> [{mod_mam, hard}]. + mod_opt_type(access_create) -> econf:acl(); mod_opt_type(name) -> @@ -90,6 +107,7 @@ mod_opt_type(hosts) -> mod_opt_type(db_type) -> econf:db_type(?MODULE). + mod_options(Host) -> [{access_create, all}, {host, <<"mix.", Host/binary>>}, @@ -97,552 +115,716 @@ mod_options(Host) -> {name, ?T("Channels")}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module is an experimental implementation of " "https://xmpp.org/extensions/xep-0369.html" "[XEP-0369: Mediated Information eXchange (MIX)]. " "It's asserted that " "the MIX protocol is going to replace the MUC protocol " - "in the future (see _`mod_muc`_)."), "", + "in the future (see _`mod_muc`_)."), + "", ?T("To learn more about how to use that feature, you can refer to " - "our tutorial: _`../../tutorials/mix-010.md|Getting started with MIX`_"), "", + "our tutorial: _`../../tutorials/mix-010.md|Getting started with MIX`_"), + "", ?T("The module depends on _`mod_mam`_.")], note => "added in 16.03 and improved in 19.02", opts => [{access_create, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("An access rule to control MIX channels creations. " - "The default value is 'all'.")}}, + "The default value is 'all'.") + }}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, - #{value => ?T("[Host, ...]"), + #{ + value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix '\"mix.\"'. " - "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + "The keyword '@HOST@' is replaced with the real virtual host name.") + }}, {name, - #{value => ?T("Name"), + #{ + value => ?T("Name"), desc => ?T("A name of the service in the Service Discovery. " "This will only be displayed by special XMPP clients. " - "The default value is 'Channels'.")}}, + "The default value is 'Channels'.") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}] + }. + -spec route(stanza()) -> ok. route(#iq{} = IQ) -> ejabberd_router:process_iq(IQ); -route(#message{type = groupchat, id = ID, lang = Lang, - to = #jid{luser = <<_, _/binary>>}} = Msg) -> +route(#message{ + type = groupchat, + id = ID, + lang = Lang, + to = #jid{luser = <<_, _/binary>>} + } = Msg) -> case ID of - <<>> -> - Txt = ?T("Attribute 'id' is mandatory for MIX messages"), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Msg, Err); - _ -> - process_mix_message(Msg) + <<>> -> + Txt = ?T("Attribute 'id' is mandatory for MIX messages"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err); + _ -> + process_mix_message(Msg) end; route(Pkt) -> ?DEBUG("Dropping packet:~n~ts", [xmpp:pp(Pkt)]). + -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_info(#iq{type = get, to = #jid{luser = <<>>} = To, - from = _From, lang = Lang, - sub_els = [#disco_info{node = <<>>}]} = IQ) -> +process_disco_info(#iq{ + type = get, + to = #jid{luser = <<>>} = To, + from = _From, + lang = Lang, + sub_els = [#disco_info{node = <<>>}] + } = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), - X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], - [ServerHost, ?MODULE, <<"">>, Lang]), + X = ejabberd_hooks:run_fold(disco_info, + ServerHost, + [], + [ServerHost, ?MODULE, <<"">>, Lang]), Name = mod_mix_opt:name(ServerHost), - Identity = #identity{category = <<"conference">>, - type = <<"mix">>, - name = translate:translate(Lang, Name)}, + Identity = #identity{ + category = <<"conference">>, + type = <<"mix">>, + name = translate:translate(Lang, Name) + }, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MIX_CORE_0, - ?NS_MIX_CORE_SEARCHABLE_0, ?NS_MIX_CORE_CREATE_CHANNEL_0, - ?NS_MIX_CORE_1, ?NS_MIX_CORE_SEARCHABLE_1, - ?NS_MIX_CORE_CREATE_CHANNEL_1], + ?NS_MIX_CORE_SEARCHABLE_0, ?NS_MIX_CORE_CREATE_CHANNEL_0, + ?NS_MIX_CORE_1, ?NS_MIX_CORE_SEARCHABLE_1, + ?NS_MIX_CORE_CREATE_CHANNEL_1], xmpp:make_iq_result( - IQ, #disco_info{features = Features, - identities = [Identity], - xdata = X}); -process_disco_info(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, - sub_els = [#disco_info{node = Node}]} = IQ) + IQ, + #disco_info{ + features = Features, + identities = [Identity], + xdata = X + }); +process_disco_info(#iq{ + type = get, + to = #jid{luser = <<_, _/binary>>} = To, + sub_els = [#disco_info{node = Node}] + } = IQ) when Node == <<"mix">>; Node == <<>> -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - Identity = #identity{category = <<"conference">>, - type = <<"mix">>}, - Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, - ?NS_MIX_CORE_0, ?NS_MIX_CORE_1, ?NS_MAM_2], - xmpp:make_iq_result( - IQ, #disco_info{node = Node, - features = Features, - identities = [Identity]}); - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, _} -> + Identity = #identity{ + category = <<"conference">>, + type = <<"mix">> + }, + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_MIX_CORE_0, ?NS_MIX_CORE_1, ?NS_MAM_2], + xmpp:make_iq_result( + IQ, + #disco_info{ + node = Node, + features = Features, + identities = [Identity] + }); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; process_disco_info(#iq{type = get, sub_els = [#disco_info{node = Node}]} = IQ) -> xmpp:make_iq_result(IQ, #disco_info{node = Node, features = [?NS_DISCO_INFO]}); process_disco_info(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). + -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_items(#iq{type = get, to = #jid{luser = <<>>} = To, - sub_els = [#disco_items{node = <<>>}]} = IQ) -> +process_disco_items(#iq{ + type = get, + to = #jid{luser = <<>>} = To, + sub_els = [#disco_items{node = <<>>}] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channels(ServerHost, Host) of - {ok, Channels} -> - Items = [#disco_item{jid = jid:make(Channel, Host)} - || Channel <- Channels], - xmpp:make_iq_result(IQ, #disco_items{items = Items}); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, Channels} -> + Items = [ #disco_item{jid = jid:make(Channel, Host)} + || Channel <- Channels ], + xmpp:make_iq_result(IQ, #disco_items{items = Items}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; -process_disco_items(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, - sub_els = [#disco_items{node = Node}]} = IQ) +process_disco_items(#iq{ + type = get, + to = #jid{luser = <<_, _/binary>>} = To, + sub_els = [#disco_items{node = Node}] + } = IQ) when Node == <<"mix">>; Node == <<>> -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - BTo = jid:remove_resource(To), - Items = [#disco_item{jid = BTo, node = N} || N <- known_nodes()], - xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, _} -> + BTo = jid:remove_resource(To), + Items = [ #disco_item{jid = BTo, node = N} || N <- known_nodes() ], + xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; process_disco_items(#iq{type = get, sub_els = [#disco_items{node = Node}]} = IQ) -> xmpp:make_iq_result(IQ, #disco_items{node = Node}); process_disco_items(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). + -spec process_mix_core(iq()) -> iq(). -process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, - sub_els = [#mix_create{}]} = IQ) -> +process_mix_core(#iq{ + type = set, + to = #jid{luser = <<>>}, + sub_els = [#mix_create{}] + } = IQ) -> process_mix_create(IQ); -process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, - sub_els = [#mix_destroy{}]} = IQ) -> +process_mix_core(#iq{ + type = set, + to = #jid{luser = <<>>}, + sub_els = [#mix_destroy{}] + } = IQ) -> process_mix_destroy(IQ); -process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, - sub_els = [#mix_join{}]} = IQ) -> +process_mix_core(#iq{ + type = set, + to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_join{}] + } = IQ) -> process_mix_join(IQ); -process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, - sub_els = [#mix_leave{}]} = IQ) -> +process_mix_core(#iq{ + type = set, + to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_leave{}] + } = IQ) -> process_mix_leave(IQ); -process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, - sub_els = [#mix_setnick{}]} = IQ) -> +process_mix_core(#iq{ + type = set, + to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_setnick{}] + } = IQ) -> process_mix_setnick(IQ); process_mix_core(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). -process_pubsub_query(#iq{type = get, - sub_els = [#pubsub{items = #ps_items{node = Node}}]} = IQ) + +process_pubsub_query(#iq{ + type = get, + sub_els = [#pubsub{items = #ps_items{node = Node}}] + } = IQ) when Node == ?NS_MIX_NODES_PARTICIPANTS -> process_participants_list(IQ); process_pubsub_query(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). -process_mam_query(#iq{from = From, to = To, type = T, - sub_els = [#mam_query{}]} = IQ) + +process_mam_query(#iq{ + from = From, + to = To, + type = T, + sub_els = [#mam_query{}] + } = IQ) when T == get; T == set -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - BFrom = jid:remove_resource(From), - case Mod:get_participant(ServerHost, Chan, Host, BFrom) of - {ok, _} -> - mod_mam:process_iq(ServerHost, IQ, mix); - {error, notfound} -> - xmpp:make_error(IQ, not_joined_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, _} -> + mod_mam:process_iq(ServerHost, IQ, mix); + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; process_mam_query(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), MyHosts = gen_mod:get_opt_hosts(Opts), case Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)) of - ok -> - lists:foreach( - fun(MyHost) -> - ejabberd_router:register_route( - MyHost, Host, {apply, ?MODULE, route}), - register_iq_handlers(MyHost) - end, MyHosts), - {ok, #state{hosts = MyHosts, server_host = Host}}; - {error, db_failure} -> - {stop, db_failure} + ok -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}), + register_iq_handlers(MyHost) + end, + MyHosts), + {ok, #state{hosts = MyHosts, server_host = Host}}; + {error, db_failure} -> + {stop, db_failure} end. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. + handle_info({route, Packet}, State) -> - try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + try + route(Packet) + catch + ?EX_RULE(Class, Reason, St) -> + StackTrace = ?EX_STACK(St), + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, State) -> lists:foreach( fun(MyHost) -> - unregister_iq_handlers(MyHost), - ejabberd_router:unregister_route(MyHost) - end, State#state.hosts). + unregister_iq_handlers(MyHost), + ejabberd_router:unregister_route(MyHost) + end, + State#state.hosts). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec process_mix_create(iq()) -> iq(). -process_mix_create(#iq{to = To, from = From, - sub_els = [#mix_create{channel = Chan, xmlns = XmlNs}]} = IQ) -> +process_mix_create(#iq{ + to = To, + from = From, + sub_els = [#mix_create{channel = Chan, xmlns = XmlNs}] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), Creator = jid:remove_resource(From), Chan1 = case Chan of - <<>> -> p1_rand:get_string(); - _ -> Chan - end, + <<>> -> p1_rand:get_string(); + _ -> Chan + end, Ret = case Mod:get_channel(ServerHost, Chan1, Host) of - {ok, {#jid{luser = U, lserver = S}, _, _}} -> - case {From#jid.luser, From#jid.lserver} of - {U, S} -> ok; - _ -> {error, conflict} - end; - {error, notfound} -> - Key = xmpp_util:hex(p1_rand:bytes(20)), - Mod:set_channel(ServerHost, Chan1, Host, - Creator, Chan == <<>>, Key); - {error, db_failure} = Err -> - Err - end, + {ok, {#jid{luser = U, lserver = S}, _, _}} -> + case {From#jid.luser, From#jid.lserver} of + {U, S} -> ok; + _ -> {error, conflict} + end; + {error, notfound} -> + Key = xmpp_util:hex(p1_rand:bytes(20)), + Mod:set_channel(ServerHost, + Chan1, + Host, + Creator, + Chan == <<>>, + Key); + {error, db_failure} = Err -> + Err + end, case Ret of - ok -> - xmpp:make_iq_result(IQ, #mix_create{channel = Chan1, xmlns = XmlNs}); - {error, conflict} -> - xmpp:make_error(IQ, channel_exists_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + ok -> + xmpp:make_iq_result(IQ, #mix_create{channel = Chan1, xmlns = XmlNs}); + {error, conflict} -> + xmpp:make_error(IQ, channel_exists_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec process_mix_destroy(iq()) -> iq(). -process_mix_destroy(#iq{to = To, - from = #jid{luser = U, lserver = S}, - sub_els = [#mix_destroy{channel = Chan, xmlns = XmlNs}]} = IQ) -> +process_mix_destroy(#iq{ + to = To, + from = #jid{luser = U, lserver = S}, + sub_els = [#mix_destroy{channel = Chan, xmlns = XmlNs}] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, {#jid{luser = U, lserver = S}, _, _}} -> - case Mod:del_channel(ServerHost, Chan, Host) of - ok -> - xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan, xmlns = XmlNs}); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {ok, _} -> - xmpp:make_error(IQ, ownership_error(IQ)); - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, {#jid{luser = U, lserver = S}, _, _}} -> + case Mod:del_channel(ServerHost, Chan, Host) of + ok -> + xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan, xmlns = XmlNs}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {ok, _} -> + xmpp:make_error(IQ, ownership_error(IQ)); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec process_mix_join(iq()) -> iq(). -process_mix_join(#iq{to = To, from = From, - sub_els = [#mix_join{xmlns = XmlNs} = JoinReq]} = IQ) -> +process_mix_join(#iq{ + to = To, + from = From, + sub_els = [#mix_join{xmlns = XmlNs} = JoinReq] + } = IQ) -> Chan = To#jid.luser, Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, {_, _, Key}} -> - ID = make_id(From, Key), - Nick = JoinReq#mix_join.nick, - BFrom = jid:remove_resource(From), - Nodes = filter_nodes(JoinReq#mix_join.subscribe), - try - ok = Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick), - ok = Mod:subscribe(ServerHost, Chan, Host, BFrom, Nodes), - notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), - xmpp:make_iq_result(IQ, #mix_join{id = ID, - subscribe = Nodes, - jid = make_channel_id(To, ID), - nick = Nick, - xmlns = XmlNs}) - catch _:{badmatch, {error, db_failure}} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, {_, _, Key}} -> + ID = make_id(From, Key), + Nick = JoinReq#mix_join.nick, + BFrom = jid:remove_resource(From), + Nodes = filter_nodes(JoinReq#mix_join.subscribe), + try + ok = Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick), + ok = Mod:subscribe(ServerHost, Chan, Host, BFrom, Nodes), + notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), + xmpp:make_iq_result(IQ, + #mix_join{ + id = ID, + subscribe = Nodes, + jid = make_channel_id(To, ID), + nick = Nick, + xmlns = XmlNs + }) + catch + _:{badmatch, {error, db_failure}} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec process_mix_leave(iq()) -> iq(). -process_mix_leave(#iq{to = To, from = From, - sub_els = [#mix_leave{xmlns = XmlNs}]} = IQ) -> +process_mix_leave(#iq{ + to = To, + from = From, + sub_els = [#mix_leave{xmlns = XmlNs}] + } = IQ) -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), BFrom = jid:remove_resource(From), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - case Mod:get_participant(ServerHost, Chan, Host, BFrom) of - {ok, {ID, _}} -> - try - ok = Mod:unsubscribe(ServerHost, Chan, Host, BFrom), - ok = Mod:del_participant(ServerHost, Chan, Host, BFrom), - notify_participant_left(Mod, ServerHost, To, ID), - xmpp:make_iq_result(IQ, #mix_leave{}) - catch _:{badmatch, {error, db_failure}} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_iq_result(IQ, #mix_leave{xmlns = XmlNs}); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_iq_result(IQ, #mix_leave{}); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, _} -> + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {ID, _}} -> + try + ok = Mod:unsubscribe(ServerHost, Chan, Host, BFrom), + ok = Mod:del_participant(ServerHost, Chan, Host, BFrom), + notify_participant_left(Mod, ServerHost, To, ID), + xmpp:make_iq_result(IQ, #mix_leave{}) + catch + _:{badmatch, {error, db_failure}} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_iq_result(IQ, #mix_leave{xmlns = XmlNs}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_iq_result(IQ, #mix_leave{}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec process_mix_setnick(iq()) -> iq(). -process_mix_setnick(#iq{to = To, from = From, - sub_els = [#mix_setnick{nick = Nick, xmlns = XmlNs}]} = IQ) -> +process_mix_setnick(#iq{ + to = To, + from = From, + sub_els = [#mix_setnick{nick = Nick, xmlns = XmlNs}] + } = IQ) -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), BFrom = jid:remove_resource(From), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - case Mod:get_participant(ServerHost, Chan, Host, BFrom) of - {ok, {_, Nick}} -> - xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); - {ok, {ID, _}} -> - case Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick) of - ok -> - notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), - xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_error(IQ, not_joined_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, _} -> + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {_, Nick}} -> + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); + {ok, {ID, _}} -> + case Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick) of + ok -> + notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec process_mix_message(message()) -> ok. -process_mix_message(#message{from = From, to = To, - id = SubmissionID} = Msg) -> +process_mix_message(#message{ + from = From, + to = To, + id = SubmissionID + } = Msg) -> {Chan, Host, _} = jid:tolower(To), {FUser, FServer, _} = jid:tolower(From), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - BFrom = jid:remove_resource(From), - case Mod:get_participant(ServerHost, Chan, Host, BFrom) of - {ok, {StableID, Nick}} -> - MamID = mod_mam:make_id(), - Msg1 = xmpp:set_subtag( - Msg#message{from = jid:replace_resource(To, StableID), - to = undefined, - id = integer_to_binary(MamID)}, - #mix{jid = BFrom, nick = Nick}), - Msg2 = xmpp:put_meta(Msg1, stanza_id, MamID), - case ejabberd_hooks:run_fold( - store_mam_message, ServerHost, Msg2, - [Chan, Host, BFrom, Nick, groupchat, recv]) of - #message{} -> - multicast(Mod, ServerHost, Chan, Host, - ?NS_MIX_NODES_MESSAGES, - fun(#jid{luser = U, lserver = S}) - when U == FUser, S == FServer -> - xmpp:set_subtag( - Msg1, #mix{jid = BFrom, - nick = Nick, - submission_id = SubmissionID}); - (_) -> - Msg1 - end); - _ -> - ok - end; - {error, notfound} -> - ejabberd_router:route_error(Msg, not_joined_error(Msg)); - {error, db_failure} -> - ejabberd_router:route_error(Msg, db_error(Msg)) - end; - {error, notfound} -> - ejabberd_router:route_error(Msg, no_channel_error(Msg)); - {error, db_failure} -> - ejabberd_router:route_error(Msg, db_error(Msg)) + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {StableID, Nick}} -> + MamID = mod_mam:make_id(), + Msg1 = xmpp:set_subtag( + Msg#message{ + from = jid:replace_resource(To, StableID), + to = undefined, + id = integer_to_binary(MamID) + }, + #mix{jid = BFrom, nick = Nick}), + Msg2 = xmpp:put_meta(Msg1, stanza_id, MamID), + case ejabberd_hooks:run_fold( + store_mam_message, + ServerHost, + Msg2, + [Chan, Host, BFrom, Nick, groupchat, recv]) of + #message{} -> + multicast(Mod, + ServerHost, + Chan, + Host, + ?NS_MIX_NODES_MESSAGES, + fun(#jid{luser = U, lserver = S}) + when U == FUser, S == FServer -> + xmpp:set_subtag( + Msg1, + #mix{ + jid = BFrom, + nick = Nick, + submission_id = SubmissionID + }); + (_) -> + Msg1 + end); + _ -> + ok + end; + {error, notfound} -> + ejabberd_router:route_error(Msg, not_joined_error(Msg)); + {error, db_failure} -> + ejabberd_router:route_error(Msg, db_error(Msg)) + end; + {error, notfound} -> + ejabberd_router:route_error(Msg, no_channel_error(Msg)); + {error, db_failure} -> + ejabberd_router:route_error(Msg, db_error(Msg)) end. + -spec process_participants_list(iq()) -> iq(). process_participants_list(#iq{from = From, to = To} = IQ) -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of - {ok, _} -> - BFrom = jid:remove_resource(From), - case Mod:get_participant(ServerHost, Chan, Host, BFrom) of - {ok, _} -> - case Mod:get_participants(ServerHost, Chan, Host) of - {ok, Participants} -> - Items = items_of_participants(Participants), - Pubsub = #pubsub{ - items = #ps_items{ - node = ?NS_MIX_NODES_PARTICIPANTS, - items = Items}}, - xmpp:make_iq_result(IQ, Pubsub); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_error(IQ, not_joined_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) - end; - {error, notfound} -> - xmpp:make_error(IQ, no_channel_error(IQ)); - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, _} -> + case Mod:get_participants(ServerHost, Chan, Host) of + {ok, Participants} -> + Items = items_of_participants(Participants), + Pubsub = #pubsub{ + items = #ps_items{ + node = ?NS_MIX_NODES_PARTICIPANTS, + items = Items + } + }, + xmpp:make_iq_result(IQ, Pubsub); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec items_of_participants([{jid(), binary(), binary()}]) -> [ps_item()]. items_of_participants(Participants) -> lists:map( fun({JID, ID, Nick}) -> - Participant = #mix_participant{jid = JID, nick = Nick}, - #ps_item{id = ID, - sub_els = [xmpp:encode(Participant)]} - end, Participants). + Participant = #mix_participant{jid = JID, nick = Nick}, + #ps_item{ + id = ID, + sub_els = [xmpp:encode(Participant)] + } + end, + Participants). + -spec known_nodes() -> [binary()]. known_nodes() -> [?NS_MIX_NODES_MESSAGES, ?NS_MIX_NODES_PARTICIPANTS]. + -spec filter_nodes([binary()]) -> [binary()]. filter_nodes(Nodes) -> KnownNodes = known_nodes(), - [Node || KnownNode <- KnownNodes, Node <- Nodes, KnownNode == Node]. + [ Node || KnownNode <- KnownNodes, Node <- Nodes, KnownNode == Node ]. --spec multicast(module(), binary(), binary(), - binary(), binary(), fun((jid()) -> message())) -> ok. + +-spec multicast(module(), + binary(), + binary(), + binary(), + binary(), + fun((jid()) -> message())) -> ok. multicast(Mod, LServer, Chan, Service, Node, F) -> case Mod:get_subscribed(LServer, Chan, Service, Node) of - {ok, Subscribers} -> - lists:foreach( - fun(To) -> - Msg = xmpp:set_to(F(To), To), - ejabberd_router:route(Msg) - end, Subscribers); - {error, db_failure} -> - ok + {ok, Subscribers} -> + lists:foreach( + fun(To) -> + Msg = xmpp:set_to(F(To), To), + ejabberd_router:route(Msg) + end, + Subscribers); + {error, db_failure} -> + ok end. --spec notify_participant_joined(module(), binary(), - jid(), jid(), binary(), binary()) -> ok. + +-spec notify_participant_joined(module(), + binary(), + jid(), + jid(), + binary(), + binary()) -> ok. notify_participant_joined(Mod, LServer, To, From, ID, Nick) -> {Chan, Host, _} = jid:tolower(To), - Participant = #mix_participant{jid = jid:remove_resource(From), - nick = Nick}, - Item = #ps_item{id = ID, - sub_els = [xmpp:encode(Participant)]}, - Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, - items = [Item]}, + Participant = #mix_participant{ + jid = jid:remove_resource(From), + nick = Nick + }, + Item = #ps_item{ + id = ID, + sub_els = [xmpp:encode(Participant)] + }, + Items = #ps_items{ + node = ?NS_MIX_NODES_PARTICIPANTS, + items = [Item] + }, Event = #ps_event{items = Items}, - Msg = #message{from = jid:remove_resource(To), - id = p1_rand:get_string(), - sub_els = [Event]}, - multicast(Mod, LServer, Chan, Host, - ?NS_MIX_NODES_PARTICIPANTS, - fun(_) -> Msg end). + Msg = #message{ + from = jid:remove_resource(To), + id = p1_rand:get_string(), + sub_els = [Event] + }, + multicast(Mod, + LServer, + Chan, + Host, + ?NS_MIX_NODES_PARTICIPANTS, + fun(_) -> Msg end). + -spec notify_participant_left(module(), binary(), jid(), binary()) -> ok. notify_participant_left(Mod, LServer, To, ID) -> {Chan, Host, _} = jid:tolower(To), - Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, - retract = [ID]}, + Items = #ps_items{ + node = ?NS_MIX_NODES_PARTICIPANTS, + retract = [ID] + }, Event = #ps_event{items = Items}, - Msg = #message{from = jid:remove_resource(To), - id = p1_rand:get_string(), - sub_els = [Event]}, - multicast(Mod, LServer, Chan, Host, ?NS_MIX_NODES_PARTICIPANTS, - fun(_) -> Msg end). + Msg = #message{ + from = jid:remove_resource(To), + id = p1_rand:get_string(), + sub_els = [Event] + }, + multicast(Mod, + LServer, + Chan, + Host, + ?NS_MIX_NODES_PARTICIPANTS, + fun(_) -> Msg end). + -spec make_id(jid(), binary()) -> binary(). make_id(JID, Key) -> Data = jid:encode(jid:tolower(jid:remove_resource(JID))), xmpp_util:hex(misc:crypto_hmac(sha256, Data, Key, 10)). + -spec make_channel_id(jid(), binary()) -> jid(). make_channel_id(JID, ID) -> - {U, S, R} = jid:split(JID), - jid:make(<>, S, R). + {U, S, R} = jid:split(JID), + jid:make(<>, S, R). + %%%=================================================================== %%% Error generators @@ -652,56 +834,93 @@ db_error(Pkt) -> Txt = ?T("Database failure"), xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). + -spec channel_exists_error(stanza()) -> stanza_error(). channel_exists_error(Pkt) -> Txt = ?T("Channel already exists"), xmpp:err_conflict(Txt, xmpp:get_lang(Pkt)). + -spec no_channel_error(stanza()) -> stanza_error(). no_channel_error(Pkt) -> Txt = ?T("Channel does not exist"), xmpp:err_item_not_found(Txt, xmpp:get_lang(Pkt)). + -spec not_joined_error(stanza()) -> stanza_error(). not_joined_error(Pkt) -> Txt = ?T("You are not joined to the channel"), xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + -spec unsupported_error(stanza()) -> stanza_error(). unsupported_error(Pkt) -> Txt = ?T("No module is handling this query"), xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). + -spec ownership_error(stanza()) -> stanza_error(). ownership_error(Pkt) -> Txt = ?T("Owner privileges required"), xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + %%%=================================================================== %%% IQ handlers %%%=================================================================== -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0, - ?MODULE, process_mix_core), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_1, - ?MODULE, process_mix_core), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0, - ?MODULE, process_mix_core), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_1, - ?MODULE, process_mix_core), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB, - ?MODULE, process_pubsub_query), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, - ?MODULE, process_mam_query). + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MIX_CORE_0, + ?MODULE, + process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MIX_CORE_1, + ?MODULE, + process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MIX_CORE_0, + ?MODULE, + process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MIX_CORE_1, + ?MODULE, + process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_PUBSUB, + ?MODULE, + process_pubsub_query), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_MAM_2, + ?MODULE, + process_mam_query). + -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> diff --git a/src/mod_mix_mnesia.erl b/src/mod_mix_mnesia.erl index 0d3a4d20c..74b2be48c 100644 --- a/src/mod_mix_mnesia.erl +++ b/src/mod_mix_mnesia.erl @@ -31,158 +31,192 @@ -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). --record(mix_channel, - {chan_serv :: {binary(), binary()}, - service :: binary(), - creator :: jid:jid(), - hidden :: boolean(), - hmac_key :: binary(), - created_at :: erlang:timestamp()}). +-record(mix_channel, { + chan_serv :: {binary(), binary()}, + service :: binary(), + creator :: jid:jid(), + hidden :: boolean(), + hmac_key :: binary(), + created_at :: erlang:timestamp() + }). --record(mix_participant, - {user_chan :: {binary(), binary(), binary(), binary()}, - chan_serv :: {binary(), binary()}, - jid :: jid:jid(), - id :: binary(), - nick :: binary(), - created_at :: erlang:timestamp()}). +-record(mix_participant, { + user_chan :: {binary(), binary(), binary(), binary()}, + chan_serv :: {binary(), binary()}, + jid :: jid:jid(), + id :: binary(), + nick :: binary(), + created_at :: erlang:timestamp() + }). + +-record(mix_subscription, { + user_chan_node :: {binary(), binary(), binary(), binary(), binary()}, + user_chan :: {binary(), binary(), binary(), binary()}, + chan_serv_node :: {binary(), binary(), binary()}, + chan_serv :: {binary(), binary()}, + jid :: jid:jid() + }). --record(mix_subscription, - {user_chan_node :: {binary(), binary(), binary(), binary(), binary()}, - user_chan :: {binary(), binary(), binary(), binary()}, - chan_serv_node :: {binary(), binary(), binary()}, - chan_serv :: {binary(), binary()}, - jid :: jid:jid()}). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> try - {atomic, _} = ejabberd_mnesia:create( - ?MODULE, mix_channel, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, mix_channel)}, - {index, [service]}]), - {atomic, _} = ejabberd_mnesia:create( - ?MODULE, mix_participant, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, mix_participant)}, - {index, [chan_serv]}]), - {atomic, _} = ejabberd_mnesia:create( - ?MODULE, mix_subscription, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, mix_subscription)}, - {index, [user_chan, chan_serv_node, chan_serv]}]), - ok - catch _:{badmatch, _} -> - {error, db_failure} + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, + mix_channel, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_channel)}, + {index, [service]}]), + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, + mix_participant, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_participant)}, + {index, [chan_serv]}]), + {atomic, _} = ejabberd_mnesia:create( + ?MODULE, + mix_subscription, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_subscription)}, + {index, [user_chan, chan_serv_node, chan_serv]}]), + ok + catch + _:{badmatch, _} -> + {error, db_failure} end. + set_channel(_LServer, Channel, Service, CreatorJID, Hidden, Key) -> mnesia:dirty_write( - #mix_channel{chan_serv = {Channel, Service}, - service = Service, - creator = jid:remove_resource(CreatorJID), - hidden = Hidden, - hmac_key = Key, - created_at = erlang:timestamp()}). + #mix_channel{ + chan_serv = {Channel, Service}, + service = Service, + creator = jid:remove_resource(CreatorJID), + hidden = Hidden, + hmac_key = Key, + created_at = erlang:timestamp() + }). + get_channels(_LServer, Service) -> Ret = mnesia:dirty_index_read(mix_channel, Service, #mix_channel.service), {ok, lists:filtermap( - fun(#mix_channel{chan_serv = {Channel, _}, - hidden = false}) -> - {true, Channel}; - (_) -> - false - end, Ret)}. + fun(#mix_channel{ + chan_serv = {Channel, _}, + hidden = false + }) -> + {true, Channel}; + (_) -> + false + end, + Ret)}. + get_channel(_LServer, Channel, Service) -> case mnesia:dirty_read(mix_channel, {Channel, Service}) of - [#mix_channel{creator = JID, - hidden = Hidden, - hmac_key = Key}] -> - {ok, {JID, Hidden, Key}}; - [] -> - {error, notfound} + [#mix_channel{ + creator = JID, + hidden = Hidden, + hmac_key = Key + }] -> + {ok, {JID, Hidden, Key}}; + [] -> + {error, notfound} end. + del_channel(_LServer, Channel, Service) -> Key = {Channel, Service}, L1 = mnesia:dirty_read(mix_channel, Key), - L2 = mnesia:dirty_index_read(mix_participant, Key, - #mix_participant.chan_serv), - L3 = mnesia:dirty_index_read(mix_subscription, Key, - #mix_subscription.chan_serv), - lists:foreach(fun mnesia:dirty_delete_object/1, L1++L2++L3). + L2 = mnesia:dirty_index_read(mix_participant, + Key, + #mix_participant.chan_serv), + L3 = mnesia:dirty_index_read(mix_subscription, + Key, + #mix_subscription.chan_serv), + lists:foreach(fun mnesia:dirty_delete_object/1, L1 ++ L2 ++ L3). + set_participant(_LServer, Channel, Service, JID, ID, Nick) -> {User, Domain, _} = jid:tolower(JID), mnesia:dirty_write( #mix_participant{ - user_chan = {User, Domain, Channel, Service}, - chan_serv = {Channel, Service}, - jid = jid:remove_resource(JID), - id = ID, - nick = Nick, - created_at = erlang:timestamp()}). + user_chan = {User, Domain, Channel, Service}, + chan_serv = {Channel, Service}, + jid = jid:remove_resource(JID), + id = ID, + nick = Nick, + created_at = erlang:timestamp() + }). + -spec get_participant(binary(), binary(), binary(), jid:jid()) -> {ok, {binary(), binary()}} | {error, notfound}. get_participant(_LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case mnesia:dirty_read(mix_participant, {User, Domain, Channel, Service}) of - [#mix_participant{id = ID, nick = Nick}] -> {ok, {ID, Nick}}; - [] -> {error, notfound} + [#mix_participant{id = ID, nick = Nick}] -> {ok, {ID, Nick}}; + [] -> {error, notfound} end. + get_participants(_LServer, Channel, Service) -> Ret = mnesia:dirty_index_read(mix_participant, - {Channel, Service}, - #mix_participant.chan_serv), + {Channel, Service}, + #mix_participant.chan_serv), {ok, lists:map( - fun(#mix_participant{jid = JID, id = ID, nick = Nick}) -> - {JID, ID, Nick} - end, Ret)}. + fun(#mix_participant{jid = JID, id = ID, nick = Nick}) -> + {JID, ID, Nick} + end, + Ret)}. + del_participant(_LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), mnesia:dirty_delete(mix_participant, {User, Domain, Channel, Service}). + subscribe(_LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), BJID = jid:remove_resource(JID), lists:foreach( fun(Node) -> - mnesia:dirty_write( - #mix_subscription{ - user_chan_node = {User, Domain, Channel, Service, Node}, - user_chan = {User, Domain, Channel, Service}, - chan_serv_node = {Channel, Service, Node}, - chan_serv = {Channel, Service}, - jid = BJID}) - end, Nodes). + mnesia:dirty_write( + #mix_subscription{ + user_chan_node = {User, Domain, Channel, Service, Node}, + user_chan = {User, Domain, Channel, Service}, + chan_serv_node = {Channel, Service, Node}, + chan_serv = {Channel, Service}, + jid = BJID + }) + end, + Nodes). + get_subscribed(_LServer, Channel, Service, Node) -> Ret = mnesia:dirty_index_read(mix_subscription, - {Channel, Service, Node}, - #mix_subscription.chan_serv_node), - {ok, [JID || #mix_subscription{jid = JID} <- Ret]}. + {Channel, Service, Node}, + #mix_subscription.chan_serv_node), + {ok, [ JID || #mix_subscription{jid = JID} <- Ret ]}. + unsubscribe(_LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), Ret = mnesia:dirty_index_read(mix_subscription, - {User, Domain, Channel, Service}, - #mix_subscription.user_chan), + {User, Domain, Channel, Service}, + #mix_subscription.user_chan), lists:foreach(fun mnesia:dirty_delete_object/1, Ret). + unsubscribe(_LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), lists:foreach( fun(Node) -> - mnesia:dirty_delete(mix_subscription, - {User, Domain, Channel, Service, Node}) - end, Nodes). + mnesia:dirty_delete(mix_subscription, + {User, Domain, Channel, Service, Node}) + end, + Nodes). %%%=================================================================== %%% Internal functions diff --git a/src/mod_mix_opt.erl b/src/mod_mix_opt.erl index b8225b19e..8765ef712 100644 --- a/src/mod_mix_opt.erl +++ b/src/mod_mix_opt.erl @@ -9,33 +9,37 @@ -export([hosts/1]). -export([name/1]). + -spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_create(Opts) when is_map(Opts) -> gen_mod:get_opt(access_create, Opts); access_create(Host) -> gen_mod:get_module_opt(Host, mod_mix, access_create). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mix, db_type). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_mix, host). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_mix, hosts). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_mix, name). - diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl index bae6133fb..7dabcd50f 100644 --- a/src/mod_mix_pam.erl +++ b/src/mod_mix_pam.erl @@ -29,14 +29,16 @@ -export([mod_doc/0]). %% Hooks and handlers -export([bounce_sm_packet/1, - disco_sm_features/5, - remove_user/2, - process_iq/1, - get_mix_roster_items/2, - webadmin_user/4, - webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). + disco_sm_features/5, + remove_user/2, + process_iq/1, + get_mix_roster_items/2, + webadmin_user/4, + webadmin_menu_hostuser/4, + webadmin_page_hostuser/4]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("mod_roster.hrl"). -include("translate.hrl"). @@ -45,6 +47,7 @@ -define(MIX_PAM_CACHE, mix_pam_cache). + -callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. -callback add_channel(jid(), jid(), binary()) -> ok | {error, db_failure}. -callback del_channel(jid(), jid()) -> ok | {error, db_failure}. @@ -56,44 +59,50 @@ -optional_callbacks([use_cache/1, cache_nodes/1]). + %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), case Mod:init(Host, Opts) of - ok -> - init_cache(Mod, Host, Opts), - {ok, - [{hook, bounce_sm_packet, bounce_sm_packet, 50}, - {hook, disco_sm_features, disco_sm_features, 50}, - {hook, remove_user, remove_user, 50}, - {hook, roster_get, get_mix_roster_items, 50}, - {hook, webadmin_user, webadmin_user, 50}, - {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, - {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, - {iq_handler, ejabberd_sm, ?NS_MIX_PAM_0, process_iq}, - {iq_handler, ejabberd_sm, ?NS_MIX_PAM_2, process_iq}]}; - Err -> - Err + ok -> + init_cache(Mod, Host, Opts), + {ok, + [{hook, bounce_sm_packet, bounce_sm_packet, 50}, + {hook, disco_sm_features, disco_sm_features, 50}, + {hook, remove_user, remove_user, 50}, + {hook, roster_get, get_mix_roster_items, 50}, + {hook, webadmin_user, webadmin_user, 50}, + {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, + {iq_handler, ejabberd_sm, ?NS_MIX_PAM_0, process_iq}, + {iq_handler, ejabberd_sm, ?NS_MIX_PAM_2, process_iq}]}; + Err -> + Err end. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> + if + NewMod /= OldMod -> NewMod:init(Host, NewOpts); - true -> + true -> ok end, init_cache(NewMod, Host, NewOpts). + depends(_Host, _Opts) -> []. + mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> @@ -105,6 +114,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, @@ -112,248 +122,314 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0405.html" "[XEP-0405: Mediated Information eXchange (MIX): " "Participant Server Requirements]. " "The module is needed if MIX compatible clients " "on your server are going to join MIX channels " - "(either on your server or on any remote servers)."), "", + "(either on your server or on any remote servers)."), + "", ?T("NOTE: _`mod_mix`_ is not required for this module " "to work, however, without 'mod_mix_pam' the MIX " "functionality of your local XMPP clients will be impaired.")], opts => [{db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}] + }. + -spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}. -bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To, - from = From, - type = groupchat} = Msg} = Acc) -> +bounce_sm_packet({_, + #message{ + to = #jid{lresource = <<>>} = To, + from = From, + type = groupchat + } = Msg} = Acc) -> case xmpp:has_subtag(Msg, #mix{}) of - true -> - {LUser, LServer, _} = jid:tolower(To), - case get_channel(To, From) of - {ok, _} -> - lists:foreach( - fun(R) -> - To1 = jid:replace_resource(To, R), - ejabberd_router:route(xmpp:set_to(Msg, To1)) - end, ejabberd_sm:get_user_resources(LUser, LServer)), - {pass, Msg}; - _ -> - Acc - end; - false -> - Acc + true -> + {LUser, LServer, _} = jid:tolower(To), + case get_channel(To, From) of + {ok, _} -> + lists:foreach( + fun(R) -> + To1 = jid:replace_resource(To, R), + ejabberd_router:route(xmpp:set_to(Msg, To1)) + end, + ejabberd_sm:get_user_resources(LUser, LServer)), + {pass, Msg}; + _ -> + Acc + end; + false -> + Acc end; bounce_sm_packet(Acc) -> Acc. + -spec disco_sm_features({error, stanza_error()} | empty | {result, [binary()]}, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | empty | {result, [binary()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; disco_sm_features(Acc, _From, _To, <<"">>, _Lang) -> - {result, [?NS_MIX_PAM_0, ?NS_MIX_PAM_2 | - case Acc of - {result, Features} -> Features; - empty -> [] - end]}; + {result, [?NS_MIX_PAM_0, ?NS_MIX_PAM_2 | case Acc of + {result, Features} -> Features; + empty -> [] + end]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec process_iq(iq()) -> iq() | ignore. -process_iq(#iq{from = #jid{luser = U1, lserver = S1}, - to = #jid{luser = U2, lserver = S2}} = IQ) +process_iq(#iq{ + from = #jid{luser = U1, lserver = S1}, + to = #jid{luser = U2, lserver = S2} + } = IQ) when {U1, S1} /= {U2, S2} -> xmpp:make_error(IQ, forbidden_query_error(IQ)); -process_iq(#iq{type = set, - sub_els = [#mix_client_join{} = Join]} = IQ) -> +process_iq(#iq{ + type = set, + sub_els = [#mix_client_join{} = Join] + } = IQ) -> case Join#mix_client_join.channel of - undefined -> - xmpp:make_error(IQ, missing_channel_error(IQ)); - _ -> - process_join(IQ) + undefined -> + xmpp:make_error(IQ, missing_channel_error(IQ)); + _ -> + process_join(IQ) end; -process_iq(#iq{type = set, - sub_els = [#mix_client_leave{} = Leave]} = IQ) -> +process_iq(#iq{ + type = set, + sub_els = [#mix_client_leave{} = Leave] + } = IQ) -> case Leave#mix_client_leave.channel of - undefined -> - xmpp:make_error(IQ, missing_channel_error(IQ)); - _ -> - process_leave(IQ) + undefined -> + xmpp:make_error(IQ, missing_channel_error(IQ)); + _ -> + process_leave(IQ) end; process_iq(IQ) -> xmpp:make_error(IQ, unsupported_query_error(IQ)). + -spec get_mix_roster_items([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. get_mix_roster_items(Acc, {LUser, LServer}) -> JID = jid:make(LUser, LServer), case get_channels(JID) of {ok, Channels} -> lists:map( - fun({ItemJID, Id}) -> - #roster_item{ + fun({ItemJID, Id}) -> + #roster_item{ jid = ItemJID, name = <<>>, subscription = both, ask = undefined, groups = [], mix_channel = #mix_roster_channel{participant_id = Id} - } - end, Channels); + } + end, + Channels); _ -> [] end ++ Acc. + -spec remove_user(binary(), binary()) -> ok | {error, db_failure}. remove_user(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), JID = jid:make(LUser, LServer), Chans = case Mod:get_channels(JID) of - {ok, Channels} -> - lists:map( - fun({Channel, _}) -> - ejabberd_router:route( - #iq{from = JID, - to = Channel, - id = p1_rand:get_string(), - type = set, - sub_els = [#mix_leave{}]}), - Channel - end, Channels); - _ -> - [] - end, + {ok, Channels} -> + lists:map( + fun({Channel, _}) -> + ejabberd_router:route( + #iq{ + from = JID, + to = Channel, + id = p1_rand:get_string(), + type = set, + sub_els = [#mix_leave{}] + }), + Channel + end, + Channels); + _ -> + [] + end, Mod:del_channels(jid:make(LUser, LServer)), lists:foreach( fun(Chan) -> - delete_cache(Mod, JID, Chan) - end, Chans). + delete_cache(Mod, JID, Chan) + end, + Chans). + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec process_join(iq()) -> ignore. -process_join(#iq{from = From, lang = Lang, - sub_els = [#mix_client_join{channel = Channel, - join = Join}]} = IQ) -> +process_join(#iq{ + from = From, + lang = Lang, + sub_els = [#mix_client_join{ + channel = Channel, + join = Join + }] + } = IQ) -> ejabberd_router:route_iq( - #iq{from = jid:remove_resource(From), - to = Channel, type = set, sub_els = [Join]}, + #iq{ + from = jid:remove_resource(From), + to = Channel, + type = set, + sub_els = [Join] + }, fun(#iq{sub_els = [El]} = ResIQ) -> - try xmpp:decode(El) of - MixJoin -> - process_join_result(ResIQ#iq { - sub_els = [MixJoin] - }, IQ) - catch - _:{xmpp_codec, Reason} -> - Txt = xmpp:io_format_error(Reason), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(IQ, Err) - end + try xmpp:decode(El) of + MixJoin -> + process_join_result(ResIQ#iq{ + sub_els = [MixJoin] + }, + IQ) + catch + _:{xmpp_codec, Reason} -> + Txt = xmpp:io_format_error(Reason), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(IQ, Err) + end end), ignore. + -spec process_leave(iq()) -> iq() | error. -process_leave(#iq{from = From, - sub_els = [#mix_client_leave{channel = Channel, - leave = Leave}]} = IQ) -> +process_leave(#iq{ + from = From, + sub_els = [#mix_client_leave{ + channel = Channel, + leave = Leave + }] + } = IQ) -> case del_channel(From, Channel) of - ok -> - ejabberd_router:route_iq( - #iq{from = jid:remove_resource(From), - to = Channel, type = set, sub_els = [Leave]}, - fun(ResIQ) -> process_leave_result(ResIQ, IQ) end), - ignore; - {error, db_failure} -> - xmpp:make_error(IQ, db_error(IQ)) + ok -> + ejabberd_router:route_iq( + #iq{ + from = jid:remove_resource(From), + to = Channel, + type = set, + sub_els = [Leave] + }, + fun(ResIQ) -> process_leave_result(ResIQ, IQ) end), + ignore; + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. + -spec process_join_result(iq(), iq()) -> ok. -process_join_result(#iq{from = #jid{} = Channel, - type = result, sub_els = [#mix_join{id = ID, xmlns = XmlNs} = Join]}, - #iq{to = To} = IQ) -> +process_join_result(#iq{ + from = #jid{} = Channel, + type = result, + sub_els = [#mix_join{id = ID, xmlns = XmlNs} = Join] + }, + #iq{to = To} = IQ) -> case add_channel(To, Channel, ID) of - ok -> - % Do roster push - mod_roster:push_item(To, #roster_item{jid = #jid{}}, #roster_item{ - jid = Channel, - name = <<>>, - subscription = none, - ask = undefined, - groups = [], - mix_channel = #mix_roster_channel{participant_id = ID} - }), - % send IQ result - ChanID = make_channel_id(Channel, ID), - Join1 = Join#mix_join{id = <<"">>, jid = ChanID}, - ResIQ = xmpp:make_iq_result(IQ, #mix_client_join{join = Join1, xmlns = XmlNs}), - ejabberd_router:route(ResIQ); - {error, db_failure} -> - ejabberd_router:route_error(IQ, db_error(IQ)) + ok -> + % Do roster push + mod_roster:push_item(To, + #roster_item{jid = #jid{}}, + #roster_item{ + jid = Channel, + name = <<>>, + subscription = none, + ask = undefined, + groups = [], + mix_channel = #mix_roster_channel{participant_id = ID} + }), + % send IQ result + ChanID = make_channel_id(Channel, ID), + Join1 = Join#mix_join{id = <<"">>, jid = ChanID}, + ResIQ = xmpp:make_iq_result(IQ, #mix_client_join{join = Join1, xmlns = XmlNs}), + ejabberd_router:route(ResIQ); + {error, db_failure} -> + ejabberd_router:route_error(IQ, db_error(IQ)) end; process_join_result(#iq{type = error} = Err, IQ) -> process_iq_error(Err, IQ). + -spec process_leave_result(iq(), iq()) -> ok. process_leave_result(#iq{from = Channel, type = result, sub_els = [#mix_leave{xmlns = XmlNs} = Leave]}, - #iq{to = User} = IQ) -> + #iq{to = User} = IQ) -> % Do roster push mod_roster:push_item(User, - #roster_item{jid = Channel, subscription = none}, - #roster_item{jid = Channel, subscription = remove}), + #roster_item{jid = Channel, subscription = none}, + #roster_item{jid = Channel, subscription = remove}), % send iq result ResIQ = xmpp:make_iq_result(IQ, #mix_client_leave{leave = Leave, xmlns = XmlNs}), ejabberd_router:route(ResIQ); process_leave_result(Err, IQ) -> process_iq_error(Err, IQ). + -spec process_iq_error(iq(), iq()) -> ok. process_iq_error(#iq{type = error} = ErrIQ, #iq{sub_els = [El]} = IQ) -> case xmpp:get_error(ErrIQ) of - undefined -> - %% Not sure if this stuff is correct because - %% RFC6120 section 8.3.1 bullet 4 states that - %% an error stanza MUST contain an child element - IQ1 = xmpp:make_iq_result(IQ, El), - ejabberd_router:route(IQ1#iq{type = error}); - Err -> - ejabberd_router:route_error(IQ, Err) + undefined -> + %% Not sure if this stuff is correct because + %% RFC6120 section 8.3.1 bullet 4 states that + %% an error stanza MUST contain an child element + IQ1 = xmpp:make_iq_result(IQ, El), + ejabberd_router:route(IQ1#iq{type = error}); + Err -> + ejabberd_router:route_error(IQ, Err) end; process_iq_error(timeout, IQ) -> Txt = ?T("Request has timed out"), Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), ejabberd_router:route_error(IQ, Err). + -spec make_channel_id(jid(), binary()) -> jid(). make_channel_id(JID, ID) -> {U, S, R} = jid:split(JID), jid:make(<>, S, R). + %%%=================================================================== %%% Error generators %%%=================================================================== @@ -362,21 +438,25 @@ missing_channel_error(Pkt) -> Txt = ?T("Attribute 'channel' is required for this request"), xmpp:err_bad_request(Txt, xmpp:get_lang(Pkt)). + -spec forbidden_query_error(stanza()) -> stanza_error(). forbidden_query_error(Pkt) -> Txt = ?T("Query to another users is forbidden"), xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + -spec unsupported_query_error(stanza()) -> stanza_error(). unsupported_query_error(Pkt) -> Txt = ?T("No module is handling this query"), xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). + -spec db_error(stanza()) -> stanza_error(). db_error(Pkt) -> Txt = ?T("Database failure"), xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). + %%%=================================================================== %%% Database queries %%%=================================================================== @@ -385,48 +465,54 @@ get_channel(JID, Channel) -> {Chan, Service, _} = jid:tolower(Channel), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of - false -> Mod:get_channel(JID, Channel); - true -> - case ets_cache:lookup( - ?MIX_PAM_CACHE, {LUser, LServer, Chan, Service}, - fun() -> Mod:get_channel(JID, Channel) end) of - error -> {error, notfound}; - Ret -> Ret - end + false -> Mod:get_channel(JID, Channel); + true -> + case ets_cache:lookup( + ?MIX_PAM_CACHE, + {LUser, LServer, Chan, Service}, + fun() -> Mod:get_channel(JID, Channel) end) of + error -> {error, notfound}; + Ret -> Ret + end end. + get_channels(JID) -> {_, LServer, _} = jid:tolower(JID), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:get_channels(JID). + add_channel(JID, Channel, ID) -> Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE), case Mod:add_channel(JID, Channel, ID) of - ok -> delete_cache(Mod, JID, Channel); - Err -> Err + ok -> delete_cache(Mod, JID, Channel); + Err -> Err end. + del_channel(JID, Channel) -> Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE), case Mod:del_channel(JID, Channel) of - ok -> delete_cache(Mod, JID, Channel); - Err -> Err + ok -> delete_cache(Mod, JID, Channel); + Err -> Err end. + %%%=================================================================== %%% Cache management %%%=================================================================== -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?MIX_PAM_CACHE, CacheOpts); - false -> - ets_cache:delete(?MIX_PAM_CACHE) + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?MIX_PAM_CACHE, CacheOpts); + false -> + ets_cache:delete(?MIX_PAM_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_mix_pam_opt:cache_size(Opts), @@ -434,82 +520,92 @@ cache_opts(Opts) -> LifeTime = mod_mix_pam_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(Host); - false -> mod_mix_pam_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_mix_pam_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + -spec delete_cache(module(), jid(), jid()) -> ok. delete_cache(Mod, JID, Channel) -> {LUser, LServer, _} = jid:tolower(JID), {Chan, Service, _} = jid:tolower(Channel), case use_cache(Mod, LServer) of - true -> - ets_cache:delete(?MIX_PAM_CACHE, - {LUser, LServer, Chan, Service}, - cache_nodes(Mod, LServer)); - false -> - ok + true -> + ets_cache:delete(?MIX_PAM_CACHE, + {LUser, LServer, Chan, Service}, + cache_nodes(Mod, LServer)); + false -> + ok end. + %%%=================================================================== %%% Webadmin interface %%%=================================================================== webadmin_user(Acc, User, Server, #request{lang = Lang}) -> QueueLen = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of - {ok, Channels} -> length(Channels); - error -> -1 - end, + {ok, Channels} -> length(Channels); + error -> -1 + end, FQueueLen = ?C(integer_to_binary(QueueLen)), FQueueView = ?AC(<<"mix_channels/">>, ?T("View joined MIX channels")), Acc ++ - [?XCT(<<"h3">>, ?T("Joined MIX channels:")), - FQueueLen, - ?C(<<" | ">>), - FQueueView]. + [?XCT(<<"h3">>, ?T("Joined MIX channels:")), + FQueueLen, + ?C(<<" | ">>), + FQueueView]. + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"mix_channels">>, <<"MIX Channels">>}]. + webadmin_page_hostuser(_, Host, U, #request{path = [<<"mix_channels">>], lang = Lang}) -> Res = web_mix_channels(U, Host, Lang), {stop, Res}; webadmin_page_hostuser(Acc, _, _, _) -> Acc. + web_mix_channels(User, Server, Lang) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, Items = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of - {ok, Channels} -> Channels; - error -> [] - end, + {ok, Channels} -> Channels; + error -> [] + end, SItems = lists:sort(Items), FItems = case SItems of - [] -> [?CT(?T("None"))]; - _ -> - THead = ?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Channel JID")), - ?XCT(<<"td">>, ?T("Participant ID"))])]), - Entries = lists:map(fun ({JID, ID}) -> - ?XE(<<"tr">>, [ - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], jid:encode(JID)), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], ID) - ]) - end, SItems), - [?XE(<<"table">>, [THead, ?XE(<<"tbody">>, Entries)])] - end, + [] -> [?CT(?T("None"))]; + _ -> + THead = ?XE(<<"thead">>, + [?XE(<<"tr">>, + [?XCT(<<"td">>, ?T("Channel JID")), + ?XCT(<<"td">>, ?T("Participant ID"))])]), + Entries = lists:map(fun({JID, ID}) -> + ?XE(<<"tr">>, + [?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], jid:encode(JID)), + ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], ID)]) + end, + SItems), + [?XE(<<"table">>, [THead, ?XE(<<"tbody">>, Entries)])] + end, PageTitle = str:translate_and_format(Lang, ?T("Joined MIX channels of ~ts"), [us_to_list(US)]), - (?H1GL(PageTitle, <<"modules/#mod_mix_pam">>, <<"mod_mix_pam">>)) - ++ FItems. + (?H1GL(PageTitle, <<"modules/#mod_mix_pam">>, <<"mod_mix_pam">>)) ++ + FItems. + us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). diff --git a/src/mod_mix_pam_mnesia.erl b/src/mod_mix_pam_mnesia.erl index 7d14579eb..31753bd57 100644 --- a/src/mod_mix_pam_mnesia.erl +++ b/src/mod_mix_pam_mnesia.erl @@ -24,26 +24,35 @@ -behaviour(mod_mix_pam). %% API --export([init/2, add_channel/3, get_channel/2, - get_channels/1, del_channel/2, del_channels/1, - use_cache/1]). +-export([init/2, + add_channel/3, + get_channel/2, + get_channels/1, + del_channel/2, + del_channels/1, + use_cache/1]). + +-record(mix_pam, { + user_channel :: {binary(), binary(), binary(), binary()}, + user :: {binary(), binary()}, + id :: binary() + }). --record(mix_pam, {user_channel :: {binary(), binary(), binary(), binary()}, - user :: {binary(), binary()}, - id :: binary()}). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - case ejabberd_mnesia:create(?MODULE, mix_pam, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, mix_pam)}, - {index, [user]}]) of - {atomic, _} -> ok; - _ -> {error, db_failure} + case ejabberd_mnesia:create(?MODULE, + mix_pam, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, mix_pam)}, + {index, [user]}]) of + {atomic, _} -> ok; + _ -> {error, db_failure} end. + use_cache(Host) -> case mnesia:table_info(mix_pam, storage_type) of disc_only_copies -> @@ -52,35 +61,45 @@ use_cache(Host) -> false end. + add_channel(User, Channel, ID) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), - mnesia:dirty_write(#mix_pam{user_channel = {LUser, LServer, Chan, Service}, - user = {LUser, LServer}, - id = ID}). + mnesia:dirty_write(#mix_pam{ + user_channel = {LUser, LServer, Chan, Service}, + user = {LUser, LServer}, + id = ID + }). + get_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case mnesia:dirty_read(mix_pam, {LUser, LServer, Chan, Service}) of - [#mix_pam{id = ID}] -> {ok, ID}; - [] -> {error, notfound} + [#mix_pam{id = ID}] -> {ok, ID}; + [] -> {error, notfound} end. + get_channels(User) -> {LUser, LServer, _} = jid:tolower(User), Ret = mnesia:dirty_index_read(mix_pam, {LUser, LServer}, #mix_pam.user), {ok, lists:map( - fun(#mix_pam{user_channel = {_, _, Chan, Service}, - id = ID}) -> - {jid:make(Chan, Service), ID} - end, Ret)}. + fun(#mix_pam{ + user_channel = {_, _, Chan, Service}, + id = ID + }) -> + {jid:make(Chan, Service), ID} + end, + Ret)}. + del_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), mnesia:dirty_delete(mix_pam, {LUser, LServer, Chan, Service}). + del_channels(User) -> {LUser, LServer, _} = jid:tolower(User), Ret = mnesia:dirty_index_read(mix_pam, {LUser, LServer}, #mix_pam.user), diff --git a/src/mod_mix_pam_opt.erl b/src/mod_mix_pam_opt.erl index 103e6039c..bfd94eb38 100644 --- a/src/mod_mix_pam_opt.erl +++ b/src/mod_mix_pam_opt.erl @@ -9,33 +9,37 @@ -export([db_type/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, db_type). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, use_cache). - diff --git a/src/mod_mix_pam_sql.erl b/src/mod_mix_pam_sql.erl index af22c74f4..2aaa87be8 100644 --- a/src/mod_mix_pam_sql.erl +++ b/src/mod_mix_pam_sql.erl @@ -24,13 +24,18 @@ -behaviour(mod_mix_pam). %% API --export([init/2, add_channel/3, get_channel/2, - get_channels/1, del_channel/2, del_channels/1]). +-export([init/2, + add_channel/3, + get_channel/2, + get_channels/1, + del_channel/2, + del_channels/1]). -export([sql_schemas/0]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -38,97 +43,114 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"mix_pam">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"channel">>, type = text}, - #sql_column{name = <<"service">>, type = text}, - #sql_column{name = <<"id">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"username">>, <<"server_host">>, - <<"channel">>, <<"service">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"mix_pam">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"id">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"username">>, + <<"server_host">>, + <<"channel">>, + <<"service">>], + unique = true + }] + }] + }]. + add_channel(User, Channel, ID) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), - case ?SQL_UPSERT(LServer, "mix_pam", - ["!channel=%(Chan)s", - "!service=%(Service)s", - "!username=%(LUser)s", - "!server_host=%(LServer)s", - "id=%(ID)s"]) of - ok -> ok; - _Err -> {error, db_failure} + case ?SQL_UPSERT(LServer, + "mix_pam", + ["!channel=%(Chan)s", + "!service=%(Service)s", + "!username=%(LUser)s", + "!server_host=%(LServer)s", + "id=%(ID)s"]) of + ok -> ok; + _Err -> {error, db_failure} end. + get_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(id)s from mix_pam where " - "channel=%(Chan)s and service=%(Service)s " - "and username=%(LUser)s and %(LServer)H")) of - {selected, [{ID}]} -> {ok, ID}; - {selected, []} -> {error, notfound}; - _Err -> {error, db_failure} + LServer, + ?SQL("select @(id)s from mix_pam where " + "channel=%(Chan)s and service=%(Service)s " + "and username=%(LUser)s and %(LServer)H")) of + {selected, [{ID}]} -> {ok, ID}; + {selected, []} -> {error, notfound}; + _Err -> {error, db_failure} end. + get_channels(User) -> {LUser, LServer, _} = jid:tolower(User), SQL = ?SQL("select @(channel)s, @(service)s, @(id)s from mix_pam " - "where username=%(LUser)s and %(LServer)H"), + "where username=%(LUser)s and %(LServer)H"), case ejabberd_sql:sql_query(LServer, SQL) of - {selected, Ret} -> - {ok, lists:filtermap( - fun({Chan, Service, ID}) -> - case jid:make(Chan, Service) of - error -> - report_corrupted(SQL), - false; - JID -> - {true, {JID, ID}} - end - end, Ret)}; - _Err -> - {error, db_failure} + {selected, Ret} -> + {ok, lists:filtermap( + fun({Chan, Service, ID}) -> + case jid:make(Chan, Service) of + error -> + report_corrupted(SQL), + false; + JID -> + {true, {JID, ID}} + end + end, + Ret)}; + _Err -> + {error, db_failure} end. + del_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from mix_pam where " - "channel=%(Chan)s and service=%(Service)s " - "and username=%(LUser)s and %(LServer)H")) of - {updated, _} -> ok; - _Err -> {error, db_failure} + LServer, + ?SQL("delete from mix_pam where " + "channel=%(Chan)s and service=%(Service)s " + "and username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + _Err -> {error, db_failure} end. + del_channels(User) -> {LUser, LServer, _} = jid:tolower(User), case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from mix_pam where " - "username=%(LUser)s and %(LServer)H")) of - {updated, _} -> ok; - _Err -> {error, db_failure} + LServer, + ?SQL("delete from mix_pam where " + "username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + _Err -> {error, db_failure} end. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec report_corrupted(#sql_query{}) -> ok. report_corrupted(SQL) -> ?ERROR_MSG("Corrupted values returned by SQL request: ~ts", - [SQL#sql_query.hash]). + [SQL#sql_query.hash]). diff --git a/src/mod_mix_sql.erl b/src/mod_mix_sql.erl index be3b28124..ed83e021e 100644 --- a/src/mod_mix_sql.erl +++ b/src/mod_mix_sql.erl @@ -32,6 +32,7 @@ -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,254 +40,298 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"mix_channel">>, - columns = - [#sql_column{name = <<"channel">>, type = text}, - #sql_column{name = <<"service">>, type = text}, - #sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"domain">>, type = text}, - #sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"hidden">>, type = boolean}, - #sql_column{name = <<"hmac_key">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"channel">>, <<"service">>], - unique = true}, - #sql_index{ - columns = [<<"service">>]}]}, - #sql_table{ - name = <<"mix_participant">>, - columns = - [#sql_column{name = <<"channel">>, type = text}, - #sql_column{name = <<"service">>, type = text}, - #sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"domain">>, type = text}, - #sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"id">>, type = text}, - #sql_column{name = <<"nick">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"channel">>, <<"service">>, - <<"username">>, <<"domain">>], - unique = true}]}, - #sql_table{ - name = <<"mix_subscription">>, - columns = - [#sql_column{name = <<"channel">>, type = text}, - #sql_column{name = <<"service">>, type = {text, 75}}, - #sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"domain">>, type = {text, 75}}, - #sql_column{name = <<"node">>, type = text}, - #sql_column{name = <<"jid">>, type = text}], - indices = [#sql_index{ - columns = [<<"channel">>, <<"service">>, - <<"username">>, <<"domain">>, - <<"node">>], - unique = true}, - #sql_index{ - columns = [<<"channel">>, <<"service">>, - <<"node">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"mix_channel">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"hidden">>, type = boolean}, + #sql_column{name = <<"hmac_key">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"channel">>, <<"service">>], + unique = true + }, + #sql_index{ + columns = [<<"service">>] + }] + }, + #sql_table{ + name = <<"mix_participant">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"id">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"channel">>, + <<"service">>, + <<"username">>, + <<"domain">>], + unique = true + }] + }, + #sql_table{ + name = <<"mix_subscription">>, + columns = + [#sql_column{name = <<"channel">>, type = text}, + #sql_column{name = <<"service">>, type = {text, 75}}, + #sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"domain">>, type = {text, 75}}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"jid">>, type = text}], + indices = [#sql_index{ + columns = [<<"channel">>, + <<"service">>, + <<"username">>, + <<"domain">>, + <<"node">>], + unique = true + }, + #sql_index{ + columns = [<<"channel">>, + <<"service">>, + <<"node">>] + }] + }] + }]. + set_channel(LServer, Channel, Service, CreatorJID, Hidden, Key) -> {User, Domain, _} = jid:tolower(CreatorJID), RawJID = jid:encode(jid:remove_resource(CreatorJID)), - case ?SQL_UPSERT(LServer, "mix_channel", - ["!channel=%(Channel)s", - "!service=%(Service)s", - "username=%(User)s", - "domain=%(Domain)s", - "jid=%(RawJID)s", - "hidden=%(Hidden)b", - "hmac_key=%(Key)s"]) of - ok -> ok; - _Err -> {error, db_failure} + case ?SQL_UPSERT(LServer, + "mix_channel", + ["!channel=%(Channel)s", + "!service=%(Service)s", + "username=%(User)s", + "domain=%(Domain)s", + "jid=%(RawJID)s", + "hidden=%(Hidden)b", + "hmac_key=%(Key)s"]) of + ok -> ok; + _Err -> {error, db_failure} end. + get_channels(LServer, Service) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(channel)s, @(hidden)b from mix_channel " - "where service=%(Service)s")) of - {selected, Ret} -> - {ok, [Channel || {Channel, Hidden} <- Ret, Hidden == false]}; - _Err -> - {error, db_failure} + LServer, + ?SQL("select @(channel)s, @(hidden)b from mix_channel " + "where service=%(Service)s")) of + {selected, Ret} -> + {ok, [ Channel || {Channel, Hidden} <- Ret, Hidden == false ]}; + _Err -> + {error, db_failure} end. + get_channel(LServer, Channel, Service) -> SQL = ?SQL("select @(jid)s, @(hidden)b, @(hmac_key)s from mix_channel " - "where channel=%(Channel)s and service=%(Service)s"), + "where channel=%(Channel)s and service=%(Service)s"), case ejabberd_sql:sql_query(LServer, SQL) of - {selected, [{RawJID, Hidden, Key}]} -> - try jid:decode(RawJID) of - JID -> {ok, {JID, Hidden, Key}} - catch _:{bad_jid, _} -> - report_corrupted(jid, SQL), - {error, db_failure} - end; - {selected, []} -> {error, notfound}; - _Err -> {error, db_failure} + {selected, [{RawJID, Hidden, Key}]} -> + try jid:decode(RawJID) of + JID -> {ok, {JID, Hidden, Key}} + catch + _:{bad_jid, _} -> + report_corrupted(jid, SQL), + {error, db_failure} + end; + {selected, []} -> {error, notfound}; + _Err -> {error, db_failure} end. + del_channel(LServer, Channel, Service) -> F = fun() -> - ejabberd_sql:sql_query_t( - ?SQL("delete from mix_channel where " - "channel=%(Channel)s and service=%(Service)s")), - ejabberd_sql:sql_query_t( - ?SQL("delete from mix_participant where " - "channel=%(Channel)s and service=%(Service)s")), - ejabberd_sql:sql_query_t( - ?SQL("delete from mix_subscription where " - "channel=%(Channel)s and service=%(Service)s")) - end, + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_channel where " + "channel=%(Channel)s and service=%(Service)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_participant where " + "channel=%(Channel)s and service=%(Service)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_subscription where " + "channel=%(Channel)s and service=%(Service)s")) + end, case ejabberd_sql:sql_transaction(LServer, F) of - {atomic, _} -> ok; - _Err -> {error, db_failure} + {atomic, _} -> ok; + _Err -> {error, db_failure} end. + set_participant(LServer, Channel, Service, JID, ID, Nick) -> {User, Domain, _} = jid:tolower(JID), RawJID = jid:encode(jid:remove_resource(JID)), - case ?SQL_UPSERT(LServer, "mix_participant", - ["!channel=%(Channel)s", - "!service=%(Service)s", - "!username=%(User)s", - "!domain=%(Domain)s", - "jid=%(RawJID)s", - "id=%(ID)s", - "nick=%(Nick)s"]) of - ok -> ok; - _Err -> {error, db_failure} + case ?SQL_UPSERT(LServer, + "mix_participant", + ["!channel=%(Channel)s", + "!service=%(Service)s", + "!username=%(User)s", + "!domain=%(Domain)s", + "jid=%(RawJID)s", + "id=%(ID)s", + "nick=%(Nick)s"]) of + ok -> ok; + _Err -> {error, db_failure} end. + -spec get_participant(binary(), binary(), binary(), jid:jid()) -> {ok, {binary(), binary()}} | {error, notfound | db_failure}. get_participant(LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(id)s, @(nick)s from mix_participant " - "where channel=%(Channel)s and service=%(Service)s " - "and username=%(User)s and domain=%(Domain)s")) of - {selected, [Ret]} -> {ok, Ret}; - {selected, []} -> {error, notfound}; - _Err -> {error, db_failure} + LServer, + ?SQL("select @(id)s, @(nick)s from mix_participant " + "where channel=%(Channel)s and service=%(Service)s " + "and username=%(User)s and domain=%(Domain)s")) of + {selected, [Ret]} -> {ok, Ret}; + {selected, []} -> {error, notfound}; + _Err -> {error, db_failure} end. + get_participants(LServer, Channel, Service) -> SQL = ?SQL("select @(jid)s, @(id)s, @(nick)s from mix_participant " - "where channel=%(Channel)s and service=%(Service)s"), + "where channel=%(Channel)s and service=%(Service)s"), case ejabberd_sql:sql_query(LServer, SQL) of - {selected, Ret} -> - {ok, lists:filtermap( - fun({RawJID, ID, Nick}) -> - try jid:decode(RawJID) of - JID -> {true, {JID, ID, Nick}} - catch _:{bad_jid, _} -> - report_corrupted(jid, SQL), - false - end - end, Ret)}; - _Err -> - {error, db_failure} + {selected, Ret} -> + {ok, lists:filtermap( + fun({RawJID, ID, Nick}) -> + try jid:decode(RawJID) of + JID -> {true, {JID, ID, Nick}} + catch + _:{bad_jid, _} -> + report_corrupted(jid, SQL), + false + end + end, + Ret)}; + _Err -> + {error, db_failure} end. + del_participant(LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from mix_participant where " - "channel=%(Channel)s and service=%(Service)s " - "and username=%(User)s and domain=%(Domain)s")) of - {updated, _} -> ok; - _Err -> {error, db_failure} + LServer, + ?SQL("delete from mix_participant where " + "channel=%(Channel)s and service=%(Service)s " + "and username=%(User)s and domain=%(Domain)s")) of + {updated, _} -> ok; + _Err -> {error, db_failure} end. + subscribe(_LServer, _Channel, _Service, _JID, []) -> ok; subscribe(LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), RawJID = jid:encode(jid:remove_resource(JID)), F = fun() -> - lists:foreach( - fun(Node) -> - ?SQL_UPSERT_T( - "mix_subscription", - ["!channel=%(Channel)s", - "!service=%(Service)s", - "!username=%(User)s", - "!domain=%(Domain)s", - "!node=%(Node)s", - "jid=%(RawJID)s"]) - end, Nodes) - end, + lists:foreach( + fun(Node) -> + ?SQL_UPSERT_T( + "mix_subscription", + ["!channel=%(Channel)s", + "!service=%(Service)s", + "!username=%(User)s", + "!domain=%(Domain)s", + "!node=%(Node)s", + "jid=%(RawJID)s"]) + end, + Nodes) + end, case ejabberd_sql:sql_transaction(LServer, F) of - {atomic, _} -> ok; - _Err -> {error, db_failure} + {atomic, _} -> ok; + _Err -> {error, db_failure} end. + get_subscribed(LServer, Channel, Service, Node) -> SQL = ?SQL("select @(jid)s from mix_subscription " - "where channel=%(Channel)s and service=%(Service)s " - "and node=%(Node)s"), + "where channel=%(Channel)s and service=%(Service)s " + "and node=%(Node)s"), case ejabberd_sql:sql_query(LServer, SQL) of - {selected, Ret} -> - {ok, lists:filtermap( - fun({RawJID}) -> - try jid:decode(RawJID) of - JID -> {true, JID} - catch _:{bad_jid, _} -> - report_corrupted(jid, SQL), - false - end - end, Ret)}; - _Err -> - {error, db_failure} + {selected, Ret} -> + {ok, lists:filtermap( + fun({RawJID}) -> + try jid:decode(RawJID) of + JID -> {true, JID} + catch + _:{bad_jid, _} -> + report_corrupted(jid, SQL), + false + end + end, + Ret)}; + _Err -> + {error, db_failure} end. + unsubscribe(LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from mix_subscription " - "where channel=%(Channel)s and service=%(Service)s " - "and username=%(User)s and domain=%(Domain)s")) of - {updated, _} -> ok; - _Err -> {error, db_failure} + LServer, + ?SQL("delete from mix_subscription " + "where channel=%(Channel)s and service=%(Service)s " + "and username=%(User)s and domain=%(Domain)s")) of + {updated, _} -> ok; + _Err -> {error, db_failure} end. + unsubscribe(_LServer, _Channel, _Service, _JID, []) -> ok; unsubscribe(LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), F = fun() -> - lists:foreach( - fun(Node) -> - ejabberd_sql:sql_query_t( - ?SQL("delete from mix_subscription " - "where channel=%(Channel)s " - "and service=%(Service)s " - "and username=%(User)s " - "and domain=%(Domain)s " - "and node=%(Node)s")) - end, Nodes) - end, + lists:foreach( + fun(Node) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from mix_subscription " + "where channel=%(Channel)s " + "and service=%(Service)s " + "and username=%(User)s " + "and domain=%(Domain)s " + "and node=%(Node)s")) + end, + Nodes) + end, case ejabberd_sql:sql_transaction(LServer, F) of - {atomic, ok} -> ok; - _Err -> {error, db_failure} + {atomic, ok} -> ok; + _Err -> {error, db_failure} end. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec report_corrupted(atom(), #sql_query{}) -> ok. report_corrupted(Column, SQL) -> ?ERROR_MSG("Corrupted value of '~ts' column returned by " - "SQL request: ~ts", [Column, SQL#sql_query.hash]). + "SQL request: ~ts", + [Column, SQL#sql_query.hash]). diff --git a/src/mod_mqtt.erl b/src/mod_mqtt.erl index e38c7aae6..3739ca3af 100644 --- a/src/mod_mqtt.erl +++ b/src/mod_mqtt.erl @@ -25,8 +25,12 @@ -export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). -export([mod_doc/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% ejabberd_listener API -export([start/3, start_link/3, listen_opt_type/1, listen_options/0, accept/1]). %% ejabberd_http API @@ -34,9 +38,15 @@ %% Legacy ejabberd_listener API -export([become_controller/2, socket_type/0]). %% API --export([open_session/1, close_session/1, lookup_session/1, - publish/3, subscribe/4, unsubscribe/2, select_retained/4, - check_publish_access/2, check_subscribe_access/2]). +-export([open_session/1, + close_session/1, + lookup_session/1, + publish/3, + subscribe/4, + unsubscribe/2, + select_retained/4, + check_publish_access/2, + check_subscribe_access/2]). %% ejabberd_hooks -export([remove_user/2]). @@ -44,12 +54,13 @@ -include("mqtt.hrl"). -include("translate.hrl"). --define(MQTT_TOPIC_CACHE, mqtt_topic_cache). +-define(MQTT_TOPIC_CACHE, mqtt_topic_cache). -define(MQTT_PAYLOAD_CACHE, mqtt_payload_cache). -type continuation() :: term(). -type seconds() :: non_neg_integer(). + %% RAM backend callbacks -callback init() -> ok | {error, any()}. -callback open_session(jid:ljid()) -> ok | {error, db_failure}. @@ -59,15 +70,15 @@ -callback subscribe(jid:ljid(), binary(), sub_opts(), non_neg_integer()) -> ok | {error, db_failure}. -callback unsubscribe(jid:ljid(), binary()) -> ok | {error, notfound | db_failure}. -callback find_subscriber(binary(), binary() | continuation()) -> - {ok, {pid(), qos()}, continuation()} | {error, notfound | db_failure}. + {ok, {pid(), qos()}, continuation()} | {error, notfound | db_failure}. %% Disc backend callbacks -callback init(binary(), gen_mod:opts()) -> ok | {error, any()}. -callback publish(jid:ljid(), binary(), binary(), qos(), properties(), seconds()) -> - ok | {error, db_failure}. + ok | {error, db_failure}. -callback delete_published(jid:ljid(), binary()) -> ok | {error, db_failure}. -callback lookup_published(jid:ljid(), binary()) -> - {ok, {binary(), qos(), properties(), seconds()}} | - {error, notfound | db_failure}. + {ok, {binary(), qos(), properties(), seconds()}} | + {error, notfound | db_failure}. -callback list_topics(binary()) -> {ok, [binary()]} | {error, db_failure}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. @@ -76,59 +87,73 @@ -record(state, {host :: binary()}). + %%%=================================================================== %%% API %%%=================================================================== start(SockMod, Sock, ListenOpts) -> mod_mqtt_session:start(SockMod, Sock, ListenOpts). + start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + start_link(SockMod, Sock, ListenOpts) -> mod_mqtt_session:start_link(SockMod, Sock, ListenOpts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + socket_type() -> raw. + become_controller(Pid, _) -> accept(Pid). + accept(Pid) -> mod_mqtt_session:accept(Pid). + socket_handoff(LocalPath, Request, Opts) -> mod_mqtt_ws:socket_handoff(LocalPath, Request, Opts). + open_session({U, S, R}) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:open_session({U, S, R}). + close_session({U, S, R}) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:close_session({U, S, R}). + lookup_session({U, S, R}) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:lookup_session({U, S, R}). + -spec publish(jid:ljid(), publish(), seconds()) -> - {ok, non_neg_integer()} | {error, db_failure | publish_forbidden}. + {ok, non_neg_integer()} | {error, db_failure | publish_forbidden}. publish({_, S, _} = USR, Pkt, ExpiryTime) -> case check_publish_access(Pkt#publish.topic, USR) of allow -> case retain(USR, Pkt, ExpiryTime) of ok -> - ejabberd_hooks:run(mqtt_publish, S, [USR, Pkt, ExpiryTime]), + ejabberd_hooks:run(mqtt_publish, S, [USR, Pkt, ExpiryTime]), Mod = gen_mod:ram_db_mod(S, ?MODULE), route(Mod, S, Pkt, ExpiryTime); {error, _} = Err -> @@ -138,86 +163,98 @@ publish({_, S, _} = USR, Pkt, ExpiryTime) -> {error, publish_forbidden} end. + -spec subscribe(jid:ljid(), binary(), sub_opts(), non_neg_integer()) -> - ok | {error, db_failure | subscribe_forbidden}. + ok | {error, db_failure | subscribe_forbidden}. subscribe({_, S, _} = USR, TopicFilter, SubOpts, ID) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Limit = mod_mqtt_opt:max_topic_depth(S), case check_topic_depth(TopicFilter, Limit) of - allow -> + allow -> case check_subscribe_access(TopicFilter, USR) of allow -> - ejabberd_hooks:run(mqtt_subscribe, S, [USR, TopicFilter, SubOpts, ID]), + ejabberd_hooks:run(mqtt_subscribe, S, [USR, TopicFilter, SubOpts, ID]), Mod:subscribe(USR, TopicFilter, SubOpts, ID); deny -> {error, subscribe_forbidden} end; - deny -> - {error, subscribe_forbidden} + deny -> + {error, subscribe_forbidden} end. + -spec unsubscribe(jid:ljid(), binary()) -> ok | {error, notfound | db_failure}. unsubscribe({U, S, R}, Topic) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), ejabberd_hooks:run(mqtt_unsubscribe, S, [{U, S, R}, Topic]), Mod:unsubscribe({U, S, R}, Topic). + -spec select_retained(jid:ljid(), binary(), qos(), non_neg_integer()) -> - [{publish(), seconds()}]. + [{publish(), seconds()}]. select_retained({_, S, _} = USR, TopicFilter, QoS, SubID) -> Mod = gen_mod:db_mod(S, ?MODULE), Limit = mod_mqtt_opt:match_retained_limit(S), select_retained(Mod, USR, TopicFilter, QoS, SubID, Limit). + remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:ram_db_mod(LServer, ?MODULE), Sessions = Mod:get_sessions(LUser, LServer), - [close_session(Session) || Session <- Sessions]. + [ close_session(Session) || Session <- Sessions ]. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Host|_]) -> +init([Host | _]) -> Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), RMod = gen_mod:ram_db_mod(Opts, ?MODULE), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), try - ok = Mod:init(Host, Opts), - ok = RMod:init(), - ok = init_cache(Mod, Host, Opts), - {ok, #state{host = Host}} - catch _:{badmatch, {error, Why}} -> - {stop, Why} + ok = Mod:init(Host, Opts), + ok = RMod:init(), + ok = init_cache(Mod, Host, Opts), + {ok, #state{host = Host}} + catch + _:{badmatch, {error, Why}} -> + {stop, Why} end. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Options %%%=================================================================== -spec mod_options(binary()) -> [{access_publish, [{[binary()], acl:acl()}]} | - {access_subscribe, [{[binary()], acl:acl()}]} | - {atom(), any()}]. + {access_subscribe, [{[binary()], acl:acl()}]} | + {atom(), any()}]. mod_options(Host) -> [{match_retained_limit, 1000}, {max_topic_depth, 8}, @@ -234,6 +271,7 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_opt_type(max_queue) -> econf:pos_int(unlimited); mod_opt_type(session_expiry) -> @@ -265,103 +303,137 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + listen_opt_type(tls_verify) -> econf:bool(); listen_opt_type(max_payload_size) -> econf:pos_int(infinity). + listen_options() -> [{max_fsm_queue, 10000}, {max_payload_size, infinity}, {tls, false}, {tls_verify, false}]. + %%%=================================================================== %%% Doc %%%=================================================================== mod_doc() -> - #{desc => + #{ + desc => ?T("This module adds " "_`../guide/mqtt/index.md|support for the MQTT`_ " "protocol version '3.1.1' and '5.0'. Remember to configure " - "'mod_mqtt' in 'modules' and 'listen' sections."), + "'mod_mqtt' in 'modules' and 'listen' sections."), opts => [{access_subscribe, - #{value => "{TopicFilter: AccessName}", + #{ + value => "{TopicFilter: AccessName}", desc => ?T("Access rules to restrict access to topics " - "for subscribers. By default there are no restrictions.")}}, + "for subscribers. By default there are no restrictions.") + }}, {access_publish, - #{value => "{TopicFilter: AccessName}", + #{ + value => "{TopicFilter: AccessName}", desc => ?T("Access rules to restrict access to topics " - "for publishers. By default there are no restrictions.")}}, + "for publishers. By default there are no restrictions.") + }}, {max_queue, - #{value => ?T("Size"), + #{ + value => ?T("Size"), desc => ?T("Maximum queue size for outgoing packets. " - "The default value is '5000'.")}}, + "The default value is '5000'.") + }}, {session_expiry, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("The option specifies how long to wait for " "an MQTT session resumption. When '0' is set, " "the session gets destroyed when the underlying " "client connection is closed. The default value is " - "'5' minutes.")}}, + "'5' minutes.") + }}, {max_topic_depth, - #{value => ?T("Depth"), + #{ + value => ?T("Depth"), desc => ?T("The maximum topic depth, i.e. the number of " "slashes ('/') in the topic. The default " - "value is '8'.")}}, + "value is '8'.") + }}, {max_topic_aliases, - #{value => "0..65535", + #{ + value => "0..65535", desc => ?T("The maximum number of aliases a client " "is able to associate with the topics. " - "The default value is '100'.")}}, + "The default value is '100'.") + }}, {match_retained_limit, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => ?T("The option limits the number of retained messages " "returned to a client when it subscribes to some " - "topic filter. The default value is '1000'.")}}, + "topic filter. The default value is '1000'.") + }}, {queue_type, - #{value => "ram | file", + #{ + value => "ram | file", desc => ?T("Same as top-level _`queue_type`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {ram_db_type, - #{value => "mnesia", + #{ + value => "mnesia", desc => ?T("Same as top-level _`default_ram_db`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, " - "but applied to this module only.")}}]}. + "but applied to this module only.") + }}] + }. + %%%=================================================================== %%% Internal functions @@ -369,147 +441,177 @@ mod_doc() -> route(Mod, LServer, Pkt, ExpiryTime) -> route(Mod, LServer, Pkt, ExpiryTime, Pkt#publish.topic, 0). + route(Mod, LServer, Pkt, ExpiryTime, Continuation, Num) -> case Mod:find_subscriber(LServer, Continuation) of {ok, {Pid, #sub_opts{no_local = true}, _}, Continuation1} when Pid == self() -> route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num); - {ok, {Pid, SubOpts, ID}, Continuation1} -> - ?DEBUG("Route to ~p: ~ts", [Pid, Pkt#publish.topic]), + {ok, {Pid, SubOpts, ID}, Continuation1} -> + ?DEBUG("Route to ~p: ~ts", [Pid, Pkt#publish.topic]), MinQoS = min(SubOpts#sub_opts.qos, Pkt#publish.qos), Retain = case SubOpts#sub_opts.retain_as_published of false -> false; true -> Pkt#publish.retain end, Props = set_sub_id(ID, Pkt#publish.properties), - mod_mqtt_session:route( - Pid, {Pkt#publish{qos = MinQoS, - dup = false, - retain = Retain, - properties = Props}, - ExpiryTime}), - route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num+1); - {error, _} -> - {ok, Num} + mod_mqtt_session:route( + Pid, + {Pkt#publish{ + qos = MinQoS, + dup = false, + retain = Retain, + properties = Props + }, + ExpiryTime}), + route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num + 1); + {error, _} -> + {ok, Num} end. + select_retained(Mod, {_, LServer, _} = USR, TopicFilter, QoS, SubID, Limit) -> Topics = match_topics(TopicFilter, LServer, Limit), lists:filtermap( fun({{Filter, _}, Topic}) -> - case lookup_published(Mod, USR, Topic) of - {ok, {Payload, QoS1, Props, ExpiryTime}} -> + case lookup_published(Mod, USR, Topic) of + {ok, {Payload, QoS1, Props, ExpiryTime}} -> Props1 = set_sub_id(SubID, Props), - {true, {#publish{topic = Topic, - payload = Payload, - retain = true, - properties = Props1, - qos = min(QoS, QoS1)}, + {true, {#publish{ + topic = Topic, + payload = Payload, + retain = true, + properties = Props1, + qos = min(QoS, QoS1) + }, ExpiryTime}}; - error -> - ets:delete(?MQTT_TOPIC_CACHE, {Filter, LServer}), - false; - _ -> - false - end - end, Topics). + error -> + ets:delete(?MQTT_TOPIC_CACHE, {Filter, LServer}), + false; + _ -> + false + end + end, + Topics). + match_topics(Topic, LServer, Limit) -> Filter = topic_filter(Topic), case Limit of - infinity -> - ets:match_object(?MQTT_TOPIC_CACHE, {{Filter, LServer}, '_'}); - _ -> - case ets:select(?MQTT_TOPIC_CACHE, - [{{{Filter, LServer}, '_'}, [], ['$_']}], Limit) of - {Topics, _} -> Topics; - '$end_of_table' -> [] - end + infinity -> + ets:match_object(?MQTT_TOPIC_CACHE, {{Filter, LServer}, '_'}); + _ -> + case ets:select(?MQTT_TOPIC_CACHE, + [{{{Filter, LServer}, '_'}, [], ['$_']}], + Limit) of + {Topics, _} -> Topics; + '$end_of_table' -> [] + end end. -retain({_, S, _} = USR, #publish{retain = true, - topic = Topic, payload = Data, - qos = QoS, properties = Props}, + +retain({_, S, _} = USR, + #publish{ + retain = true, + topic = Topic, + payload = Data, + qos = QoS, + properties = Props + }, ExpiryTime) -> Mod = gen_mod:db_mod(S, ?MODULE), TopicKey = topic_key(Topic), case Data of - <<>> -> - ets:delete(?MQTT_TOPIC_CACHE, {TopicKey, S}), - case use_cache(Mod, S) of - true -> - ets_cache:delete(?MQTT_PAYLOAD_CACHE, {S, Topic}, - cache_nodes(Mod, S)); - false -> - ok - end, - Mod:delete_published(USR, Topic); - _ -> - ets:insert(?MQTT_TOPIC_CACHE, {{TopicKey, S}, Topic}), - case use_cache(Mod, S) of - true -> - case ets_cache:update( - ?MQTT_PAYLOAD_CACHE, {S, Topic}, - {ok, {Data, QoS, Props, ExpiryTime}}, - fun() -> - Mod:publish(USR, Topic, Data, - QoS, Props, ExpiryTime) - end, cache_nodes(Mod, S)) of - {ok, _} -> ok; - {error, _} = Err -> Err - end; - false -> - Mod:publish(USR, Topic, Data, QoS, Props, ExpiryTime) - end + <<>> -> + ets:delete(?MQTT_TOPIC_CACHE, {TopicKey, S}), + case use_cache(Mod, S) of + true -> + ets_cache:delete(?MQTT_PAYLOAD_CACHE, + {S, Topic}, + cache_nodes(Mod, S)); + false -> + ok + end, + Mod:delete_published(USR, Topic); + _ -> + ets:insert(?MQTT_TOPIC_CACHE, {{TopicKey, S}, Topic}), + case use_cache(Mod, S) of + true -> + case ets_cache:update( + ?MQTT_PAYLOAD_CACHE, + {S, Topic}, + {ok, {Data, QoS, Props, ExpiryTime}}, + fun() -> + Mod:publish(USR, + Topic, + Data, + QoS, + Props, + ExpiryTime) + end, + cache_nodes(Mod, S)) of + {ok, _} -> ok; + {error, _} = Err -> Err + end; + false -> + Mod:publish(USR, Topic, Data, QoS, Props, ExpiryTime) + end end; retain(_, _, _) -> ok. + lookup_published(Mod, {_, LServer, _} = USR, Topic) -> case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?MQTT_PAYLOAD_CACHE, {LServer, Topic}, - fun() -> - Mod:lookup_published(USR, Topic) - end); - false -> - Mod:lookup_published(USR, Topic) + true -> + ets_cache:lookup( + ?MQTT_PAYLOAD_CACHE, + {LServer, Topic}, + fun() -> + Mod:lookup_published(USR, Topic) + end); + false -> + Mod:lookup_published(USR, Topic) end. + set_sub_id(0, Props) -> Props; set_sub_id(ID, Props) -> Props#{subscription_identifier => [ID]}. + %%%=================================================================== %%% Matching functions %%%=================================================================== topic_key(S) -> Parts = split_path(S), case join_key(Parts) of - [<<>>|T] -> T; - T -> T + [<<>> | T] -> T; + T -> T end. + topic_filter(S) -> Parts = split_path(S), case join_filter(Parts) of - [<<>>|T] -> T; - T -> T + [<<>> | T] -> T; + T -> T end. -join_key([X,Y|T]) -> - [X, $/|join_key([Y|T])]; + +join_key([X, Y | T]) -> + [X, $/ | join_key([Y | T])]; join_key([X]) -> [X]; join_key([]) -> []. + join_filter([X, <<$#>>]) -> - [wildcard(X)|'_']; -join_filter([X,Y|T]) -> - [wildcard(X), $/|join_filter([Y|T])]; + [wildcard(X) | '_']; +join_filter([X, Y | T]) -> + [wildcard(X), $/ | join_filter([Y | T])]; join_filter([<<>>]) -> []; join_filter([<<$#>>]) -> @@ -519,23 +621,27 @@ join_filter([X]) -> join_filter([]) -> []. + wildcard(<<$+>>) -> '_'; wildcard(Bin) -> Bin. + check_topic_depth(_Topic, infinity) -> allow; -check_topic_depth(_, N) when N=<0 -> +check_topic_depth(_, N) when N =< 0 -> deny; check_topic_depth(<<$/, T/binary>>, N) -> - check_topic_depth(T, N-1); + check_topic_depth(T, N - 1); check_topic_depth(<<_, T/binary>>, N) -> check_topic_depth(T, N); check_topic_depth(<<>>, _) -> allow. + split_path(Path) -> binary:split(Path, <<$/>>, [global]). + %%%=================================================================== %%% Validators %%%=================================================================== @@ -543,16 +649,19 @@ split_path(Path) -> topic_access_validator() -> econf:and_then( econf:map( - fun(TF) -> - try split_path(mqtt_codec:topic_filter(TF)) - catch _:{mqtt_codec, _} = Reason -> - econf:fail(Reason) - end - end, - econf:acl(), - [{return, orddict}]), + fun(TF) -> + try + split_path(mqtt_codec:topic_filter(TF)) + catch + _:{mqtt_codec, _} = Reason -> + econf:fail(Reason) + end + end, + econf:acl(), + [{return, orddict}]), fun lists:reverse/1). + %%%=================================================================== %%% ACL checks %%%=================================================================== @@ -560,12 +669,14 @@ check_subscribe_access(Topic, {_, S, _} = USR) -> Rules = mod_mqtt_opt:access_subscribe(S), check_access(Topic, USR, Rules). + check_publish_access(<<$$, _/binary>>, _) -> deny; check_publish_access(Topic, {_, S, _} = USR) -> Rules = mod_mqtt_opt:access_publish(S), check_access(Topic, USR, Rules). + check_access(_, _, []) -> allow; check_access(Topic, {U, S, R} = USR, FilterRules) -> @@ -578,33 +689,35 @@ check_access(Topic, {U, S, R} = USR, FilterRules) -> false -> false end - end, FilterRules) of + end, + FilterRules) of true -> allow; false -> deny end. -match(_, [<<"#">>|_], _, _, _) -> + +match(_, [<<"#">> | _], _, _, _) -> true; -match([], [<<>>, <<"#">>|_], _, _, _) -> +match([], [<<>>, <<"#">> | _], _, _, _) -> true; -match([_|T1], [<<"+">>|T2], U, S, R) -> +match([_ | T1], [<<"+">> | T2], U, S, R) -> match(T1, T2, U, S, R); -match([H|T1], [<<"%u">>|T2], U, S, R) -> +match([H | T1], [<<"%u">> | T2], U, S, R) -> case jid:nodeprep(H) of U -> match(T1, T2, U, S, R); _ -> false end; -match([H|T1], [<<"%d">>|T2], U, S, R) -> +match([H | T1], [<<"%d">> | T2], U, S, R) -> case jid:nameprep(H) of S -> match(T1, T2, U, S, R); _ -> false end; -match([H|T1], [<<"%c">>|T2], U, S, R) -> +match([H | T1], [<<"%c">> | T2], U, S, R) -> case jid:resourceprep(H) of R -> match(T1, T2, U, S, R); _ -> false end; -match([H|T1], [<<"%g">>|T2], U, S, R) -> +match([H | T1], [<<"%g">> | T2], U, S, R) -> case jid:resourceprep(H) of H -> case acl:loaded_shared_roster_module(S) of @@ -621,13 +734,14 @@ match([H|T1], [<<"%g">>|T2], U, S, R) -> end; _ -> false end; -match([H|T1], [H|T2], U, S, R) -> +match([H | T1], [H | T2], U, S, R) -> match(T1, T2, U, S, R); match([], [], _, _, _) -> true; match(_, _, _, _, _) -> false. + %%%=================================================================== %%% Cache stuff %%%=================================================================== @@ -636,10 +750,13 @@ init_cache(Mod, Host, Opts) -> init_payload_cache(Mod, Host, Opts), init_topic_cache(Mod, Host). + -spec init_topic_cache(module(), binary()) -> ok | {error, db_failure}. init_topic_cache(Mod, Host) -> catch ets:new(?MQTT_TOPIC_CACHE, - [named_table, ordered_set, public, + [named_table, + ordered_set, + public, {heir, erlang:group_leader(), none}]), ?INFO_MSG("Building MQTT cache for ~ts, this may take a while", [Host]), case Mod:list_topics(Host) of @@ -648,11 +765,13 @@ init_topic_cache(Mod, Host) -> fun(Topic) -> ets:insert(?MQTT_TOPIC_CACHE, {{topic_key(Topic), Host}, Topic}) - end, Topics); + end, + Topics); {error, _} = Err -> Err end. + -spec init_payload_cache(module(), binary(), gen_mod:opts()) -> ok. init_payload_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of @@ -663,6 +782,7 @@ init_payload_cache(Mod, Host, Opts) -> ets_cache:delete(?MQTT_PAYLOAD_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_mqtt_opt:cache_size(Opts), @@ -670,6 +790,7 @@ cache_opts(Opts) -> LifeTime = mod_mqtt_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of @@ -677,6 +798,7 @@ use_cache(Mod, Host) -> false -> mod_mqtt_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of diff --git a/src/mod_mqtt_bridge.erl b/src/mod_mqtt_bridge.erl index cb60594e9..d8e39581b 100644 --- a/src/mod_mqtt_bridge.erl +++ b/src/mod_mqtt_bridge.erl @@ -29,6 +29,7 @@ -include("mqtt.hrl"). -include("translate.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -37,181 +38,231 @@ start(_Host, Opts) -> start_servers(User, element(1, mod_mqtt_bridge_opt:servers(Opts))), {ok, [{hook, mqtt_publish, mqtt_publish_hook, 50}]}. + stop(Host) -> stop_servers(element(1, mod_mqtt_bridge_opt:servers(Host))), ok. + start_servers(User, Servers) -> lists:foldl( - fun({Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication}, Started) -> - case Started of - #{Proc := _} -> - ?DEBUG("Already started ~p", [Proc]), - Started; - _ -> - ChildSpec = {Proc, - {mod_mqtt_bridge_session, start_link, - [Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication, User]}, - transient, - 1000, - worker, - [mod_mqtt_bridge_session]}, - Res = supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec), - ?DEBUG("Starting ~p ~p", [Proc, Res]), - Started#{Proc => true} - end - end, #{}, Servers). + fun({Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication}, Started) -> + case Started of + #{Proc := _} -> + ?DEBUG("Already started ~p", [Proc]), + Started; + _ -> + ChildSpec = {Proc, + {mod_mqtt_bridge_session, start_link, + [Proc, Transport, HostAddr, Port, Path, Publish, Subscribe, Authentication, User]}, + transient, + 1000, + worker, + [mod_mqtt_bridge_session]}, + Res = supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec), + ?DEBUG("Starting ~p ~p", [Proc, Res]), + Started#{Proc => true} + end + end, + #{}, + Servers). + stop_servers(Servers) -> lists:foreach( - fun({Proc, _Transport, _Host, _Port, _Path, _Publish, _Subscribe, _Authentication}) -> - try p1_server:call(Proc, stop) - catch _:_ -> ok - end, - supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), - supervisor:delete_child(ejabberd_gen_mod_sup, Proc) - end, Servers). + fun({Proc, _Transport, _Host, _Port, _Path, _Publish, _Subscribe, _Authentication}) -> + try + p1_server:call(Proc, stop) + catch + _:_ -> ok + end, + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc) + end, + Servers). + reload(_Host, NewOpts, OldOpts) -> OldServers = element(1, mod_mqtt_bridge_opt:servers(OldOpts)), NewServers = element(1, mod_mqtt_bridge_opt:servers(NewOpts)), Deleted = lists:filter( - fun(E) -> not lists:keymember(element(1, E), 1, NewServers) end, - OldServers), + fun(E) -> not lists:keymember(element(1, E), 1, NewServers) end, + OldServers), Added = lists:filter( - fun(E) -> not lists:keymember(element(1, E), 1, OldServers) end, - NewServers), + fun(E) -> not lists:keymember(element(1, E), 1, OldServers) end, + NewServers), stop_servers(Deleted), start_servers(mod_mqtt_bridge_opt:replication_user(NewOpts), Added), ok. + depends(_Host, _Opts) -> [{mod_mqtt, hard}]. + proc_name(Proto, Host, Port, Path) -> HostB = list_to_binary(Host), TransportB = list_to_binary(Proto), PathB = case Path of - V when is_list(V) -> - list_to_binary(V); - _ -> <<>> - end, - binary_to_atom(<<"mod_mqtt_bridge_", TransportB/binary, "_", HostB/binary, - "_", (integer_to_binary(Port))/binary, PathB/binary>>, utf8). + V when is_list(V) -> + list_to_binary(V); + _ -> <<>> + end, + binary_to_atom(<<"mod_mqtt_bridge_", + TransportB/binary, + "_", + HostB/binary, + "_", + (integer_to_binary(Port))/binary, + PathB/binary>>, + utf8). + -spec mqtt_publish_hook(jid:ljid(), publish(), non_neg_integer()) -> ok. mqtt_publish_hook({_, S, _}, #publish{topic = Topic} = Pkt, _ExpiryTime) -> {_, Publish} = mod_mqtt_bridge_opt:servers(S), case maps:find(Topic, Publish) of - error -> ok; - {ok, Procs} -> - lists:foreach( - fun(Proc) -> - Proc ! {publish, Pkt} - end, Procs) + error -> ok; + {ok, Procs} -> + lists:foreach( + fun(Proc) -> + Proc ! {publish, Pkt} + end, + Procs) end. + %%%=================================================================== %%% Options %%%=================================================================== -spec mod_options(binary()) -> - [{servers, - {[{atom(), mqtt | mqtts | mqtt5 | mqtt5s, binary(), non_neg_integer(), - #{binary() => binary()}, #{binary() => binary()}, map()}], - #{binary() => [atom()]}}} | - {atom(), any()}]. + [{servers, + {[{atom(), + mqtt | mqtts | mqtt5 | mqtt5s, + binary(), + non_neg_integer(), + #{binary() => binary()}, + #{binary() => binary()}, + map()}], + #{binary() => [atom()]}}} | + {atom(), any()}]. mod_options(Host) -> [{servers, []}, {replication_user, jid:make(<<"admin">>, Host)}]. + mod_opt_type(replication_user) -> econf:jid(); mod_opt_type(servers) -> econf:and_then( - econf:map(econf:url([mqtt, mqtts, mqtt5, mqtt5s, ws, wss, ws5, wss5]), - econf:options( - #{ - publish => econf:map(econf:binary(), econf:binary(), [{return, map}]), - subscribe => econf:map(econf:binary(), econf:binary(), [{return, map}]), - authentication => econf:either( - econf:options( - #{ - username => econf:binary(), - password => econf:binary() - }, [{return, map}]), - econf:options( - #{ - certfile => econf:pem() - }, [{return, map}]))}, - [{return, map}]), - [{return, map}]), - fun(Servers) -> - maps:fold( - fun(Url, Opts, {HAcc, PAcc}) -> - {ok, Scheme, _UserInfo, Host, Port, Path, _Query} = - misc:uri_parse(Url, [{mqtt, 1883}, {mqtts, 8883}, - {mqtt5, 1883}, {mqtt5s, 8883}, - {ws, 80}, {wss, 443}, - {ws5, 80}, {wss5, 443}]), - Publish = maps:get(publish, Opts, #{}), - Subscribe = maps:get(subscribe, Opts, #{}), - Authentication = maps:get(authentication, Opts, []), - Proto = list_to_atom(Scheme), - Proc = proc_name(Scheme, Host, Port, Path), - PAcc2 = maps:fold( - fun(Topic, _RemoteTopic, Acc) -> - maps:update_with(Topic, fun(V) -> [Proc | V] end, [Proc], Acc) - end, PAcc, Publish), - {[{Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication} | HAcc], PAcc2} - end, {[], #{}}, Servers) - end - ). + econf:map(econf:url([mqtt, mqtts, mqtt5, mqtt5s, ws, wss, ws5, wss5]), + econf:options( + #{ + publish => econf:map(econf:binary(), econf:binary(), [{return, map}]), + subscribe => econf:map(econf:binary(), econf:binary(), [{return, map}]), + authentication => econf:either( + econf:options( + #{ + username => econf:binary(), + password => econf:binary() + }, + [{return, map}]), + econf:options( + #{ + certfile => econf:pem() + }, + [{return, map}])) + }, + [{return, map}]), + [{return, map}]), + fun(Servers) -> + maps:fold( + fun(Url, Opts, {HAcc, PAcc}) -> + {ok, Scheme, _UserInfo, Host, Port, Path, _Query} = + misc:uri_parse(Url, + [{mqtt, 1883}, + {mqtts, 8883}, + {mqtt5, 1883}, + {mqtt5s, 8883}, + {ws, 80}, + {wss, 443}, + {ws5, 80}, + {wss5, 443}]), + Publish = maps:get(publish, Opts, #{}), + Subscribe = maps:get(subscribe, Opts, #{}), + Authentication = maps:get(authentication, Opts, []), + Proto = list_to_atom(Scheme), + Proc = proc_name(Scheme, Host, Port, Path), + PAcc2 = maps:fold( + fun(Topic, _RemoteTopic, Acc) -> + maps:update_with(Topic, fun(V) -> [Proc | V] end, [Proc], Acc) + end, + PAcc, + Publish), + {[{Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication} | HAcc], PAcc2} + end, + {[], #{}}, + Servers) + end). + %%%=================================================================== %%% Doc %%%=================================================================== mod_doc() -> - #{desc => - [?T("This module adds ability to synchronize local MQTT topics with data on remote servers"), - ?T("It can update topics on remote servers when local user updates local topic, or can subscribe " - "for changes on remote server, and update local copy when remote data is updated."), - ?T("It is available since ejabberd 23.01.")], + #{ + desc => + [?T("This module adds ability to synchronize local MQTT topics with data on remote servers"), + ?T("It can update topics on remote servers when local user updates local topic, or can subscribe " + "for changes on remote server, and update local copy when remote data is updated."), + ?T("It is available since ejabberd 23.01.")], example => - ["modules:", - " mod_mqtt_bridge:", - " replication_user: \"mqtt@xmpp.server.com\"", - " servers:", - " \"mqtt://server.com\":", - " authentication:", - " certfile: \"/etc/ejabberd/mqtt_server.pem\"", - " publish:", - " \"localA\": \"remoteA\" # local changes to 'localA' will be replicated on remote server as 'remoteA'", - " \"topicB\": \"topicB\"", - " subscribe:", - " \"remoteB\": \"localB\" # changes to 'remoteB' on remote server will be stored as 'localB' on local server"], + ["modules:", + " mod_mqtt_bridge:", + " replication_user: \"mqtt@xmpp.server.com\"", + " servers:", + " \"mqtt://server.com\":", + " authentication:", + " certfile: \"/etc/ejabberd/mqtt_server.pem\"", + " publish:", + " \"localA\": \"remoteA\" # local changes to 'localA' will be replicated on remote server as 'remoteA'", + " \"topicB\": \"topicB\"", + " subscribe:", + " \"remoteB\": \"localB\" # changes to 'remoteB' on remote server will be stored as 'localB' on local server"], opts => - [{servers, - #{value => "{ServerUrl: {Key: Value}}", - desc => - ?T("Declaration of data to share for each ServerUrl. " - "Server URLs can use schemas: 'mqtt', 'mqtts' (mqtt with tls), 'mqtt5', " - "'mqtt5s' (both to trigger v5 protocol), 'ws', 'wss', 'ws5', 'wss5'. " - "Keys must be:")}, - [{authentication, - #{value => "{AuthKey: AuthValue}", - desc => ?T("List of authentication information, where AuthKey can be: " - "'username' and 'password' fields, or 'certfile' pointing to client certificate. " - "Certificate authentication can be used only with mqtts, mqtt5s, wss, wss5.")}}, - {publish, - #{value => "{LocalTopic: RemoteTopic}", - desc => ?T("Either publish or subscribe must be set, or both.")}}, - {subscribe, - #{value => "{RemoteTopic: LocalTopic}", - desc => ?T("Either publish or subscribe must be set, or both.")}}]}, - {replication_user, - #{value => "JID", - desc => - ?T("Identifier of a user that will be assigned as owner of local changes.")}}]}. + [{servers, + #{ + value => "{ServerUrl: {Key: Value}}", + desc => + ?T("Declaration of data to share for each ServerUrl. " + "Server URLs can use schemas: 'mqtt', 'mqtts' (mqtt with tls), 'mqtt5', " + "'mqtt5s' (both to trigger v5 protocol), 'ws', 'wss', 'ws5', 'wss5'. " + "Keys must be:") + }, + [{authentication, + #{ + value => "{AuthKey: AuthValue}", + desc => ?T("List of authentication information, where AuthKey can be: " + "'username' and 'password' fields, or 'certfile' pointing to client certificate. " + "Certificate authentication can be used only with mqtts, mqtt5s, wss, wss5.") + }}, + {publish, + #{ + value => "{LocalTopic: RemoteTopic}", + desc => ?T("Either publish or subscribe must be set, or both.") + }}, + {subscribe, + #{ + value => "{RemoteTopic: LocalTopic}", + desc => ?T("Either publish or subscribe must be set, or both.") + }}]}, + {replication_user, + #{ + value => "JID", + desc => + ?T("Identifier of a user that will be assigned as owner of local changes.") + }}] + }. %%%=================================================================== %%% Internal functions diff --git a/src/mod_mqtt_bridge_opt.erl b/src/mod_mqtt_bridge_opt.erl index e10f72e1d..ee0d4f5da 100644 --- a/src/mod_mqtt_bridge_opt.erl +++ b/src/mod_mqtt_bridge_opt.erl @@ -6,15 +6,16 @@ -export([replication_user/1]). -export([servers/1]). + -spec replication_user(gen_mod:opts() | global | binary()) -> jid:jid(). replication_user(Opts) when is_map(Opts) -> gen_mod:get_opt(replication_user, Opts); replication_user(Host) -> gen_mod:get_module_opt(Host, mod_mqtt_bridge, replication_user). --spec servers(gen_mod:opts() | global | binary()) -> {[{atom(),'mqtt' | 'mqtts' | 'mqtt5' | 'mqtt5s',binary(),non_neg_integer(),#{binary()=>binary()},#{binary()=>binary()},map()}],#{binary()=>[atom()]}}. + +-spec servers(gen_mod:opts() | global | binary()) -> {[{atom(), 'mqtt' | 'mqtts' | 'mqtt5' | 'mqtt5s', binary(), non_neg_integer(), #{binary() => binary()}, #{binary() => binary()}, map()}], #{binary() => [atom()]}}. servers(Opts) when is_map(Opts) -> gen_mod:get_opt(servers, Opts); servers(Host) -> gen_mod:get_module_opt(Host, mod_mqtt_bridge, servers). - diff --git a/src/mod_mqtt_bridge_session.erl b/src/mod_mqtt_bridge_session.erl index 1c5f53f9d..5694a260b 100644 --- a/src/mod_mqtt_bridge_session.erl +++ b/src/mod_mqtt_bridge_session.erl @@ -23,220 +23,254 @@ %% API -export([start/9, start_link/9]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). -include("mqtt.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). -include_lib("public_key/include/public_key.hrl"). -type error_reason() :: - {auth, reason_code()} | - {code, reason_code()} | - {peer_disconnected, reason_code(), binary()} | - {socket, socket_error_reason()} | - {codec, mqtt_codec:error_reason()} | - {unexpected_packet, atom()} | - {tls, inet:posix() | atom() | binary()} | - {replaced, pid()} | - {resumed, pid()} | - subscribe_forbidden | publish_forbidden | - will_topic_forbidden | internal_server_error | - session_expired | idle_connection | - queue_full | shutdown | db_failure | - {payload_format_invalid, will | publish} | - session_expiry_non_zero | unknown_topic_alias. + {auth, reason_code()} | + {code, reason_code()} | + {peer_disconnected, reason_code(), binary()} | + {socket, socket_error_reason()} | + {codec, mqtt_codec:error_reason()} | + {unexpected_packet, atom()} | + {tls, inet:posix() | atom() | binary()} | + {replaced, pid()} | + {resumed, pid()} | + subscribe_forbidden | + publish_forbidden | + will_topic_forbidden | + internal_server_error | + session_expired | + idle_connection | + queue_full | + shutdown | + db_failure | + {payload_format_invalid, will | publish} | + session_expiry_non_zero | + unknown_topic_alias. -type socket() :: - {gen_tcp, inet:socket()} | - {fast_tls, fast_tls:tls_socket()} | - {mod_mqtt_ws, mod_mqtt_ws:socket()}. + {gen_tcp, inet:socket()} | + {fast_tls, fast_tls:tls_socket()} | + {mod_mqtt_ws, mod_mqtt_ws:socket()}. -type seconds() :: non_neg_integer(). -type socket_error_reason() :: closed | timeout | inet:posix(). -define(PING_TIMEOUT, timer:seconds(50)). --define(MAX_UINT32, 4294967295). +-define(MAX_UINT32, 4294967295). --record(state, {vsn = ?VSN :: integer(), - version :: undefined | mqtt_version(), - socket :: undefined | socket(), - usr :: undefined | {binary(), binary(), binary()}, - ping_timer = undefined :: undefined | reference(), - stop_reason :: undefined | error_reason(), - subscriptions = #{}, - publish = #{}, - ws_codec = none, - id = 0 :: non_neg_integer(), - codec :: mqtt_codec:state(), - authentication :: #{username => binary(), password => binary(), certfile => binary()}}). +-record(state, { + vsn = ?VSN :: integer(), + version :: undefined | mqtt_version(), + socket :: undefined | socket(), + usr :: undefined | {binary(), binary(), binary()}, + ping_timer = undefined :: undefined | reference(), + stop_reason :: undefined | error_reason(), + subscriptions = #{}, + publish = #{}, + ws_codec = none, + id = 0 :: non_neg_integer(), + codec :: mqtt_codec:state(), + authentication :: #{username => binary(), password => binary(), certfile => binary()} + }). -type state() :: #state{}. + %%%=================================================================== %%% API %%%=================================================================== start(Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser) -> - p1_server:start({local, Proc}, ?MODULE, [Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, - ReplicationUser], []). + p1_server:start({local, Proc}, + ?MODULE, + [Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, + ReplicationUser], + []). + start_link(Proc, Transport, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser) -> - p1_server:start_link({local, Proc}, ?MODULE, [Proc, Transport, Host, Port, Path, Publish, Subscribe, - Authentication, ReplicationUser], []). + p1_server:start_link({local, Proc}, + ?MODULE, + [Proc, Transport, Host, Port, Path, Publish, Subscribe, + Authentication, ReplicationUser], + []). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([_Proc, Proto, Host, Port, Path, Publish, Subscribe, Authentication, ReplicationUser]) -> {Version, Transport, IsWs} = - case Proto of - mqtt -> {4, gen_tcp, false}; - mqtts -> {4, ssl, false}; - mqtt5 -> {5, gen_tcp, false}; - mqtt5s -> {5, ssl, false}; - ws -> {4, gen_tcp, true}; - wss -> {4, ssl, true}; - ws5 -> {5, gen_tcp, true}; - wss5 -> {5, ssl, true} - end, - State = #state{version = Version, - id = p1_rand:uniform(65535), - codec = mqtt_codec:new(4096), - subscriptions = Subscribe, - authentication = Authentication, - usr = jid:tolower(ReplicationUser), - publish = Publish}, + case Proto of + mqtt -> {4, gen_tcp, false}; + mqtts -> {4, ssl, false}; + mqtt5 -> {5, gen_tcp, false}; + mqtt5s -> {5, ssl, false}; + ws -> {4, gen_tcp, true}; + wss -> {4, ssl, true}; + ws5 -> {5, gen_tcp, true}; + wss5 -> {5, ssl, true} + end, + State = #state{ + version = Version, + id = p1_rand:uniform(65535), + codec = mqtt_codec:new(4096), + subscriptions = Subscribe, + authentication = Authentication, + usr = jid:tolower(ReplicationUser), + publish = Publish + }, case Authentication of - #{certfile := Cert} when Transport == ssl -> - Sock = ssl:connect(Host, Port, [binary, {active, true}, {certfile, Cert}]), - if IsWs -> - connect_ws(Host, Port, Path, Sock, State, ssl, none); - true -> connect(Sock, State, ssl, none) - end; - #{username := User, password := Pass} -> - Sock = Transport:connect(Host, Port, [binary, {active, true}]), - if IsWs -> - connect_ws(Host, Port, Path, Sock, State, Transport, {User, Pass}); - true -> connect(Sock, State, Transport, {User, Pass}) - end; - _ -> - {stop, {error, <<"Certificate can be only used for encrypted connections">> }} + #{certfile := Cert} when Transport == ssl -> + Sock = ssl:connect(Host, Port, [binary, {active, true}, {certfile, Cert}]), + if + IsWs -> + connect_ws(Host, Port, Path, Sock, State, ssl, none); + true -> connect(Sock, State, ssl, none) + end; + #{username := User, password := Pass} -> + Sock = Transport:connect(Host, Port, [binary, {active, true}]), + if + IsWs -> + connect_ws(Host, Port, Path, Sock, State, Transport, {User, Pass}); + true -> connect(Sock, State, Transport, {User, Pass}) + end; + _ -> + {stop, {error, <<"Certificate can be only used for encrypted connections">>}} end. + handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + -spec handle_info(term(), state()) -> - {noreply, state()} | {noreply, state(), timeout()} | {stop, term(), state()}. + {noreply, state()} | {noreply, state(), timeout()} | {stop, term(), state()}. handle_info({Tag, TCPSock, TCPData}, - #state{ws_codec = {init, Hash, Auth, Last}} = State) - when (Tag == tcp orelse Tag == ssl) -> + #state{ws_codec = {init, Hash, Auth, Last}} = State) + when (Tag == tcp orelse Tag == ssl) -> Data = <>, case erlang:decode_packet(http_bin, Data, []) of - {ok, {http_response, _, 101, _}, Rest} -> - handle_info({tcp, TCPSock, Rest}, State#state{ws_codec = {inith, Hash, none, Auth, <<>>}}); - {ok, {http_response, _, _, _}, _Rest} -> - stop(State, {socket, closed}); - {ok, {http_error, _}, _} -> - stop(State, {socket, closed}); - {error, _} -> - stop(State, {socket, closed}); - {more, _} -> - {noreply, State#state{ws_codec = {init, Hash, Auth, Data}}} + {ok, {http_response, _, 101, _}, Rest} -> + handle_info({tcp, TCPSock, Rest}, State#state{ws_codec = {inith, Hash, none, Auth, <<>>}}); + {ok, {http_response, _, _, _}, _Rest} -> + stop(State, {socket, closed}); + {ok, {http_error, _}, _} -> + stop(State, {socket, closed}); + {error, _} -> + stop(State, {socket, closed}); + {more, _} -> + {noreply, State#state{ws_codec = {init, Hash, Auth, Data}}} end; handle_info({Tag, TCPSock, TCPData}, - #state{ws_codec = {inith, Hash, Upgrade, Auth, Last}, - socket = {Transport, _}} = State) - when (Tag == tcp orelse Tag == ssl) -> + #state{ + ws_codec = {inith, Hash, Upgrade, Auth, Last}, + socket = {Transport, _} + } = State) + when (Tag == tcp orelse Tag == ssl) -> Data = <>, case erlang:decode_packet(httph_bin, Data, []) of - {ok, {http_header, _, <<"Sec-Websocket-Accept">>, _, Val}, Rest} -> - case str:to_lower(Val) of - Hash -> - handle_info({tcp, TCPSock, Rest}, - State#state{ws_codec = {inith, ok, Upgrade, Auth, <<>>}}); - _ -> - stop(State, {socket, closed}) - end; - {ok, {http_header, _, 'Connection', _, Val}, Rest} -> - case str:to_lower(Val) of - <<"upgrade">> -> - handle_info({tcp, TCPSock, Rest}, - State#state{ws_codec = {inith, Hash, ok, Auth, <<>>}}); - _ -> - stop(State, {socket, closed}) - end; - {ok, {http_header, _, _, _, _}, Rest} -> - handle_info({tcp, TCPSock, Rest}, State); - {ok, {http_error, _}, _} -> - stop(State, {socket, closed}); - {ok, http_eoh, Rest} -> - case {Hash, Upgrade} of - {ok, ok} -> - {ok, State2} = connect({ok, TCPSock}, - State#state{ws_codec = ejabberd_websocket_codec:new_client()}, - Transport, Auth), - handle_info({tcp, TCPSock, Rest}, State2); - _ -> - stop(State, {socket, closed}) - end; - {error, _} -> - stop(State, {socket, closed}); - {more, _} -> - {noreply, State#state{ws_codec = {inith, Hash, Upgrade, Data}}} + {ok, {http_header, _, <<"Sec-Websocket-Accept">>, _, Val}, Rest} -> + case str:to_lower(Val) of + Hash -> + handle_info({tcp, TCPSock, Rest}, + State#state{ws_codec = {inith, ok, Upgrade, Auth, <<>>}}); + _ -> + stop(State, {socket, closed}) + end; + {ok, {http_header, _, 'Connection', _, Val}, Rest} -> + case str:to_lower(Val) of + <<"upgrade">> -> + handle_info({tcp, TCPSock, Rest}, + State#state{ws_codec = {inith, Hash, ok, Auth, <<>>}}); + _ -> + stop(State, {socket, closed}) + end; + {ok, {http_header, _, _, _, _}, Rest} -> + handle_info({tcp, TCPSock, Rest}, State); + {ok, {http_error, _}, _} -> + stop(State, {socket, closed}); + {ok, http_eoh, Rest} -> + case {Hash, Upgrade} of + {ok, ok} -> + {ok, State2} = connect({ok, TCPSock}, + State#state{ws_codec = ejabberd_websocket_codec:new_client()}, + Transport, + Auth), + handle_info({tcp, TCPSock, Rest}, State2); + _ -> + stop(State, {socket, closed}) + end; + {error, _} -> + stop(State, {socket, closed}); + {more, _} -> + {noreply, State#state{ws_codec = {inith, Hash, Upgrade, Data}}} end; handle_info({Tag, TCPSock, TCPData}, - #state{ws_codec = WSCodec} = State) - when (Tag == tcp orelse Tag == ssl) andalso WSCodec /= none -> + #state{ws_codec = WSCodec} = State) + when (Tag == tcp orelse Tag == ssl) andalso WSCodec /= none -> {Packets, Acc0} = - case ejabberd_websocket_codec:decode(WSCodec, TCPData) of - {ok, NewWSCodec, Packets0} -> - {Packets0, {noreply, ok, State#state{ws_codec = NewWSCodec}}}; - {error, _Error, Packets0} -> - {Packets0, {stop_after, {socket, closed}, State}} - end, + case ejabberd_websocket_codec:decode(WSCodec, TCPData) of + {ok, NewWSCodec, Packets0} -> + {Packets0, {noreply, ok, State#state{ws_codec = NewWSCodec}}}; + {error, _Error, Packets0} -> + {Packets0, {stop_after, {socket, closed}, State}} + end, Res2 = - lists:foldl( - fun(_, {stop, _, _} = Res) -> Res; - ({_Op, Data}, {Tag2, Res, S}) -> - case handle_info({tcp_decoded, TCPSock, Data}, S) of - {stop, _, _} = Stop -> - Stop; - {_, NewState} -> - {Tag2, Res, NewState} - end - end, Acc0, Packets), + lists:foldl( + fun(_, {stop, _, _} = Res) -> Res; + ({_Op, Data}, {Tag2, Res, S}) -> + case handle_info({tcp_decoded, TCPSock, Data}, S) of + {stop, _, _} = Stop -> + Stop; + {_, NewState} -> + {Tag2, Res, NewState} + end + end, + Acc0, + Packets), case Res2 of - {noreply, _, State2} -> - {noreply, State2}; - {Tag3, Res3, State2} when Tag3 == stop; Tag3 == stop_after -> - {stop, Res3, State2} + {noreply, _, State2} -> + {noreply, State2}; + {Tag3, Res3, State2} when Tag3 == stop; Tag3 == stop_after -> + {stop, Res3, State2} end; handle_info({Tag, TCPSock, TCPData}, - #state{codec = Codec} = State) - when Tag == tcp; Tag == ssl; Tag == tcp_decoded -> + #state{codec = Codec} = State) + when Tag == tcp; Tag == ssl; Tag == tcp_decoded -> case mqtt_codec:decode(Codec, TCPData) of - {ok, Pkt, Codec1} -> - ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), - State1 = State#state{codec = Codec1}, - case handle_packet(Pkt, State1) of - {ok, State2} -> - handle_info({tcp_decoded, TCPSock, <<>>}, State2); - {error, State2, Reason} -> - stop(State2, Reason) - end; - {more, Codec1} -> - State1 = State#state{codec = Codec1}, - {noreply, State1}; - {error, Why} -> - stop(State, {codec, Why}) + {ok, Pkt, Codec1} -> + ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), + State1 = State#state{codec = Codec1}, + case handle_packet(Pkt, State1) of + {ok, State2} -> + handle_info({tcp_decoded, TCPSock, <<>>}, State2); + {error, State2, Reason} -> + stop(State2, Reason) + end; + {more, Codec1} -> + State1 = State#state{codec = Codec1}, + {noreply, State1}; + {error, Why} -> + stop(State, {codec, Why}) end; handle_info({tcp_closed, _Sock}, State) -> ?DEBUG("MQTT connection reset by peer", []), @@ -252,26 +286,27 @@ handle_info({ssl_error, _Sock, Reason}, State) -> stop(State, {socket, Reason}); handle_info({publish, #publish{topic = Topic} = Pkt}, #state{publish = Publish} = State) -> case maps:find(Topic, Publish) of - {ok, RemoteTopic} -> - case send(State, Pkt#publish{qos = 0, topic = RemoteTopic}) of - {ok, State2} -> - {noreply, State2} - end; - _ -> - {noreply, State} + {ok, RemoteTopic} -> + case send(State, Pkt#publish{qos = 0, topic = RemoteTopic}) of + {ok, State2} -> + {noreply, State2} + end; + _ -> + {noreply, State} end; handle_info({timeout, _TRef, ping_timeout}, State) -> case send(State, #pingreq{}) of - {ok, State2} -> - {noreply, State2} + {ok, State2} -> + {noreply, State2} end; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -spec handle_packet(mqtt_packet(), state()) -> - {ok, state()} | - {error, state(), error_reason()}. + {ok, state()} | + {error, state(), error_reason()}. handle_packet(#connack{} = Pkt, State) -> handle_connack(Pkt, State); handle_packet(#suback{}, State) -> @@ -281,31 +316,34 @@ handle_packet(#publish{} = Pkt, State) -> handle_packet(#pingresp{}, State) -> {ok, State}; handle_packet(#disconnect{properties = #{session_expiry_interval := SE}}, - State) when SE > 0 -> + State) when SE > 0 -> %% Protocol violation {error, State, session_expiry_non_zero}; handle_packet(#disconnect{code = Code, properties = Props}, - State) -> + State) -> Reason = maps:get(reason_string, Props, <<>>), {error, State, {peer_disconnected, Code, Reason}}; handle_packet(Pkt, State) -> ?WARNING_MSG("Unexpected packet:~n~ts~n** when state:~n~ts", - [pp(Pkt), pp(State)]), + [pp(Pkt), pp(State)]), {error, State, {unexpected_packet, element(1, Pkt)}}. + terminate(Reason, State) -> Reason1 = case Reason of - shutdown -> shutdown; - {shutdown, _} -> shutdown; - normal -> State#state.stop_reason; - {process_limit, _} -> queue_full; - _ -> internal_server_error - end, + shutdown -> shutdown; + {shutdown, _} -> shutdown; + normal -> State#state.stop_reason; + {process_limit, _} -> queue_full; + _ -> internal_server_error + end, disconnect(State, Reason1). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% State transitions %%%=================================================================== @@ -314,43 +352,57 @@ connect({error, Reason}, _State, _Transport, _Auth) -> connect({ok, Sock}, State0, Transport, Auth) -> State = State0#state{socket = {Transport, Sock}}, Connect = case Auth of - {User, Pass} -> - #connect{client_id = integer_to_binary(State#state.id), - clean_start = true, - username = User, - password = Pass, - keep_alive = 60, - proto_level = State#state.version}; - _ -> - #connect{client_id = integer_to_binary(State#state.id), - clean_start = true, - keep_alive = 60, - proto_level = State#state.version} - end, + {User, Pass} -> + #connect{ + client_id = integer_to_binary(State#state.id), + clean_start = true, + username = User, + password = Pass, + keep_alive = 60, + proto_level = State#state.version + }; + _ -> + #connect{ + client_id = integer_to_binary(State#state.id), + clean_start = true, + keep_alive = 60, + proto_level = State#state.version + } + end, Pkt = mqtt_codec:encode(State#state.version, Connect), send(State, Connect), {ok, _, Codec2} = mqtt_codec:decode(State#state.codec, Pkt), {ok, State#state{codec = Codec2}}. + connect_ws(_Host, _Port, _Path, {error, Reason}, _State, _Transport, _Auth) -> {stop, {error, Reason}}; connect_ws(Host, Port, Path, {ok, Sock}, State0, Transport, Auth) -> Key = base64:encode(p1_rand:get_string()), Hash = str:to_lower(base64:encode(crypto:hash(sha, <>))), - Data = <<"GET ", (list_to_binary(Path))/binary, " HTTP/1.1\r\n", - "Host: ", (list_to_binary(Host))/binary, ":", (integer_to_binary(Port))/binary,"\r\n", - "Upgrade: websocket\r\n", - "Connection: Upgrade\r\n", - "Sec-WebSocket-Protocol: mqtt\r\n", - "Sec-WebSocket-Key: ", Key/binary, "\r\n", - "Sec-WebSocket-Version: 13\r\n\r\n">>, + Data = <<"GET ", + (list_to_binary(Path))/binary, + " HTTP/1.1\r\n", + "Host: ", + (list_to_binary(Host))/binary, + ":", + (integer_to_binary(Port))/binary, + "\r\n", + "Upgrade: websocket\r\n", + "Connection: Upgrade\r\n", + "Sec-WebSocket-Protocol: mqtt\r\n", + "Sec-WebSocket-Key: ", + Key/binary, + "\r\n", + "Sec-WebSocket-Version: 13\r\n\r\n">>, Res = Transport:send(Sock, Data), check_sock_result({Transport, Sock}, Res), {ok, State0#state{ws_codec = {init, Hash, Auth, <<>>}, socket = {Transport, Sock}}}. + -spec stop(state(), error_reason()) -> - {noreply, state(), infinity} | - {stop, normal, state()}. + {noreply, state(), infinity} | + {stop, normal, state()}. stop(State, Reason) -> {stop, normal, State#state{stop_reason = Reason}}. @@ -359,53 +411,59 @@ stop(State, Reason) -> %%% CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers %%%=================================================================== -spec handle_connack(connack(), state()) -> - {ok, state()} | - {error, state(), error_reason()}. + {ok, state()} | + {error, state(), error_reason()}. handle_connack(#connack{code = success}, #state{subscriptions = Subs} = State) -> Filters = maps:fold( - fun(RemoteTopic, _LocalTopic, Acc) -> - [{RemoteTopic, #sub_opts{}} | Acc] - end, [], Subs), + fun(RemoteTopic, _LocalTopic, Acc) -> + [{RemoteTopic, #sub_opts{}} | Acc] + end, + [], + Subs), Pkt = #subscribe{id = 1, filters = Filters}, send(State, Pkt); handle_connack(#connack{}, State) -> {error, State, {auth, 'not-authorized'}}. + -spec handle_publish(publish(), state()) -> - {ok, state()} | - {error, state(), error_reason()}. + {ok, state()} | + {error, state(), error_reason()}. handle_publish(#publish{topic = Topic, payload = Payload, properties = Props}, - #state{usr = USR, subscriptions = Subs} = State) -> + #state{usr = USR, subscriptions = Subs} = State) -> case maps:get(Topic, Subs, none) of - none -> - {ok, State}; - LocalTopic -> - MessageExpiry = maps:get(message_expiry_interval, Props, ?MAX_UINT32), - ExpiryTime = min(unix_time() + MessageExpiry, ?MAX_UINT32), - mod_mqtt:publish(USR, #publish{retain = true, topic = LocalTopic, payload = Payload, properties = Props}, - ExpiryTime), - {ok, State} + none -> + {ok, State}; + LocalTopic -> + MessageExpiry = maps:get(message_expiry_interval, Props, ?MAX_UINT32), + ExpiryTime = min(unix_time() + MessageExpiry, ?MAX_UINT32), + mod_mqtt:publish(USR, + #publish{retain = true, topic = LocalTopic, payload = Payload, properties = Props}, + ExpiryTime), + {ok, State} end. + %%%=================================================================== %%% Socket management %%%=================================================================== -spec send(state(), mqtt_packet()) -> - {ok, state()} | - {error, state(), error_reason()}. + {ok, state()} | + {error, state(), error_reason()}. send(State, #publish{} = Pkt) -> case is_expired(Pkt) of - {false, Pkt1} -> - {ok, do_send(State, Pkt1)}; - true -> - {ok, State} + {false, Pkt1} -> + {ok, do_send(State, Pkt1)}; + true -> + {ok, State} end; send(State, Pkt) -> {ok, do_send(State, Pkt)}. + -spec do_send(state(), mqtt_packet()) -> state(). do_send(#state{ws_codec = WSCodec, socket = {SockMod, Sock} = Socket} = State, Pkt) - when WSCodec /= none -> + when WSCodec /= none -> ?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]), Data = mqtt_codec:encode(State#state.version, Pkt), WSData = ejabberd_websocket_codec:encode(WSCodec, 2, Data), @@ -421,43 +479,48 @@ do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) -> do_send(State, _Pkt) -> State. + -spec disconnect(state(), error_reason()) -> state(). disconnect(#state{socket = {SockMod, Sock}} = State, Err) -> State1 = case Err of - {auth, Code} -> - do_send(State, #connack{code = Code}); - {codec, {Tag, _, _} = CErr} when Tag == unsupported_protocol_version; - Tag == unsupported_protocol_name -> - do_send(State#state{version = ?MQTT_VERSION_4}, - #connack{code = mqtt_codec:error_reason_code(CErr)}); - _ when State#state.version == undefined -> - State; - {Tag, _} when Tag == socket; Tag == tls -> - State; - {peer_disconnected, _, _} -> - State; - _ -> - case State of - _ when State#state.version == ?MQTT_VERSION_5 -> - Code = disconnect_reason_code(Err), - Pkt = #disconnect{code = Code}, - do_send(State, Pkt); - _ -> - State - end - end, + {auth, Code} -> + do_send(State, #connack{code = Code}); + {codec, {Tag, _, _} = CErr} when Tag == unsupported_protocol_version; + Tag == unsupported_protocol_name -> + do_send(State#state{version = ?MQTT_VERSION_4}, + #connack{code = mqtt_codec:error_reason_code(CErr)}); + _ when State#state.version == undefined -> + State; + {Tag, _} when Tag == socket; Tag == tls -> + State; + {peer_disconnected, _, _} -> + State; + _ -> + case State of + _ when State#state.version == ?MQTT_VERSION_5 -> + Code = disconnect_reason_code(Err), + Pkt = #disconnect{code = Code}, + do_send(State, Pkt); + _ -> + State + end + end, SockMod:close(Sock), - State1#state{socket = undefined, - version = undefined, - codec = mqtt_codec:renew(State#state.codec)}; + State1#state{ + socket = undefined, + version = undefined, + codec = mqtt_codec:renew(State#state.codec) + }; disconnect(State, _) -> State. + -spec reset_ping_timer(state()) -> state(). reset_ping_timer(State) -> misc:cancel_timer(State#state.ping_timer), State#state{ping_timer = erlang:start_timer(?PING_TIMEOUT, self(), ping_timeout)}. + -spec check_sock_result(socket(), ok | {error, inet:posix()}) -> ok. check_sock_result(_, ok) -> ok; @@ -465,6 +528,7 @@ check_sock_result({_, Sock}, {error, Why}) -> self() ! {tcp_closed, Sock}, ?DEBUG("MQTT socket error: ~p", [format_inet_error(Why)]). + %%%=================================================================== %%% Formatters %%%=================================================================== @@ -472,6 +536,7 @@ check_sock_result({_, Sock}, {error, Why}) -> pp(Term) -> io_lib_pretty:print(Term, fun pp/2). + -spec format_inet_error(socket_error_reason()) -> string(). format_inet_error(closed) -> "connection closed"; @@ -479,14 +544,16 @@ format_inet_error(timeout) -> format_inet_error(etimedout); format_inet_error(Reason) -> case inet:format_error(Reason) of - "unknown POSIX error" -> atom_to_list(Reason); - Txt -> Txt + "unknown POSIX error" -> atom_to_list(Reason); + Txt -> Txt end. + -spec pp(atom(), non_neg_integer()) -> [atom()] | no. pp(state, 17) -> record_info(fields, state); pp(Rec, Size) -> mqtt_codec:pp(Rec, Size). + -spec disconnect_reason_code(error_reason()) -> reason_code(). disconnect_reason_code({code, Code}) -> Code; disconnect_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); @@ -506,6 +573,7 @@ disconnect_reason_code(session_expiry_non_zero) -> 'protocol-error'; disconnect_reason_code(unknown_topic_alias) -> 'protocol-error'; disconnect_reason_code(_) -> 'unspecified-error'. + %%%=================================================================== %%% Timings %%%=================================================================== @@ -513,18 +581,20 @@ disconnect_reason_code(_) -> 'unspecified-error'. unix_time() -> erlang:system_time(second). + -spec is_expired(publish()) -> true | {false, publish()}. is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> case maps:get(expiry_time, Meta, ?MAX_UINT32) of - ?MAX_UINT32 -> - {false, Pkt}; - ExpiryTime -> - Left = ExpiryTime - unix_time(), - if Left > 0 -> - Props1 = Props#{message_expiry_interval => Left}, - {false, Pkt#publish{properties = Props1}}; - true -> - ?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]), - true - end + ?MAX_UINT32 -> + {false, Pkt}; + ExpiryTime -> + Left = ExpiryTime - unix_time(), + if + Left > 0 -> + Props1 = Props#{message_expiry_interval => Left}, + {false, Pkt#publish{properties = Props1}}; + true -> + ?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]), + true + end end. diff --git a/src/mod_mqtt_mnesia.erl b/src/mod_mqtt_mnesia.erl index 62437d597..1b32bdca6 100644 --- a/src/mod_mqtt_mnesia.erl +++ b/src/mod_mqtt_mnesia.erl @@ -45,25 +45,26 @@ user_properties = [] :: [{binary(), binary()}]}). -record(mqtt_sub, {topic :: {binary(), binary(), binary(), binary()}, - options :: sub_opts(), - id :: non_neg_integer(), - pid :: pid(), - timestamp :: erlang:timestamp()}). + options :: sub_opts(), + id :: non_neg_integer(), + pid :: pid(), + timestamp :: erlang:timestamp()}). -record(mqtt_session, {usr :: jid:ljid() | {'_', '_', '$1'}, - pid :: pid() | '_', - timestamp :: erlang:timestamp() | '_'}). + pid :: pid() | '_', + timestamp :: erlang:timestamp() | '_'}). %% @indent-end %% @efmt:on -%% + %% %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> case ejabberd_mnesia:create( - ?MODULE, mqtt_pub, + ?MODULE, + mqtt_pub, [{disc_only_copies, [node()]}, {attributes, record_info(fields, mqtt_pub)}]) of {atomic, _} -> @@ -72,6 +73,7 @@ init(_Host, _Opts) -> {error, Err} end. + use_cache(Host) -> case mnesia:table_info(mqtt_pub, storage_type) of disc_only_copies -> @@ -80,49 +82,60 @@ use_cache(Host) -> false end. + publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> PayloadFormat = maps:get(payload_format_indicator, Props, binary), ResponseTopic = maps:get(response_topic, Props, <<"">>), CorrelationData = maps:get(correlation_data, Props, <<"">>), ContentType = maps:get(content_type, Props, <<"">>), UserProps = maps:get(user_property, Props, []), - mnesia:dirty_write(#mqtt_pub{topic_server = {Topic, LServer}, - user = U, - resource = R, - qos = QoS, - payload = Payload, - expiry = ExpiryTime, - payload_format = PayloadFormat, - response_topic = ResponseTopic, - correlation_data = CorrelationData, - content_type = ContentType, - user_properties = UserProps}). + mnesia:dirty_write(#mqtt_pub{ + topic_server = {Topic, LServer}, + user = U, + resource = R, + qos = QoS, + payload = Payload, + expiry = ExpiryTime, + payload_format = PayloadFormat, + response_topic = ResponseTopic, + correlation_data = CorrelationData, + content_type = ContentType, + user_properties = UserProps + }). + delete_published({_, S, _}, Topic) -> mnesia:dirty_delete(mqtt_pub, {Topic, S}). + lookup_published({_, S, _}, Topic) -> case mnesia:dirty_read(mqtt_pub, {Topic, S}) of - [#mqtt_pub{qos = QoS, - payload = Payload, - expiry = ExpiryTime, - payload_format = PayloadFormat, - response_topic = ResponseTopic, - correlation_data = CorrelationData, - content_type = ContentType, - user_properties = UserProps}] -> - Props = #{payload_format_indicator => PayloadFormat, + [#mqtt_pub{ + qos = QoS, + payload = Payload, + expiry = ExpiryTime, + payload_format = PayloadFormat, + response_topic = ResponseTopic, + correlation_data = CorrelationData, + content_type = ContentType, + user_properties = UserProps + }] -> + Props = #{ + payload_format_indicator => PayloadFormat, response_topic => ResponseTopic, correlation_data => CorrelationData, content_type => ContentType, - user_property => UserProps}, + user_property => UserProps + }, {ok, {Payload, QoS, Props, ExpiryTime}}; [] -> {error, notfound} end. + list_topics(S) -> - {ok, [Topic || {Topic, S1} <- mnesia:dirty_all_keys(mqtt_pub), S1 == S]}. + {ok, [ Topic || {Topic, S1} <- mnesia:dirty_all_keys(mqtt_pub), S1 == S ]}. + init() -> case mqtree:whereis(mqtt_sub_index) of @@ -130,144 +143,168 @@ init() -> T = mqtree:new(), mqtree:register(mqtt_sub_index, T); _ -> - ok + ok end, try - {atomic, ok} = ejabberd_mnesia:create( - ?MODULE, - mqtt_session, - [{ram_copies, [node()]}, - {attributes, record_info(fields, mqtt_session)}]), - {atomic, ok} = ejabberd_mnesia:create( - ?MODULE, - mqtt_sub, - [{ram_copies, [node()]}, - {type, ordered_set}, - {attributes, record_info(fields, mqtt_sub)}]), - ok - catch _:{badmatch, Err} -> - {error, Err} + {atomic, ok} = ejabberd_mnesia:create( + ?MODULE, + mqtt_session, + [{ram_copies, [node()]}, + {attributes, record_info(fields, mqtt_session)}]), + {atomic, ok} = ejabberd_mnesia:create( + ?MODULE, + mqtt_sub, + [{ram_copies, [node()]}, + {type, ordered_set}, + {attributes, record_info(fields, mqtt_sub)}]), + ok + catch + _:{badmatch, Err} -> + {error, Err} end. + open_session(USR) -> TS1 = misc:unique_timestamp(), P1 = self(), F = fun() -> - case mnesia:read(mqtt_session, USR) of - [#mqtt_session{pid = P2, timestamp = TS2}] -> - if TS1 >= TS2 -> - mod_mqtt_session:route(P2, {replaced, P1}), - mnesia:write( - #mqtt_session{usr = USR, - pid = P1, - timestamp = TS1}); - true -> - case is_process_dead(P2) of - true -> - mnesia:write( - #mqtt_session{usr = USR, - pid = P1, - timestamp = TS1}); - false -> - mod_mqtt_session:route(P1, {replaced, P2}) - end - end; - [] -> - mnesia:write( - #mqtt_session{usr = USR, - pid = P1, - timestamp = TS1}) - end - end, + case mnesia:read(mqtt_session, USR) of + [#mqtt_session{pid = P2, timestamp = TS2}] -> + if + TS1 >= TS2 -> + mod_mqtt_session:route(P2, {replaced, P1}), + mnesia:write( + #mqtt_session{ + usr = USR, + pid = P1, + timestamp = TS1 + }); + true -> + case is_process_dead(P2) of + true -> + mnesia:write( + #mqtt_session{ + usr = USR, + pid = P1, + timestamp = TS1 + }); + false -> + mod_mqtt_session:route(P1, {replaced, P2}) + end + end; + [] -> + mnesia:write( + #mqtt_session{ + usr = USR, + pid = P1, + timestamp = TS1 + }) + end + end, case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> - db_fail("Failed to register MQTT session for ~ts", - Reason, [jid:encode(USR)]) + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to register MQTT session for ~ts", + Reason, + [jid:encode(USR)]) end. + close_session(USR) -> close_session(USR, self()). + lookup_session(USR) -> case mnesia:dirty_read(mqtt_session, USR) of - [#mqtt_session{pid = Pid}] -> - case is_process_dead(Pid) of - true -> - %% Read-Repair - close_session(USR, Pid), - {error, notfound}; - false -> - {ok, Pid} - end; - [] -> - {error, notfound} + [#mqtt_session{pid = Pid}] -> + case is_process_dead(Pid) of + true -> + %% Read-Repair + close_session(USR, Pid), + {error, notfound}; + false -> + {ok, Pid} + end; + [] -> + {error, notfound} end. + get_sessions(U, S) -> Resources = mnesia:dirty_select(mqtt_session, - [{#mqtt_session{usr = {U, S, '$1'}, - _ = '_'}, + [{#mqtt_session{ + usr = {U, S, '$1'}, + _ = '_' + }, [], ['$1']}]), - [{U, S, Resource} || Resource <- Resources]. + [ {U, S, Resource} || Resource <- Resources ]. + subscribe({U, S, R} = USR, TopicFilter, SubOpts, ID) -> T1 = misc:unique_timestamp(), P1 = self(), Key = {TopicFilter, S, U, R}, F = fun() -> - case mnesia:read(mqtt_sub, Key) of - [#mqtt_sub{timestamp = T2}] when T1 < T2 -> - ok; - _ -> - Tree = mqtree:whereis(mqtt_sub_index), - mqtree:insert(Tree, TopicFilter), - mnesia:write( - #mqtt_sub{topic = {TopicFilter, S, U, R}, - options = SubOpts, - id = ID, - pid = P1, - timestamp = T1}) - end - end, + case mnesia:read(mqtt_sub, Key) of + [#mqtt_sub{timestamp = T2}] when T1 < T2 -> + ok; + _ -> + Tree = mqtree:whereis(mqtt_sub_index), + mqtree:insert(Tree, TopicFilter), + mnesia:write( + #mqtt_sub{ + topic = {TopicFilter, S, U, R}, + options = SubOpts, + id = ID, + pid = P1, + timestamp = T1 + }) + end + end, case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> - db_fail("Failed to subscribe ~ts to ~ts", - Reason, [jid:encode(USR), TopicFilter]) + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to subscribe ~ts to ~ts", + Reason, + [jid:encode(USR), TopicFilter]) end. + unsubscribe({U, S, R} = USR, Topic) -> Pid = self(), F = fun() -> - Tree = mqtree:whereis(mqtt_sub_index), - mqtree:delete(Tree, Topic), - case mnesia:read(mqtt_sub, {Topic, S, U, R}) of - [#mqtt_sub{pid = Pid} = Obj] -> - mnesia:delete_object(Obj); - _ -> - ok - end - end, + Tree = mqtree:whereis(mqtt_sub_index), + mqtree:delete(Tree, Topic), + case mnesia:read(mqtt_sub, {Topic, S, U, R}) of + [#mqtt_sub{pid = Pid} = Obj] -> + mnesia:delete_object(Obj); + _ -> + ok + end + end, case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> - db_fail("Failed to unsubscribe ~ts from ~ts", - Reason, [jid:encode(USR), Topic]) + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to unsubscribe ~ts from ~ts", + Reason, + [jid:encode(USR), Topic]) end. + mqtree_match(Topic) -> Tree = mqtree:whereis(mqtt_sub_index), mqtree:match(Tree, Topic). + mqtree_multi_match(Topic) -> {Res, []} = ejabberd_cluster:multicall(?MODULE, mqtree_match, [Topic]), lists:umerge(Res). + find_subscriber(S, Topic) when is_binary(Topic) -> case mqtree_multi_match(Topic) of - [Filter|Filters] -> + [Filter | Filters] -> find_subscriber(S, {Filters, {Filter, S, '_', '_'}}); [] -> {error, notfound} @@ -276,13 +313,13 @@ find_subscriber(S, {Filters, {Filter, S, _, _} = Prev}) -> case mnesia:dirty_next(mqtt_sub, Prev) of {Filter, S, _, _} = Next -> case mnesia:dirty_read(mqtt_sub, Next) of - [#mqtt_sub{options = SubOpts, id = ID, pid = Pid}] -> - case is_process_dead(Pid) of - true -> - find_subscriber(S, {Filters, Next}); - false -> + [#mqtt_sub{options = SubOpts, id = ID, pid = Pid}] -> + case is_process_dead(Pid) of + true -> + find_subscriber(S, {Filters, Next}); + false -> {ok, {Pid, SubOpts, ID}, {Filters, Next}} - end; + end; [] -> find_subscriber(S, {Filters, Next}) end; @@ -290,33 +327,37 @@ find_subscriber(S, {Filters, {Filter, S, _, _} = Prev}) -> case Filters of [] -> {error, notfound}; - [Filter1|Filters1] -> + [Filter1 | Filters1] -> find_subscriber(S, {Filters1, {Filter1, S, '_', '_'}}) end end. + %%%=================================================================== %%% Internal functions %%%=================================================================== close_session(USR, Pid) -> F = fun() -> - case mnesia:read(mqtt_session, USR) of - [#mqtt_session{pid = Pid} = Obj] -> - mnesia:delete_object(Obj); - _ -> - ok - end - end, + case mnesia:read(mqtt_session, USR) of + [#mqtt_session{pid = Pid} = Obj] -> + mnesia:delete_object(Obj); + _ -> + ok + end + end, case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> - db_fail("Failed to unregister MQTT session for ~ts", - Reason, [jid:encode(USR)]) + {atomic, _} -> ok; + {aborted, Reason} -> + db_fail("Failed to unregister MQTT session for ~ts", + Reason, + [jid:encode(USR)]) end. + is_process_dead(Pid) -> node(Pid) == node() andalso not is_process_alive(Pid). + db_fail(Format, Reason, Args) -> ?ERROR_MSG(Format ++ ": ~p", Args ++ [Reason]), {error, db_failure}. diff --git a/src/mod_mqtt_opt.erl b/src/mod_mqtt_opt.erl index 5459f39e8..030abc3c3 100644 --- a/src/mod_mqtt_opt.erl +++ b/src/mod_mqtt_opt.erl @@ -18,87 +18,100 @@ -export([session_expiry/1]). -export([use_cache/1]). --spec access_publish(gen_mod:opts() | global | binary()) -> [{[binary()],acl:acl()}]. + +-spec access_publish(gen_mod:opts() | global | binary()) -> [{[binary()], acl:acl()}]. access_publish(Opts) when is_map(Opts) -> gen_mod:get_opt(access_publish, Opts); access_publish(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, access_publish). --spec access_subscribe(gen_mod:opts() | global | binary()) -> [{[binary()],acl:acl()}]. + +-spec access_subscribe(gen_mod:opts() | global | binary()) -> [{[binary()], acl:acl()}]. access_subscribe(Opts) when is_map(Opts) -> gen_mod:get_opt(access_subscribe, Opts); access_subscribe(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, access_subscribe). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, db_type). + -spec match_retained_limit(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). match_retained_limit(Opts) when is_map(Opts) -> gen_mod:get_opt(match_retained_limit, Opts); match_retained_limit(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, match_retained_limit). + -spec max_queue(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer(). max_queue(Opts) when is_map(Opts) -> gen_mod:get_opt(max_queue, Opts); max_queue(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, max_queue). + -spec max_topic_aliases(gen_mod:opts() | global | binary()) -> char(). max_topic_aliases(Opts) when is_map(Opts) -> gen_mod:get_opt(max_topic_aliases, Opts); max_topic_aliases(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, max_topic_aliases). + -spec max_topic_depth(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_topic_depth(Opts) when is_map(Opts) -> gen_mod:get_opt(max_topic_depth, Opts); max_topic_depth(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, max_topic_depth). + -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, queue_type). + -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, ram_db_type). + -spec session_expiry(gen_mod:opts() | global | binary()) -> non_neg_integer(). session_expiry(Opts) when is_map(Opts) -> gen_mod:get_opt(session_expiry, Opts); session_expiry(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, session_expiry). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, use_cache). - diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl index c6d0338d9..9a33bf308 100644 --- a/src/mod_mqtt_session.erl +++ b/src/mod_mqtt_session.erl @@ -23,33 +23,40 @@ %% API -export([start/3, start_link/3, accept/1, route/2]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). -include("mqtt.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). -include_lib("public_key/include/public_key.hrl"). --record(state, {vsn = ?VSN :: integer(), - version :: undefined | mqtt_version(), - socket :: undefined | socket(), - peername :: undefined | peername(), - timeout = infinity :: timer(), - jid :: undefined | jid:jid(), - session_expiry = 0 :: milli_seconds(), - will :: undefined | publish(), - will_delay = 0 :: milli_seconds(), - stop_reason :: undefined | error_reason(), - acks = #{} :: acks(), - subscriptions = #{} :: subscriptions(), - topic_aliases = #{} :: topic_aliases(), - id = 0 :: non_neg_integer(), - in_flight :: undefined | publish() | pubrel(), - codec :: mqtt_codec:state(), - queue :: undefined | p1_queue:queue(publish()), - tls :: boolean(), - tls_verify :: boolean()}). +-record(state, { + vsn = ?VSN :: integer(), + version :: undefined | mqtt_version(), + socket :: undefined | socket(), + peername :: undefined | peername(), + timeout = infinity :: timer(), + jid :: undefined | jid:jid(), + session_expiry = 0 :: milli_seconds(), + will :: undefined | publish(), + will_delay = 0 :: milli_seconds(), + stop_reason :: undefined | error_reason(), + acks = #{} :: acks(), + subscriptions = #{} :: subscriptions(), + topic_aliases = #{} :: topic_aliases(), + id = 0 :: non_neg_integer(), + in_flight :: undefined | publish() | pubrel(), + codec :: mqtt_codec:state(), + queue :: undefined | p1_queue:queue(publish()), + tls :: boolean(), + tls_verify :: boolean() + }). -type acks() :: #{non_neg_integer() => pubrec()}. -type subscriptions() :: #{binary() => {sub_opts(), non_neg_integer()}}. @@ -58,51 +65,65 @@ -type error_reason() :: {auth, reason_code()} | {code, reason_code()} | {peer_disconnected, reason_code(), binary()} | - {socket, socket_error_reason()} | - {codec, mqtt_codec:error_reason()} | - {unexpected_packet, atom()} | - {tls, inet:posix() | atom() | binary()} | - {replaced, pid()} | {resumed, pid()} | - subscribe_forbidden | publish_forbidden | - will_topic_forbidden | internal_server_error | - session_expired | idle_connection | - queue_full | shutdown | db_failure | + {socket, socket_error_reason()} | + {codec, mqtt_codec:error_reason()} | + {unexpected_packet, atom()} | + {tls, inet:posix() | atom() | binary()} | + {replaced, pid()} | + {resumed, pid()} | + subscribe_forbidden | + publish_forbidden | + will_topic_forbidden | + internal_server_error | + session_expired | + idle_connection | + queue_full | + shutdown | + db_failure | {payload_format_invalid, will | publish} | - session_expiry_non_zero | unknown_topic_alias. + session_expiry_non_zero | + unknown_topic_alias. -type state() :: #state{}. -type socket() :: {gen_tcp, inet:socket()} | - {fast_tls, fast_tls:tls_socket()} | - {mod_mqtt_ws, mod_mqtt_ws:socket()}. + {fast_tls, fast_tls:tls_socket()} | + {mod_mqtt_ws, mod_mqtt_ws:socket()}. -type peername() :: {inet:ip_address(), inet:port_number()}. -type seconds() :: non_neg_integer(). -type milli_seconds() :: non_neg_integer(). -type timer() :: infinity | {milli_seconds(), integer()}. -type socket_error_reason() :: closed | timeout | inet:posix(). --define(CALL_TIMEOUT, timer:minutes(1)). +-define(CALL_TIMEOUT, timer:minutes(1)). -define(RELAY_TIMEOUT, timer:minutes(1)). --define(MAX_UINT32, 4294967295). +-define(MAX_UINT32, 4294967295). + %%%=================================================================== %%% API %%%=================================================================== start(SockMod, Socket, ListenOpts) -> - p1_server:start(?MODULE, [SockMod, Socket, ListenOpts], - ejabberd_config:fsm_limit_opts(ListenOpts)). + p1_server:start(?MODULE, + [SockMod, Socket, ListenOpts], + ejabberd_config:fsm_limit_opts(ListenOpts)). + start_link(SockMod, Socket, ListenOpts) -> - p1_server:start_link(?MODULE, [SockMod, Socket, ListenOpts], - ejabberd_config:fsm_limit_opts(ListenOpts)). + p1_server:start_link(?MODULE, + [SockMod, Socket, ListenOpts], + ejabberd_config:fsm_limit_opts(ListenOpts)). + -spec accept(pid()) -> ok. accept(Pid) -> p1_server:cast(Pid, accept). + -spec route(pid(), term()) -> boolean(). route(Pid, Term) -> ejabberd_cluster:send(Pid, Term). + -spec format_error(error_reason()) -> string(). format_error(session_expired) -> "Disconnected session is expired"; @@ -156,20 +177,24 @@ format_error(A) when is_atom(A) -> format_error(Reason) -> format("Unrecognized error: ~w", [Reason]). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([SockMod, Socket, ListenOpts]) -> MaxSize = proplists:get_value(max_payload_size, ListenOpts, infinity), - State1 = #state{socket = {SockMod, Socket}, - id = p1_rand:uniform(65535), - tls = proplists:get_bool(tls, ListenOpts), - tls_verify = proplists:get_bool(tls_verify, ListenOpts), - codec = mqtt_codec:new(MaxSize)}, + State1 = #state{ + socket = {SockMod, Socket}, + id = p1_rand:uniform(65535), + tls = proplists:get_bool(tls, ListenOpts), + tls_verify = proplists:get_bool(tls_verify, ListenOpts), + codec = mqtt_codec:new(MaxSize) + }, Timeout = timer:seconds(30), State2 = set_timeout(State1, Timeout), {ok, State2, Timeout}. + handle_call({get_state, _}, From, #state{stop_reason = {resumed, Pid}} = State) -> p1_server:reply(From, {error, {resumed, Pid}}), noreply(State); @@ -183,47 +208,51 @@ handle_call({get_state, Pid}, From, State) -> p1_server:reply(From, {ok, State1#state{queue = Q1}}), SessionExpiry = State1#state.session_expiry, State2 = set_timeout(State1, min(SessionExpiry, ?RELAY_TIMEOUT)), - State3 = State2#state{queue = undefined, - stop_reason = {resumed, Pid}, - acks = #{}, - will = undefined, - session_expiry = 0, - topic_aliases = #{}, - subscriptions = #{}}, + State3 = State2#state{ + queue = undefined, + stop_reason = {resumed, Pid}, + acks = #{}, + will = undefined, + session_expiry = 0, + topic_aliases = #{}, + subscriptions = #{} + }, noreply(State3) end; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), noreply(State). + handle_cast(accept, #state{socket = {_, Sock}} = State) -> case peername(State) of - {ok, IPPort} -> - State1 = State#state{peername = IPPort}, - case starttls(State) of - {ok, Socket1} -> - State2 = State1#state{socket = Socket1}, - handle_info({tcp, Sock, <<>>}, State2); - {error, Why} -> - stop(State1, Why) - end; - {error, Why} -> - stop(State, {socket, Why}) + {ok, IPPort} -> + State1 = State#state{peername = IPPort}, + case starttls(State) of + {ok, Socket1} -> + State2 = State1#state{socket = Socket1}, + handle_info({tcp, Sock, <<>>}, State2); + {error, Why} -> + stop(State1, Why) + end; + {error, Why} -> + stop(State, {socket, Why}) end; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), noreply(State). + handle_info(Msg, #state{stop_reason = {resumed, Pid} = Reason} = State) -> case Msg of - {#publish{}, _} -> - ?DEBUG("Relaying delayed publish to ~p at ~ts", [Pid, node(Pid)]), - ejabberd_cluster:send(Pid, Msg), - noreply(State); - timeout -> - stop(State, Reason); - _ -> - noreply(State) + {#publish{}, _} -> + ?DEBUG("Relaying delayed publish to ~p at ~ts", [Pid, node(Pid)]), + ejabberd_cluster:send(Pid, Msg), + noreply(State); + timeout -> + stop(State, Reason); + _ -> + noreply(State) end; handle_info({#publish{meta = Meta} = Pkt, ExpiryTime}, State) -> ID = next_id(State#state.id), @@ -231,33 +260,33 @@ handle_info({#publish{meta = Meta} = Pkt, ExpiryTime}, State) -> Pkt1 = Pkt#publish{id = ID, meta = Meta1}, State1 = State#state{id = ID}, case send(State1, Pkt1) of - {ok, State2} -> noreply(State2); - {error, State2, Reason} -> stop(State2, Reason) + {ok, State2} -> noreply(State2); + {error, State2, Reason} -> stop(State2, Reason) end; handle_info({tcp, TCPSock, TCPData}, - #state{codec = Codec, socket = Socket} = State) -> + #state{codec = Codec, socket = Socket} = State) -> case recv_data(Socket, TCPData) of - {ok, Data} -> - case mqtt_codec:decode(Codec, Data) of - {ok, Pkt, Codec1} -> - ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), - State1 = State#state{codec = Codec1}, - case handle_packet(Pkt, State1) of - {ok, State2} -> - handle_info({tcp, TCPSock, <<>>}, State2); - {error, State2, Reason} -> - stop(State2, Reason) - end; - {more, Codec1} -> - State1 = State#state{codec = Codec1}, - State2 = reset_keep_alive(State1), - activate(Socket), - noreply(State2); - {error, Why} -> - stop(State, {codec, Why}) - end; - {error, Why} -> - stop(State, Why) + {ok, Data} -> + case mqtt_codec:decode(Codec, Data) of + {ok, Pkt, Codec1} -> + ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), + State1 = State#state{codec = Codec1}, + case handle_packet(Pkt, State1) of + {ok, State2} -> + handle_info({tcp, TCPSock, <<>>}, State2); + {error, State2, Reason} -> + stop(State2, Reason) + end; + {more, Codec1} -> + State1 = State#state{codec = Codec1}, + State2 = reset_keep_alive(State1), + activate(Socket), + noreply(State2); + {error, Why} -> + stop(State, {codec, Why}) + end; + {error, Why} -> + stop(State, Why) end; handle_info({tcp_closed, _Sock}, State) -> ?DEBUG("MQTT connection reset by peer", []), @@ -267,12 +296,12 @@ handle_info({tcp_error, _Sock, Reason}, State) -> stop(State, {socket, Reason}); handle_info(timeout, #state{socket = Socket} = State) -> case Socket of - undefined -> - ?DEBUG("MQTT session expired", []), - stop(State#state{session_expiry = 0}, session_expired); - _ -> - ?DEBUG("MQTT connection timed out", []), - stop(State, idle_connection) + undefined -> + ?DEBUG("MQTT session expired", []), + stop(State#state{session_expiry = 0}, session_expired); + _ -> + ?DEBUG("MQTT connection timed out", []), + stop(State, idle_connection) end; handle_info({replaced, Pid}, State) -> stop(State#state{session_expiry = 0}, {replaced, Pid}); @@ -285,8 +314,9 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), noreply(State). + -spec handle_packet(mqtt_packet(), state()) -> {ok, state()} | - {error, state(), error_reason()}. + {error, state(), error_reason()}. handle_packet(#connect{proto_level = Version} = Pkt, State) -> handle_connect(Pkt, State#state{version = Version}); handle_packet(#publish{} = Pkt, State) -> @@ -301,7 +331,8 @@ handle_packet(#pubrec{id = ID, code = Code}, case mqtt_codec:is_error_code(Code) of true -> ?DEBUG("Got PUBREC with error code '~ts', " - "aborting acknowledgement", [Code]), + "aborting acknowledgement", + [Code]), resend(State#state{in_flight = undefined}); false -> Pubrel = #pubrel{id = ID}, @@ -316,14 +347,16 @@ handle_packet(#pubrec{id = ID, code = Code}, State) -> false -> Code1 = 'packet-identifier-not-found', ?DEBUG("Unexpected PUBREC with id=~B, " - "sending PUBREL with error code '~ts'", [ID, Code1]), + "sending PUBREL with error code '~ts'", + [ID, Code1]), send(State, #pubrel{id = ID, code = Code1}) end; handle_packet(#pubcomp{id = ID}, #state{in_flight = #pubrel{id = ID}} = State) -> resend(State#state{in_flight = undefined}); handle_packet(#pubcomp{id = ID}, State) -> ?DEBUG("Ignoring unexpected PUBCOMP with id=~B: most likely " - "it's a repeated response to duplicated PUBREL", [ID]), + "it's a repeated response to duplicated PUBREL", + [ID]), {ok, State}; handle_packet(#pubrel{id = ID}, State) -> case maps:take(ID, State#state.acks) of @@ -332,7 +365,8 @@ handle_packet(#pubrel{id = ID}, State) -> error -> Code = 'packet-identifier-not-found', ?DEBUG("Unexpected PUBREL with id=~B, " - "sending PUBCOMP with error code '~ts'", [ID, Code]), + "sending PUBCOMP with error code '~ts'", + [ID, Code]), Pubcomp = #pubcomp{id = ID, code = Code}, send(State, Pubcomp) end; @@ -343,7 +377,7 @@ handle_packet(#unsubscribe{} = Pkt, State) -> handle_packet(#pingreq{}, State) -> send(State, #pingresp{}); handle_packet(#disconnect{properties = #{session_expiry_interval := SE}}, - #state{session_expiry = 0} = State) when SE>0 -> + #state{session_expiry = 0} = State) when SE > 0 -> %% Protocol violation {error, State, session_expiry_non_zero}; handle_packet(#disconnect{code = Code, properties = Props}, @@ -361,29 +395,32 @@ handle_packet(#disconnect{code = Code, properties = Props}, {error, State2, {peer_disconnected, Code, Reason}}; handle_packet(Pkt, State) -> ?WARNING_MSG("Unexpected packet:~n~ts~n** when state:~n~ts", - [pp(Pkt), pp(State)]), + [pp(Pkt), pp(State)]), {error, State, {unexpected_packet, element(1, Pkt)}}. + terminate(_, #state{peername = undefined}) -> ok; terminate(Reason, State) -> Reason1 = case Reason of - shutdown -> shutdown; + shutdown -> shutdown; {shutdown, _} -> shutdown; - normal -> State#state.stop_reason; + normal -> State#state.stop_reason; {process_limit, _} -> queue_full; - _ -> internal_server_error - end, + _ -> internal_server_error + end, case State#state.jid of - #jid{} -> unregister_session(State, Reason1); - undefined -> log_disconnection(State, Reason1) + #jid{} -> unregister_session(State, Reason1); + undefined -> log_disconnection(State, Reason1) end, State1 = disconnect(State, Reason1), publish_will(State1). + code_change(_OldVsn, State, _Extra) -> {ok, upgrade_state(State)}. + %%%=================================================================== %%% State transitions %%%=================================================================== @@ -395,8 +432,9 @@ noreply(#state{timeout = {MSecs, StartTime}} = State) -> Timeout = max(0, MSecs - CurrentTime + StartTime), {noreply, State, Timeout}. + -spec stop(state(), error_reason()) -> {noreply, state(), infinity} | - {stop, normal, state()}. + {stop, normal, state()}. stop(#state{session_expiry = 0} = State, Reason) -> {stop, normal, State#state{stop_reason = Reason}}; stop(#state{session_expiry = SessExp} = State, Reason) -> @@ -406,20 +444,22 @@ stop(#state{session_expiry = SessExp} = State, Reason) -> _ -> WillDelay = State#state.will_delay, log_disconnection(State, Reason), - State1 = disconnect(State, Reason), - State2 = if WillDelay == 0 -> + State1 = disconnect(State, Reason), + State2 = if + WillDelay == 0 -> publish_will(State1); - WillDelay < SessExp -> + WillDelay < SessExp -> erlang:start_timer(WillDelay, self(), publish_will), State1; - true -> + true -> State1 end, - State3 = set_timeout(State2, SessExp), - State4 = State3#state{stop_reason = Reason}, - noreply(State4) + State3 = set_timeout(State2, SessExp), + State4 = State3#state{stop_reason = Reason}, + noreply(State4) end. + %% Here is the code upgrading state between different %% code versions. This is needed when doing session resumption from %% remote node running the version of the code with incompatible #state{} @@ -427,15 +467,16 @@ stop(#state{session_expiry = SessExp} = State, Reason) -> -spec upgrade_state(tuple()) -> state(). upgrade_state(State) -> case element(2, State) of - ?VSN -> - State; - VSN when VSN > ?VSN -> - erlang:error({downgrade_not_supported, State}); - VSN -> - State1 = upgrade_state(State, VSN), - upgrade_state(setelement(2, State1, VSN+1)) + ?VSN -> + State; + VSN when VSN > ?VSN -> + erlang:error({downgrade_not_supported, State}); + VSN -> + State1 = upgrade_state(State, VSN), + upgrade_state(setelement(2, State1, VSN + 1)) end. + -spec upgrade_state(tuple(), integer()) -> tuple(). upgrade_state(OldState, 1) -> %% Appending 'tls' field @@ -443,82 +484,89 @@ upgrade_state(OldState, 1) -> upgrade_state(State, _VSN) -> State. + %%%=================================================================== %%% Session management %%%=================================================================== -spec open_session(state(), jid(), boolean()) -> {ok, boolean(), state()} | - {error, state(), error_reason()}. + {error, state(), error_reason()}. open_session(State, JID, _CleanStart = false) -> USR = {_, S, _} = jid:tolower(JID), case mod_mqtt:lookup_session(USR) of - {ok, Pid} -> - try p1_server:call(Pid, {get_state, self()}, ?CALL_TIMEOUT) of - {ok, State1} -> + {ok, Pid} -> + try p1_server:call(Pid, {get_state, self()}, ?CALL_TIMEOUT) of + {ok, State1} -> State2 = upgrade_state(State1), - Q1 = case queue_type(S) of - ram -> State2#state.queue; - _ -> p1_queue:ram_to_file(State2#state.queue) - end, - Q2 = p1_queue:set_limit(Q1, queue_limit(S)), - State3 = State#state{queue = Q2, - acks = State2#state.acks, - subscriptions = State2#state.subscriptions, - id = State2#state.id, - in_flight = State2#state.in_flight}, - ?DEBUG("Resumed state from ~p at ~ts:~n~ts", - [Pid, node(Pid), pp(State3)]), - register_session(State3, JID, Pid); - {error, Why} -> - {error, State, Why} - catch exit:{Why, {p1_server, _, _}} -> - ?WARNING_MSG("Failed to copy session state from ~p at ~ts: ~ts", - [Pid, node(Pid), format_exit_reason(Why)]), - register_session(State, JID, undefined) - end; - {error, notfound} -> - register_session(State, JID, undefined); - {error, Why} -> - {error, State, Why} + Q1 = case queue_type(S) of + ram -> State2#state.queue; + _ -> p1_queue:ram_to_file(State2#state.queue) + end, + Q2 = p1_queue:set_limit(Q1, queue_limit(S)), + State3 = State#state{ + queue = Q2, + acks = State2#state.acks, + subscriptions = State2#state.subscriptions, + id = State2#state.id, + in_flight = State2#state.in_flight + }, + ?DEBUG("Resumed state from ~p at ~ts:~n~ts", + [Pid, node(Pid), pp(State3)]), + register_session(State3, JID, Pid); + {error, Why} -> + {error, State, Why} + catch + exit:{Why, {p1_server, _, _}} -> + ?WARNING_MSG("Failed to copy session state from ~p at ~ts: ~ts", + [Pid, node(Pid), format_exit_reason(Why)]), + register_session(State, JID, undefined) + end; + {error, notfound} -> + register_session(State, JID, undefined); + {error, Why} -> + {error, State, Why} end; open_session(State, JID, _CleanStart = true) -> register_session(State, JID, undefined). + -spec register_session(state(), jid(), undefined | pid()) -> - {ok, boolean(), state()} | {error, state(), error_reason()}. + {ok, boolean(), state()} | {error, state(), error_reason()}. register_session(#state{peername = IP} = State, JID, Parent) -> USR = {_, S, _} = jid:tolower(JID), case mod_mqtt:open_session(USR) of - ok -> - case resubscribe(USR, State#state.subscriptions) of - ok -> - ?INFO_MSG("~ts for ~ts from ~ts", - [if is_pid(Parent) -> - io_lib:format( - "Reopened MQTT session via ~p", - [Parent]); - true -> - "Opened MQTT session" - end, - jid:encode(JID), - ejabberd_config:may_hide_data( - misc:ip_to_list(IP))]), - Q = case State#state.queue of - undefined -> - p1_queue:new(queue_type(S), queue_limit(S)); - Q1 -> - Q1 - end, + ok -> + case resubscribe(USR, State#state.subscriptions) of + ok -> + ?INFO_MSG("~ts for ~ts from ~ts", + [if + is_pid(Parent) -> + io_lib:format( + "Reopened MQTT session via ~p", + [Parent]); + true -> + "Opened MQTT session" + end, + jid:encode(JID), + ejabberd_config:may_hide_data( + misc:ip_to_list(IP))]), + Q = case State#state.queue of + undefined -> + p1_queue:new(queue_type(S), queue_limit(S)); + Q1 -> + Q1 + end, {ok, is_pid(Parent), State#state{jid = JID, queue = Q}}; - {error, Why} -> + {error, Why} -> mod_mqtt:close_session(USR), - {error, State#state{session_expiry = 0}, Why} - end; - {error, Reason} -> - ?ERROR_MSG("Failed to register MQTT session for ~ts from ~ts: ~ts", - err_args(JID, IP, Reason)), - {error, State, Reason} + {error, State#state{session_expiry = 0}, Why} + end; + {error, Reason} -> + ?ERROR_MSG("Failed to register MQTT session for ~ts from ~ts: ~ts", + err_args(JID, IP, Reason)), + {error, State, Reason} end. + -spec unregister_session(state(), error_reason()) -> ok. unregister_session(#state{jid = #jid{} = JID, peername = IP} = State, Reason) -> Msg = "Closing MQTT session for ~ts from ~ts: ~ts", @@ -540,22 +588,23 @@ unregister_session(#state{jid = #jid{} = JID, peername = IP} = State, Reason) -> USR = jid:tolower(JID), unsubscribe(maps:keys(State#state.subscriptions), USR, #{}), case mod_mqtt:close_session(USR) of - ok -> ok; - {error, Why} -> + ok -> ok; + {error, Why} -> ?ERROR_MSG( - "Failed to close MQTT session for ~ts from ~ts: ~ts", - err_args(JID, IP, Why)) + "Failed to close MQTT session for ~ts from ~ts: ~ts", + err_args(JID, IP, Why)) end; unregister_session(_, _) -> ok. + %%%=================================================================== %%% CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers %%%=================================================================== -spec handle_connect(connect(), state()) -> {ok, state()} | - {error, state(), error_reason()}. + {error, state(), error_reason()}. handle_connect(#connect{clean_start = CleanStart} = Pkt, - #state{jid = undefined, peername = IP} = State) -> + #state{jid = undefined, peername = IP} = State) -> case authenticate(Pkt, IP, State) of {ok, JID} -> case validate_will(Pkt, JID) of @@ -564,8 +613,10 @@ handle_connect(#connect{clean_start = CleanStart} = Pkt, {ok, SessionPresent, State1} -> State2 = set_session_properties(State1, Pkt), ConnackProps = get_connack_properties(State2, Pkt), - Connack = #connack{session_present = SessionPresent, - properties = ConnackProps}, + Connack = #connack{ + session_present = SessionPresent, + properties = ConnackProps + }, case send(State2, Connack) of {ok, State3} -> resend(State3); {error, _, _} = Err -> Err @@ -580,13 +631,14 @@ handle_connect(#connect{clean_start = CleanStart} = Pkt, {error, State, {auth, Code}} end. + -spec handle_publish(publish(), state()) -> {ok, state()} | - {error, state(), error_reason()}. + {error, state(), error_reason()}. handle_publish(#publish{qos = QoS, id = ID} = Publish, State) -> case QoS == 2 andalso maps:is_key(ID, State#state.acks) of - true -> - send(State, maps:get(ID, State#state.acks)); - false -> + true -> + send(State, maps:get(ID, State#state.acks)); + false -> case validate_publish(Publish, State) of ok -> State1 = store_topic_alias(State, Publish), @@ -594,18 +646,27 @@ handle_publish(#publish{qos = QoS, id = ID} = Publish, State) -> {Code, Props} = get_publish_code_props(Ret), case Ret of {ok, _} when QoS == 2 -> - Pkt = #pubrec{id = ID, code = Code, - properties = Props}, + Pkt = #pubrec{ + id = ID, + code = Code, + properties = Props + }, Acks = maps:put(ID, Pkt, State1#state.acks), State2 = State1#state{acks = Acks}, send(State2, Pkt); {error, _} when QoS == 2 -> - Pkt = #pubrec{id = ID, code = Code, - properties = Props}, + Pkt = #pubrec{ + id = ID, + code = Code, + properties = Props + }, send(State1, Pkt); _ when QoS == 1 -> - Pkt = #puback{id = ID, code = Code, - properties = Props}, + Pkt = #puback{ + id = ID, + code = Code, + properties = Props + }, send(State1, Pkt); _ -> {ok, State1} @@ -615,8 +676,9 @@ handle_publish(#publish{qos = QoS, id = ID} = Publish, State) -> end end. + -spec handle_subscribe(subscribe(), state()) -> - {ok, state()} | {error, state(), error_reason()}. + {ok, state()} | {error, state(), error_reason()}. handle_subscribe(#subscribe{id = ID, filters = TopicFilters} = Pkt, State) -> case validate_subscribe(Pkt) of ok -> @@ -638,8 +700,9 @@ handle_subscribe(#subscribe{id = ID, filters = TopicFilters} = Pkt, State) -> {error, State, Why} end. + -spec handle_unsubscribe(unsubscribe(), state()) -> - {ok, state()} | {error, state(), error_reason()}. + {ok, state()} | {error, state(), error_reason()}. handle_unsubscribe(#unsubscribe{id = ID, filters = TopicFilters}, State) -> USR = jid:tolower(State#state.jid), {Codes, Subs, Props} = unsubscribe(TopicFilters, USR, State#state.subscriptions), @@ -647,15 +710,20 @@ handle_unsubscribe(#unsubscribe{id = ID, filters = TopicFilters}, State) -> Unsuback = #unsuback{id = ID, codes = Codes, properties = Props}, send(State1, Unsuback). + %%%=================================================================== %%% Aux functions for CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers %%%=================================================================== -spec set_session_properties(state(), connect()) -> state(). -set_session_properties(#state{version = Version, - jid = #jid{lserver = Server}} = State, - #connect{clean_start = CleanStart, - keep_alive = KeepAlive, - properties = Props} = Pkt) -> +set_session_properties(#state{ + version = Version, + jid = #jid{lserver = Server} + } = State, + #connect{ + clean_start = CleanStart, + keep_alive = KeepAlive, + properties = Props + } = Pkt) -> SEMin = case CleanStart of false when Version == ?MQTT_VERSION_4 -> infinity; _ -> timer:seconds(maps:get(session_expiry_interval, Props, 0)) @@ -665,53 +733,74 @@ set_session_properties(#state{version = Version, State2 = set_will_properties(State1, Pkt), set_keep_alive(State2, KeepAlive). + -spec set_will_properties(state(), connect()) -> state(). -set_will_properties(State, #connect{will = #publish{} = Will, - will_properties = Props}) -> +set_will_properties(State, + #connect{ + will = #publish{} = Will, + will_properties = Props + }) -> {WillDelay, Props1} = case maps:take(will_delay_interval, Props) of error -> {0, Props}; Ret -> Ret end, - State#state{will = Will#publish{properties = Props1}, - will_delay = timer:seconds(WillDelay)}; + State#state{ + will = Will#publish{properties = Props1}, + will_delay = timer:seconds(WillDelay) + }; set_will_properties(State, _) -> State. + -spec get_connack_properties(state(), connect()) -> properties(). get_connack_properties(#state{session_expiry = SessExp, jid = JID}, - #connect{client_id = ClientID, - keep_alive = KeepAlive, - properties = Props}) -> + #connect{ + client_id = ClientID, + keep_alive = KeepAlive, + properties = Props + }) -> Props1 = case ClientID of <<>> -> #{assigned_client_identifier => JID#jid.lresource}; _ -> #{} end, Props2 = case maps:find(authentication_method, Props) of - {ok, Method} -> Props1#{authentication_method => Method}; - error -> Props1 - end, - Props2#{session_expiry_interval => SessExp div 1000, - shared_subscription_available => false, - topic_alias_maximum => topic_alias_maximum(JID#jid.lserver), - server_keep_alive => KeepAlive}. + {ok, Method} -> Props1#{authentication_method => Method}; + error -> Props1 + end, + Props2#{ + session_expiry_interval => SessExp div 1000, + shared_subscription_available => false, + topic_alias_maximum => topic_alias_maximum(JID#jid.lserver), + server_keep_alive => KeepAlive + }. + -spec subscribe([{binary(), sub_opts()}], jid:ljid(), non_neg_integer()) -> - {[reason_code()], subscriptions(), properties()}. + {[reason_code()], subscriptions(), properties()}. subscribe(TopicFilters, USR, SubID) -> subscribe(TopicFilters, USR, SubID, [], #{}, ok). --spec subscribe([{binary(), sub_opts()}], jid:ljid(), non_neg_integer(), - [reason_code()], subscriptions(), ok | {error, error_reason()}) -> - {[reason_code()], subscriptions(), properties()}. -subscribe([{TopicFilter, SubOpts}|TopicFilters], USR, SubID, Codes, Subs, Err) -> + +-spec subscribe([{binary(), sub_opts()}], + jid:ljid(), + non_neg_integer(), + [reason_code()], + subscriptions(), + ok | {error, error_reason()}) -> + {[reason_code()], subscriptions(), properties()}. +subscribe([{TopicFilter, SubOpts} | TopicFilters], USR, SubID, Codes, Subs, Err) -> case mod_mqtt:subscribe(USR, TopicFilter, SubOpts, SubID) of ok -> Code = subscribe_reason_code(SubOpts#sub_opts.qos), - subscribe(TopicFilters, USR, SubID, [Code|Codes], - maps:put(TopicFilter, {SubOpts, SubID}, Subs), Err); + subscribe(TopicFilters, + USR, + SubID, + [Code | Codes], + maps:put(TopicFilter, {SubOpts, SubID}, Subs), + Err); {error, Why} = Err1 -> Code = subscribe_reason_code(Why), - subscribe(TopicFilters, USR, SubID, [Code|Codes], Subs, Err1) + subscribe(TopicFilters, USR, SubID, [Code | Codes], Subs, Err1) end; subscribe([], _USR, _SubID, Codes, Subs, Err) -> Props = case Err of @@ -721,27 +810,36 @@ subscribe([], _USR, _SubID, Codes, Subs, Err) -> end, {lists:reverse(Codes), Subs, Props}. + -spec unsubscribe([binary()], jid:ljid(), subscriptions()) -> - {[reason_code()], subscriptions(), properties()}. + {[reason_code()], subscriptions(), properties()}. unsubscribe(TopicFilters, USR, Subs) -> unsubscribe(TopicFilters, USR, [], Subs, ok). --spec unsubscribe([binary()], jid:ljid(), - [reason_code()], subscriptions(), + +-spec unsubscribe([binary()], + jid:ljid(), + [reason_code()], + subscriptions(), ok | {error, error_reason()}) -> - {[reason_code()], subscriptions(), properties()}. -unsubscribe([TopicFilter|TopicFilters], USR, Codes, Subs, Err) -> + {[reason_code()], subscriptions(), properties()}. +unsubscribe([TopicFilter | TopicFilters], USR, Codes, Subs, Err) -> case mod_mqtt:unsubscribe(USR, TopicFilter) of ok -> - unsubscribe(TopicFilters, USR, [success|Codes], - maps:remove(TopicFilter, Subs), Err); + unsubscribe(TopicFilters, + USR, + [success | Codes], + maps:remove(TopicFilter, Subs), + Err); {error, notfound} -> - unsubscribe(TopicFilters, USR, - ['no-subscription-existed'|Codes], - maps:remove(TopicFilter, Subs), Err); + unsubscribe(TopicFilters, + USR, + ['no-subscription-existed' | Codes], + maps:remove(TopicFilter, Subs), + Err); {error, Why} = Err1 -> Code = unsubscribe_reason_code(Why), - unsubscribe(TopicFilters, USR, [Code|Codes], Subs, Err1) + unsubscribe(TopicFilters, USR, [Code | Codes], Subs, Err1) end; unsubscribe([], _USR, Codes, Subs, Err) -> Props = case Err of @@ -751,6 +849,7 @@ unsubscribe([], _USR, Codes, Subs, Err) -> end, {lists:reverse(Codes), Subs, Props}. + -spec select_retained(jid:ljid(), subscriptions(), subscriptions()) -> [{publish(), seconds()}]. select_retained(USR, NewSubs, OldSubs) -> lists:flatten( @@ -760,15 +859,18 @@ select_retained(USR, NewSubs, OldSubs) -> (Filter, {#sub_opts{retain_handling = 1, qos = QoS}, SubID}, Acc) -> case maps:is_key(Filter, OldSubs) of true -> Acc; - false -> [mod_mqtt:select_retained(USR, Filter, QoS, SubID)|Acc] + false -> [mod_mqtt:select_retained(USR, Filter, QoS, SubID) | Acc] end; (Filter, {#sub_opts{qos = QoS}, SubID}, Acc) -> - [mod_mqtt:select_retained(USR, Filter, QoS, SubID)|Acc] - end, [], NewSubs)). + [mod_mqtt:select_retained(USR, Filter, QoS, SubID) | Acc] + end, + [], + NewSubs)). + -spec send_retained(state(), [{publish(), seconds()}]) -> - {ok, state()} | {error, state(), error_reason()}. -send_retained(State, [{#publish{meta = Meta} = Pub, Expiry}|Pubs]) -> + {ok, state()} | {error, state(), error_reason()}. +send_retained(State, [{#publish{meta = Meta} = Pub, Expiry} | Pubs]) -> I = next_id(State#state.id), Meta1 = Meta#{expiry_time => Expiry}, Pub1 = Pub#publish{id = I, retain = true, meta = Meta1}, @@ -781,6 +883,7 @@ send_retained(State, [{#publish{meta = Meta} = Pub, Expiry}|Pubs]) -> send_retained(State, []) -> {ok, State}. + -spec publish(state(), publish()) -> {ok, non_neg_integer()} | {error, error_reason()}. publish(State, #publish{topic = Topic, properties = Props} = Pkt) -> @@ -794,7 +897,8 @@ publish(State, #publish{topic = Topic, properties = Props} = Pkt) -> (correlation_data, _) -> true; (user_property, _) -> true; (_, _) -> false - end, Props), + end, + Props), Topic1 = case Topic of <<>> -> Alias = maps:get(topic_alias, Props), @@ -805,19 +909,24 @@ publish(State, #publish{topic = Topic, properties = Props} = Pkt) -> Pkt1 = Pkt#publish{topic = Topic1, properties = Props1}, mod_mqtt:publish(USR, Pkt1, ExpiryTime). + -spec store_topic_alias(state(), publish()) -> state(). -store_topic_alias(State, #publish{topic = <<_, _/binary>> = Topic, - properties = #{topic_alias := Alias}}) -> +store_topic_alias(State, + #publish{ + topic = <<_, _/binary>> = Topic, + properties = #{topic_alias := Alias} + }) -> Aliases = maps:put(Alias, Topic, State#state.topic_aliases), State#state{topic_aliases = Aliases}; store_topic_alias(State, _) -> State. + %%%=================================================================== %%% Socket management %%%=================================================================== -spec send(state(), mqtt_packet()) -> {ok, state()} | - {error, state(), error_reason()}. + {error, state(), error_reason()}. send(State, #publish{} = Pkt) -> case is_expired(Pkt) of {false, Pkt1} -> @@ -837,7 +946,8 @@ send(State, #publish{} = Pkt) -> Q -> State1 = State#state{queue = Q}, {ok, State1} - catch error:full -> + catch + error:full -> Q = p1_queue:clear(State#state.queue), State1 = State#state{queue = Q, session_expiry = 0}, {error, State1, queue_full} @@ -849,6 +959,7 @@ send(State, #publish{} = Pkt) -> send(State, Pkt) -> {ok, do_send(State, Pkt)}. + -spec resend(state()) -> {ok, state()} | {error, state(), error_reason()}. resend(#state{in_flight = undefined} = State) -> case p1_queue:out(State#state.queue) of @@ -863,12 +974,13 @@ resend(#state{in_flight = undefined} = State) -> State1 = do_send(State#state{queue = Q}, Pkt1), resend(State1) end; - {empty, _} -> - {ok, State} + {empty, _} -> + {ok, State} end; resend(#state{in_flight = Pkt} = State) -> {ok, do_send(State, set_dup_flag(Pkt))}. + -spec do_send(state(), mqtt_packet()) -> state(). do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) -> ?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]), @@ -879,21 +991,24 @@ do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) -> do_send(State, _Pkt) -> State. + -spec activate(socket()) -> ok. activate({SockMod, Sock} = Socket) -> Res = case SockMod of - gen_tcp -> inet:setopts(Sock, [{active, once}]); - _ -> SockMod:setopts(Sock, [{active, once}]) - end, + gen_tcp -> inet:setopts(Sock, [{active, once}]); + _ -> SockMod:setopts(Sock, [{active, once}]) + end, check_sock_result(Socket, Res). + -spec peername(state()) -> {ok, peername()} | {error, socket_error_reason()}. peername(#state{socket = {SockMod, Sock}}) -> case SockMod of - gen_tcp -> inet:peername(Sock); - _ -> SockMod:peername(Sock) + gen_tcp -> inet:peername(Sock); + _ -> SockMod:peername(Sock) end. + -spec disconnect(state(), error_reason()) -> state(). disconnect(#state{socket = {SockMod, Sock}} = State, Err) -> State1 = case Err of @@ -925,12 +1040,15 @@ disconnect(#state{socket = {SockMod, Sock}} = State, Err) -> end end, SockMod:close(Sock), - State1#state{socket = undefined, - version = undefined, - codec = mqtt_codec:renew(State#state.codec)}; + State1#state{ + socket = undefined, + version = undefined, + codec = mqtt_codec:renew(State#state.codec) + }; disconnect(State, _) -> State. + -spec check_sock_result(socket(), ok | {error, inet:posix()}) -> ok. check_sock_result(_, ok) -> ok; @@ -938,37 +1056,40 @@ check_sock_result({_, Sock}, {error, Why}) -> self() ! {tcp_closed, Sock}, ?DEBUG("MQTT socket error: ~p", [format_inet_error(Why)]). + -spec starttls(state()) -> {ok, socket()} | {error, error_reason()}. starttls(#state{socket = {gen_tcp, Socket}, tls = true}) -> case ejabberd_pkix:get_certfile() of - {ok, Cert} -> + {ok, Cert} -> CAFileOpt = case ejabberd_option:c2s_cafile(ejabberd_config:get_myname()) of undefined -> []; CAFile -> [{cafile, CAFile}] end, - case fast_tls:tcp_to_tls(Socket, [{certfile, Cert}] ++ CAFileOpt) of - {ok, TLSSock} -> - {ok, {fast_tls, TLSSock}}; - {error, Why} -> - {error, {tls, Why}} - end; - error -> - {error, {tls, no_certfile}} + case fast_tls:tcp_to_tls(Socket, [{certfile, Cert}] ++ CAFileOpt) of + {ok, TLSSock} -> + {ok, {fast_tls, TLSSock}}; + {error, Why} -> + {error, {tls, Why}} + end; + error -> + {error, {tls, no_certfile}} end; starttls(#state{socket = Socket}) -> {ok, Socket}. + -spec recv_data(socket(), binary()) -> {ok, binary()} | {error, error_reason()}. recv_data({fast_tls, Sock}, Data) -> case fast_tls:recv_data(Sock, Data) of - {ok, _} = OK -> OK; - {error, E} when is_atom(E) -> {error, {socket, E}}; - {error, E} when is_binary(E) -> {error, {tls, E}} + {ok, _} = OK -> OK; + {error, E} when is_atom(E) -> {error, {socket, E}}; + {error, E} when is_binary(E) -> {error, {tls, E}} end; recv_data(_, Data) -> {ok, Data}. + %%%=================================================================== %%% Formatters %%%=================================================================== @@ -976,6 +1097,7 @@ recv_data(_, Data) -> pp(Term) -> io_lib_pretty:print(Term, fun pp/2). + -spec format_inet_error(socket_error_reason()) -> string(). format_inet_error(closed) -> "connection closed"; @@ -983,10 +1105,11 @@ format_inet_error(timeout) -> format_inet_error(etimedout); format_inet_error(Reason) -> case inet:format_error(Reason) of - "unknown POSIX error" -> atom_to_list(Reason); - Txt -> Txt + "unknown POSIX error" -> atom_to_list(Reason); + Txt -> Txt end. + -spec format_tls_error(atom() | binary()) -> string() | binary(). format_tls_error(no_certfile) -> "certificate not configured"; @@ -995,6 +1118,7 @@ format_tls_error(Reason) when is_atom(Reason) -> format_tls_error(Reason) -> Reason. + -spec format_exit_reason(term()) -> string(). format_exit_reason(noproc) -> "process is dead"; @@ -1007,6 +1131,7 @@ format_exit_reason(timeout) -> format_exit_reason(Why) -> format("unexpected error: ~p", [Why]). + %% Same as format_error/1, but hides sensitive data %% and returns result as binary -spec format_reason_string(error_reason()) -> binary(). @@ -1017,18 +1142,22 @@ format_reason_string({replaced, _}) -> format_reason_string(Err) -> list_to_binary(format_error(Err)). + -spec format(io:format(), list()) -> string(). format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). + -spec pp(atom(), non_neg_integer()) -> [atom()] | no. pp(state, 17) -> record_info(fields, state); pp(Rec, Size) -> mqtt_codec:pp(Rec, Size). + -spec publish_reason_code(error_reason()) -> reason_code(). publish_reason_code(publish_forbidden) -> 'topic-name-invalid'; publish_reason_code(_) -> 'implementation-specific-error'. + -spec subscribe_reason_code(qos() | error_reason()) -> reason_code(). subscribe_reason_code(0) -> 'granted-qos-0'; subscribe_reason_code(1) -> 'granted-qos-1'; @@ -1036,9 +1165,11 @@ subscribe_reason_code(2) -> 'granted-qos-2'; subscribe_reason_code(subscribe_forbidden) -> 'topic-filter-invalid'; subscribe_reason_code(_) -> 'implementation-specific-error'. + -spec unsubscribe_reason_code(error_reason()) -> reason_code(). unsubscribe_reason_code(_) -> 'implementation-specific-error'. + -spec disconnect_reason_code(error_reason()) -> reason_code(). disconnect_reason_code({code, Code}) -> Code; disconnect_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); @@ -1058,6 +1189,7 @@ disconnect_reason_code(session_expiry_non_zero) -> 'protocol-error'; disconnect_reason_code(unknown_topic_alias) -> 'protocol-error'; disconnect_reason_code(_) -> 'unspecified-error'. + -spec connack_reason_code(error_reason()) -> reason_code(). connack_reason_code({Tag, Code}) when Tag == auth; Tag == code -> Code; connack_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); @@ -1072,6 +1204,7 @@ connack_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid'; connack_reason_code(session_expiry_non_zero) -> 'protocol-error'; connack_reason_code(_) -> 'unspecified-error'. + %%%=================================================================== %%% Configuration processing %%%=================================================================== @@ -1079,18 +1212,22 @@ connack_reason_code(_) -> 'unspecified-error'. queue_type(Host) -> mod_mqtt_opt:queue_type(Host). + -spec queue_limit(binary()) -> non_neg_integer() | unlimited. queue_limit(Host) -> mod_mqtt_opt:max_queue(Host). + -spec session_expiry(binary()) -> milli_seconds(). session_expiry(Host) -> mod_mqtt_opt:session_expiry(Host). + -spec topic_alias_maximum(binary()) -> non_neg_integer(). topic_alias_maximum(Host) -> mod_mqtt_opt:max_topic_aliases(Host). + %%%=================================================================== %%% Timings %%%=================================================================== @@ -1098,10 +1235,12 @@ topic_alias_maximum(Host) -> current_time() -> erlang:monotonic_time(millisecond). + -spec unix_time() -> seconds(). unix_time() -> erlang:system_time(second). + -spec set_keep_alive(state(), seconds()) -> state(). set_keep_alive(State, 0) -> ?DEBUG("Disabling MQTT keep-alive", []), @@ -1111,17 +1250,20 @@ set_keep_alive(State, Secs) -> ?DEBUG("Setting MQTT keep-alive to ~B seconds", [Secs1]), set_timeout(State, timer:seconds(Secs1)). + -spec reset_keep_alive(state()) -> state(). reset_keep_alive(#state{timeout = {MSecs, _}, jid = #jid{}} = State) -> set_timeout(State, MSecs); reset_keep_alive(State) -> State. + -spec set_timeout(state(), milli_seconds()) -> state(). set_timeout(State, MSecs) -> Time = current_time(), State#state{timeout = {MSecs, Time}}. + -spec is_expired(publish()) -> true | {false, publish()}. is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> case maps:get(expiry_time, Meta, ?MAX_UINT32) of @@ -1129,15 +1271,17 @@ is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> {false, Pkt}; ExpiryTime -> Left = ExpiryTime - unix_time(), - if Left > 0 -> + if + Left > 0 -> Props1 = Props#{message_expiry_interval => Left}, {false, Pkt#publish{properties = Props1}}; - true -> + true -> ?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]), true end end. + %%%=================================================================== %%% Authentication %%%=================================================================== @@ -1153,19 +1297,21 @@ parse_credentials(#connect{username = <<>>, client_id = ClientID}) -> parse_credentials(JID, ClientID); parse_credentials(#connect{username = User} = Pkt) -> try jid:decode(User) of - #jid{luser = <<>>} -> + #jid{luser = <<>>} -> case jid:make(User, ejabberd_config:get_myname()) of error -> {error, 'bad-user-name-or-password'}; JID -> parse_credentials(JID, Pkt#connect.client_id) end; - JID -> + JID -> parse_credentials(JID, Pkt#connect.client_id) - catch _:{bad_jid, _} -> - {error, 'bad-user-name-or-password'} + catch + _:{bad_jid, _} -> + {error, 'bad-user-name-or-password'} end. + -spec parse_credentials(jid:jid(), binary()) -> {ok, jid:jid()} | {error, reason_code()}. parse_credentials(JID, ClientID) -> case gen_mod:is_loaded(JID#jid.lserver, mod_mqtt) of @@ -1180,34 +1326,36 @@ parse_credentials(JID, ClientID) -> end end. + -spec authenticate(connect(), peername(), state()) -> {ok, jid:jid()} | {error, reason_code()}. authenticate(Pkt, IP, State) -> case authenticate(Pkt, State) of - {ok, JID, AuthModule} -> - ?INFO_MSG("Accepted MQTT authentication for ~ts by ~s backend from ~s", - [jid:encode(JID), - ejabberd_auth:backend_type(AuthModule), - ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), - {ok, JID}; - {error, _} = Err -> - Err + {ok, JID, AuthModule} -> + ?INFO_MSG("Accepted MQTT authentication for ~ts by ~s backend from ~s", + [jid:encode(JID), + ejabberd_auth:backend_type(AuthModule), + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + {ok, JID}; + {error, _} = Err -> + Err end. + -spec authenticate(connect(), state()) -> {ok, jid:jid(), module()} | {error, reason_code()}. authenticate(#connect{password = Pass, properties = Props} = Pkt, State) -> case parse_credentials(Pkt) of - {ok, #jid{luser = LUser, lserver = LServer} = JID} -> - case maps:find(authentication_method, Props) of - {ok, <<"X-OAUTH2">>} -> - Token = maps:get(authentication_data, Props, <<>>), - case ejabberd_oauth:check_token( - LUser, LServer, [<<"sasl_auth">>], Token) of - true -> {ok, JID, ejabberd_oauth}; - _ -> {error, 'not-authorized'} - end; - {ok, _} -> - {error, 'bad-authentication-method'}; - error -> + {ok, #jid{luser = LUser, lserver = LServer} = JID} -> + case maps:find(authentication_method, Props) of + {ok, <<"X-OAUTH2">>} -> + Token = maps:get(authentication_data, Props, <<>>), + case ejabberd_oauth:check_token( + LUser, LServer, [<<"sasl_auth">>], Token) of + true -> {ok, JID, ejabberd_oauth}; + _ -> {error, 'not-authorized'} + end; + {ok, _} -> + {error, 'bad-authentication-method'}; + error -> case Pass of <<>> -> case tls_auth(JID, State) of @@ -1215,12 +1363,12 @@ authenticate(#connect{password = Pass, properties = Props} = Pkt, State) -> {ok, JID, pkix}; false -> {error, 'not-authorized'}; - no_cert -> - case ejabberd_auth:check_password_with_authmodule( - LUser, <<>>, LServer, Pass) of - {true, AuthModule} -> {ok, JID, AuthModule}; - false -> {error, 'not-authorized'} - end + no_cert -> + case ejabberd_auth:check_password_with_authmodule( + LUser, <<>>, LServer, Pass) of + {true, AuthModule} -> {ok, JID, AuthModule}; + false -> {error, 'not-authorized'} + end end; _ -> case ejabberd_auth:check_password_with_authmodule( @@ -1229,11 +1377,12 @@ authenticate(#connect{password = Pass, properties = Props} = Pkt, State) -> false -> {error, 'not-authorized'} end end - end; - {error, _} = Err -> - Err + end; + {error, _} = Err -> + Err end. + -spec tls_auth(jid:jid(), state()) -> boolean() | no_cert. tls_auth(_JID, #state{tls_verify = false}) -> no_cert; @@ -1247,7 +1396,7 @@ tls_auth(JID, State) -> case get_cert_jid(Cert) of {ok, JID2} -> jid:remove_resource(jid:tolower(JID)) == - jid:remove_resource(jid:tolower(JID2)); + jid:remove_resource(jid:tolower(JID2)); error -> false end; @@ -1263,12 +1412,14 @@ tls_auth(JID, State) -> no_cert end. + get_cert_jid(Cert) -> case Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.subject of {rdnSequence, Attrs1} -> Attrs = lists:flatten(Attrs1), case lists:keyfind(?'id-at-commonName', - #'AttributeTypeAndValue'.type, Attrs) of + #'AttributeTypeAndValue'.type, + Attrs) of #'AttributeTypeAndValue'{value = {utf8String, Val}} -> try jid:decode(Val) of #jid{luser = <<>>} -> @@ -1280,7 +1431,8 @@ get_cert_jid(Cert) -> end; JID -> {ok, JID} - catch _:{bad_jid, _} -> + catch + _:{bad_jid, _} -> error end; _ -> @@ -1290,39 +1442,51 @@ get_cert_jid(Cert) -> error end. + %%%=================================================================== %%% Validators %%%=================================================================== -spec validate_will(connect(), jid:jid()) -> ok | {error, error_reason()}. validate_will(#connect{will = undefined}, _) -> ok; -validate_will(#connect{will = #publish{topic = Topic, payload = Payload}, - will_properties = Props}, JID) -> +validate_will(#connect{ + will = #publish{topic = Topic, payload = Payload}, + will_properties = Props + }, + JID) -> case mod_mqtt:check_publish_access(Topic, jid:tolower(JID)) of deny -> {error, will_topic_forbidden}; allow -> validate_payload(Props, Payload, will) end. + -spec validate_publish(publish(), state()) -> ok | {error, error_reason()}. -validate_publish(#publish{topic = Topic, payload = Payload, - properties = Props}, State) -> +validate_publish(#publish{ + topic = Topic, + payload = Payload, + properties = Props + }, + State) -> case validate_topic(Topic, Props, State) of ok -> validate_payload(Props, Payload, publish); Err -> Err end. + -spec validate_subscribe(subscribe()) -> ok | {error, error_reason()}. validate_subscribe(#subscribe{filters = Filters}) -> case lists:any( fun({<<"$share/", _/binary>>, _}) -> true; (_) -> false - end, Filters) of + end, + Filters) of true -> {error, {code, 'shared-subscriptions-not-supported'}}; false -> ok end. + -spec validate_topic(binary(), properties(), state()) -> ok | {error, error_reason()}. validate_topic(<<>>, Props, State) -> case maps:get(topic_alias, Props, 0) of @@ -1337,24 +1501,28 @@ validate_topic(<<>>, Props, State) -> validate_topic(_, #{topic_alias := Alias}, State) -> JID = State#state.jid, Max = topic_alias_maximum(JID#jid.lserver), - if Alias > Max -> + if + Alias > Max -> {error, {code, 'topic-alias-invalid'}}; - true -> + true -> ok end; validate_topic(_, _, _) -> ok. + -spec validate_payload(properties(), binary(), will | publish) -> ok | {error, error_reason()}. validate_payload(#{payload_format_indicator := utf8}, Payload, Type) -> try mqtt_codec:utf8(Payload) of _ -> ok - catch _:_ -> + catch + _:_ -> {error, {payload_format_invalid, Type}} end; validate_payload(_, _, _) -> ok. + %%%=================================================================== %%% Misc %%%=================================================================== @@ -1365,7 +1533,9 @@ resubscribe(USR, Subs) -> mod_mqtt:subscribe(USR, TopicFilter, SubOpts, ID); (_, _, {error, _} = Err) -> Err - end, ok, Subs) of + end, + ok, + Subs) of ok -> ok; {error, _} = Err1 -> @@ -1373,32 +1543,39 @@ resubscribe(USR, Subs) -> Err1 end. + -spec publish_will(state()) -> state(). -publish_will(#state{will = #publish{} = Will, - jid = #jid{} = JID} = State) -> +publish_will(#state{ + will = #publish{} = Will, + jid = #jid{} = JID + } = State) -> case publish(State, Will) of {ok, _} -> ?DEBUG("Will of ~ts has been published to ~ts", [jid:encode(JID), Will#publish.topic]); {error, Why} -> ?WARNING_MSG("Failed to publish will of ~ts to ~ts: ~ts", - [jid:encode(JID), Will#publish.topic, + [jid:encode(JID), + Will#publish.topic, format_error(Why)]) end, State#state{will = undefined}; publish_will(State) -> State. + -spec next_id(non_neg_integer()) -> pos_integer(). next_id(ID) -> (ID rem 65535) + 1. + -spec set_dup_flag(mqtt_packet()) -> mqtt_packet(). -set_dup_flag(#publish{qos = QoS} = Pkt) when QoS>0 -> +set_dup_flag(#publish{qos = QoS} = Pkt) when QoS > 0 -> Pkt#publish{dup = true}; set_dup_flag(Pkt) -> Pkt. + -spec get_publish_code_props({ok, non_neg_integer()} | {error, error_reason()}) -> {reason_code(), properties()}. get_publish_code_props({ok, 0}) -> @@ -1410,6 +1587,7 @@ get_publish_code_props({error, Err}) -> Reason = format_reason_string(Err), {Code, #{reason_string => Reason}}. + -spec err_args(undefined | jid:jid(), peername(), error_reason()) -> iolist(). err_args(undefined, IP, Reason) -> [ejabberd_config:may_hide_data(misc:ip_to_list(IP)), @@ -1419,6 +1597,7 @@ err_args(JID, IP, Reason) -> ejabberd_config:may_hide_data(misc:ip_to_list(IP)), format_error(Reason)]. + -spec log_disconnection(state(), error_reason()) -> ok. log_disconnection(#state{jid = JID, peername = IP}, Reason) -> Msg = case JID of diff --git a/src/mod_mqtt_sql.erl b/src/mod_mqtt_sql.erl index 0f2b05b35..fb6be7415 100644 --- a/src/mod_mqtt_sql.erl +++ b/src/mod_mqtt_sql.erl @@ -30,6 +30,7 @@ -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -37,32 +38,38 @@ init() -> ?ERROR_MSG("Backend 'sql' is only supported for db_type", []), {error, db_failure}. + init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"mqtt_pub">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"resource">>, type = text}, - #sql_column{name = <<"topic">>, type = text}, - #sql_column{name = <<"qos">>, type = smallint}, - #sql_column{name = <<"payload">>, type = blob}, - #sql_column{name = <<"payload_format">>, type = smallint}, - #sql_column{name = <<"content_type">>, type = text}, - #sql_column{name = <<"response_topic">>, type = text}, - #sql_column{name = <<"correlation_data">>, type = blob}, - #sql_column{name = <<"user_properties">>, type = blob}, - #sql_column{name = <<"expiry">>, type = bigint}], - indices = [#sql_index{ - columns = [<<"topic">>, <<"server_host">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"mqtt_pub">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"topic">>, type = text}, + #sql_column{name = <<"qos">>, type = smallint}, + #sql_column{name = <<"payload">>, type = blob}, + #sql_column{name = <<"payload_format">>, type = smallint}, + #sql_column{name = <<"content_type">>, type = text}, + #sql_column{name = <<"response_topic">>, type = text}, + #sql_column{name = <<"correlation_data">>, type = blob}, + #sql_column{name = <<"user_properties">>, type = blob}, + #sql_column{name = <<"expiry">>, type = bigint}], + indices = [#sql_index{ + columns = [<<"topic">>, <<"server_host">>], + unique = true + }] + }] + }]. + publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> PayloadFormat = encode_pfi(maps:get(payload_format_indicator, Props, binary)), @@ -70,109 +77,130 @@ publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> CorrelationData = maps:get(correlation_data, Props, <<"">>), ContentType = maps:get(content_type, Props, <<"">>), UserProps = encode_props(maps:get(user_property, Props, [])), - case ?SQL_UPSERT(LServer, "mqtt_pub", - ["!topic=%(Topic)s", + case ?SQL_UPSERT(LServer, + "mqtt_pub", + ["!topic=%(Topic)s", "!server_host=%(LServer)s", - "username=%(U)s", - "resource=%(R)s", - "payload=%(Payload)s", - "qos=%(QoS)d", + "username=%(U)s", + "resource=%(R)s", + "payload=%(Payload)s", + "qos=%(QoS)d", "payload_format=%(PayloadFormat)d", "response_topic=%(ResponseTopic)s", "correlation_data=%(CorrelationData)s", "content_type=%(ContentType)s", "user_properties=%(UserProps)s", "expiry=%(ExpiryTime)d"]) of - ok -> ok; - _Err -> {error, db_failure} + ok -> ok; + _Err -> {error, db_failure} end. + delete_published({_, LServer, _}, Topic) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from mqtt_pub where " + LServer, + ?SQL("delete from mqtt_pub where " "topic=%(Topic)s and %(LServer)H")) of - {updated, _} -> ok; - _Err -> {error, db_failure} + {updated, _} -> ok; + _Err -> {error, db_failure} end. + lookup_published({_, LServer, _}, Topic) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(payload)s, @(qos)d, @(payload_format)d, " + LServer, + ?SQL("select @(payload)s, @(qos)d, @(payload_format)d, " "@(content_type)s, @(response_topic)s, " "@(correlation_data)s, @(user_properties)s, @(expiry)d " "from mqtt_pub where topic=%(Topic)s and %(LServer)H")) of - {selected, [{Payload, QoS, PayloadFormat, ContentType, + {selected, [{Payload, QoS, PayloadFormat, ContentType, ResponseTopic, CorrelationData, EncProps, Expiry}]} -> try decode_props(EncProps) of UserProps -> try decode_pfi(PayloadFormat) of PFI -> - Props = #{payload_format_indicator => PFI, + Props = #{ + payload_format_indicator => PFI, content_type => ContentType, response_topic => ResponseTopic, correlation_data => CorrelationData, - user_property => UserProps}, + user_property => UserProps + }, {ok, {Payload, QoS, Props, Expiry}} - catch _:badarg -> + catch + _:badarg -> ?ERROR_MSG("Malformed value of 'payload_format' column " - "for topic '~ts'", [Topic]), + "for topic '~ts'", + [Topic]), {error, db_failure} end - catch _:badarg -> + catch + _:badarg -> ?ERROR_MSG("Malformed value of 'user_properties' column " - "for topic '~ts'", [Topic]), + "for topic '~ts'", + [Topic]), {error, db_failure} end; - {selected, []} -> - {error, notfound}; - _ -> - {error, db_failure} + {selected, []} -> + {error, notfound}; + _ -> + {error, db_failure} end. + list_topics(LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(topic)s from mqtt_pub where %(LServer)H")) of - {selected, Res} -> - {ok, [Topic || {Topic} <- Res]}; - _ -> - {error, db_failure} + LServer, + ?SQL("select @(topic)s from mqtt_pub where %(LServer)H")) of + {selected, Res} -> + {ok, [ Topic || {Topic} <- Res ]}; + _ -> + {error, db_failure} end. + open_session(_) -> erlang:nif_error(unsupported_db). + close_session(_) -> erlang:nif_error(unsupported_db). + lookup_session(_) -> erlang:nif_error(unsupported_db). + get_sessions(_, _) -> erlang:nif_error(unsupported_db). + subscribe(_, _, _, _) -> erlang:nif_error(unsupported_db). + unsubscribe(_, _) -> erlang:nif_error(unsupported_db). + find_subscriber(_, _) -> erlang:nif_error(unsupported_db). + %%%=================================================================== %%% Internal functions %%%=================================================================== encode_pfi(binary) -> 0; encode_pfi(utf8) -> 1. + decode_pfi(0) -> binary; decode_pfi(1) -> utf8. + encode_props([]) -> <<"">>; encode_props(L) -> term_to_binary(L). + decode_props(<<"">>) -> []; decode_props(Bin) -> binary_to_term(Bin). diff --git a/src/mod_mqtt_ws.erl b/src/mod_mqtt_ws.erl index fd1e7d871..91952ea8e 100644 --- a/src/mod_mqtt_ws.erl +++ b/src/mod_mqtt_ws.erl @@ -26,23 +26,31 @@ -export([start/1, start_link/1]). -export([peername/1, setopts/2, send/2, close/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include_lib("xmpp/include/xmpp.hrl"). + -include("ejabberd_http.hrl"). -include("logger.hrl"). -define(SEND_TIMEOUT, timer:seconds(15)). --record(state, {socket :: socket(), - ws_pid :: pid(), - mqtt_session :: undefined | pid()}). +-record(state, { + socket :: socket(), + ws_pid :: pid(), + mqtt_session :: undefined | pid() + }). -type peername() :: {inet:ip_address(), inet:port_number()}. -type socket() :: {http_ws, pid(), peername()}. -export_type([socket/0]). + %%%=================================================================== %%% API %%%=================================================================== @@ -50,53 +58,65 @@ socket_handoff(LocalPath, Request, Opts) -> ejabberd_websocket:socket_handoff( LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0). + start({#ws{http_opts = Opts}, _} = WS) -> ?GEN_SERVER:start(?MODULE, [WS], ejabberd_config:fsm_limit_opts(Opts)). + start_link({#ws{http_opts = Opts}, _} = WS) -> ?GEN_SERVER:start_link(?MODULE, [WS], ejabberd_config:fsm_limit_opts(Opts)). + -spec peername(socket()) -> {ok, peername()}. peername({http_ws, _, IP}) -> {ok, IP}. + -spec setopts(socket(), list()) -> ok. setopts(_WSock, _Opts) -> ok. + -spec send(socket(), iodata()) -> ok | {error, timeout | einval}. send({http_ws, Pid, _}, Data) -> - try ?GEN_SERVER:call(Pid, {send, Data}, ?SEND_TIMEOUT) - catch exit:{timeout, {?GEN_SERVER, _, _}} -> - {error, timeout}; - exit:{_, {?GEN_SERVER, _, _}} -> - {error, einval} + try + ?GEN_SERVER:call(Pid, {send, Data}, ?SEND_TIMEOUT) + catch + exit:{timeout, {?GEN_SERVER, _, _}} -> + {error, timeout}; + exit:{_, {?GEN_SERVER, _, _}} -> + {error, einval} end. + -spec close(socket()) -> ok. close({http_ws, Pid, _}) -> ?GEN_SERVER:cast(Pid, close). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([{#ws{ip = IP, http_opts = ListenOpts}, WsPid}]) -> Socket = {http_ws, self(), IP}, case mod_mqtt_session:start(?MODULE, Socket, ListenOpts) of - {ok, Pid} -> - erlang:monitor(process, Pid), - erlang:monitor(process, WsPid), - mod_mqtt_session:accept(Pid), - State = #state{socket = Socket, - ws_pid = WsPid, - mqtt_session = Pid}, - {ok, State}; - {error, Reason} -> - {stop, Reason}; - ignore -> - ignore + {ok, Pid} -> + erlang:monitor(process, Pid), + erlang:monitor(process, WsPid), + mod_mqtt_session:accept(Pid), + State = #state{ + socket = Socket, + ws_pid = WsPid, + mqtt_session = Pid + }, + {ok, State}; + {error, Reason} -> + {stop, Reason}; + ignore -> + ignore end. + handle_call({send, Data}, _From, #state{ws_pid = WsPid} = State) -> WsPid ! {data, Data}, {reply, ok, State}; @@ -104,12 +124,14 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(close, State) -> {stop, normal, State#state{mqtt_session = undefined}}; handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. + handle_info(closed, State) -> {stop, normal, State}; handle_info({received, Data}, State) -> @@ -122,47 +144,73 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, State) -> - if State#state.mqtt_session /= undefined -> - State#state.mqtt_session ! {tcp_closed, State#state.socket}; - true -> - ok + if + State#state.mqtt_session /= undefined -> + State#state.mqtt_session ! {tcp_closed, State#state.socket}; + true -> + ok end. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_human_html_xmlel() -> xmlel(). get_human_html_xmlel() -> Heading = <<"ejabberd mod_mqtt">>, - #xmlel{name = <<"html">>, - attrs = - [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], - children = - [#xmlel{name = <<"head">>, attrs = [], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = [{xmlcdata, Heading}]}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [#xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, Heading}]}, - #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, <<"An implementation of ">>}, - #xmlel{name = <<"a">>, - attrs = - [{<<"href">>, - <<"http://tools.ietf.org/html/rfc6455">>}], - children = - [{xmlcdata, - <<"WebSocket protocol">>}]}]}, - #xmlel{name = <<"p">>, attrs = [], - children = - [{xmlcdata, - <<"This web page is only informative. To " - "use WebSocket connection you need an MQTT " - "client that supports it.">>}]}]}]}. + #xmlel{ + name = <<"html">>, + attrs = + [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], + children = + [#xmlel{ + name = <<"head">>, + attrs = [], + children = + [#xmlel{ + name = <<"title">>, + attrs = [], + children = [{xmlcdata, Heading}] + }] + }, + #xmlel{ + name = <<"body">>, + attrs = [], + children = + [#xmlel{ + name = <<"h1">>, + attrs = [], + children = [{xmlcdata, Heading}] + }, + #xmlel{ + name = <<"p">>, + attrs = [], + children = + [{xmlcdata, <<"An implementation of ">>}, + #xmlel{ + name = <<"a">>, + attrs = + [{<<"href">>, + <<"http://tools.ietf.org/html/rfc6455">>}], + children = + [{xmlcdata, + <<"WebSocket protocol">>}] + }] + }, + #xmlel{ + name = <<"p">>, + attrs = [], + children = + [{xmlcdata, + <<"This web page is only informative. To " + "use WebSocket connection you need an MQTT " + "client that supports it.">>}] + }] + }] + }. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 0ca31cb80..811ff1891 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -35,67 +35,78 @@ %% API -export([start/2, - stop/1, - start_link/2, - reload/3, + stop/1, + start_link/2, + reload/3, mod_doc/0, - room_destroyed/4, - store_room/4, - store_room/5, - store_changes/4, - restore_room/3, - forget_room/3, - create_room/3, - create_room/5, - shutdown_rooms/1, - process_disco_info/1, - process_disco_items/1, - process_vcard/1, - process_register/1, - process_iq_register/1, - process_muc_unique/1, - process_mucsub/1, - broadcast_service_message/3, - export/1, - import_info/0, - import/5, - import_start/2, - opts_to_binary/1, - find_online_room/2, - register_online_room/3, - get_online_rooms/1, - count_online_rooms/1, - register_online_user/4, - unregister_online_user/4, - iq_set_register_info/5, - count_online_rooms_by_user/3, - get_online_rooms_by_user/3, - can_use_nick/4, - get_subscribed_rooms/2, + room_destroyed/4, + store_room/4, store_room/5, + store_changes/4, + restore_room/3, + forget_room/3, + create_room/3, create_room/5, + shutdown_rooms/1, + process_disco_info/1, + process_disco_items/1, + process_vcard/1, + process_register/1, + process_iq_register/1, + process_muc_unique/1, + process_mucsub/1, + broadcast_service_message/3, + export/1, + import_info/0, + import/5, + import_start/2, + opts_to_binary/1, + find_online_room/2, + register_online_room/3, + get_online_rooms/1, + count_online_rooms/1, + register_online_user/4, + unregister_online_user/4, + iq_set_register_info/5, + count_online_rooms_by_user/3, + get_online_rooms_by_user/3, + can_use_nick/4, + get_subscribed_rooms/2, remove_user/2, - procname/2, - route/1, unhibernate_room/3]). + procname/2, + route/1, + unhibernate_room/3]). --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3, - mod_opt_type/1, mod_options/1, depends/2]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + mod_opt_type/1, + mod_options/1, + depends/2]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). --type state() :: #{hosts := [binary()], - server_host := binary(), - worker := pos_integer()}. +-type state() :: #{ + hosts := [binary()], + server_host := binary(), + worker := pos_integer() + }. -type access() :: {acl:acl(), acl:acl(), acl:acl(), acl:acl(), acl:acl()}. -type muc_room_opts() :: [{atom(), any()}]. -export_type([access/0]). + + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. --callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}. +-callback store_room(binary(), binary(), binary(), list(), list() | undefined) -> {atomic, any()}. -callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}. -callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error | {error, atom()}. -callback forget_room(binary(), binary(), binary()) -> {atomic, any()}. @@ -115,36 +126,45 @@ -callback count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). -callback get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. -callback get_subscribed_rooms(binary(), binary(), jid()) -> - {ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}. + {ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}. -optional_callbacks([get_subscribed_rooms/3, store_changes/4]). + %%==================================================================== %% API %%==================================================================== 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), - Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), - RMod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), - load_permanent_rooms(MyHosts, Host, Opts); - Err -> - Err + {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), + Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), + RMod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), + load_permanent_rooms(MyHosts, Host, Opts); + Err -> + Err end. + stop(Host) -> - ejabberd_hooks:delete(remove_user, Host, ?MODULE, - remove_user, 50), + 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). + -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(ServerHost, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -155,57 +175,73 @@ reload(ServerHost, NewOpts, OldOpts) -> OldHosts = gen_mod:get_opt_hosts(OldOpts), AddHosts = NewHosts -- OldHosts, DelHosts = OldHosts -- NewHosts, - if NewMod /= OldMod -> - NewMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); + true -> + ok end, - if NewRMod /= OldRMod -> - NewRMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); - true -> - ok + if + NewRMod /= OldRMod -> + NewRMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); + true -> + ok end, lists:foreach( fun(I) -> - ?GEN_SERVER:cast(procname(ServerHost, I), - {reload, AddHosts, DelHosts, NewHosts}) - end, lists:seq(1, misc:logical_processors())), + ?GEN_SERVER:cast(procname(ServerHost, I), + {reload, AddHosts, DelHosts, NewHosts}) + end, + lists:seq(1, misc:logical_processors())), load_permanent_rooms(AddHosts, ServerHost, NewOpts), shutdown_rooms(ServerHost, DelHosts, OldRMod), lists:foreach( fun(Host) -> - lists:foreach( - fun({_, _, Pid}) when node(Pid) == node() -> - mod_muc_room:config_reloaded(Pid); - (_) -> - ok - end, get_online_rooms(ServerHost, Host)) - end, misc:intersection(NewHosts, OldHosts)). + lists:foreach( + fun({_, _, Pid}) when node(Pid) == node() -> + mod_muc_room:config_reloaded(Pid); + (_) -> + ok + end, + get_online_rooms(ServerHost, Host)) + end, + misc:intersection(NewHosts, OldHosts)). + depends(_Host, _Opts) -> [{mod_mam, soft}]. + start_link(Host, I) -> Proc = procname(Host, I), - ?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host, I], - ejabberd_config:fsm_limit_opts([])). + ?GEN_SERVER:start_link({local, Proc}, + ?MODULE, + [Host, I], + ejabberd_config:fsm_limit_opts([])). + -spec procname(binary(), pos_integer() | {binary(), binary()}) -> atom(). procname(Host, I) when is_integer(I) -> binary_to_atom( - <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary, - "_", (integer_to_binary(I))/binary>>, utf8); + <<(atom_to_binary(?MODULE, latin1))/binary, + "_", + Host/binary, + "_", + (integer_to_binary(I))/binary>>, + utf8); procname(Host, RoomHost) -> Cores = misc:logical_processors(), I = erlang:phash2(RoomHost, Cores) + 1, procname(Host, I). + -spec route(stanza()) -> ok. route(Pkt) -> To = xmpp:get_to(Pkt), ServerHost = ejabberd_router:host_of_route(To#jid.lserver), route(Pkt, ServerHost). + -spec route(stanza(), binary()) -> ok. route(Pkt, ServerHost) -> From = xmpp:get_from(Pkt), @@ -213,25 +249,33 @@ route(Pkt, ServerHost) -> Host = To#jid.lserver, Access = mod_muc_opt:access(ServerHost), case acl:match_rule(ServerHost, Access, From) of - allow -> - route(Pkt, Host, ServerHost); - deny -> - Lang = xmpp:get_lang(Pkt), + allow -> + route(Pkt, Host, ServerHost); + deny -> + Lang = xmpp:get_lang(Pkt), ErrText = ?T("Access denied by service policy"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Pkt, Err) end. + -spec route(stanza(), binary(), binary()) -> ok. route(#iq{to = #jid{luser = <<"">>, lresource = <<"">>}} = IQ, _, _) -> ejabberd_router:process_iq(IQ); -route(#message{lang = Lang, body = Body, type = Type, from = From, - to = #jid{luser = <<"">>, lresource = <<"">>}} = Pkt, - Host, ServerHost) -> - if Type == error -> +route(#message{ + lang = Lang, + body = Body, + type = Type, + from = From, + to = #jid{luser = <<"">>, lresource = <<"">>} + } = Pkt, + Host, + ServerHost) -> + if + Type == error -> ok; - true -> - AccessAdmin = mod_muc_opt:access_admin(ServerHost), + true -> + AccessAdmin = mod_muc_opt:access_admin(ServerHost), case acl:match_rule(ServerHost, AccessAdmin, From) of allow -> Msg = xmpp:get_text(Body), @@ -246,48 +290,52 @@ route(#message{lang = Lang, body = Body, type = Type, from = From, route(Pkt, Host, ServerHost) -> {Room, _, _} = jid:tolower(xmpp:get_to(Pkt)), case Room of - <<"">> -> - Txt = ?T("No module is handling this query"), - Err = xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)), - ejabberd_router:route_error(Pkt, Err); - _ -> - RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), - case RMod:find_online_room(ServerHost, Room, Host) of - error -> - Proc = procname(ServerHost, {Room, Host}), - case whereis(Proc) of - Pid when Pid == self() -> - route_to_room(Pkt, ServerHost); - Pid when is_pid(Pid) -> - ?DEBUG("Routing to MUC worker ~p:~n~ts", [Proc, xmpp:pp(Pkt)]), - ?GEN_SERVER:cast(Pid, {route_to_room, Pkt}); - undefined -> - ?DEBUG("MUC worker ~p is dead", [Proc]), - Err = xmpp:err_internal_server_error(), - ejabberd_router:route_error(Pkt, Err) - end; - {ok, Pid} -> - mod_muc_room:route(Pid, Pkt) - end + <<"">> -> + Txt = ?T("No module is handling this query"), + Err = xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)), + ejabberd_router:route_error(Pkt, Err); + _ -> + RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), + case RMod:find_online_room(ServerHost, Room, Host) of + error -> + Proc = procname(ServerHost, {Room, Host}), + case whereis(Proc) of + Pid when Pid == self() -> + route_to_room(Pkt, ServerHost); + Pid when is_pid(Pid) -> + ?DEBUG("Routing to MUC worker ~p:~n~ts", [Proc, xmpp:pp(Pkt)]), + ?GEN_SERVER:cast(Pid, {route_to_room, Pkt}); + undefined -> + ?DEBUG("MUC worker ~p is dead", [Proc]), + Err = xmpp:err_internal_server_error(), + ejabberd_router:route_error(Pkt, Err) + end; + {ok, Pid} -> + mod_muc_room:route(Pid, Pkt) + end end. + -spec shutdown_rooms(binary()) -> [pid()]. shutdown_rooms(ServerHost) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), Hosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), shutdown_rooms(ServerHost, Hosts, RMod). + -spec shutdown_rooms(binary(), [binary()], module()) -> [pid()]. shutdown_rooms(ServerHost, Hosts, RMod) -> - Rooms = [RMod:get_online_rooms(ServerHost, Host, undefined) - || Host <- Hosts], + Rooms = [ RMod:get_online_rooms(ServerHost, Host, undefined) + || Host <- Hosts ], lists:flatmap( fun({_, _, Pid}) when node(Pid) == node() -> - mod_muc_room:shutdown(Pid), - [Pid]; - (_) -> - [] - end, lists:flatten(Rooms)). + mod_muc_room:shutdown(Pid), + [Pid]; + (_) -> + [] + end, + lists:flatten(Rooms)). + %% This function is called by a room in three situations: %% A) The owner of the room destroyed it @@ -300,6 +348,7 @@ room_destroyed(Host, Room, Pid, ServerHost) -> Proc = procname(ServerHost, {Room, Host}), ?GEN_SERVER:cast(Proc, {room_destroyed, {Room, Host}, Pid}). + %% @doc Create a room. %% If Opts = default, the default room options are used. %% Else use the passed options as defined in mod_muc_room. @@ -308,6 +357,7 @@ create_room(Host, Name, From, Nick, Opts) -> Proc = procname(ServerHost, {Name, Host}), ?GEN_SERVER:call(Proc, {create, Name, Host, From, Nick, Opts}). + %% @doc Create a room. %% If Opts = default, the default room options are used. %% Else use the passed options as defined in mod_muc_room. @@ -316,87 +366,103 @@ create_room(Host, Name, Opts) -> Proc = procname(ServerHost, {Name, Host}), ?GEN_SERVER:call(Proc, {create, Name, Host, Opts}). + store_room(ServerHost, Host, Name, Opts) -> store_room(ServerHost, Host, Name, Opts, undefined). + maybe_store_new_room(ServerHost, Host, Name, Opts) -> case {proplists:get_bool(persistent, Opts), proplists:get_value(subscribers, Opts, [])} of - {false, []} -> - {atomic, ok}; - {_, Subs} -> - Changes = [{add_subscription, JID, Nick, Nodes} || {JID, Nick, Nodes} <- Subs], - store_room(ServerHost, Host, Name, Opts, Changes) + {false, []} -> + {atomic, ok}; + {_, Subs} -> + Changes = [ {add_subscription, JID, Nick, Nodes} || {JID, Nick, Nodes} <- Subs ], + store_room(ServerHost, Host, Name, Opts, Changes) end. + store_room(ServerHost, Host, Name, Opts, ChangesHints) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:store_room(LServer, Host, Name, Opts, ChangesHints). + store_changes(ServerHost, Host, Name, ChangesHints) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:store_changes(LServer, Host, Name, ChangesHints). + restore_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:restore_room(LServer, Host, Name). + forget_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:forget_room(LServer, Host, Name). + can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false; can_use_nick(ServerHost, Host, JID, Nick) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:can_use_nick(LServer, Host, JID, Nick). + -spec find_online_room(binary(), binary()) -> {ok, pid()} | error. find_online_room(Room, Host) -> ServerHost = ejabberd_router:host_of_route(Host), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:find_online_room(ServerHost, Room, Host). + -spec register_online_room(binary(), binary(), pid()) -> any(). register_online_room(Room, Host, Pid) -> ServerHost = ejabberd_router:host_of_route(Host), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:register_online_room(ServerHost, Room, Host, Pid). + -spec get_online_rooms(binary()) -> [{binary(), binary(), pid()}]. get_online_rooms(Host) -> ServerHost = ejabberd_router:host_of_route(Host), get_online_rooms(ServerHost, Host). + -spec count_online_rooms(binary()) -> non_neg_integer(). count_online_rooms(Host) -> ServerHost = ejabberd_router:host_of_route(Host), count_online_rooms(ServerHost, Host). + -spec register_online_user(binary(), ljid(), binary(), binary()) -> any(). register_online_user(ServerHost, LJID, Name, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:register_online_user(ServerHost, LJID, Name, Host). + -spec unregister_online_user(binary(), ljid(), binary(), binary()) -> any(). unregister_online_user(ServerHost, LJID, Name, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:unregister_online_user(ServerHost, LJID, Name, Host). + -spec count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). count_online_rooms_by_user(ServerHost, LUser, LServer) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:count_online_rooms_by_user(ServerHost, LUser, LServer). + -spec get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. get_online_rooms_by_user(ServerHost, LUser, LServer) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:get_online_rooms_by_user(ServerHost, LUser, LServer). + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -409,65 +475,72 @@ init([Host, Worker]) -> register_iq_handlers(MyHosts, Worker), {ok, #{server_host => Host, hosts => MyHosts, worker => Worker}}. + -spec handle_call(term(), {pid(), term()}, state()) -> - {reply, ok | {ok, pid()} | {error, any()}, state()} | - {stop, normal, ok, state()}. + {reply, ok | {ok, pid()} | {error, any()}, state()} | + {stop, normal, ok, state()}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call({unhibernate, Room, Host, ResetHibernationTime}, _From, - #{server_host := ServerHost} = State) -> +handle_call({unhibernate, Room, Host, ResetHibernationTime}, + _From, + #{server_host := ServerHost} = State) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), {reply, load_room(RMod, Host, ServerHost, Room, ResetHibernationTime), State}; -handle_call({create, Room, Host, Opts}, _From, - #{server_host := ServerHost} = State) -> +handle_call({create, Room, Host, Opts}, + _From, + #{server_host := ServerHost} = State) -> ?DEBUG("MUC: create new room '~ts'~n", [Room]), NewOpts = case Opts of - default -> mod_muc_opt:default_room_options(ServerHost); - _ -> Opts - end, + default -> mod_muc_opt:default_room_options(ServerHost); + _ -> Opts + end, RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case start_room(RMod, Host, ServerHost, Room, NewOpts) of - {ok, _} -> - maybe_store_new_room(ServerHost, Host, Room, NewOpts), - ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), - {reply, ok, State}; - Err -> - {reply, Err, State} + {ok, _} -> + maybe_store_new_room(ServerHost, Host, Room, NewOpts), + ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), + {reply, ok, State}; + Err -> + {reply, Err, State} end; -handle_call({create, Room, Host, From, Nick, Opts}, _From, - #{server_host := ServerHost} = State) -> +handle_call({create, Room, Host, From, Nick, Opts}, + _From, + #{server_host := ServerHost} = State) -> ?DEBUG("MUC: create new room '~ts'~n", [Room]), NewOpts = case Opts of - default -> mod_muc_opt:default_room_options(ServerHost); - _ -> Opts - end, + default -> mod_muc_opt:default_room_options(ServerHost); + _ -> Opts + end, RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case start_room(RMod, Host, ServerHost, Room, NewOpts, From, Nick) of - {ok, _} -> - maybe_store_new_room(ServerHost, Host, Room, NewOpts), - ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), - {reply, ok, State}; - Err -> - {reply, Err, State} + {ok, _} -> + maybe_store_new_room(ServerHost, Host, Room, NewOpts), + ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), + {reply, ok, State}; + Err -> + {reply, Err, State} end. + -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({route_to_room, Packet}, #{server_host := ServerHost} = State) -> - try route_to_room(Packet, ServerHost) - catch ?EX_RULE(Class, Reason, St) -> + try + route_to_room(Packet, ServerHost) + catch + ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_cast({room_destroyed, {Room, Host}, Pid}, - #{server_host := ServerHost} = State) -> + #{server_host := ServerHost} = State) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:unregister_online_room(ServerHost, Room, Host, Pid), {noreply, State}; handle_cast({reload, AddHosts, DelHosts, NewHosts}, - #{server_host := ServerHost, worker := Worker} = State) -> + #{server_host := ServerHost, worker := Worker} = State) -> register_routes(ServerHost, AddHosts, Worker), register_iq_handlers(AddHosts, Worker), unregister_routes(DelHosts, Worker), @@ -477,43 +550,49 @@ handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + -spec handle_info(term(), state()) -> {noreply, state()}. handle_info({route, Packet}, #{server_host := ServerHost} = State) -> %% We can only receive the packet here from other nodes %% where mod_muc is not loaded. Such configuration %% is *highly* discouraged - try route(Packet, ServerHost) - catch ?EX_RULE(Class, Reason, St) -> + try + route(Packet, ServerHost) + catch + ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info({room_destroyed, {Room, Host}, Pid}, State) -> %% For backward compat handle_cast({room_destroyed, {Room, Host}, Pid}, State); handle_info({'DOWN', _Ref, process, Pid, _Reason}, - #{server_host := ServerHost} = State) -> + #{server_host := ServerHost} = State) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room_by_pid(ServerHost, Pid) of - {ok, Room, Host} -> - handle_cast({room_destroyed, {Room, Host}, Pid}, State); - _ -> - {noreply, State} + {ok, Room, Host} -> + handle_cast({room_destroyed, {Room, Host}, Pid}, State); + _ -> + {noreply, State} end; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + -spec terminate(term(), state()) -> any(). terminate(_Reason, #{hosts := Hosts, worker := Worker}) -> unregister_routes(Hosts, Worker), unregister_iq_handlers(Hosts, Worker). + -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- @@ -522,58 +601,84 @@ register_iq_handlers(Hosts, 1) -> %% Only register handlers on first worker lists:foreach( fun(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_REGISTER, - ?MODULE, process_register), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, - ?MODULE, process_vcard), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUCSUB, - ?MODULE, process_mucsub), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE, - ?MODULE, process_muc_unique), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items) - end, Hosts); + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_REGISTER, + ?MODULE, + process_register), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_VCARD, + ?MODULE, + process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MUCSUB, + ?MODULE, + process_mucsub), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_MUC_UNIQUE, + ?MODULE, + process_muc_unique), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items) + end, + Hosts); register_iq_handlers(_, _) -> ok. + -spec unregister_iq_handlers([binary()], pos_integer()) -> ok. unregister_iq_handlers(Hosts, 1) -> %% Only unregister handlers on first worker lists:foreach( fun(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUCSUB), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS) - end, Hosts); + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUCSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS) + end, + Hosts); unregister_iq_handlers(_, _) -> ok. + -spec register_routes(binary(), [binary()], pos_integer()) -> ok. register_routes(ServerHost, Hosts, 1) -> %% Only register routes on first worker lists:foreach( fun(Host) -> - ejabberd_router:register_route( - Host, ServerHost, {apply, ?MODULE, route}) - end, Hosts); + ejabberd_router:register_route( + Host, ServerHost, {apply, ?MODULE, route}) + end, + Hosts); register_routes(_, _, _) -> ok. + -spec unregister_routes([binary()], pos_integer()) -> ok. unregister_routes(Hosts, 1) -> %% Only unregister routes on first worker lists:foreach( fun(Host) -> - ejabberd_router:unregister_route(Host) - end, Hosts); + ejabberd_router:unregister_route(Host) + end, + Hosts); unregister_routes(_, _) -> ok. + %% Function copied from mod_muc_room.erl -spec extract_password(presence() | iq()) -> binary() | false. extract_password(#presence{} = Pres) -> @@ -591,20 +696,23 @@ extract_password(#iq{} = IQ) -> false end. + -spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | {error, notfound | db_failure | term()}. unhibernate_room(ServerHost, Host, Room) -> unhibernate_room(ServerHost, Host, Room, true). + -spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | {error, notfound | db_failure | term()}. unhibernate_room(ServerHost, Host, Room, ResetHibernationTime) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room(ServerHost, Room, Host) of - error -> - Proc = procname(ServerHost, {Room, Host}), - ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000); - {ok, _} = R2 -> R2 + error -> + Proc = procname(ServerHost, {Room, Host}), + ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000); + {ok, _} = R2 -> R2 end. + -spec route_to_room(stanza(), binary()) -> ok. route_to_room(Packet, ServerHost) -> From = xmpp:get_from(Packet), @@ -612,59 +720,62 @@ route_to_room(Packet, ServerHost) -> {Room, Host, Nick} = jid:tolower(To), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room(ServerHost, Room, Host) of - error -> - case should_start_room(Packet) of - false -> - Lang = xmpp:get_lang(Packet), - ErrText = ?T("Conference room does not exist"), - Err = xmpp:err_item_not_found(ErrText, Lang), - ejabberd_router:route_error(Packet, Err); - StartType -> - case load_room(RMod, Host, ServerHost, Room, true) of - {error, notfound} when StartType == start -> - case check_create_room(ServerHost, Host, Room, From) of - true -> - Pass = extract_password(Packet), - case start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) of - {ok, Pid} -> - mod_muc_room:route(Pid, Packet); - _Err -> - Err = xmpp:err_internal_server_error(), - ejabberd_router:route_error(Packet, Err) - end; - false -> - Lang = xmpp:get_lang(Packet), - ErrText = ?T("Room creation is denied by service policy"), - Err = xmpp:err_forbidden(ErrText, Lang), - ejabberd_router:route_error(Packet, Err) - end; - {error, notfound} -> - Lang = xmpp:get_lang(Packet), - ErrText = ?T("Conference room does not exist"), - Err = xmpp:err_item_not_found(ErrText, Lang), - ejabberd_router:route_error(Packet, Err); - {error, _} -> - Err = xmpp:err_internal_server_error(), - ejabberd_router:route_error(Packet, Err); - {ok, Pid2} -> - mod_muc_room:route(Pid2, Packet) - end - end; - {ok, Pid} -> - mod_muc_room:route(Pid, Packet) + error -> + case should_start_room(Packet) of + false -> + Lang = xmpp:get_lang(Packet), + ErrText = ?T("Conference room does not exist"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + StartType -> + case load_room(RMod, Host, ServerHost, Room, true) of + {error, notfound} when StartType == start -> + case check_create_room(ServerHost, Host, Room, From) of + true -> + Pass = extract_password(Packet), + case start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) of + {ok, Pid} -> + mod_muc_room:route(Pid, Packet); + _Err -> + Err = xmpp:err_internal_server_error(), + ejabberd_router:route_error(Packet, Err) + end; + false -> + Lang = xmpp:get_lang(Packet), + ErrText = ?T("Room creation is denied by service policy"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + end; + {error, notfound} -> + Lang = xmpp:get_lang(Packet), + ErrText = ?T("Conference room does not exist"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {error, _} -> + Err = xmpp:err_internal_server_error(), + ejabberd_router:route_error(Packet, Err); + {ok, Pid2} -> + mod_muc_room:route(Pid2, Packet) + end + end; + {ok, Pid} -> + mod_muc_room:route(Pid, Packet) end. + -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = get, to = To, lang = Lang, sub_els = [#vcard_temp{}]} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), VCard = case mod_muc_opt:vcard(ServerHost) of - undefined -> - #vcard_temp{fn = <<"ejabberd/mod_muc">>, - url = ejabberd_config:get_uri(), - desc = misc:get_descr(Lang, ?T("ejabberd MUC module"))}; - V -> - V - end, + undefined -> + #vcard_temp{ + fn = <<"ejabberd/mod_muc">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("ejabberd MUC module")) + }; + V -> + V + end, xmpp:make_iq_result(IQ, VCard); process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -673,181 +784,230 @@ process_vcard(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_register(iq()) -> iq(). process_register(IQ) -> case process_iq_register(IQ) of {result, Result} -> - xmpp:make_iq_result(IQ, Result); + xmpp:make_iq_result(IQ, Result); {error, Err} -> - xmpp:make_error(IQ, Err) + xmpp:make_error(IQ, Err) end. + -spec process_iq_register(iq()) -> {result, register()} | {error, stanza_error()}. -process_iq_register(#iq{type = Type, from = From, to = To, lang = Lang, - sub_els = [El = #register{}]}) -> +process_iq_register(#iq{ + type = Type, + from = From, + to = To, + lang = Lang, + sub_els = [El = #register{}] + }) -> Host = To#jid.lserver, RegisterDestination = jid:encode(To), ServerHost = ejabberd_router:host_of_route(Host), AccessRegister = mod_muc_opt:access_register(ServerHost), case acl:match_rule(ServerHost, AccessRegister, From) of - allow -> - case Type of - get -> + allow -> + case Type of + get -> {result, iq_get_register_info(ServerHost, RegisterDestination, From, Lang)}; - set -> - process_iq_register_set(ServerHost, RegisterDestination, From, El, Lang) - end; - deny -> - ErrText = ?T("Access denied by service policy"), - Err = xmpp:err_forbidden(ErrText, Lang), - {error, Err} + set -> + process_iq_register_set(ServerHost, RegisterDestination, From, El, Lang) + end; + deny -> + ErrText = ?T("Access denied by service policy"), + Err = xmpp:err_forbidden(ErrText, Lang), + {error, Err} end. + -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, - sub_els = [#disco_info{node = <<"">>}]} = IQ) -> +process_disco_info(#iq{ + type = get, + from = From, + to = To, + lang = Lang, + sub_els = [#disco_info{node = <<"">>}] + } = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), AccessRegister = mod_muc_opt:access_register(ServerHost), - X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], - [ServerHost, ?MODULE, <<"">>, Lang]), + X = ejabberd_hooks:run_fold(disco_info, + ServerHost, + [], + [ServerHost, ?MODULE, <<"">>, Lang]), MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of - true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]; - false -> [] - end, + true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]; + false -> [] + end, OccupantIdFeatures = case gen_mod:is_loaded(ServerHost, mod_muc_occupantid) of - true -> [?NS_OCCUPANT_ID]; - false -> [] - end, + true -> [?NS_OCCUPANT_ID]; + false -> [] + end, RSMFeatures = case RMod:rsm_supported() of - true -> [?NS_RSM]; - false -> [] - end, + true -> [?NS_RSM]; + false -> [] + end, RegisterFeatures = case acl:match_rule(ServerHost, AccessRegister, From) of - allow -> [?NS_REGISTER]; - deny -> [] - end, + allow -> [?NS_REGISTER]; + deny -> [] + end, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, - ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE - | RegisterFeatures ++ RSMFeatures ++ MAMFeatures ++ OccupantIdFeatures], + ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE | RegisterFeatures ++ RSMFeatures ++ MAMFeatures ++ OccupantIdFeatures], Name = mod_muc_opt:name(ServerHost), - Identity = #identity{category = <<"conference">>, - type = <<"text">>, - name = translate:translate(Lang, Name)}, + Identity = #identity{ + category = <<"conference">>, + type = <<"text">>, + name = translate:translate(Lang, Name) + }, xmpp:make_iq_result( - IQ, #disco_info{features = Features, - identities = [Identity], - xdata = X}); -process_disco_info(#iq{type = get, lang = Lang, - sub_els = [#disco_info{}]} = IQ) -> + IQ, + #disco_info{ + features = Features, + identities = [Identity], + xdata = X + }); +process_disco_info(#iq{ + type = get, + lang = Lang, + sub_els = [#disco_info{}] + } = IQ) -> xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); process_disco_info(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_items(#iq{type = get, from = From, to = To, lang = Lang, - sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) -> +process_disco_items(#iq{ + type = get, + from = From, + to = To, + lang = Lang, + sub_els = [#disco_items{node = Node, rsm = RSM}] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), MaxRoomsDiscoItems = mod_muc_opt:max_rooms_discoitems(ServerHost), - case iq_disco_items(ServerHost, Host, From, Lang, - MaxRoomsDiscoItems, Node, RSM) of - {error, Err} -> - xmpp:make_error(IQ, Err); - {result, Result} -> - xmpp:make_iq_result(IQ, Result) + case iq_disco_items(ServerHost, + Host, + From, + Lang, + MaxRoomsDiscoItems, + Node, + RSM) of + {error, Err} -> + xmpp:make_error(IQ, Err); + {result, Result} -> + xmpp:make_iq_result(IQ, Result) end; process_disco_items(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_muc_unique(iq()) -> iq(). process_muc_unique(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_muc_unique(#iq{from = From, type = get, - sub_els = [#muc_unique{}]} = IQ) -> - Name = str:sha(term_to_binary([From, erlang:timestamp(), - p1_rand:get_string()])), +process_muc_unique(#iq{ + from = From, + type = get, + sub_els = [#muc_unique{}] + } = IQ) -> + Name = str:sha(term_to_binary([From, + erlang:timestamp(), + p1_rand:get_string()])), xmpp:make_iq_result(IQ, #muc_unique{name = Name}). + -spec process_mucsub(iq()) -> iq(). process_mucsub(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_mucsub(#iq{type = get, from = From, to = To, lang = Lang, - sub_els = [#muc_subscriptions{}]} = IQ) -> +process_mucsub(#iq{ + type = get, + from = From, + to = To, + lang = Lang, + sub_els = [#muc_subscriptions{}] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), case get_subscribed_rooms(ServerHost, Host, From) of - {ok, Subs} -> - List = [#muc_subscription{jid = JID, nick = Nick, events = Nodes} - || {JID, Nick, Nodes} <- Subs], - xmpp:make_iq_result(IQ, #muc_subscriptions{list = List}); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + {ok, Subs} -> + List = [ #muc_subscription{jid = JID, nick = Nick, events = Nodes} + || {JID, Nick, Nodes} <- Subs ], + xmpp:make_iq_result(IQ, #muc_subscriptions{list = List}); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; process_mucsub(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec should_start_room(stanza()) -> start | load | false. should_start_room(#presence{type = available}) -> start; should_start_room(#iq{type = T} = IQ) when T == get; T == set -> case xmpp:has_subtag(IQ, #muc_subscribe{}) orelse - xmpp:has_subtag(IQ, #muc_owner{}) of - true -> - start; - _ -> - load + xmpp:has_subtag(IQ, #muc_owner{}) of + true -> + start; + _ -> + load end; should_start_room(#message{type = T, to = #jid{lresource = <<>>}}) - when T == groupchat; T == normal-> + when T == groupchat; T == normal -> load; should_start_room(#message{type = T, to = #jid{lresource = Res}}) - when Res /= <<>> andalso T /= groupchat andalso T /= error -> + when Res /= <<>> andalso T /= groupchat andalso T /= error -> load; should_start_room(_) -> false. + -spec check_create_room(binary(), binary(), binary(), jid()) -> boolean(). check_create_room(ServerHost, Host, Room, From) -> AccessCreate = mod_muc_opt:access_create(ServerHost), case acl:match_rule(ServerHost, AccessCreate, From) of - allow -> - case mod_muc_opt:max_room_id(ServerHost) of - Max when byte_size(Room) =< Max -> - Regexp = mod_muc_opt:regexp_room_id(ServerHost), - case re:run(Room, Regexp, [{capture, none}]) of - match -> - AccessAdmin = mod_muc_opt:access_admin(ServerHost), - case acl:match_rule(ServerHost, AccessAdmin, From) of - allow -> - true; - _ -> - ejabberd_hooks:run_fold( - check_create_room, ServerHost, true, - [ServerHost, Room, Host]) - end; - _ -> - false - end; - _ -> - false - end; - _ -> - false + allow -> + case mod_muc_opt:max_room_id(ServerHost) of + Max when byte_size(Room) =< Max -> + Regexp = mod_muc_opt:regexp_room_id(ServerHost), + case re:run(Room, Regexp, [{capture, none}]) of + match -> + AccessAdmin = mod_muc_opt:access_admin(ServerHost), + case acl:match_rule(ServerHost, AccessAdmin, From) of + allow -> + true; + _ -> + ejabberd_hooks:run_fold( + check_create_room, + ServerHost, + true, + [ServerHost, Room, Host]) + end; + _ -> + false + end; + _ -> + false + end; + _ -> + false end. + -spec get_access(binary() | gen_mod:opts()) -> access(). get_access(ServerHost) -> Access = mod_muc_opt:access(ServerHost), @@ -857,69 +1017,76 @@ get_access(ServerHost) -> AccessMam = mod_muc_opt:access_mam(ServerHost), {Access, AccessCreate, AccessAdmin, AccessPersistent, AccessMam}. + -spec get_rooms(binary(), binary()) -> [#muc_room{}]. get_rooms(ServerHost, Host) -> Mod = gen_mod:db_mod(ServerHost, ?MODULE), Mod:get_rooms(ServerHost, Host). + -spec load_permanent_rooms([binary()], binary(), gen_mod:opts()) -> ok. load_permanent_rooms(Hosts, ServerHost, Opts) -> case mod_muc_opt:preload_rooms(Opts) of - true -> - lists:foreach( - fun(Host) -> - ?DEBUG("Loading rooms at ~ts", [Host]), - lists:foreach( - fun(R) -> - {Room, _} = R#muc_room.name_host, - unhibernate_room(ServerHost, Host, Room, false) - end, get_rooms(ServerHost, Host)) - end, Hosts); - false -> - ok + true -> + lists:foreach( + fun(Host) -> + ?DEBUG("Loading rooms at ~ts", [Host]), + lists:foreach( + fun(R) -> + {Room, _} = R#muc_room.name_host, + unhibernate_room(ServerHost, Host, Room, false) + end, + get_rooms(ServerHost, Host)) + end, + Hosts); + false -> + ok end. + -spec load_room(module(), binary(), binary(), binary(), boolean()) -> - {ok, pid()} | {error, notfound | term()}. + {ok, pid()} | {error, notfound | term()}. load_room(RMod, Host, ServerHost, Room, ResetHibernationTime) -> case restore_room(ServerHost, Host, Room) of - error -> - {error, notfound}; + error -> + {error, notfound}; {error, _} = Err -> Err; - Opts0 -> - Mod = gen_mod:db_mod(ServerHost, mod_muc), - case proplists:get_bool(persistent, Opts0) of - true -> - ?DEBUG("Restore room: ~ts", [Room]), - Res2 = start_room(RMod, Host, ServerHost, Room, Opts0), - case {Res2, ResetHibernationTime} of - {{ok, _}, true} -> - NewOpts = lists:keyreplace(hibernation_time, 1, Opts0, {hibernation_time, undefined}), - store_room(ServerHost, Host, Room, NewOpts, []); - _ -> - ok - end, - Res2; - _ -> - ?DEBUG("Restore hibernated non-persistent room: ~ts", [Room]), - Res = start_room(RMod, Host, ServerHost, Room, Opts0), - case erlang:function_exported(Mod, get_subscribed_rooms, 3) of - true -> - ok; - _ -> - forget_room(ServerHost, Host, Room) - end, - Res - end + Opts0 -> + Mod = gen_mod:db_mod(ServerHost, mod_muc), + case proplists:get_bool(persistent, Opts0) of + true -> + ?DEBUG("Restore room: ~ts", [Room]), + Res2 = start_room(RMod, Host, ServerHost, Room, Opts0), + case {Res2, ResetHibernationTime} of + {{ok, _}, true} -> + NewOpts = lists:keyreplace(hibernation_time, 1, Opts0, {hibernation_time, undefined}), + store_room(ServerHost, Host, Room, NewOpts, []); + _ -> + ok + end, + Res2; + _ -> + ?DEBUG("Restore hibernated non-persistent room: ~ts", [Room]), + Res = start_room(RMod, Host, ServerHost, Room, Opts0), + case erlang:function_exported(Mod, get_subscribed_rooms, 3) of + true -> + ok; + _ -> + forget_room(ServerHost, Host, Room) + end, + Res + end end. + start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) -> ?DEBUG("Open new room: ~ts", [Room]), DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), DefRoomOpts2 = add_password_options(Pass, DefRoomOpts), start_room(RMod, Host, ServerHost, Room, DefRoomOpts2, From, Nick). + add_password_options(false, DefRoomOpts) -> DefRoomOpts; add_password_options(<<>>, DefRoomOpts) -> @@ -928,268 +1095,365 @@ add_password_options(Pass, DefRoomOpts) when is_binary(Pass) -> O2 = lists:keystore(password, 1, DefRoomOpts, {password, Pass}), lists:keystore(password_protected, 1, O2, {password_protected, true}). + start_room(Mod, Host, ServerHost, Room, DefOpts) -> Access = get_access(ServerHost), HistorySize = mod_muc_opt:history_size(ServerHost), QueueType = mod_muc_opt:queue_type(ServerHost), RoomShaper = mod_muc_opt:room_shaper(ServerHost), - start_room(Mod, Host, ServerHost, Access, Room, HistorySize, - RoomShaper, DefOpts, QueueType). + start_room(Mod, + Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + DefOpts, + QueueType). + start_room(Mod, Host, ServerHost, Room, DefOpts, Creator, Nick) -> Access = get_access(ServerHost), HistorySize = mod_muc_opt:history_size(ServerHost), QueueType = mod_muc_opt:queue_type(ServerHost), RoomShaper = mod_muc_opt:room_shaper(ServerHost), - start_room(Mod, Host, ServerHost, Access, Room, - HistorySize, RoomShaper, - Creator, Nick, DefOpts, QueueType). + start_room(Mod, + Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + Creator, + Nick, + DefOpts, + QueueType). -start_room(Mod, Host, ServerHost, Access, Room, - HistorySize, RoomShaper, DefOpts, QueueType) -> - case mod_muc_room:start(Host, ServerHost, Access, Room, - HistorySize, RoomShaper, DefOpts, QueueType) of - {ok, Pid} -> - erlang:monitor(process, Pid), - Mod:register_online_room(ServerHost, Room, Host, Pid), - {ok, Pid}; - Err -> - Err + +start_room(Mod, + Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + DefOpts, + QueueType) -> + case mod_muc_room:start(Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + DefOpts, + QueueType) of + {ok, Pid} -> + erlang:monitor(process, Pid), + Mod:register_online_room(ServerHost, Room, Host, Pid), + {ok, Pid}; + Err -> + Err end. -start_room(Mod, Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Creator, Nick, DefOpts, QueueType) -> - case mod_muc_room:start(Host, ServerHost, Access, Room, - HistorySize, RoomShaper, - Creator, Nick, DefOpts, QueueType) of - {ok, Pid} -> - erlang:monitor(process, Pid), - Mod:register_online_room(ServerHost, Room, Host, Pid), - {ok, Pid}; - Err -> - Err + +start_room(Mod, + Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + Creator, + Nick, + DefOpts, + QueueType) -> + case mod_muc_room:start(Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + Creator, + Nick, + DefOpts, + QueueType) of + {ok, Pid} -> + erlang:monitor(process, Pid), + Mod:register_online_room(ServerHost, Room, Host, Pid), + {ok, Pid}; + Err -> + Err end. --spec iq_disco_items(binary(), binary(), jid(), binary(), integer(), binary(), - rsm_set() | undefined) -> - {result, disco_items()} | {error, stanza_error()}. + +-spec iq_disco_items(binary(), + binary(), + jid(), + binary(), + integer(), + binary(), + rsm_set() | undefined) -> + {result, disco_items()} | {error, stanza_error()}. iq_disco_items(ServerHost, Host, From, Lang, MaxRoomsDiscoItems, Node, RSM) when Node == <<"">>; Node == <<"nonemptyrooms">>; Node == <<"emptyrooms">> -> Count = count_online_rooms(ServerHost, Host), - Query = if Node == <<"">>, RSM == undefined, Count > MaxRoomsDiscoItems -> - {only_non_empty, From, Lang}; - Node == <<"nonemptyrooms">> -> - {only_non_empty, From, Lang}; - Node == <<"emptyrooms">> -> - {0, From, Lang}; - true -> - {all, From, Lang} - end, + Query = if + Node == <<"">>, RSM == undefined, Count > MaxRoomsDiscoItems -> + {only_non_empty, From, Lang}; + Node == <<"nonemptyrooms">> -> + {only_non_empty, From, Lang}; + Node == <<"emptyrooms">> -> + {0, From, Lang}; + true -> + {all, From, Lang} + end, MaxItems = case RSM of - undefined -> - MaxRoomsDiscoItems; - #rsm_set{max = undefined} -> - MaxRoomsDiscoItems; - #rsm_set{max = Max} when Max > MaxRoomsDiscoItems -> - MaxRoomsDiscoItems; - #rsm_set{max = Max} -> - Max - end, + undefined -> + MaxRoomsDiscoItems; + #rsm_set{max = undefined} -> + MaxRoomsDiscoItems; + #rsm_set{max = Max} when Max > MaxRoomsDiscoItems -> + MaxRoomsDiscoItems; + #rsm_set{max = Max} -> + Max + end, RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RsmSupported = RMod:rsm_supported(), GetRooms = - fun GetRooms(AccInit, Rooms) -> - {Items, HitMax, DidSkip, Last, First} = lists:foldr( - fun(_, {Acc, _, Skip, F, L}) when length(Acc) >= MaxItems -> - {Acc, true, Skip, F, L}; - ({RN, _, _} = R, {Acc, _, Skip, F, _}) -> - F2 = if F == undefined -> RN; true -> F end, - case get_room_disco_item(R, Query) of - {ok, Item} -> {[Item | Acc], false, Skip, F2, RN}; - {error, _} -> {Acc, false, true, F2, RN} - end - end, AccInit, Rooms), - if RsmSupported andalso not HitMax andalso DidSkip -> - RSM2 = case RSM of - #rsm_set{'after' = undefined, before = undefined} -> - #rsm_set{max = MaxItems - length(Items), 'after' = Last}; - #rsm_set{'after' = undefined} -> - #rsm_set{max = MaxItems - length(Items), 'before' = First}; - _ -> - #rsm_set{max = MaxItems - length(Items), 'after' = Last} - end, - GetRooms({Items, false, false, undefined, undefined}, - get_online_rooms(ServerHost, Host, RSM2)); - true -> {Items, HitMax} - end - end, + fun GetRooms(AccInit, Rooms) -> + {Items, HitMax, DidSkip, Last, First} = lists:foldr( + fun(_, {Acc, _, Skip, F, L}) when length(Acc) >= MaxItems -> + {Acc, true, Skip, F, L}; + ({RN, _, _} = R, {Acc, _, Skip, F, _}) -> + F2 = if F == undefined -> RN; true -> F end, + case get_room_disco_item(R, Query) of + {ok, Item} -> {[Item | Acc], false, Skip, F2, RN}; + {error, _} -> {Acc, false, true, F2, RN} + end + end, + AccInit, + Rooms), + if + RsmSupported andalso not HitMax andalso DidSkip -> + RSM2 = case RSM of + #rsm_set{'after' = undefined, before = undefined} -> + #rsm_set{max = MaxItems - length(Items), 'after' = Last}; + #rsm_set{'after' = undefined} -> + #rsm_set{max = MaxItems - length(Items), 'before' = First}; + _ -> + #rsm_set{max = MaxItems - length(Items), 'after' = Last} + end, + GetRooms({Items, false, false, undefined, undefined}, + get_online_rooms(ServerHost, Host, RSM2)); + true -> {Items, HitMax} + end + end, {Items, HitMax} = - GetRooms({[], false, false, undefined, undefined}, - get_online_rooms(ServerHost, Host, RSM)), + GetRooms({[], false, false, undefined, undefined}, + get_online_rooms(ServerHost, Host, RSM)), ResRSM = case Items of - [_|_] when RSM /= undefined; HitMax -> - #disco_item{jid = #jid{luser = First}} = hd(Items), - #disco_item{jid = #jid{luser = Last}} = lists:last(Items), - #rsm_set{first = #rsm_first{data = First}, - last = Last, - count = Count}; - [] when RSM /= undefined -> - #rsm_set{count = Count}; - _ -> - undefined - end, + [_ | _] when RSM /= undefined; HitMax -> + #disco_item{jid = #jid{luser = First}} = hd(Items), + #disco_item{jid = #jid{luser = Last}} = lists:last(Items), + #rsm_set{ + first = #rsm_first{data = First}, + last = Last, + count = Count + }; + [] when RSM /= undefined -> + #rsm_set{count = Count}; + _ -> + undefined + end, {result, #disco_items{node = Node, items = Items, rsm = ResRSM}}; iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM) -> {error, xmpp:err_item_not_found(?T("Node not found"), Lang)}. + -spec get_room_disco_item({binary(), binary(), pid()}, - {mod_muc_room:disco_item_filter(), - jid(), binary()}) -> {ok, disco_item()} | - {error, timeout | notfound}. + {mod_muc_room:disco_item_filter(), + jid(), + binary()}) -> {ok, disco_item()} | + {error, timeout | notfound}. get_room_disco_item({Name, Host, Pid}, {Filter, JID, Lang}) -> case mod_muc_room:get_disco_item(Pid, Filter, JID, Lang) of - {ok, Desc} -> - RoomJID = jid:make(Name, Host), - {ok, #disco_item{jid = RoomJID, name = Desc}}; - {error, _} = Err -> - Err + {ok, Desc} -> + RoomJID = jid:make(Name, Host), + {ok, #disco_item{jid = RoomJID, name = Desc}}; + {error, _} = Err -> + Err end. + -spec get_subscribed_rooms(binary(), jid()) -> {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. get_subscribed_rooms(Host, User) -> ServerHost = ejabberd_router:host_of_route(Host), get_subscribed_rooms(ServerHost, Host, User). + -spec get_subscribed_rooms(binary(), binary(), jid()) -> - {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. + {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. get_subscribed_rooms(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), BareFrom = jid:remove_resource(From), case erlang:function_exported(Mod, get_subscribed_rooms, 3) of - false -> - Rooms = get_online_rooms(ServerHost, Host), - {ok, lists:flatmap( - fun({Name, _, Pid}) when Pid == self() -> - USR = jid:split(BareFrom), - case erlang:get(muc_subscribers) of - #{USR := #subscriber{nodes = Nodes, nick = Nick}} -> - [{jid:make(Name, Host), Nick, Nodes}]; - _ -> - [] - end; - ({Name, _, Pid}) -> - case mod_muc_room:is_subscribed(Pid, BareFrom) of - {true, Nick, Nodes} -> - [{jid:make(Name, Host), Nick, Nodes}]; - false -> [] - end; - (_) -> - [] - end, Rooms)}; - true -> - Mod:get_subscribed_rooms(LServer, Host, BareFrom) + false -> + Rooms = get_online_rooms(ServerHost, Host), + {ok, lists:flatmap( + fun({Name, _, Pid}) when Pid == self() -> + USR = jid:split(BareFrom), + case erlang:get(muc_subscribers) of + #{USR := #subscriber{nodes = Nodes, nick = Nick}} -> + [{jid:make(Name, Host), Nick, Nodes}]; + _ -> + [] + end; + ({Name, _, Pid}) -> + case mod_muc_room:is_subscribed(Pid, BareFrom) of + {true, Nick, Nodes} -> + [{jid:make(Name, Host), Nick, Nodes}]; + false -> [] + end; + (_) -> + [] + end, + Rooms)}; + true -> + Mod:get_subscribed_rooms(LServer, Host, BareFrom) end. + get_nick(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:get_nick(LServer, Host, From). + iq_get_register_info(ServerHost, Host, From, Lang) -> {Nick, Registered} = case get_nick(ServerHost, Host, From) of - error -> {<<"">>, false}; - N -> {N, true} - end, + error -> {<<"">>, false}; + N -> {N, true} + end, Title = <<(translate:translate( - Lang, ?T("Nickname Registration at ")))/binary, Host/binary>>, + Lang, ?T("Nickname Registration at ")))/binary, + Host/binary>>, Inst = translate:translate(Lang, ?T("Enter nickname you want to register")), Fields = muc_register:encode([{roomnick, Nick}], Lang), - X = #xdata{type = form, title = Title, - instructions = [Inst], fields = Fields}, - #register{nick = Nick, - registered = Registered, - instructions = - translate:translate( - Lang, ?T("You need a client that supports x:data " - "to register the nickname")), - xdata = X}. + X = #xdata{ + type = form, + title = Title, + instructions = [Inst], + fields = Fields + }, + #register{ + nick = Nick, + registered = Registered, + instructions = + translate:translate( + Lang, + ?T("You need a client that supports x:data " + "to register the nickname")), + xdata = X + }. + set_nick(ServerHost, Host, From, Nick) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:set_nick(LServer, Host, From, Nick). -iq_set_register_info(ServerHost, Host, From, Nick, - Lang) -> + +iq_set_register_info(ServerHost, + Host, + From, + Nick, + Lang) -> case set_nick(ServerHost, Host, From, Nick) of - {atomic, ok} -> {result, undefined}; - {atomic, false} -> - ErrText = ?T("That nickname is registered by another person"), - {error, xmpp:err_conflict(ErrText, Lang)}; - _ -> - Txt = ?T("Database failure"), - {error, xmpp:err_internal_server_error(Txt, Lang)} + {atomic, ok} -> {result, undefined}; + {atomic, false} -> + ErrText = ?T("That nickname is registered by another person"), + {error, xmpp:err_conflict(ErrText, Lang)}; + _ -> + Txt = ?T("Database failure"), + {error, xmpp:err_internal_server_error(Txt, Lang)} end. -process_iq_register_set(ServerHost, Host, From, - #register{remove = true}, Lang) -> + +process_iq_register_set(ServerHost, + Host, + From, + #register{remove = true}, + Lang) -> iq_set_register_info(ServerHost, Host, From, <<"">>, Lang); -process_iq_register_set(_ServerHost, _Host, _From, - #register{xdata = #xdata{type = cancel}}, _Lang) -> +process_iq_register_set(_ServerHost, + _Host, + _From, + #register{xdata = #xdata{type = cancel}}, + _Lang) -> {result, undefined}; -process_iq_register_set(ServerHost, Host, From, - #register{nick = Nick, xdata = XData}, Lang) -> +process_iq_register_set(ServerHost, + Host, + From, + #register{nick = Nick, xdata = XData}, + Lang) -> case XData of - #xdata{type = submit, fields = Fs} -> - try - Options = muc_register:decode(Fs), - N = proplists:get_value(roomnick, Options), - iq_set_register_info(ServerHost, Host, From, N, Lang) - catch _:{muc_register, Why} -> - ErrText = muc_register:format_error(Why), - {error, xmpp:err_bad_request(ErrText, Lang)} - end; - #xdata{} -> - Txt = ?T("Incorrect data form"), - {error, xmpp:err_bad_request(Txt, Lang)}; - _ when is_binary(Nick), Nick /= <<"">> -> - iq_set_register_info(ServerHost, Host, From, Nick, Lang); - _ -> - ErrText = ?T("You must fill in field \"Nickname\" in the form"), - {error, xmpp:err_not_acceptable(ErrText, Lang)} + #xdata{type = submit, fields = Fs} -> + try + Options = muc_register:decode(Fs), + N = proplists:get_value(roomnick, Options), + iq_set_register_info(ServerHost, Host, From, N, Lang) + catch + _:{muc_register, Why} -> + ErrText = muc_register:format_error(Why), + {error, xmpp:err_bad_request(ErrText, Lang)} + end; + #xdata{} -> + Txt = ?T("Incorrect data form"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ when is_binary(Nick), Nick /= <<"">> -> + iq_set_register_info(ServerHost, Host, From, Nick, Lang); + _ -> + ErrText = ?T("You must fill in field \"Nickname\" in the form"), + {error, xmpp:err_not_acceptable(ErrText, Lang)} end. + -spec broadcast_service_message(binary(), binary(), binary()) -> ok. broadcast_service_message(ServerHost, Host, Msg) -> lists:foreach( fun({_, _, Pid}) -> - mod_muc_room:service_message(Pid, Msg) - end, get_online_rooms(ServerHost, Host)). + mod_muc_room:service_message(Pid, Msg) + end, + get_online_rooms(ServerHost, Host)). + -spec get_online_rooms(binary(), binary()) -> [{binary(), binary(), pid()}]. get_online_rooms(ServerHost, Host) -> get_online_rooms(ServerHost, Host, undefined). + -spec get_online_rooms(binary(), binary(), undefined | rsm_set()) -> - [{binary(), binary(), pid()}]. + [{binary(), binary(), pid()}]. get_online_rooms(ServerHost, Host, RSM) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:get_online_rooms(ServerHost, Host, RSM). + -spec count_online_rooms(binary(), binary()) -> non_neg_integer(). 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 -> + true -> Mod:remove_user(LUser, LServer); false -> ok @@ -1209,6 +1473,7 @@ remove_user(User, Server) -> gen_mod:get_module_opt_hosts(LServer, mod_muc)), ok. + opts_to_binary(Opts) -> lists:map( fun({title, Title}) -> @@ -1217,13 +1482,13 @@ opts_to_binary(Opts) -> {description, iolist_to_binary(Desc)}; ({password, Pass}) -> {password, iolist_to_binary(Pass)}; - ({subject, [C|_] = Subj}) when is_integer(C), C >= 0, C =< 255 -> + ({subject, [C | _] = Subj}) when is_integer(C), C >= 0, C =< 255 -> {subject, iolist_to_binary(Subj)}; ({subject_author, {AuthorNick, AuthorJID}}) -> {subject_author, {iolist_to_binary(AuthorNick), AuthorJID}}; - ({subject_author, AuthorNick}) -> % ejabberd 23.04 or older + ({subject_author, AuthorNick}) -> % ejabberd 23.04 or older {subject_author, {iolist_to_binary(AuthorNick), #jid{}}}; - ({allow_private_messages, Value}) -> % ejabberd 23.04 or older + ({allow_private_messages, Value}) -> % ejabberd 23.04 or older Value2 = case Value of true -> anyone; false -> none; @@ -1244,33 +1509,41 @@ opts_to_binary(Opts) -> iolist_to_binary(S), iolist_to_binary(R)}, NewAff} - end, Affs)}; + end, + Affs)}; ({captcha_whitelist, CWList}) -> {captcha_whitelist, lists:map( fun({U, S, R}) -> {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)} - end, CWList)}; + end, + CWList)}; (Opt) -> Opt - end, Opts). + end, + Opts). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + import_info() -> [{<<"muc_room">>, 4}, {<<"muc_registered">>, 4}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). + import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). + mod_opt_type(access) -> econf:acl(); mod_opt_type(access_admin) -> @@ -1325,41 +1598,43 @@ mod_opt_type(cleanup_affiliations_on_start) -> econf:bool(); mod_opt_type(default_room_options) -> econf:options( - #{allow_change_subj => econf:bool(), - allowpm => - econf:enum([anyone, participants, moderators, none]), - allow_private_messages_from_visitors => - econf:enum([anyone, moderators, nobody]), - allow_query_users => econf:bool(), - allow_subscription => econf:bool(), - allow_user_invites => econf:bool(), - allow_visitor_nickchange => econf:bool(), - allow_visitor_status => econf:bool(), - allow_voice_requests => econf:bool(), - anonymous => econf:bool(), - captcha_protected => econf:bool(), - description => econf:binary(), - enable_hats => econf:bool(), - lang => econf:lang(), - logging => econf:bool(), - mam => econf:bool(), - max_users => econf:pos_int(), - members_by_default => econf:bool(), - members_only => econf:bool(), - moderated => econf:bool(), - password => econf:binary(), - password_protected => econf:bool(), - persistent => econf:bool(), - presence_broadcast => - econf:list( - econf:enum([moderator, participant, visitor])), - public => econf:bool(), - public_list => econf:bool(), - pubsub => econf:binary(), - title => econf:binary(), - vcard => econf:vcard_temp(), - vcard_xupdate => econf:binary(), - voice_request_min_interval => econf:pos_int()}); + #{ + allow_change_subj => econf:bool(), + allowpm => + econf:enum([anyone, participants, moderators, none]), + allow_private_messages_from_visitors => + econf:enum([anyone, moderators, nobody]), + allow_query_users => econf:bool(), + allow_subscription => econf:bool(), + allow_user_invites => econf:bool(), + allow_visitor_nickchange => econf:bool(), + allow_visitor_status => econf:bool(), + allow_voice_requests => econf:bool(), + anonymous => econf:bool(), + captcha_protected => econf:bool(), + description => econf:binary(), + enable_hats => econf:bool(), + lang => econf:lang(), + logging => econf:bool(), + mam => econf:bool(), + max_users => econf:pos_int(), + members_by_default => econf:bool(), + members_only => econf:bool(), + moderated => econf:bool(), + password => econf:binary(), + password_protected => econf:bool(), + persistent => econf:bool(), + presence_broadcast => + econf:list( + econf:enum([moderator, participant, visitor])), + public => econf:bool(), + public_list => econf:bool(), + pubsub => econf:binary(), + title => econf:binary(), + vcard => econf:vcard_temp(), + vcard_xupdate => econf:binary(), + voice_request_min_interval => econf:pos_int() + }); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(ram_db_type) -> @@ -1375,6 +1650,7 @@ mod_opt_type(hibernation_timeout) -> mod_opt_type(vcard) -> econf:vcard_temp(). + mod_options(Host) -> [{access, all}, {access_admin, none}, @@ -1410,60 +1686,68 @@ mod_options(Host) -> {vcard, undefined}, {cleanup_affiliations_on_start, false}, {default_room_options, - [{allow_change_subj,true}, - {allowpm,anyone}, - {allow_query_users,true}, - {allow_user_invites,false}, - {allow_visitor_nickchange,true}, - {allow_visitor_status,true}, - {anonymous,true}, - {captcha_protected,false}, - {lang,<<>>}, - {logging,false}, - {members_by_default,true}, - {members_only,false}, - {moderated,true}, - {password_protected,false}, - {persistent,false}, - {public,true}, - {public_list,true}, - {mam,false}, - {allow_subscription,false}, - {password,<<>>}, - {title,<<>>}, - {allow_private_messages_from_visitors,anyone}, - {max_users,200}, - {presence_broadcast,[moderator,participant,visitor]}]}]. + [{allow_change_subj, true}, + {allowpm, anyone}, + {allow_query_users, true}, + {allow_user_invites, false}, + {allow_visitor_nickchange, true}, + {allow_visitor_status, true}, + {anonymous, true}, + {captcha_protected, false}, + {lang, <<>>}, + {logging, false}, + {members_by_default, true}, + {members_only, false}, + {moderated, true}, + {password_protected, false}, + {persistent, false}, + {public, true}, + {public_list, true}, + {mam, false}, + {allow_subscription, false}, + {password, <<>>}, + {title, <<>>}, + {allow_private_messages_from_visitors, anyone}, + {max_users, 200}, + {presence_broadcast, [moderator, participant, visitor]}]}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module provides support for https://xmpp.org/extensions/xep-0045.html" - "[XEP-0045: Multi-User Chat]. Users can discover existing rooms, " - "join or create them. Occupants of a room can chat in public or have private chats."), "", - ?T("The MUC service allows any Jabber ID to register a nickname, so " - "nobody else can use that nickname in any room in the MUC " - "service. To register a nickname, open the Service Discovery in " - "your XMPP client and register in the MUC service."), "", - ?T("It is also possible to register a nickname in a room, so " - "nobody else can use that nickname in that room. If a nick is " + "[XEP-0045: Multi-User Chat]. Users can discover existing rooms, " + "join or create them. Occupants of a room can chat in public or have private chats."), + "", + ?T("The MUC service allows any Jabber ID to register a nickname, so " + "nobody else can use that nickname in any room in the MUC " + "service. To register a nickname, open the Service Discovery in " + "your XMPP client and register in the MUC service."), + "", + ?T("It is also possible to register a nickname in a room, so " + "nobody else can use that nickname in that room. If a nick is " "registered in the MUC service, that nick cannot be registered in " "any room, and vice versa: a nick that is registered in a room " - "cannot be registered at the MUC service."), "", - ?T("This module supports clustering and load balancing. One module " - "can be started per cluster node. Rooms are distributed at " - "creation time on all available MUC module instances. The " - "multi-user chat module is clustered but the rooms themselves " - "are not clustered nor fault-tolerant: if the node managing a " - "set of rooms goes down, the rooms disappear and they will be " - "recreated on an available node on first connection attempt.")], + "cannot be registered at the MUC service."), + "", + ?T("This module supports clustering and load balancing. One module " + "can be started per cluster node. Rooms are distributed at " + "creation time on all available MUC module instances. The " + "multi-user chat module is clustered but the rooms themselves " + "are not clustered nor fault-tolerant: if the node managing a " + "set of rooms goes down, the rooms disappear and they will be " + "recreated on an available node on first connection attempt.")], opts => [{access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("You can specify who is allowed to use the Multi-User Chat service. " - "By default everyone is allowed to use it.")}}, + "By default everyone is allowed to use it.") + }}, {access_admin, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option specifies who is allowed to administrate " "the Multi-User Chat service. The default value is 'none', " @@ -1472,51 +1756,67 @@ mod_doc() -> "to the service JID, and it will be shown in all active " "rooms as a service message. The administrators can send a " "groupchat message to the JID of an active room, and the " - "message will be shown in the room as a service message.")}}, + "message will be shown in the room as a service message.") + }}, {access_create, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("To configure who is allowed to create new rooms at the " "Multi-User Chat service, this option can be used. " "The default value is 'all', which means everyone is " - "allowed to create rooms.")}}, + "allowed to create rooms.") + }}, {access_persistent, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("To configure who is allowed to modify the 'persistent' room option. " "The default value is 'all', which means everyone is allowed to " - "modify that option.")}}, + "modify that option.") + }}, {access_mam, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("To configure who is allowed to modify the 'mam' room option. " "The default value is 'all', which means everyone is allowed to " - "modify that option.")}}, + "modify that option.") + }}, {access_register, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), note => "improved in 23.10", desc => ?T("This option specifies who is allowed to register nickname " "within the Multi-User Chat service and rooms. The default is 'all' for " "backward compatibility, which means that any user is allowed " - "to register any free nick in the MUC service and in the rooms.")}}, + "to register any free nick in the MUC service and in the rooms.") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {ram_db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => ?T("Same as top-level _`default_ram_db`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {hibernation_timeout, - #{value => "infinity | Seconds", + #{ + value => "infinity | Seconds", desc => ?T("Timeout before hibernating the room process, expressed " - "in seconds. The default value is 'infinity'.")}}, + "in seconds. The default value is 'infinity'.") + }}, {history_size, - #{value => ?T("Size"), + #{ + value => ?T("Size"), desc => ?T("A small history of the current discussion is sent to users " "when they enter the room. With this option you can define the " @@ -1526,90 +1826,116 @@ mod_doc() -> "The default value is '20'. This value affects all rooms on the service. " "NOTE: modern XMPP clients rely on Message Archives (XEP-0313), so feel " "free to disable the history feature if you're only using modern clients " - "and have _`mod_mam`_ module loaded.")}}, + "and have _`mod_mam`_ module loaded.") + }}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, - #{value => ?T("[Host, ...]"), + #{ + value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"conference.\". " - "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + "The keyword '@HOST@' is replaced with the real virtual host name.") + }}, {name, - #{value => "string()", + #{ + value => "string()", desc => ?T("The value of the service name. This name is only visible in some " "clients that support https://xmpp.org/extensions/xep-0030.html" - "[XEP-0030: Service Discovery]. The default is 'Chatrooms'.")}}, + "[XEP-0030: Service Discovery]. The default is 'Chatrooms'.") + }}, {max_room_desc, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the maximum number of characters that " "Room Description can have when configuring the room. " - "The default value is 'infinity'.")}}, + "The default value is 'infinity'.") + }}, {max_room_id, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the maximum number of characters that " "Room ID can have when creating a new room. " - "The default value is 'infinity'.")}}, + "The default value is 'infinity'.") + }}, {max_room_name, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the maximum number of characters " "that Room Name can have when configuring the room. " - "The default value is 'infinity'.")}}, + "The default value is 'infinity'.") + }}, {max_password, - #{value => ?T("Number"), + #{ + value => ?T("Number"), note => "added in 21.01", desc => ?T("This option defines the maximum number of characters " "that Password can have when configuring the room. " - "The default value is 'infinity'.")}}, + "The default value is 'infinity'.") + }}, {max_captcha_whitelist, - #{value => ?T("Number"), + #{ + value => ?T("Number"), note => "added in 21.01", desc => ?T("This option defines the maximum number of characters " "that Captcha Whitelist can have when configuring the room. " - "The default value is 'infinity'.")}}, + "The default value is 'infinity'.") + }}, {max_rooms_discoitems, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("When there are more rooms than this 'Number', " "only the non-empty ones are returned in a Service Discovery query. " - "The default value is '100'.")}}, + "The default value is '100'.") + }}, {max_user_conferences, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the maximum number of rooms that any " "given user can join. The default value is '100'. This option " "is used to prevent possible abuses. Note that this is a soft " "limit: some users can sometimes join more conferences in " - "cluster configurations.")}}, + "cluster configurations.") + }}, {max_users, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines at the service level, the maximum " "number of users allowed per room. It can be lowered in " "each room configuration but cannot be increased in " - "individual room configuration. The default value is '200'.")}}, + "individual room configuration. The default value is '200'.") + }}, {max_users_admin_threshold, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the number of service admins or room " "owners allowed to enter the room when the maximum number " - "of allowed occupants was reached. The default limit is '5'.")}}, + "of allowed occupants was reached. The default limit is '5'.") + }}, {max_users_presence, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines after how many users in the room, " "it is considered overcrowded. When a MUC room is considered " "overcrowded, presence broadcasts are limited to reduce load, " "traffic and excessive presence \"storm\" received by participants. " - "The default value is '1000'.")}}, + "The default value is '1000'.") + }}, {min_message_interval, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the minimum interval between two " "messages send by an occupant in seconds. This option " @@ -1620,9 +1946,11 @@ mod_doc() -> "the service. A good value for this minimum message interval is '0.4' second. " "If an occupant tries to send messages faster, an error is send back " "explaining that the message has been discarded and describing the " - "reason why the message is not acceptable.")}}, + "reason why the message is not acceptable.") + }}, {min_presence_interval, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("This option defines the minimum of time between presence " "changes coming from a given occupant in seconds. " @@ -1634,42 +1962,56 @@ mod_doc() -> "the presence is cached by ejabberd and only the last presence " "is broadcasted to all occupants in the room after expiration " "of the interval delay. Intermediate presence packets are " - "silently discarded. A good value for this option is '4' seconds.")}}, + "silently discarded. A good value for this option is '4' seconds.") + }}, {queue_type, - #{value => "ram | file", + #{ + value => "ram | file", desc => - ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.") + }}, {regexp_room_id, - #{value => "string()", + #{ + value => "string()", desc => ?T("This option defines the regular expression that a Room ID " "must satisfy to allow the room creation. The default value " - "is the empty string.")}}, + "is the empty string.") + }}, {preload_rooms, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Whether to load all persistent rooms in memory on startup. " "If disabled, the room is only loaded on first participant join. " "The default is 'true'. It makes sense to disable room preloading " "when the number of rooms is high: this will improve server startup " - "time and memory consumption.")}}, + "time and memory consumption.") + }}, {room_shaper, - #{value => "none | ShaperName", + #{ + value => "none | ShaperName", desc => ?T("This option defines shaper for the MUC rooms. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {user_message_shaper, - #{value => "none | ShaperName", + #{ + value => "none | ShaperName", desc => ?T("This option defines shaper for the users messages. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {user_presence_shaper, - #{value => "none | ShaperName", + #{ + value => "none | ShaperName", desc => ?T("This option defines shaper for the users presences. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {vcard, - #{value => ?T("vCard"), + #{ + value => ?T("vCard"), desc => ?T("A custom vCard of the service that will be displayed " "by some XMPP clients in Service Discovery. The value of " @@ -1692,189 +2034,255 @@ mod_doc() -> " adr:", " -", " work: true", - " street: Elm Street"]}}, + " street: Elm Street"] + }}, {cleanup_affiliations_on_start, - #{value => "true | false", + #{ + value => "true | false", note => "added in 22.05", desc => ?T("Remove affiliations for non-existing local users on startup. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {default_room_options, - #{value => ?T("Options"), + #{ + value => ?T("Options"), note => "improved in 22.05", desc => ?T("Define the " "default room options. Note that the creator of a room " "can modify the options of his room at any time using an " - "XMPP client with MUC capability. The 'Options' are:")}, + "XMPP client with MUC capability. The 'Options' are:") + }, [{allow_change_subj, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Allow occupants to change the subject. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {allowpm, - #{value => "anyone | participants | moderators | none", + #{ + value => "anyone | participants | moderators | none", desc => ?T("Who can send private messages. " - "The default value is 'anyone'.")}}, + "The default value is 'anyone'.") + }}, {allow_query_users, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Occupants can send IQ queries to other occupants. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {allow_user_invites, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Allow occupants to send invitations. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {allow_visitor_nickchange, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Allow visitors to change nickname. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {allow_visitor_status, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Allow visitors to send status text in presence updates. " "If disallowed, the status text is stripped before broadcasting " "the presence update to all the room occupants. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {allow_voice_requests, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Allow visitors in a moderated room to request voice. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {anonymous, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The room is anonymous: occupants don't see the real " "JIDs of other occupants. Note that the room moderators " "can always see the real JIDs of the occupants. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {captcha_protected, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("When a user tries to join a room where they have no " "affiliation (not owner, admin or member), the room " "requires them to fill a CAPTCHA challenge (see section " "_`basic.md#captcha|CAPTCHA`_ " "in order to accept their join in the room. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {description, - #{value => ?T("Room Description"), + #{ + value => ?T("Room Description"), desc => ?T("Short description of the room. " - "The default value is an empty string.")}}, + "The default value is an empty string.") + }}, {enable_hats, - #{value => "true | false", + #{ + value => "true | false", note => "improved in 25.03", desc => ?T("Allow extended roles as defined in XEP-0317 Hats. " "Check the _`../../tutorials/muc-hats.md|MUC Hats`_ tutorial. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {lang, - #{value => ?T("Language"), + #{ + value => ?T("Language"), desc => ?T("Preferred language for the discussions in the room. " "The language format should conform to RFC 5646. " - "There is no value by default.")}}, + "There is no value by default.") + }}, {logging, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The public messages are logged using _`mod_muc_log`_. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {members_by_default, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The occupants that enter the room are participants " "by default, so they have \"voice\". " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {members_only, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Only members of the room can enter. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {moderated, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Only occupants with \"voice\" can send public messages. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {password_protected, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The password is required to enter the room. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {password, - #{value => ?T("Password"), + #{ + value => ?T("Password"), desc => ?T("Password of the room. Implies option 'password_protected' " - "set to 'true'. There is no default value.")}}, + "set to 'true'. There is no default value.") + }}, {persistent, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The room persists even if the last participant leaves. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {public, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The room is public in the list of the MUC service, " "so it can be discovered. MUC admins and room participants " "will see private rooms in Service Discovery if their XMPP " "client supports this feature. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {public_list, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("The list of participants is public, without requiring " - "to enter the room. The default value is 'true'.")}}, + "to enter the room. The default value is 'true'.") + }}, {pubsub, - #{value => ?T("PubSub Node"), + #{ + value => ?T("PubSub Node"), desc => ?T("XMPP URI of associated Publish/Subscribe node. " - "The default value is an empty string.")}}, + "The default value is an empty string.") + }}, {vcard, - #{value => ?T("vCard"), + #{ + value => ?T("vCard"), desc => ?T("A custom vCard for the room. See the equivalent mod_muc option." - "The default value is an empty string.")}}, + "The default value is an empty string.") + }}, {vcard_xupdate, - #{value => "undefined | external | AvatarHash", + #{ + value => "undefined | external | AvatarHash", desc => ?T("Set the hash of the avatar image. " - "The default value is 'undefined'.")}}, + "The default value is 'undefined'.") + }}, {voice_request_min_interval, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("Minimum interval between voice requests, in seconds. " - "The default value is '1800'.")}}, + "The default value is '1800'.") + }}, {mam, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Enable message archiving. Implies mod_mam is enabled. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {allow_subscription, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Allow users to subscribe to room events as described in " "_`../../developer/xmpp-clients-bots/extensions/muc-sub.md|Multi-User Chat Subscriptions`_. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {title, - #{value => ?T("Room Title"), + #{ + value => ?T("Room Title"), desc => ?T("A human-readable title of the room. " - "There is no default value")}}, + "There is no default value") + }}, {allow_private_messages_from_visitors, - #{value => "anyone | moderators | nobody", + #{ + value => "anyone | moderators | nobody", desc => ?T("Visitors can send private messages to other occupants. " "The default value is 'anyone' which means visitors " - "can send private messages to any occupant.")}}, + "can send private messages to any occupant.") + }}, {max_users, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("Maximum number of occupants in the room. " - "The default value is '200'.")}}, + "The default value is '200'.") + }}, {presence_broadcast, - #{value => "[Role]", + #{ + value => "[Role]", desc => ?T("List of roles for which presence is broadcasted. " "The list can contain one or several of: 'moderator', " @@ -1884,4 +2292,6 @@ mod_doc() -> ["presence_broadcast:", " - moderator", " - participant", - " - visitor"]}}]}]}. + " - visitor"] + }}]}] + }. diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 9a8ab60b1..c6d92472c 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -28,35 +28,63 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, depends/2, mod_doc/0, - muc_online_rooms/1, muc_online_rooms_by_regex/2, - muc_register_nick/3, muc_register_nick/4, - muc_unregister_nick/2, muc_unregister_nick/3, - create_room_with_opts/4, create_room/3, destroy_room/2, - create_rooms_file/1, destroy_rooms_file/1, - rooms_unused_list/2, rooms_unused_destroy/2, - rooms_empty_list/1, rooms_empty_destroy/1, rooms_empty_destroy_restuple/1, - get_user_rooms/2, get_user_subscriptions/2, get_room_occupants/2, - get_room_occupants_number/2, send_direct_invitation/5, - change_room_option/4, get_room_options/2, - set_room_affiliation/4, set_room_affiliation/5, get_room_affiliations/2, - get_room_affiliations_v3/2, get_room_affiliation/3, - subscribe_room/4, subscribe_room/6, - subscribe_room_many/3, subscribe_room_many_v3/4, - unsubscribe_room/2, unsubscribe_room/4, get_subscribers/2, - get_room_serverhost/1, - web_menu_main/2, web_page_main/2, - web_menu_host/3, web_page_host/3, - web_menu_hostuser/4, web_page_hostuser/4, +-export([start/2, + stop/1, + reload/3, + depends/2, + mod_doc/0, + muc_online_rooms/1, + muc_online_rooms_by_regex/2, + muc_register_nick/3, muc_register_nick/4, + muc_unregister_nick/2, muc_unregister_nick/3, + create_room_with_opts/4, + create_room/3, + destroy_room/2, + create_rooms_file/1, + destroy_rooms_file/1, + rooms_unused_list/2, + rooms_unused_destroy/2, + rooms_empty_list/1, + rooms_empty_destroy/1, + rooms_empty_destroy_restuple/1, + get_user_rooms/2, + get_user_subscriptions/2, + get_room_occupants/2, + get_room_occupants_number/2, + send_direct_invitation/5, + change_room_option/4, + get_room_options/2, + set_room_affiliation/4, set_room_affiliation/5, + get_room_affiliations/2, + get_room_affiliations_v3/2, + get_room_affiliation/3, + subscribe_room/4, subscribe_room/6, + subscribe_room_many/3, + subscribe_room_many_v3/4, + unsubscribe_room/2, unsubscribe_room/4, + get_subscribers/2, + get_room_serverhost/1, + web_menu_main/2, + web_page_main/2, + web_menu_host/3, + web_page_host/3, + web_menu_hostuser/4, + web_page_hostuser/4, webadmin_muc/2, - mod_opt_type/1, mod_options/1, - get_commands_spec/0, find_hosts/1, room_diagnostics/2, - get_room_pid/2, get_room_history/2]). + mod_opt_type/1, + mod_options/1, + get_commands_spec/0, + find_hosts/1, + room_diagnostics/2, + get_room_pid/2, + get_room_history/2]). -import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_http.hrl"). @@ -68,592 +96,777 @@ %% gen_mod %%---------------------------- + start(_Host, _Opts) -> {ok, [{commands, get_commands_spec()}, {hook, webadmin_menu_main, web_menu_main, 50, global}, - {hook, webadmin_page_main, web_page_main, 50, global}, - {hook, webadmin_menu_host, web_menu_host, 50}, - {hook, webadmin_page_host, web_page_host, 50}, - {hook, webadmin_menu_hostuser, web_menu_hostuser, 50}, - {hook, webadmin_page_hostuser, web_page_hostuser, 50} - ]}. + {hook, webadmin_page_main, web_page_main, 50, global}, + {hook, webadmin_menu_host, web_menu_host, 50}, + {hook, webadmin_page_host, web_page_host, 50}, + {hook, webadmin_menu_hostuser, web_menu_hostuser, 50}, + {hook, webadmin_page_hostuser, web_page_hostuser, 50}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> [{mod_muc, hard}]. + %%% %%% Register commands %%% + get_commands_spec() -> - [ - #ejabberd_commands{name = muc_online_rooms, tags = [muc], - desc = "List existing rooms", - longdesc = "Ask for a specific host, or `global` to use all vhosts.", - policy = admin, - module = ?MODULE, function = muc_online_rooms, - args_desc = ["MUC service, or `global` for all"], - args_example = ["conference.example.com"], - result_desc = "List of rooms JIDs", - result_example = ["room1@conference.example.com", "room2@conference.example.com"], - args = [{service, binary}], - args_rename = [{host, service}], - result = {rooms, {list, {room, string}}}}, - #ejabberd_commands{name = muc_online_rooms_by_regex, tags = [muc], - desc = "List existing rooms filtered by regexp", - longdesc = "Ask for a specific host, or `global` to use all vhosts.", - policy = admin, - module = ?MODULE, function = muc_online_rooms_by_regex, - args_desc = ["MUC service, or `global` for all", - "Regex pattern for room name"], - args_example = ["conference.example.com", "^prefix"], - result_desc = "List of rooms with summary", - result_example = [{"room1@conference.example.com", "true", 10}, - {"room2@conference.example.com", "false", 10}], - args = [{service, binary}, {regex, binary}], - args_rename = [{host, service}], - result = {rooms, {list, {room, {tuple, - [{jid, string}, - {public, string}, - {participants, integer} - ]}}}}}, - #ejabberd_commands{name = muc_register_nick, tags = [muc], - desc = "Register a nick to a User JID in a MUC service", - module = ?MODULE, function = muc_register_nick, - args_desc = ["Nick", "User JID", "Service"], - args_example = [<<"Tim">>, <<"tim@example.org">>, <<"conference.example.org">>], - args = [{nick, binary}, {jid, binary}, {service, binary}], - args_rename = [{host, service}], - result = {res, rescode}}, - #ejabberd_commands{name = muc_register_nick, tags = [muc], - desc = "Register a nick to a User JID in a MUC service", - module = ?MODULE, function = muc_register_nick, - version = 3, - note = "updated in 24.12", - args_desc = ["nick", "user name", "user host", "MUC service"], - args_example = [<<"Tim">>, <<"tim">>, <<"example.org">>, <<"conference.example.org">>], - args = [{nick, binary}, {user, binary}, {host, binary}, {service, binary}], - args_rename = [{host, service}], - result = {res, rescode}}, - #ejabberd_commands{name = muc_unregister_nick, tags = [muc], - desc = "Unregister the nick registered by that account in the MUC service", - module = ?MODULE, function = muc_unregister_nick, - args_desc = ["User JID", "MUC service"], - args_example = [<<"tim@example.org">>, <<"conference.example.org">>], - args = [{jid, binary}, {service, binary}], - args_rename = [{host, service}], - result = {res, rescode}}, - #ejabberd_commands{name = muc_unregister_nick, tags = [muc], - desc = "Unregister the nick registered by that account in the MUC service", - module = ?MODULE, function = muc_unregister_nick, - version = 3, - note = "updated in 24.12", - args_desc = ["user name", "user host", "MUC service"], - args_example = [<<"tim">>, <<"example.org">>, <<"conference.example.org">>], - args = [{user, binary}, {host, binary}, {service, binary}], - args_rename = [{host, service}], - result = {res, rescode}}, + [#ejabberd_commands{ + name = muc_online_rooms, + tags = [muc], + desc = "List existing rooms", + longdesc = "Ask for a specific host, or `global` to use all vhosts.", + policy = admin, + module = ?MODULE, + function = muc_online_rooms, + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of rooms JIDs", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}} + }, + #ejabberd_commands{ + name = muc_online_rooms_by_regex, + tags = [muc], + desc = "List existing rooms filtered by regexp", + longdesc = "Ask for a specific host, or `global` to use all vhosts.", + policy = admin, + module = ?MODULE, + function = muc_online_rooms_by_regex, + args_desc = ["MUC service, or `global` for all", + "Regex pattern for room name"], + args_example = ["conference.example.com", "^prefix"], + result_desc = "List of rooms with summary", + result_example = [{"room1@conference.example.com", "true", 10}, + {"room2@conference.example.com", "false", 10}], + args = [{service, binary}, {regex, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, {tuple, + [{jid, string}, + {public, string}, + {participants, integer}]}}}} + }, + #ejabberd_commands{ + name = muc_register_nick, + tags = [muc], + desc = "Register a nick to a User JID in a MUC service", + module = ?MODULE, + function = muc_register_nick, + args_desc = ["Nick", "User JID", "Service"], + args_example = [<<"Tim">>, <<"tim@example.org">>, <<"conference.example.org">>], + args = [{nick, binary}, {jid, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = muc_register_nick, + tags = [muc], + desc = "Register a nick to a User JID in a MUC service", + module = ?MODULE, + function = muc_register_nick, + version = 3, + note = "updated in 24.12", + args_desc = ["nick", "user name", "user host", "MUC service"], + args_example = [<<"Tim">>, <<"tim">>, <<"example.org">>, <<"conference.example.org">>], + args = [{nick, binary}, {user, binary}, {host, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = muc_unregister_nick, + tags = [muc], + desc = "Unregister the nick registered by that account in the MUC service", + module = ?MODULE, + function = muc_unregister_nick, + args_desc = ["User JID", "MUC service"], + args_example = [<<"tim@example.org">>, <<"conference.example.org">>], + args = [{jid, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = muc_unregister_nick, + tags = [muc], + desc = "Unregister the nick registered by that account in the MUC service", + module = ?MODULE, + function = muc_unregister_nick, + version = 3, + note = "updated in 24.12", + args_desc = ["user name", "user host", "MUC service"], + args_example = [<<"tim">>, <<"example.org">>, <<"conference.example.org">>], + args = [{user, binary}, {host, binary}, {service, binary}], + args_rename = [{host, service}], + result = {res, rescode} + }, - #ejabberd_commands{name = create_room, tags = [muc_room], - desc = "Create a MUC room name@service in host", - module = ?MODULE, function = create_room, - args_desc = ["Room name", "MUC service", "Server host"], - args_example = ["room1", "conference.example.com", "example.com"], - args = [{room, binary}, {service, binary}, - {host, binary}], - args_rename = [{name, room}], - result = {res, rescode}}, - #ejabberd_commands{name = destroy_room, tags = [muc_room], - desc = "Destroy a MUC room", - module = ?MODULE, function = destroy_room, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - args = [{room, binary}, {service, binary}], - args_rename = [{name, room}], - result = {res, rescode}}, - #ejabberd_commands{name = create_rooms_file, tags = [muc], - desc = "Create the rooms indicated in file", - longdesc = "Provide one room JID per line. Rooms will be created after restart.", - note = "improved in 24.12", - module = ?MODULE, function = create_rooms_file, - args_desc = ["Path to the text file with one room JID per line"], - args_example = ["/home/ejabberd/rooms.txt"], - args = [{file, string}], - result = {res, rescode}}, - #ejabberd_commands{name = create_room_with_opts, tags = [muc_room, muc_sub], - desc = "Create a MUC room name@service in host with given options", - longdesc = - "Options `affiliations` and `subscribers` are lists of tuples. " - "The tuples in the list are separated with `;` and " - "the elements in each tuple are separated with `=` " - "(until ejabberd 24.12 the separators were `,` and `:` respectively). " - "Each subscriber can have one or more nodes. " - "In summary, `affiliations` is like `Type1=JID1;Type2=JID2` " - "and `subscribers` is like `JID1=Nick1=Node1A=Node1B=Node1C;JID2=Nick2=Node2`.", - note = "modified in 25.03", - module = ?MODULE, function = create_room_with_opts, - args_desc = ["Room name", "MUC service", "Server host", "List of options"], - args_example = ["room1", "conference.example.com", "localhost", - [{"members_only","true"}, - {"affiliations", "owner=user1@localhost;member=user2@localhost"}, - {"subscribers", "user3@localhost=User3=messages=subject;user4@localhost=User4=messages"}]], - args = [{room, binary}, {service, binary}, - {host, binary}, - {options, {list, - {option, {tuple, - [{name, binary}, - {value, binary} - ]}} - }}], - args_rename = [{name, room}], - result = {res, rescode}}, - #ejabberd_commands{name = destroy_rooms_file, tags = [muc], - desc = "Destroy the rooms indicated in file", - longdesc = "Provide one room JID per line.", - module = ?MODULE, function = destroy_rooms_file, - args_desc = ["Path to the text file with one room JID per line"], - args_example = ["/home/ejabberd/rooms.txt"], - args = [{file, string}], - result = {res, rescode}}, - #ejabberd_commands{name = rooms_unused_list, tags = [muc], - desc = "List the rooms that are unused for many days in the service", - longdesc = "The room recent history is used, so it's recommended " - " to wait a few days after service start before running this." - " The MUC service argument can be `global` to get all hosts.", - module = ?MODULE, function = rooms_unused_list, - args_desc = ["MUC service, or `global` for all", "Number of days"], - args_example = ["conference.example.com", 31], - result_desc = "List of unused rooms", - result_example = ["room1@conference.example.com", "room2@conference.example.com"], - args = [{service, binary}, {days, integer}], - args_rename = [{host, service}], - result = {rooms, {list, {room, string}}}}, - #ejabberd_commands{name = rooms_unused_destroy, tags = [muc], - desc = "Destroy the rooms that are unused for many days in the service", - longdesc = "The room recent history is used, so it's recommended " - " to wait a few days after service start before running this." - " The MUC service argument can be `global` to get all hosts.", - module = ?MODULE, function = rooms_unused_destroy, - args_desc = ["MUC service, or `global` for all", "Number of days"], - args_example = ["conference.example.com", 31], - result_desc = "List of unused rooms that has been destroyed", - result_example = ["room1@conference.example.com", "room2@conference.example.com"], - args = [{service, binary}, {days, integer}], - args_rename = [{host, service}], - result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{ + name = create_room, + tags = [muc_room], + desc = "Create a MUC room name@service in host", + module = ?MODULE, + function = create_room, + args_desc = ["Room name", "MUC service", "Server host"], + args_example = ["room1", "conference.example.com", "example.com"], + args = [{room, binary}, + {service, binary}, + {host, binary}], + args_rename = [{name, room}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = destroy_room, + tags = [muc_room], + desc = "Destroy a MUC room", + module = ?MODULE, + function = destroy_room, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = create_rooms_file, + tags = [muc], + desc = "Create the rooms indicated in file", + longdesc = "Provide one room JID per line. Rooms will be created after restart.", + note = "improved in 24.12", + module = ?MODULE, + function = create_rooms_file, + args_desc = ["Path to the text file with one room JID per line"], + args_example = ["/home/ejabberd/rooms.txt"], + args = [{file, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = create_room_with_opts, + tags = [muc_room, muc_sub], + desc = "Create a MUC room name@service in host with given options", + longdesc = + "Options `affiliations` and `subscribers` are lists of tuples. " + "The tuples in the list are separated with `;` and " + "the elements in each tuple are separated with `=` " + "(until ejabberd 24.12 the separators were `,` and `:` respectively). " + "Each subscriber can have one or more nodes. " + "In summary, `affiliations` is like `Type1=JID1;Type2=JID2` " + "and `subscribers` is like `JID1=Nick1=Node1A=Node1B=Node1C;JID2=Nick2=Node2`.", + note = "modified in 25.03", + module = ?MODULE, + function = create_room_with_opts, + args_desc = ["Room name", "MUC service", "Server host", "List of options"], + args_example = ["room1", + "conference.example.com", + "localhost", + [{"members_only", "true"}, + {"affiliations", "owner=user1@localhost;member=user2@localhost"}, + {"subscribers", "user3@localhost=User3=messages=subject;user4@localhost=User4=messages"}]], + args = [{room, binary}, + {service, binary}, + {host, binary}, + {options, {list, + {option, {tuple, + [{name, binary}, + {value, binary}]}}}}], + args_rename = [{name, room}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = destroy_rooms_file, + tags = [muc], + desc = "Destroy the rooms indicated in file", + longdesc = "Provide one room JID per line.", + module = ?MODULE, + function = destroy_rooms_file, + args_desc = ["Path to the text file with one room JID per line"], + args_example = ["/home/ejabberd/rooms.txt"], + args = [{file, string}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = rooms_unused_list, + tags = [muc], + desc = "List the rooms that are unused for many days in the service", + longdesc = "The room recent history is used, so it's recommended " + " to wait a few days after service start before running this." + " The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, + function = rooms_unused_list, + args_desc = ["MUC service, or `global` for all", "Number of days"], + args_example = ["conference.example.com", 31], + result_desc = "List of unused rooms", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}, {days, integer}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}} + }, + #ejabberd_commands{ + name = rooms_unused_destroy, + tags = [muc], + desc = "Destroy the rooms that are unused for many days in the service", + longdesc = "The room recent history is used, so it's recommended " + " to wait a few days after service start before running this." + " The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, + function = rooms_unused_destroy, + args_desc = ["MUC service, or `global` for all", "Number of days"], + args_example = ["conference.example.com", 31], + result_desc = "List of unused rooms that has been destroyed", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}, {days, integer}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}} + }, - #ejabberd_commands{name = rooms_empty_list, tags = [muc], - desc = "List the rooms that have no messages in archive", - longdesc = "The MUC service argument can be `global` to get all hosts.", - module = ?MODULE, function = rooms_empty_list, - args_desc = ["MUC service, or `global` for all"], - args_example = ["conference.example.com"], - result_desc = "List of empty rooms", - result_example = ["room1@conference.example.com", "room2@conference.example.com"], - args = [{service, binary}], - args_rename = [{host, service}], - result = {rooms, {list, {room, string}}}}, - #ejabberd_commands{name = rooms_empty_destroy, tags = [muc], - desc = "Destroy the rooms that have no messages in archive", - longdesc = "The MUC service argument can be `global` to get all hosts.", - module = ?MODULE, function = rooms_empty_destroy, - args_desc = ["MUC service, or `global` for all"], - args_example = ["conference.example.com"], - result_desc = "List of empty rooms that have been destroyed", - result_example = ["room1@conference.example.com", "room2@conference.example.com"], - args = [{service, binary}], - args_rename = [{host, service}], - result = {rooms, {list, {room, string}}}}, - #ejabberd_commands{name = rooms_empty_destroy, tags = [muc], - desc = "Destroy the rooms that have no messages in archive", - longdesc = "The MUC service argument can be `global` to get all hosts.", - module = ?MODULE, function = rooms_empty_destroy_restuple, - version = 2, - note = "modified in 24.06", - args_desc = ["MUC service, or `global` for all"], - args_example = ["conference.example.com"], - result_desc = "List of empty rooms that have been destroyed", - result_example = {ok, <<"Destroyed rooms: 2">>}, - args = [{service, binary}], - args_rename = [{host, service}], - result = {res, restuple}}, + #ejabberd_commands{ + name = rooms_empty_list, + tags = [muc], + desc = "List the rooms that have no messages in archive", + longdesc = "The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, + function = rooms_empty_list, + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of empty rooms", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}} + }, + #ejabberd_commands{ + name = rooms_empty_destroy, + tags = [muc], + desc = "Destroy the rooms that have no messages in archive", + longdesc = "The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, + function = rooms_empty_destroy, + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of empty rooms that have been destroyed", + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{service, binary}], + args_rename = [{host, service}], + result = {rooms, {list, {room, string}}} + }, + #ejabberd_commands{ + name = rooms_empty_destroy, + tags = [muc], + desc = "Destroy the rooms that have no messages in archive", + longdesc = "The MUC service argument can be `global` to get all hosts.", + module = ?MODULE, + function = rooms_empty_destroy_restuple, + version = 2, + note = "modified in 24.06", + args_desc = ["MUC service, or `global` for all"], + args_example = ["conference.example.com"], + result_desc = "List of empty rooms that have been destroyed", + result_example = {ok, <<"Destroyed rooms: 2">>}, + args = [{service, binary}], + args_rename = [{host, service}], + result = {res, restuple} + }, - #ejabberd_commands{name = get_user_rooms, tags = [muc], - desc = "Get the list of rooms where this user is occupant", - module = ?MODULE, function = get_user_rooms, - args_desc = ["Username", "Server host"], - args_example = ["tom", "example.com"], - result_example = ["room1@conference.example.com", "room2@conference.example.com"], - args = [{user, binary}, {host, binary}], - result = {rooms, {list, {room, string}}}}, - #ejabberd_commands{name = get_user_subscriptions, tags = [muc, muc_sub], - desc = "Get the list of rooms where this user is subscribed", - note = "added in 21.04", - module = ?MODULE, function = get_user_subscriptions, - args_desc = ["Username", "Server host"], - args_example = ["tom", "example.com"], - result_example = [{"room1@conference.example.com", "Tommy", ["mucsub:config"]}], - args = [{user, binary}, {host, binary}], - result = {rooms, - {list, - {room, - {tuple, - [{roomjid, string}, - {usernick, string}, - {nodes, {list, {node, string}}} - ]}} - }}}, + #ejabberd_commands{ + name = get_user_rooms, + tags = [muc], + desc = "Get the list of rooms where this user is occupant", + module = ?MODULE, + function = get_user_rooms, + args_desc = ["Username", "Server host"], + args_example = ["tom", "example.com"], + result_example = ["room1@conference.example.com", "room2@conference.example.com"], + args = [{user, binary}, {host, binary}], + result = {rooms, {list, {room, string}}} + }, + #ejabberd_commands{ + name = get_user_subscriptions, + tags = [muc, muc_sub], + desc = "Get the list of rooms where this user is subscribed", + note = "added in 21.04", + module = ?MODULE, + function = get_user_subscriptions, + args_desc = ["Username", "Server host"], + args_example = ["tom", "example.com"], + result_example = [{"room1@conference.example.com", "Tommy", ["mucsub:config"]}], + args = [{user, binary}, {host, binary}], + result = {rooms, + {list, + {room, + {tuple, + [{roomjid, string}, + {usernick, string}, + {nodes, {list, {node, string}}}]}}}} + }, - #ejabberd_commands{name = get_room_occupants, tags = [muc_room], - desc = "Get the list of occupants of a MUC room", - module = ?MODULE, function = get_room_occupants, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - result_desc = "The list of occupants with JID, nick and affiliation", - result_example = [{"user1@example.com/psi", "User 1", "owner"}], - args = [{room, binary}, {service, binary}], - args_rename = [{name, room}], - result = {occupants, {list, - {occupant, {tuple, - [{jid, string}, - {nick, string}, - {role, string} - ]}} - }}}, + #ejabberd_commands{ + name = get_room_occupants, + tags = [muc_room], + desc = "Get the list of occupants of a MUC room", + module = ?MODULE, + function = get_room_occupants, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of occupants with JID, nick and affiliation", + result_example = [{"user1@example.com/psi", "User 1", "owner"}], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {occupants, {list, + {occupant, {tuple, + [{jid, string}, + {nick, string}, + {role, string}]}}}} + }, - #ejabberd_commands{name = get_room_occupants_number, tags = [muc_room], - desc = "Get the number of occupants of a MUC room", - module = ?MODULE, function = get_room_occupants_number, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - result_desc = "Number of room occupants", - result_example = 7, - args = [{room, binary}, {service, binary}], - args_rename = [{name, room}], - result = {occupants, integer}}, + #ejabberd_commands{ + name = get_room_occupants_number, + tags = [muc_room], + desc = "Get the number of occupants of a MUC room", + module = ?MODULE, + function = get_room_occupants_number, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "Number of room occupants", + result_example = 7, + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {occupants, integer} + }, - #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], - desc = "Send a direct invitation to several destinations", - 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`. " - "Users JIDs are separated with `:`.", - module = ?MODULE, function = send_direct_invitation, - args_desc = ["Room name", "MUC service", "Password, or `none`", - "Reason text, or `none`", "Users JIDs separated with `:` characters"], - args_example = [<<"room1">>, <<"conference.example.com">>, - <<>>, <<"Check this out!">>, - "user2@localhost:user3@example.com"], - args = [{room, binary}, {service, binary}, {password, binary}, - {reason, binary}, {users, binary}], - args_rename = [{name, room}], - result = {res, rescode}}, - #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], - desc = "Send a direct invitation to several destinations", - 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 be set to `none`.", - module = ?MODULE, function = send_direct_invitation, - version = 1, - note = "updated in 24.02", - args_desc = ["Room name", "MUC service", "Password, or `none`", - "Reason text, or `none`", "List of users JIDs"], - args_example = [<<"room1">>, <<"conference.example.com">>, - <<>>, <<"Check this out!">>, - ["user2@localhost", "user3@example.com"]], - args = [{room, binary}, {service, binary}, {password, binary}, - {reason, binary}, {users, {list, {jid, binary}}}], - args_rename = [{name, room}], - result = {res, rescode}}, + #ejabberd_commands{ + name = send_direct_invitation, + tags = [muc_room], + desc = "Send a direct invitation to several destinations", + 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`. " + "Users JIDs are separated with `:`.", + module = ?MODULE, + function = send_direct_invitation, + args_desc = ["Room name", "MUC service", "Password, or `none`", + "Reason text, or `none`", "Users JIDs separated with `:` characters"], + args_example = [<<"room1">>, + <<"conference.example.com">>, + <<>>, + <<"Check this out!">>, + "user2@localhost:user3@example.com"], + args = [{room, binary}, + {service, binary}, + {password, binary}, + {reason, binary}, + {users, binary}], + args_rename = [{name, room}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = send_direct_invitation, + tags = [muc_room], + desc = "Send a direct invitation to several destinations", + 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 be set to `none`.", + module = ?MODULE, + function = send_direct_invitation, + version = 1, + note = "updated in 24.02", + args_desc = ["Room name", "MUC service", "Password, or `none`", + "Reason text, or `none`", "List of users JIDs"], + args_example = [<<"room1">>, + <<"conference.example.com">>, + <<>>, + <<"Check this out!">>, + ["user2@localhost", "user3@example.com"]], + args = [{room, binary}, + {service, binary}, + {password, binary}, + {reason, binary}, + {users, {list, {jid, binary}}}], + args_rename = [{name, room}], + result = {res, rescode} + }, - #ejabberd_commands{name = change_room_option, tags = [muc_room], - desc = "Change an option in a MUC room", - module = ?MODULE, function = change_room_option, - args_desc = ["Room name", "MUC service", "Option name", "Value to assign"], - args_example = ["room1", "conference.example.com", "members_only", "true"], - args = [{room, binary}, {service, binary}, - {option, binary}, {value, binary}], - args_rename = [{name, room}], - result = {res, rescode}}, - #ejabberd_commands{name = get_room_options, tags = [muc_room], - desc = "Get options from a MUC room", - module = ?MODULE, function = get_room_options, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - result_desc = "List of room options tuples with name and value", - result_example = [{"members_only", "true"}], - args = [{room, binary}, {service, binary}], - args_rename = [{name, room}], - result = {options, {list, - {option, {tuple, - [{name, string}, - {value, string} - ]}} - }}}, - #ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub], - desc = "Subscribe to a MUC conference", - module = ?MODULE, function = subscribe_room, - args_desc = ["User JID", "a user's nick", - "the room to subscribe", "nodes separated by commas: `,`"], - args_example = ["tom@localhost", "Tom", "room1@conference.localhost", - "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], - result_desc = "The list of nodes that has subscribed", - result_example = ["urn:xmpp:mucsub:nodes:messages", - "urn:xmpp:mucsub:nodes:affiliations"], - args = [{user, binary}, {nick, binary}, {room, binary}, - {nodes, binary}], - result = {nodes, {list, {node, string}}}}, - #ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub], - desc = "Subscribe to a MUC conference", - module = ?MODULE, function = subscribe_room, - version = 1, - note = "updated in 24.02", - args_desc = ["User JID", "a user's nick", - "the room to subscribe", "list of nodes"], - args_example = ["tom@localhost", "Tom", "room1@conference.localhost", - ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]], - result_desc = "The list of nodes that has subscribed", - result_example = ["urn:xmpp:mucsub:nodes:messages", - "urn:xmpp:mucsub:nodes:affiliations"], - args = [{user, binary}, {nick, binary}, {room, binary}, - {nodes, {list, {node, binary}}}], - result = {nodes, {list, {node, string}}}}, - #ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub], - desc = "Subscribe to a MUC conference", - module = ?MODULE, function = subscribe_room, - version = 3, - note = "updated in 24.12", - args_desc = ["user name", "user host", "user nick", - "room name", "MUC service", "list of nodes"], - args_example = ["tom", "localhost", "Tom", "room1", "conference.localhost", - ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]], - result_desc = "The list of nodes that has subscribed", - result_example = ["urn:xmpp:mucsub:nodes:messages", - "urn:xmpp:mucsub:nodes:affiliations"], - args = [{user, binary}, {host, binary}, {nick, binary}, {room, binary}, - {service, binary}, {nodes, {list, {node, binary}}}], - result = {nodes, {list, {node, string}}}}, - #ejabberd_commands{name = subscribe_room_many, tags = [muc_room, muc_sub], - desc = "Subscribe several users to a MUC conference", - note = "added in 22.05", - longdesc = "This command accepts up to 50 users at once " - "(this is configurable with the _`mod_muc_admin`_ option " - "`subscribe_room_many_max_users`)", - 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 = subscribe_room_many, tags = [muc_room, muc_sub], - desc = "Subscribe several users to a MUC conference", - longdesc = "This command accepts up to 50 users at once " - "(this is configurable with the _`mod_muc_admin`_ option " - "`subscribe_room_many_max_users`)", - module = ?MODULE, function = subscribe_room_many, - version = 1, - note = "updated in 24.02", - 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, {list, {node, binary}}}], - result = {res, rescode}}, - #ejabberd_commands{name = subscribe_room_many, tags = [muc_room, muc_sub], - desc = "Subscribe several users to a MUC conference", - longdesc = "This command accepts up to 50 users at once " - "(this is configurable with the _`mod_muc_admin`_ option " - "`subscribe_room_many_max_users`)", - module = ?MODULE, function = subscribe_room_many_v3, - version = 3, - note = "updated in 24.12", - args_desc = ["List of tuples with users name, host and nick", - "room name", - "MUC service", - "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, - [{user, binary}, - {host, binary}, - {nick, binary} - ]}} - }}, - {room, binary}, {service, binary}, - {nodes, {list, {node, binary}}}], - result = {res, rescode}}, - #ejabberd_commands{name = unsubscribe_room, tags = [muc_room, muc_sub], - desc = "Unsubscribe from a MUC conference", - module = ?MODULE, function = unsubscribe_room, - args_desc = ["User JID", "the room to subscribe"], - args_example = ["tom@localhost", "room1@conference.localhost"], - args = [{user, binary}, {room, binary}], - result = {res, rescode}}, - #ejabberd_commands{name = unsubscribe_room, tags = [muc_room, muc_sub], - desc = "Unsubscribe from a MUC conference", - module = ?MODULE, function = unsubscribe_room, - version = 3, - note = "updated in 24.12", - args_desc = ["user name", "user host", "room name", "MUC service"], - args_example = ["tom", "localhost", "room1", "conference.localhost"], - args = [{user, binary}, {host, binary}, {room, binary}, {service, binary}], - result = {res, rescode}}, - #ejabberd_commands{name = get_subscribers, tags = [muc_room, muc_sub], - desc = "List subscribers of a MUC conference", - module = ?MODULE, function = get_subscribers, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - result_desc = "The list of users that are subscribed to that room", - result_example = ["user2@example.com", "user3@example.com"], - args = [{room, binary}, {service, binary}], - args_rename = [{name, room}], - result = {subscribers, {list, {jid, string}}}}, - #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], - desc = "Change an affiliation in a MUC room", - module = ?MODULE, function = set_room_affiliation, - args_desc = ["Room name", "MUC service", "User JID", "Affiliation to set"], - args_example = ["room1", "conference.example.com", "user2@example.com", "member"], - args = [{name, binary}, {service, binary}, - {jid, binary}, {affiliation, binary}], - result = {res, rescode}}, - #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], - desc = "Change an affiliation in a MUC room", - longdesc = "If affiliation is `none`, then the affiliation is removed.", - module = ?MODULE, function = set_room_affiliation, - version = 3, - note = "updated in 24.12", - args_desc = ["room name", "MUC service", "user name", "user host", "affiliation to set"], - args_example = ["room1", "conference.example.com", "sun", "localhost", "member"], - args = [{room, binary}, {service, binary}, - {user, binary}, {host, binary}, {affiliation, binary}], - result = {res, rescode}}, + #ejabberd_commands{ + name = change_room_option, + tags = [muc_room], + desc = "Change an option in a MUC room", + module = ?MODULE, + function = change_room_option, + args_desc = ["Room name", "MUC service", "Option name", "Value to assign"], + args_example = ["room1", "conference.example.com", "members_only", "true"], + args = [{room, binary}, + {service, binary}, + {option, binary}, + {value, binary}], + args_rename = [{name, room}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = get_room_options, + tags = [muc_room], + desc = "Get options from a MUC room", + module = ?MODULE, + function = get_room_options, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "List of room options tuples with name and value", + result_example = [{"members_only", "true"}], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {options, {list, + {option, {tuple, + [{name, string}, + {value, string}]}}}} + }, + #ejabberd_commands{ + name = subscribe_room, + tags = [muc_room, muc_sub], + desc = "Subscribe to a MUC conference", + module = ?MODULE, + function = subscribe_room, + args_desc = ["User JID", "a user's nick", + "the room to subscribe", "nodes separated by commas: `,`"], + args_example = ["tom@localhost", "Tom", "room1@conference.localhost", + "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], + result_desc = "The list of nodes that has subscribed", + result_example = ["urn:xmpp:mucsub:nodes:messages", + "urn:xmpp:mucsub:nodes:affiliations"], + args = [{user, binary}, + {nick, binary}, + {room, binary}, + {nodes, binary}], + result = {nodes, {list, {node, string}}} + }, + #ejabberd_commands{ + name = subscribe_room, + tags = [muc_room, muc_sub], + desc = "Subscribe to a MUC conference", + module = ?MODULE, + function = subscribe_room, + version = 1, + note = "updated in 24.02", + args_desc = ["User JID", "a user's nick", + "the room to subscribe", "list of nodes"], + args_example = ["tom@localhost", + "Tom", + "room1@conference.localhost", + ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]], + result_desc = "The list of nodes that has subscribed", + result_example = ["urn:xmpp:mucsub:nodes:messages", + "urn:xmpp:mucsub:nodes:affiliations"], + args = [{user, binary}, + {nick, binary}, + {room, binary}, + {nodes, {list, {node, binary}}}], + result = {nodes, {list, {node, string}}} + }, + #ejabberd_commands{ + name = subscribe_room, + tags = [muc_room, muc_sub], + desc = "Subscribe to a MUC conference", + module = ?MODULE, + function = subscribe_room, + version = 3, + note = "updated in 24.12", + args_desc = ["user name", "user host", "user nick", + "room name", "MUC service", "list of nodes"], + args_example = ["tom", + "localhost", + "Tom", + "room1", + "conference.localhost", + ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]], + result_desc = "The list of nodes that has subscribed", + result_example = ["urn:xmpp:mucsub:nodes:messages", + "urn:xmpp:mucsub:nodes:affiliations"], + args = [{user, binary}, + {host, binary}, + {nick, binary}, + {room, binary}, + {service, binary}, + {nodes, {list, {node, binary}}}], + result = {nodes, {list, {node, string}}} + }, + #ejabberd_commands{ + name = subscribe_room_many, + tags = [muc_room, muc_sub], + desc = "Subscribe several users to a MUC conference", + note = "added in 22.05", + longdesc = "This command accepts up to 50 users at once " + "(this is configurable with the _`mod_muc_admin`_ option " + "`subscribe_room_many_max_users`)", + 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 = subscribe_room_many, + tags = [muc_room, muc_sub], + desc = "Subscribe several users to a MUC conference", + longdesc = "This command accepts up to 50 users at once " + "(this is configurable with the _`mod_muc_admin`_ option " + "`subscribe_room_many_max_users`)", + module = ?MODULE, + function = subscribe_room_many, + version = 1, + note = "updated in 24.02", + 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, {list, {node, binary}}}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = subscribe_room_many, + tags = [muc_room, muc_sub], + desc = "Subscribe several users to a MUC conference", + longdesc = "This command accepts up to 50 users at once " + "(this is configurable with the _`mod_muc_admin`_ option " + "`subscribe_room_many_max_users`)", + module = ?MODULE, + function = subscribe_room_many_v3, + version = 3, + note = "updated in 24.12", + args_desc = ["List of tuples with users name, host and nick", + "room name", + "MUC service", + "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, + [{user, binary}, + {host, binary}, + {nick, binary}]}}}}, + {room, binary}, + {service, binary}, + {nodes, {list, {node, binary}}}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = unsubscribe_room, + tags = [muc_room, muc_sub], + desc = "Unsubscribe from a MUC conference", + module = ?MODULE, + function = unsubscribe_room, + args_desc = ["User JID", "the room to subscribe"], + args_example = ["tom@localhost", "room1@conference.localhost"], + args = [{user, binary}, {room, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = unsubscribe_room, + tags = [muc_room, muc_sub], + desc = "Unsubscribe from a MUC conference", + module = ?MODULE, + function = unsubscribe_room, + version = 3, + note = "updated in 24.12", + args_desc = ["user name", "user host", "room name", "MUC service"], + args_example = ["tom", "localhost", "room1", "conference.localhost"], + args = [{user, binary}, {host, binary}, {room, binary}, {service, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = get_subscribers, + tags = [muc_room, muc_sub], + desc = "List subscribers of a MUC conference", + module = ?MODULE, + function = get_subscribers, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of users that are subscribed to that room", + result_example = ["user2@example.com", "user3@example.com"], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {subscribers, {list, {jid, string}}} + }, + #ejabberd_commands{ + name = set_room_affiliation, + tags = [muc_room], + desc = "Change an affiliation in a MUC room", + module = ?MODULE, + function = set_room_affiliation, + args_desc = ["Room name", "MUC service", "User JID", "Affiliation to set"], + args_example = ["room1", "conference.example.com", "user2@example.com", "member"], + args = [{name, binary}, + {service, binary}, + {jid, binary}, + {affiliation, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = set_room_affiliation, + tags = [muc_room], + desc = "Change an affiliation in a MUC room", + longdesc = "If affiliation is `none`, then the affiliation is removed.", + module = ?MODULE, + function = set_room_affiliation, + version = 3, + note = "updated in 24.12", + args_desc = ["room name", "MUC service", "user name", "user host", "affiliation to set"], + args_example = ["room1", "conference.example.com", "sun", "localhost", "member"], + args = [{room, binary}, + {service, binary}, + {user, binary}, + {host, binary}, + {affiliation, binary}], + result = {res, rescode} + }, + #ejabberd_commands{ + name = get_room_affiliations, + tags = [muc_room], + desc = "Get the list of affiliations of a MUC room", + module = ?MODULE, + function = get_room_affiliations, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of affiliations with username, domain, affiliation and reason", + result_example = [{"user1", "example.com", member, "member"}], + args = [{name, binary}, {service, binary}], + result = {affiliations, {list, + {affiliation, {tuple, + [{username, string}, + {domain, string}, + {affiliation, atom}, + {reason, string}]}}}} + }, + #ejabberd_commands{ + name = get_room_affiliations, + tags = [muc_room], + desc = "Get the list of affiliations of a MUC room", + module = ?MODULE, + function = get_room_affiliations_v3, + version = 3, + note = "updated in 24.12", + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + result_desc = "The list of affiliations with jid, affiliation and reason", + result_example = [{"user1@example.com", member, "member"}], + args = [{room, binary}, {service, binary}], + result = {affiliations, {list, + {affiliation, {tuple, + [{jid, string}, + {affiliation, atom}, + {reason, string}]}}}} + }, - #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], - desc = "Get the list of affiliations of a MUC room", - module = ?MODULE, function = get_room_affiliations, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - result_desc = "The list of affiliations with username, domain, affiliation and reason", - result_example = [{"user1", "example.com", member, "member"}], - args = [{name, binary}, {service, binary}], - result = {affiliations, {list, - {affiliation, {tuple, - [{username, string}, - {domain, string}, - {affiliation, atom}, - {reason, string} - ]}} - }}}, - #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], - desc = "Get the list of affiliations of a MUC room", - module = ?MODULE, function = get_room_affiliations_v3, - version = 3, - note = "updated in 24.12", - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - result_desc = "The list of affiliations with jid, affiliation and reason", - result_example = [{"user1@example.com", member, "member"}], - args = [{room, binary}, {service, binary}], - result = {affiliations, {list, - {affiliation, {tuple, - [{jid, string}, - {affiliation, atom}, - {reason, string} - ]}} - }}}, + #ejabberd_commands{ + name = get_room_affiliation, + tags = [muc_room], + desc = "Get affiliation of a user in MUC room", + module = ?MODULE, + function = get_room_affiliation, + args_desc = ["Room name", "MUC service", "User JID"], + args_example = ["room1", "conference.example.com", "user1@example.com"], + result_desc = "Affiliation of the user", + result_example = member, + args = [{room, binary}, {service, binary}, {jid, binary}], + args_rename = [{name, room}], + result = {affiliation, atom} + }, + #ejabberd_commands{ + name = get_room_history, + tags = [muc_room], + desc = "Get history of messages stored inside MUC room state", + note = "added in 23.04", + module = ?MODULE, + function = get_room_history, + args_desc = ["Room name", "MUC service"], + args_example = ["room1", "conference.example.com"], + args = [{room, binary}, {service, binary}], + args_rename = [{name, room}], + result = {history, {list, + {entry, {tuple, + [{timestamp, string}, + {message, string}]}}}} + }, - - #ejabberd_commands{name = get_room_affiliation, tags = [muc_room], - desc = "Get affiliation of a user in MUC room", - module = ?MODULE, function = get_room_affiliation, - args_desc = ["Room name", "MUC service", "User JID"], - args_example = ["room1", "conference.example.com", "user1@example.com"], - result_desc = "Affiliation of the user", - result_example = member, - args = [{room, binary}, {service, binary}, {jid, binary}], - args_rename = [{name, room}], - result = {affiliation, atom}}, - #ejabberd_commands{name = get_room_history, tags = [muc_room], - desc = "Get history of messages stored inside MUC room state", - note = "added in 23.04", - module = ?MODULE, function = get_room_history, - args_desc = ["Room name", "MUC service"], - args_example = ["room1", "conference.example.com"], - args = [{room, binary}, {service, binary}], - args_rename = [{name, room}], - result = {history, {list, - {entry, {tuple, - [{timestamp, string}, - {message, string}]}}}}}, - - #ejabberd_commands{name = webadmin_muc, tags = [internal], - desc = "Generate WebAdmin MUC Rooms HTML", - module = ?MODULE, function = webadmin_muc, - args = [{request, any}, {lang, binary}], - result = {res, any}} - ]. + #ejabberd_commands{ + name = webadmin_muc, + tags = [internal], + desc = "Generate WebAdmin MUC Rooms HTML", + module = ?MODULE, + function = webadmin_muc, + args = [{request, any}, {lang, binary}], + result = {res, any} + }]. %%% %%% ejabberd commands %%% + muc_online_rooms(ServiceArg) -> Hosts = find_services_validate(ServiceArg, <<"serverhost">>), lists:flatmap( fun(Host) -> - [<> - || {Name, _, _} <- mod_muc:get_online_rooms(Host)] - end, Hosts). + [ <> + || {Name, _, _} <- mod_muc:get_online_rooms(Host) ] + end, + Hosts). + muc_online_rooms_by_regex(ServiceArg, Regex) -> {_, P} = re:compile(Regex), Hosts = find_services_validate(ServiceArg, <<"serverhost">>), lists:flatmap( fun(Host) -> - [build_summary_room(Name, RoomHost, Pid) - || {Name, RoomHost, Pid} <- mod_muc:get_online_rooms(Host), - is_name_match(Name, P)] - end, Hosts). + [ build_summary_room(Name, RoomHost, Pid) + || {Name, RoomHost, Pid} <- mod_muc:get_online_rooms(Host), + is_name_match(Name, P) ] + end, + Hosts). + is_name_match(Name, P) -> - case re:run(Name, P) of - {match, _} -> true; - nomatch -> false - end. + case re:run(Name, P) of + {match, _} -> true; + nomatch -> false + end. + build_summary_room(Name, Host, Pid) -> C = get_room_config(Pid), @@ -661,54 +874,60 @@ build_summary_room(Name, Host, Pid) -> S = get_room_state(Pid), Participants = maps:size(S#state.users), {<>, - misc:atom_to_binary(Public), - Participants - }. + misc:atom_to_binary(Public), + Participants}. + muc_register_nick(Nick, User, Host, Service) -> muc_register_nick(Nick, makeencode(User, Host), Service). + muc_register_nick(Nick, FromBinary, Service) -> try {get_room_serverhost(Service), jid:decode(FromBinary)} of - {ServerHost, From} -> - Lang = <<"en">>, - case mod_muc:iq_set_register_info(ServerHost, Service, From, Nick, Lang) of - {result, undefined} -> ok; - {error, #stanza_error{reason = 'conflict'}} -> - throw({error, "Nick already registered"}); - {error, _} -> - throw({error, "Database error"}) - end - catch - error:{invalid_domain, _} -> - throw({error, "Invalid value of 'service'"}); - error:{unregistered_route, _} -> - throw({error, "Unknown host in 'service'"}); - error:{bad_jid, _} -> - throw({error, "Invalid 'jid'"}); - _ -> - throw({error, "Internal error"}) + {ServerHost, From} -> + Lang = <<"en">>, + case mod_muc:iq_set_register_info(ServerHost, Service, From, Nick, Lang) of + {result, undefined} -> ok; + {error, #stanza_error{reason = 'conflict'}} -> + throw({error, "Nick already registered"}); + {error, _} -> + throw({error, "Database error"}) + end + catch + error:{invalid_domain, _} -> + throw({error, "Invalid value of 'service'"}); + error:{unregistered_route, _} -> + throw({error, "Unknown host in 'service'"}); + error:{bad_jid, _} -> + throw({error, "Invalid 'jid'"}); + _ -> + throw({error, "Internal error"}) end. + muc_unregister_nick(User, Host, Service) -> muc_unregister_nick(makeencode(User, Host), Service). + muc_unregister_nick(FromBinary, Service) -> muc_register_nick(<<"">>, FromBinary, Service). + get_user_rooms(User, Server) -> lists:flatmap( fun(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_muc) of - true -> - Rooms = mod_muc:get_online_rooms_by_user( - ServerHost, jid:nodeprep(User), jid:nodeprep(Server)), - [<> - || {Name, Host} <- Rooms]; - false -> - [] - end - end, ejabberd_option:hosts()). + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + Rooms = mod_muc:get_online_rooms_by_user( + ServerHost, jid:nodeprep(User), jid:nodeprep(Server)), + [ <> + || {Name, Host} <- Rooms ]; + false -> + [] + end + end, + ejabberd_option:hosts()). + get_user_subscriptions(User, Server) -> User2 = validate_user(User, <<"user">>), @@ -718,15 +937,16 @@ get_user_subscriptions(User, Server) -> lists:flatmap( fun(ServerHost) -> {ok, Rooms} = mod_muc:get_subscribed_rooms(ServerHost, UserJid), - [{jid:encode(RoomJid), UserNick, Nodes} - || {RoomJid, UserNick, Nodes} <- Rooms] - end, Services). + [ {jid:encode(RoomJid), UserNick, Nodes} + || {RoomJid, UserNick, Nodes} <- Rooms ] + end, + Services). + %%---------------------------- %% Ad-hoc commands %%---------------------------- - %%---------------------------- %% Web Admin %%---------------------------- @@ -736,15 +956,19 @@ get_user_subscriptions(User, Server) -> %%--------------- %% Web Admin Menu + web_menu_main(Acc, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. + web_menu_host(Acc, _Host, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. + %%--------------- %% Web Admin Page + web_page_main(_, #request{path = [<<"muc">>], lang = Lang} = R) -> PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), @@ -753,6 +977,7 @@ web_page_main(_, #request{path = [<<"muc">>], lang = Lang} = R) -> web_page_main(Acc, _) -> Acc. + web_page_host(_, Host, #request{path = [<<"muc">> | RPath], lang = Lang} = R) -> PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), Service = find_service(Host), @@ -762,9 +987,11 @@ web_page_host(_, Host, #request{path = [<<"muc">> | RPath], lang = Lang} = R) -> web_page_host(Acc, _, _) -> Acc. + %%--------------- %% WebAdmin MUC Host Page + webadmin_muc_host(Host, Service, [<<"create-room">> | RPath], @@ -985,7 +1212,7 @@ webadmin_muc_host(_Host, {<<"options/">>, <<"Options">>}, {<<"subscribers/">>, <<"Subscribers">>}, {<<"destroy/">>, <<"Destroy">>}], - Get = [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Get = [?XE(<<"ul">>, [ ?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems ])], Title ++ Breadcrumb ++ Get; webadmin_muc_host(_Host, Service, [<<"rooms">> | RPath], R, _Lang, Level, PageTitle) -> Title = ?H1GL(PageTitle, <<"modules/#mod_muc">>, <<"mod_muc">>), @@ -993,16 +1220,16 @@ webadmin_muc_host(_Host, Service, [<<"rooms">> | RPath], R, _Lang, Level, PageTi Columns = [<<"jid">>, <<"occupants">>], Rows = lists:map(fun(NameService) -> - #jid{user = Name} = jid:decode(NameService), - {make_command(echo, - R, - [{<<"sentence">>, jid:encode({Name, Service, <<"">>})}], - [{only, raw_and_value}, - {result_links, [{sentence, room, 3 + Level, <<"">>}]}]), - make_command(get_room_occupants_number, - R, - [{<<"room">>, Name}, {<<"service">>, Service}], - [{only, raw_and_value}])} + #jid{user = Name} = jid:decode(NameService), + {make_command(echo, + R, + [{<<"sentence">>, jid:encode({Name, Service, <<"">>})}], + [{only, raw_and_value}, + {result_links, [{sentence, room, 3 + Level, <<"">>}]}]), + make_command(get_room_occupants_number, + R, + [{<<"room">>, Name}, {<<"service">>, Service}], + [{only, raw_and_value}])} end, make_command_raw_value(muc_online_rooms, R, [{<<"service">>, Service}])), Get = [make_command(muc_online_rooms, R, [], [{only, presentation}]), @@ -1019,11 +1246,12 @@ webadmin_muc_host(_Host, Service, [], _R, Lang, _Level, PageTitle) -> {<<"rooms-empty/">>, <<"Rooms Empty">>}, {<<"rooms-unused/">>, <<"Rooms Unused">>}, {<<"nick-register/">>, <<"Nick Register">>}], - Get = [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Get = [?XE(<<"ul">>, [ ?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- MenuItems ])], Title ++ Breadcrumb ++ Get; webadmin_muc_host(_Host, _Service, _RPath, _R, _Lang, _Level, _PageTitle) -> []. + make_breadcrumb({service, Service}) -> make_breadcrumb([Service]); make_breadcrumb({service_section, Level, Service, Section, RPath}) -> @@ -1045,27 +1273,28 @@ make_breadcrumb({room_section, Level, Service, Section, Name, R, RPath}) -> [{only, value}, {result_links, [{sentence, room, 3 + Level, <<"">>}]}]), separator, - Section - | RPath]); + Section | RPath]); make_breadcrumb(Elements) -> - lists:map(fun ({xmlel, _, _, _} = Xmlel) -> + lists:map(fun({xmlel, _, _, _} = Xmlel) -> Xmlel; - (<<"sort">>) -> + (<<"sort">>) -> ?C(<<" +">>); - (<<"page">>) -> + (<<"page">>) -> ?C(<<" #">>); - (separator) -> + (separator) -> ?C(<<" > ">>); - (Bin) when is_binary(Bin) -> + (Bin) when is_binary(Bin) -> ?C(Bin); - ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> + ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> ?AC(binary:copy(<<"../">>, Level), Bin) end, Elements). + %%--------------- %% + %% Returns: {normal | reverse, Integer} get_sort_query(Q) -> case catch get_sort_query2(Q) of @@ -1075,6 +1304,7 @@ get_sort_query(Q) -> {normal, 1} end. + get_sort_query2(Q) -> {value, {_, Binary}} = lists:keysearch(<<"sort">>, 1, Q), Integer = list_to_integer(string:strip(binary_to_list(Binary), right, $/)), @@ -1085,6 +1315,7 @@ get_sort_query2(Q) -> {ok, {reverse, abs(Integer)}} end. + webadmin_muc(#request{q = Q} = R, Lang) -> {Sort_direction, Sort_column} = get_sort_query(Q), Host = global, @@ -1095,13 +1326,13 @@ webadmin_muc(#request{q = Q} = R, Lang) -> Rooms_prepared = prepare_rooms_infos(Rooms_sorted), TList = lists:map(fun([RoomJid | Room]) -> - JidLink = - make_command(echo, - R, - [{<<"sentence">>, RoomJid}], - [{only, value}, - {result_links, [{sentence, room, 1, <<"">>}]}]), - ?XE(<<"tr">>, [?XE(<<"td">>, [JidLink]) | [?XC(<<"td">>, E) || E <- Room]]) + JidLink = + make_command(echo, + R, + [{<<"sentence">>, RoomJid}], + [{only, value}, + {result_links, [{sentence, room, 1, <<"">>}]}]), + ?XE(<<"tr">>, [?XE(<<"td">>, [JidLink]) | [ ?XC(<<"td">>, E) || E <- Room ]]) end, Rooms_prepared), Titles = @@ -1116,14 +1347,14 @@ webadmin_muc(#request{q = Q} = R, Lang) -> ?T("Node")], {Titles_TR, _} = lists:mapfoldl(fun(Title, Num_column) -> - NCS = integer_to_binary(Num_column), - TD = ?XE(<<"td">>, - [?CT(Title), - ?C(<<" ">>), - ?AC(<<"?sort=", NCS/binary>>, <<"<">>), - ?C(<<" ">>), - ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]), - {TD, Num_column + 1} + NCS = integer_to_binary(Num_column), + TD = ?XE(<<"td">>, + [?CT(Title), + ?C(<<" ">>), + ?AC(<<"?sort=", NCS/binary>>, <<"<">>), + ?C(<<" ">>), + ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]), + {TD, Num_column + 1} end, 1, Titles), @@ -1131,6 +1362,7 @@ webadmin_muc(#request{q = Q} = R, Lang) -> ?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, Titles_TR)]), ?XE(<<"tbody">>, TList)])]. + sort_rooms(Direction, Column, Rooms) -> Rooms2 = lists:keysort(Column, Rooms), case Direction of @@ -1140,8 +1372,10 @@ sort_rooms(Direction, Column, Rooms) -> lists:reverse(Rooms2) end. + build_info_rooms(Rooms) -> - [build_info_room(Room) || Room <- Rooms]. + [ build_info_room(Room) || Room <- Rooms ]. + build_info_room({Name, Host, _ServerHost, Pid}) -> C = get_room_config(Pid), @@ -1176,12 +1410,15 @@ build_info_room({Name, Host, _ServerHost, Pid}) -> Title, Node}. + get_queue_last(Queue) -> List = p1_queue:to_list(Queue), lists:last(List). + prepare_rooms_infos(Rooms) -> - [prepare_room_info(Room) || Room <- Rooms]. + [ prepare_room_info(Room) || Room <- Rooms ]. + prepare_room_info(Room_info) -> {NameHost, @@ -1204,6 +1441,7 @@ prepare_room_info(Room_info) -> Title, misc:atom_to_binary(Node)]. + justcreated_to_binary(J) when is_integer(J) -> JNow = misc:usec_to_now(J), {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(JNow), @@ -1212,31 +1450,34 @@ justcreated_to_binary(J) when is_integer(J) -> justcreated_to_binary(J) when is_atom(J) -> misc:atom_to_binary(J). + %%-------------------- %% Web Admin Host User + web_menu_hostuser(Acc, _Host, _Username, _Lang) -> - Acc - ++ [{<<"muc-rooms">>, <<"MUC Rooms Online">>}, - {<<"muc-affiliations">>, <<"MUC Rooms Affiliations">>}, - {<<"muc-sub">>, <<"MUC Rooms Subscriptions">>}, - {<<"muc-register">>, <<"MUC Service Registration">>}]. + Acc ++ + [{<<"muc-rooms">>, <<"MUC Rooms Online">>}, + {<<"muc-affiliations">>, <<"MUC Rooms Affiliations">>}, + {<<"muc-sub">>, <<"MUC Rooms Subscriptions">>}, + {<<"muc-register">>, <<"MUC Service Registration">>}]. + web_page_hostuser(_, Host, User, #request{path = [<<"muc-rooms">> | RPath]} = R) -> Level = 5 + length(RPath), - Res = ?H1GL(<<"MUC Rooms Online">>, <<"modules/#mod_muc">>, <<"mod_muc">>) - ++ [make_command(get_user_rooms, - R, - [{<<"user">>, User}, {<<"host">>, Host}], - [{table_options, {2, RPath}}, - {result_links, [{room, room, Level, <<"">>}]}])], + Res = ?H1GL(<<"MUC Rooms Online">>, <<"modules/#mod_muc">>, <<"mod_muc">>) ++ + [make_command(get_user_rooms, + R, + [{<<"user">>, User}, {<<"host">>, Host}], + [{table_options, {2, RPath}}, + {result_links, [{room, room, Level, <<"">>}]}])], {stop, Res}; web_page_hostuser(_, Host, User, #request{path = [<<"muc-affiliations">>]} = R) -> Jid = jid:encode( - jid:make(User, Host)), - Res = ?H1GL(<<"MUC Rooms Affiliations">>, <<"modules/#mod_muc">>, <<"mod_muc">>) - ++ [make_command(set_room_affiliation, R, [{<<"jid">>, Jid}], []), - make_command(get_room_affiliation, R, [{<<"jid">>, Jid}], [])], + jid:make(User, Host)), + Res = ?H1GL(<<"MUC Rooms Affiliations">>, <<"modules/#mod_muc">>, <<"mod_muc">>) ++ + [make_command(set_room_affiliation, R, [{<<"jid">>, Jid}], []), + make_command(get_room_affiliation, R, [{<<"jid">>, Jid}], [])], {stop, Res}; web_page_hostuser(_, Host, User, #request{path = [<<"muc-sub">> | RPath]} = R) -> Title = @@ -1254,68 +1495,74 @@ web_page_hostuser(_, Host, User, #request{path = [<<"muc-sub">> | RPath]} = R) - {stop, Title ++ Get ++ Set}; web_page_hostuser(_, Host, User, #request{path = [<<"muc-register">>]} = R) -> Jid = jid:encode( - jid:make(User, Host)), - Res = ?H1GL(<<"MUC Service Registration">>, <<"modules/#mod_muc">>, <<"mod_muc">>) - ++ [make_command(muc_register_nick, R, [{<<"jid">>, Jid}], []), - make_command(muc_unregister_nick, R, [{<<"jid">>, Jid}], [])], + jid:make(User, Host)), + Res = ?H1GL(<<"MUC Service Registration">>, <<"modules/#mod_muc">>, <<"mod_muc">>) ++ + [make_command(muc_register_nick, R, [{<<"jid">>, Jid}], []), + make_command(muc_unregister_nick, R, [{<<"jid">>, Jid}], [])], {stop, Res}; web_page_hostuser(Acc, _, _, _) -> Acc. %% @format-end + %%---------------------------- %% Create/Delete Room %%---------------------------- --spec create_room(Name::binary(), Host::binary(), ServerHost::binary()) -> ok | error. + +-spec create_room(Name :: binary(), Host :: binary(), ServerHost :: binary()) -> ok | error. %% @doc Create a room immediately with the default options. create_room(Name1, Host1, ServerHost) -> create_room_with_opts(Name1, Host1, ServerHost, []). + create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) -> ServerHost = validate_host(ServerHost1, <<"serverhost">>), case get_room_pid_validate(Name1, Host1, <<"service">>) of - {room_not_found, Name, Host} -> - %% Get the default room options from the muc configuration - DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), - %% Change default room options as required - FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts], - RoomOpts = lists:ukeymerge(1, - lists:keysort(1, FormattedRoomOpts), - lists:keysort(1, DefRoomOpts)), - case mod_muc:create_room(Host, Name, RoomOpts) of - ok -> - ok; - {error, _} -> - throw({error, "Unable to start room"}) - end; - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "Room already exists"}) + {room_not_found, Name, Host} -> + %% Get the default room options from the muc configuration + DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), + %% Change default room options as required + FormattedRoomOpts = [ format_room_option(Opt, Val) || {Opt, Val} <- CustomRoomOpts ], + RoomOpts = lists:ukeymerge(1, + lists:keysort(1, FormattedRoomOpts), + lists:keysort(1, DefRoomOpts)), + case mod_muc:create_room(Host, Name, RoomOpts) of + ok -> + ok; + {error, _} -> + throw({error, "Unable to start room"}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "Room already exists"}) end. + %% Create the room only in the database. %% It is required to restart the MUC service for the room to appear. muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> io:format("Creating room ~ts@~ts~n", [Name, Host]), mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). --spec destroy_room(Name::binary(), Host::binary()) -> ok | {error, room_not_exists}. + +-spec destroy_room(Name :: binary(), Host :: binary()) -> ok | {error, room_not_exists}. %% @doc Destroy the room immediately. %% If the room has participants, they are not notified that the room was destroyed; %% they will notice when they try to chat and receive an error that the room doesn't exist. destroy_room(Name1, Service1) -> case get_room_pid_validate(Name1, Service1, <<"service">>) of - {room_not_found, _, _} -> - throw({error, "Room doesn't exists"}); - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - {Pid, _, _} -> - mod_muc_room:destroy(Pid), - ok + {room_not_found, _, _} -> + throw({error, "Room doesn't exists"}); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + {Pid, _, _} -> + mod_muc_room:destroy(Pid), + ok end. + destroy_room({N, H, SH}) -> io:format("Destroying room: ~ts@~ts - vhost: ~ts~n", [N, H, SH]), destroy_room(N, H). @@ -1328,14 +1575,16 @@ destroy_room({N, H, SH}) -> %% The format of the file is: one chatroom JID per line %% The file encoding must be UTF-8 + destroy_rooms_file(Filename) -> {ok, F} = file:open(Filename, [read]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), - [destroy_room(A) || A <- Rooms], + [ destroy_room(A) || A <- Rooms ], ok. + read_rooms(_F, eof, L) -> L; read_rooms(F, no_room, L) -> @@ -1345,62 +1594,82 @@ read_rooms(F, RJID, L) -> RJID2 = read_room(F), read_rooms(F, RJID2, [RJID | L]). + read_room(F) -> case io:get_line(F, "") of - eof -> eof; - String -> - case io_lib:fread("~ts", String) of - {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID)); - {error, What} -> - io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) - end + eof -> eof; + String -> + case io_lib:fread("~ts", String) of + {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID)); + {error, What} -> + io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) + end end. + %% This function is quite rudimentary %% and may not be accurate split_roomjid(RoomJID) -> split_roomjid2(binary:split(RoomJID, <<"@">>)). + + split_roomjid2([Name, Host]) -> [_MUC_service_name, ServerHost] = binary:split(Host, <<".">>), {Name, Host, ServerHost}; split_roomjid2(_) -> no_room. + %%---------------------------- %% Create Rooms in File %%---------------------------- + create_rooms_file(Filename) -> {ok, F} = file:open(Filename, [read]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), HostsDetails = get_hosts_details(Rooms), - [muc_create_room(HostsDetails, A) || A <- Rooms], + [ muc_create_room(HostsDetails, A) || A <- Rooms ], ok. + muc_create_room(HostsDetails, {_, Host, _} = RoomTuple) -> {_Host, ServerHost, DefRoomOpts} = get_host_details(Host, HostsDetails), muc_create_room(ServerHost, RoomTuple, DefRoomOpts). + get_hosts_details(Rooms) -> - Hosts = lists_uniq([Host || {_, Host, _} <- Rooms]), + Hosts = lists_uniq([ Host || {_, Host, _} <- Rooms ]), lists:map(fun(H) -> SH = get_room_serverhost(H), {H, SH, mod_muc_opt:default_room_options(SH)} - end, Hosts). + end, + Hosts). + -ifdef(OTP_BELOW_25). + + lists_uniq(List) -> lists:usort(List). + + -else. + + lists_uniq(List) -> lists:uniq(List). + + -endif. + get_host_details(Host, ServerHostsDetails) -> lists:keyfind(Host, 1, ServerHostsDetails). + %%--------------------------------- %% List/Delete Unused/Empty Rooms %%--------------------------------- @@ -1408,29 +1677,38 @@ get_host_details(Host, ServerHostsDetails) -> %%--------------- %% Control + rooms_unused_list(Service, Days) -> rooms_report(unused, list, Service, Days). + + rooms_unused_destroy(Service, Days) -> rooms_report(unused, destroy, Service, Days). + rooms_empty_list(Service) -> rooms_report(empty, list, Service, 0). + + rooms_empty_destroy(Service) -> rooms_report(empty, destroy, Service, 0). + rooms_empty_destroy_restuple(Service) -> DestroyedRooms = rooms_report(empty, destroy, Service, 0), NumberBin = integer_to_binary(length(DestroyedRooms)), {ok, <<"Destroyed rooms: ", NumberBin/binary>>}. + rooms_report(Method, Action, Service, Days) -> {NA, NP, RP} = muc_unused(Method, Action, Service, Days), io:format("rooms ~ts: ~p out of ~p~n", [Method, NP, NA]), - [<> || {R, H, _SH, _P} <- RP]. + [ <> || {R, H, _SH, _P} <- RP ]. + muc_unused(Method, Action, Service, Last_allowed) -> %% Get all required info about all existing rooms - Rooms_all = get_all_rooms(Service, erlang:system_time(microsecond) - Last_allowed*24*60*60*1000), + Rooms_all = get_all_rooms(Service, erlang:system_time(microsecond) - Last_allowed * 24 * 60 * 60 * 1000), %% Decide which ones pass the requirements Rooms_pass = decide_rooms(Method, Rooms_all, Last_allowed), @@ -1443,157 +1721,175 @@ muc_unused(Method, Action, Service, Last_allowed) -> {Num_rooms_all, Num_rooms_pass, Rooms_pass}. + %%--------------- %% Get info + get_online_rooms(ServiceArg) -> Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> - ServerHost = get_room_serverhost(Host), - [{RoomName, RoomHost, ServerHost, Pid} - || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)] - end, Hosts). + ServerHost = get_room_serverhost(Host), + [ {RoomName, RoomHost, ServerHost, Pid} + || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host) ] + end, + Hosts). + get_all_rooms(ServiceArg, Timestamp) -> Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> get_all_rooms2(Host, Timestamp) - end, Hosts). + end, + Hosts). + get_all_rooms2(Host, Timestamp) -> ServerHost = ejabberd_router:host_of_route(Host), OnlineRooms = get_online_rooms(Host), OnlineMap = lists:foldl( - fun({Room, _, _, _}, Map) -> - Map#{Room => 1} - end, #{}, OnlineRooms), + fun({Room, _, _, _}, Map) -> + Map#{Room => 1} + end, + #{}, + OnlineRooms), Mod = gen_mod:db_mod(ServerHost, mod_muc), DbRooms = - case {erlang:function_exported(Mod, get_rooms_without_subscribers, 2), - erlang:function_exported(Mod, get_hibernated_rooms_older_than, 3)} of - {_, true} -> - Mod:get_hibernated_rooms_older_than(ServerHost, Host, Timestamp); - {true, _} -> - Mod:get_rooms_without_subscribers(ServerHost, Host); - _ -> - Mod:get_rooms(ServerHost, Host) - end, + case {erlang:function_exported(Mod, get_rooms_without_subscribers, 2), + erlang:function_exported(Mod, get_hibernated_rooms_older_than, 3)} of + {_, true} -> + Mod:get_hibernated_rooms_older_than(ServerHost, Host, Timestamp); + {true, _} -> + Mod:get_rooms_without_subscribers(ServerHost, Host); + _ -> + Mod:get_rooms(ServerHost, Host) + end, StoredRooms = lists:filtermap( - fun(#muc_room{name_host = {Room, _}, opts = Opts}) -> - case maps:is_key(Room, OnlineMap) of - true -> - false; - _ -> - {true, {Room, Host, ServerHost, Opts}} - end - end, DbRooms), + fun(#muc_room{name_host = {Room, _}, opts = Opts}) -> + case maps:is_key(Room, OnlineMap) of + true -> + false; + _ -> + {true, {Room, Host, ServerHost, Opts}} + end + end, + DbRooms), OnlineRooms ++ StoredRooms. + get_room_config(Room_pid) -> {ok, R} = mod_muc_room:get_config(Room_pid), R. + get_room_state(Room_pid) -> {ok, R} = mod_muc_room:get_state(Room_pid), R. + %%--------------- %% Decide + decide_rooms(Method, Rooms, Last_allowed) -> Decide = fun(R) -> decide_room(Method, R, Last_allowed) end, lists:filter(Decide, Rooms). + decide_room(unused, {_Room_name, _Host, ServerHost, Room_pid}, Last_allowed) -> NodeStartTime = erlang:system_time(microsecond) - - 1000000*(erlang:monotonic_time(second)-ejabberd_config:get_node_start()), + 1000000 * (erlang:monotonic_time(second) - ejabberd_config:get_node_start()), OnlyHibernated = case mod_muc_opt:hibernation_timeout(ServerHost) of - Value when Value < Last_allowed*24*60*60*1000 -> - true; - _ -> - false - end, + Value when Value < Last_allowed * 24 * 60 * 60 * 1000 -> + true; + _ -> + false + end, {Just_created, Num_users} = - case Room_pid of - Pid when is_pid(Pid) andalso OnlyHibernated -> - {erlang:system_time(microsecond), 0}; - Pid when is_pid(Pid) -> - case mod_muc_room:get_state(Room_pid) of - {ok, #state{just_created = JC, users = U}} -> - {JC, maps:size(U)}; - _ -> - {erlang:system_time(microsecond), 0} - end; - Opts -> - case lists:keyfind(hibernation_time, 1, Opts) of - false -> - {NodeStartTime, 0}; - {_, undefined} -> - {NodeStartTime, 0}; - {_, T} -> - {T, 0} - end - end, + case Room_pid of + Pid when is_pid(Pid) andalso OnlyHibernated -> + {erlang:system_time(microsecond), 0}; + Pid when is_pid(Pid) -> + case mod_muc_room:get_state(Room_pid) of + {ok, #state{just_created = JC, users = U}} -> + {JC, maps:size(U)}; + _ -> + {erlang:system_time(microsecond), 0} + end; + Opts -> + case lists:keyfind(hibernation_time, 1, Opts) of + false -> + {NodeStartTime, 0}; + {_, undefined} -> + {NodeStartTime, 0}; + {_, T} -> + {T, 0} + end + end, Last = case Just_created of - true -> - 0; - _ -> - (erlang:system_time(microsecond) - - Just_created) div 1000000 - end, + true -> + 0; + _ -> + (erlang:system_time(microsecond) - + Just_created) div 1000000 + end, case {Num_users, seconds_to_days(Last)} of - {0, Last_days} when (Last_days >= Last_allowed) -> - true; - _ -> - false + {0, Last_days} when (Last_days >= Last_allowed) -> + true; + _ -> + false end; decide_room(empty, {Room_name, Host, ServerHost, Room_pid}, _Last_allowed) -> case gen_mod:is_loaded(ServerHost, mod_mam) of - true -> - Room_options = case Room_pid of - _ when is_pid(Room_pid) -> - get_room_options(Room_pid); - Opts -> - Opts - end, - case lists:keyfind(<<"mam">>, 1, Room_options) of - {<<"mam">>, <<"true">>} -> - mod_mam:is_empty_for_room(ServerHost, Room_name, Host); - _ -> - false - end; - _ -> - false + true -> + Room_options = case Room_pid of + _ when is_pid(Room_pid) -> + get_room_options(Room_pid); + Opts -> + Opts + end, + case lists:keyfind(<<"mam">>, 1, Room_options) of + {<<"mam">>, <<"true">>} -> + mod_mam:is_empty_for_room(ServerHost, Room_name, Host); + _ -> + false + end; + _ -> + false end. + seconds_to_days(S) -> - S div (60*60*24). + S div (60 * 60 * 24). + %%--------------- %% Act + act_on_rooms(Method, Action, Rooms) -> Delete = fun(Room) -> - act_on_room(Method, Action, Room) - end, + act_on_room(Method, Action, Room) + end, lists:foreach(Delete, Rooms). + act_on_room(Method, destroy, {N, H, _SH, Pid}) -> Message = iolist_to_binary(io_lib:format( - <<"Room destroyed by rooms_~s_destroy.">>, [Method])), + <<"Room destroyed by rooms_~s_destroy.">>, [Method])), case Pid of - V when is_pid(V) -> - mod_muc_room:destroy(Pid, Message); - _ -> - case get_room_pid(N, H) of - Pid2 when is_pid(Pid2) -> - mod_muc_room:destroy(Pid2, Message); - _ -> - ok - end + V when is_pid(V) -> + mod_muc_room:destroy(Pid, Message); + _ -> + case get_room_pid(N, H) of + Pid2 when is_pid(Pid2) -> + mod_muc_room:destroy(Pid2, Message); + _ -> + ok + end end; act_on_room(_Method, list, _) -> ok. @@ -1603,95 +1899,111 @@ act_on_room(_Method, list, _) -> %% Change Room Option %%---------------------------- + get_room_occupants(Room, Host) -> case get_room_pid_validate(Room, Host, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> get_room_occupants(Pid); - _ -> throw({error, room_not_found}) + {Pid, _, _} when is_pid(Pid) -> get_room_occupants(Pid); + _ -> throw({error, room_not_found}) end. + get_room_occupants(Pid) -> S = get_room_state(Pid), lists:map( fun({_LJID, Info}) -> - {jid:encode(Info#user.jid), - Info#user.nick, - atom_to_list(Info#user.role)} + {jid:encode(Info#user.jid), + Info#user.nick, + atom_to_list(Info#user.role)} end, maps:to_list(S#state.users)). + get_room_occupants_number(Room, Host) -> case get_room_pid_validate(Room, Host, <<"service">>) of - {Pid, _, _} when is_pid(Pid)-> - {ok, #{occupants_number := N}} = mod_muc_room:get_info(Pid), - N; - _ -> - throw({error, room_not_found}) + {Pid, _, _} when is_pid(Pid) -> + {ok, #{occupants_number := N}} = mod_muc_room:get_info(Pid), + N; + _ -> + throw({error, room_not_found}) end. + %%---------------------------- %% Send Direct Invitation %%---------------------------- %% http://xmpp.org/extensions/xep-0249.html + send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) when is_binary(UsersString) -> UsersStrings = binary:split(UsersString, <<":">>, [global]), send_direct_invitation(RoomName, RoomService, Password, Reason, UsersStrings); send_direct_invitation(RoomName, RoomService, Password, Reason, UsersStrings) -> case jid:make(RoomName, RoomService) of - error -> - throw({error, "Invalid 'roomname' or 'service'"}); - RoomJid -> - XmlEl = build_invitation(Password, Reason, RoomJid), - Users = get_users_to_invite(RoomJid, UsersStrings), - [send_direct_invitation(RoomJid, UserJid, XmlEl) - || UserJid <- Users], - ok + error -> + throw({error, "Invalid 'roomname' or 'service'"}); + RoomJid -> + XmlEl = build_invitation(Password, Reason, RoomJid), + Users = get_users_to_invite(RoomJid, UsersStrings), + [ send_direct_invitation(RoomJid, UserJid, XmlEl) + || UserJid <- Users ], + ok end. + get_users_to_invite(RoomJid, UsersStrings) -> OccupantsTuples = get_room_occupants(RoomJid#jid.luser, - RoomJid#jid.lserver), - OccupantsJids = try [jid:decode(JidString) - || {JidString, _Nick, _} <- OccupantsTuples] - catch _:{bad_jid, _} -> throw({error, "Malformed JID of invited user"}) - end, + RoomJid#jid.lserver), + OccupantsJids = try + [ jid:decode(JidString) + || {JidString, _Nick, _} <- OccupantsTuples ] + catch + _:{bad_jid, _} -> throw({error, "Malformed JID of invited user"}) + end, lists:filtermap( fun(UserString) -> - UserJid = jid:decode(UserString), - Val = lists:all(fun(OccupantJid) -> - UserJid#jid.luser /= OccupantJid#jid.luser - orelse UserJid#jid.lserver /= OccupantJid#jid.lserver - end, - OccupantsJids), - case {UserJid#jid.luser, Val} of - {<<>>, _} -> false; - {_, true} -> {true, UserJid}; - _ -> false - end + UserJid = jid:decode(UserString), + Val = lists:all(fun(OccupantJid) -> + UserJid#jid.luser /= OccupantJid#jid.luser orelse + UserJid#jid.lserver /= OccupantJid#jid.lserver + end, + OccupantsJids), + case {UserJid#jid.luser, Val} of + {<<>>, _} -> false; + {_, true} -> {true, UserJid}; + _ -> false + end end, UsersStrings). + build_invitation(Password, Reason, RoomJid) -> - Invite = #x_conference{jid = RoomJid, - password = case Password of - <<"none">> -> <<>>; - _ -> Password - end, - reason = case Reason of - <<"none">> -> <<>>; - _ -> Reason - end}, + Invite = #x_conference{ + jid = RoomJid, + password = case Password of + <<"none">> -> <<>>; + _ -> Password + end, + reason = case Reason of + <<"none">> -> <<>>; + _ -> Reason + end + }, #message{sub_els = [Invite]}. + send_direct_invitation(FromJid, UserJid, Msg) -> ejabberd_router:route(xmpp:set_from_to(Msg, FromJid, UserJid)). + %%---------------------------- %% Change Room Option %%---------------------------- --spec change_room_option(Name::binary(), Service::binary(), Option::binary(), - Value::atom() | integer() | string()) -> ok | mod_muc_log_not_enabled. + +-spec change_room_option(Name :: binary(), + Service :: binary(), + Option :: binary(), + Value :: atom() | integer() | string()) -> ok | mod_muc_log_not_enabled. %% @doc Change an option in an existing room. %% Requires the name of the room, the MUC service where it exists, %% the option to change (for example title or max_users), @@ -1700,113 +2012,120 @@ send_direct_invitation(FromJid, UserJid, Msg) -> %% `change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)' change_room_option(Name, Service, OptionString, ValueString) -> case get_room_pid_validate(Name, Service, <<"service">>) of - {room_not_found, _, _} -> - throw({error, "Room not found"}); - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - {Pid, _, _} -> - {Option, Value} = format_room_option(OptionString, ValueString), - change_room_option(Pid, Option, Value) + {room_not_found, _, _} -> + throw({error, "Room not found"}); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + {Pid, _, _} -> + {Option, Value} = format_room_option(OptionString, ValueString), + change_room_option(Pid, Option, Value) end. + change_room_option(Pid, Option, Value) -> case {Option, - gen_mod:is_loaded((get_room_state(Pid))#state.server_host, mod_muc_log)} of - {logging, false} -> - mod_muc_log_not_enabled; - _ -> - Config = get_room_config(Pid), - Config2 = change_option(Option, Value, Config), - {ok, _} = mod_muc_room:set_config(Pid, Config2), - ok + gen_mod:is_loaded((get_room_state(Pid))#state.server_host, mod_muc_log)} of + {logging, false} -> + mod_muc_log_not_enabled; + _ -> + Config = get_room_config(Pid), + Config2 = change_option(Option, Value, Config), + {ok, _} = mod_muc_room:set_config(Pid, Config2), + ok end. + format_room_option(OptionString, ValueString) -> Option = misc:binary_to_atom(OptionString), Value = case Option of - title -> ValueString; - description -> ValueString; - password -> ValueString; - subject ->ValueString; - subject_author ->ValueString; - max_users -> try_convert_integer(Option, ValueString); - voice_request_min_interval -> try_convert_integer(Option, ValueString); - vcard -> ValueString; - vcard_xupdate when ValueString /= <<"undefined">>, - ValueString /= <<"external">> -> - ValueString; - lang -> ValueString; - pubsub -> ValueString; - affiliations -> - [parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)]; - subscribers -> - [parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)]; - allow_private_messages_from_visitors when - (ValueString == <<"anyone">>) or - (ValueString == <<"moderators">>) or - (ValueString == <<"nobody">>) -> binary_to_existing_atom(ValueString, utf8); - allowpm when - (ValueString == <<"anyone">>) or - (ValueString == <<"participants">>) or - (ValueString == <<"moderators">>) or - (ValueString == <<"none">>) -> binary_to_existing_atom(ValueString, utf8); - presence_broadcast when - (ValueString == <<"participant">>) or - (ValueString == <<"moderator">>) or - (ValueString == <<"visitor">>) -> binary_to_existing_atom(ValueString, utf8); - _ when ValueString == <<"true">> -> true; - _ when ValueString == <<"false">> -> false; + title -> ValueString; + description -> ValueString; + password -> ValueString; + subject -> ValueString; + subject_author -> ValueString; + max_users -> try_convert_integer(Option, ValueString); + voice_request_min_interval -> try_convert_integer(Option, ValueString); + vcard -> ValueString; + vcard_xupdate when ValueString /= <<"undefined">>, + ValueString /= <<"external">> -> + ValueString; + lang -> ValueString; + pubsub -> ValueString; + affiliations -> + [ parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>) ]; + subscribers -> + [ parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>) ]; + allow_private_messages_from_visitors when (ValueString == <<"anyone">>) or + (ValueString == <<"moderators">>) or + (ValueString == <<"nobody">>) -> binary_to_existing_atom(ValueString, utf8); + allowpm when (ValueString == <<"anyone">>) or + (ValueString == <<"participants">>) or + (ValueString == <<"moderators">>) or + (ValueString == <<"none">>) -> binary_to_existing_atom(ValueString, utf8); + presence_broadcast when (ValueString == <<"participant">>) or + (ValueString == <<"moderator">>) or + (ValueString == <<"visitor">>) -> binary_to_existing_atom(ValueString, utf8); + _ when ValueString == <<"true">> -> true; + _ when ValueString == <<"false">> -> false; _ -> throw_error(Option, ValueString) - end, + end, {Option, Value}. + try_convert_integer(Option, ValueString) -> try binary_to_integer(ValueString) of - I when is_integer(I) -> I - catch _:badarg -> - throw_error(Option, ValueString) + I when is_integer(I) -> I + catch + _:badarg -> + throw_error(Option, ValueString) end. + throw_error(O, V) -> throw({error, "Invalid value for that option", O, V}). + parse_affiliation_string(String) -> {Type, JidS} = case String of %% Old syntax - <<"owner:", Jid/binary>> -> {owner, Jid}; - <<"admin:", Jid/binary>> -> {admin, Jid}; - <<"member:", Jid/binary>> -> {member, Jid}; - <<"outcast:", Jid/binary>> -> {outcast, Jid}; + <<"owner:", Jid/binary>> -> {owner, Jid}; + <<"admin:", Jid/binary>> -> {admin, Jid}; + <<"member:", Jid/binary>> -> {member, Jid}; + <<"outcast:", Jid/binary>> -> {outcast, Jid}; %% New syntax - <<"owner=", Jid/binary>> -> {owner, Jid}; - <<"admin=", Jid/binary>> -> {admin, Jid}; - <<"member=", Jid/binary>> -> {member, Jid}; - <<"outcast=", Jid/binary>> -> {outcast, Jid}; - _ -> throw({error, "Invalid 'affiliation'"}) - end, + <<"owner=", Jid/binary>> -> {owner, Jid}; + <<"admin=", Jid/binary>> -> {admin, Jid}; + <<"member=", Jid/binary>> -> {member, Jid}; + <<"outcast=", Jid/binary>> -> {outcast, Jid}; + _ -> throw({error, "Invalid 'affiliation'"}) + end, try jid:decode(JidS) of - #jid{luser = U, lserver = S, lresource = R} -> - {{U, S, R}, {Type, <<>>}} - catch _:{bad_jid, _} -> - throw({error, "Malformed JID in affiliation"}) + #jid{luser = U, lserver = S, lresource = R} -> + {{U, S, R}, {Type, <<>>}} + catch + _:{bad_jid, _} -> + throw({error, "Malformed JID in affiliation"}) end. + parse_subscription_string(String) -> case str:tokens(String, <<"=:">>) of - [_] -> - throw({error, "Invalid 'subscribers' - missing nick"}); - [_, _] -> - throw({error, "Invalid 'subscribers' - missing nodes"}); - [JidS, Nick | Nodes] -> - Nodes2 = parse_nodes(Nodes, []), - try jid:decode(JidS) of - Jid -> - {Jid, Nick, Nodes2} - catch _:{bad_jid, _} -> - throw({error, "Malformed JID in 'subscribers'"}) - end + [_] -> + throw({error, "Invalid 'subscribers' - missing nick"}); + [_, _] -> + throw({error, "Invalid 'subscribers' - missing nodes"}); + [JidS, Nick | Nodes] -> + Nodes2 = parse_nodes(Nodes, []), + try jid:decode(JidS) of + Jid -> + {Jid, Nick, Nodes2} + catch + _:{bad_jid, _} -> + throw({error, "Malformed JID in 'subscribers'"}) + end end. + parse_nodes([], Acc) -> Acc; parse_nodes([<<"presence">> | Rest], Acc) -> @@ -1828,191 +2147,209 @@ parse_nodes([<<"subscribers">> | Rest], Acc) -> parse_nodes(_, _) -> throw({error, "Invalid 'subscribers' - unknown node name used"}). + -spec get_room_pid_validate(binary(), binary(), binary()) -> - {pid() | room_not_found | db_failure, binary(), binary()}. + {pid() | room_not_found | db_failure, binary(), binary()}. get_room_pid_validate(Name, Service, ServiceArg) -> Name2 = validate_room(Name), {ServerHost, Service2} = validate_muc2(Service, ServiceArg), case mod_muc:unhibernate_room(ServerHost, Service2, Name2) of - {error, notfound} -> - {room_not_found, Name2, Service2}; - {error, db_failure} -> - {db_failure, Name2, Service2}; - {ok, Pid} -> - {Pid, Name2, Service2} + {error, notfound} -> + {room_not_found, Name2, Service2}; + {error, db_failure} -> + {db_failure, Name2, Service2}; + {ok, Pid} -> + {Pid, Name2, Service2} end. + %% @doc Get the Pid of an existing MUC room, or 'room_not_found'. -spec get_room_pid(binary(), binary()) -> pid() | room_not_found | db_failure | invalid_service | unknown_service. get_room_pid(Name, Service) -> try get_room_serverhost(Service) of - ServerHost -> - case mod_muc:unhibernate_room(ServerHost, Service, Name) of - {error, notfound} -> - room_not_found; - {error, db_failure} -> - db_failure; - {ok, Pid} -> - Pid - end + ServerHost -> + case mod_muc:unhibernate_room(ServerHost, Service, Name) of + {error, notfound} -> + room_not_found; + {error, db_failure} -> + db_failure; + {ok, Pid} -> + Pid + end catch - error:{invalid_domain, _} -> - invalid_service; - error:{unregistered_route, _} -> - unknown_service + error:{invalid_domain, _} -> + invalid_service; + error:{unregistered_route, _} -> + unknown_service end. + room_diagnostics(Name, Service) -> try get_room_serverhost(Service) of - ServerHost -> - RMod = gen_mod:ram_db_mod(ServerHost, mod_muc), - case RMod:find_online_room(ServerHost, Name, Service) of - error -> - room_hibernated; - {ok, Pid} -> - case rpc:pinfo(Pid, [current_stacktrace, message_queue_len, messages]) of - [{_, R}, {_, QL}, {_, Q}] -> - #{stacktrace => R, queue_size => QL, queue => lists:sublist(Q, 10)}; - _ -> - unable_to_probe_process - end - end + ServerHost -> + RMod = gen_mod:ram_db_mod(ServerHost, mod_muc), + case RMod:find_online_room(ServerHost, Name, Service) of + error -> + room_hibernated; + {ok, Pid} -> + case rpc:pinfo(Pid, [current_stacktrace, message_queue_len, messages]) of + [{_, R}, {_, QL}, {_, Q}] -> + #{stacktrace => R, queue_size => QL, queue => lists:sublist(Q, 10)}; + _ -> + unable_to_probe_process + end + end catch - error:{invalid_domain, _} -> - invalid_service; - error:{unregistered_route, _} -> - unknown_service + error:{invalid_domain, _} -> + invalid_service; + error:{unregistered_route, _} -> + unknown_service end. + %% It is required to put explicitly all the options because %% the record elements are replaced at compile time. %% So, this can't be parametrized. change_option(Option, Value, Config) -> case Option of - allow_change_subj -> Config#config{allow_change_subj = Value}; - allowpm -> Config#config{allowpm = Value}; - allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value}; - allow_query_users -> Config#config{allow_query_users = Value}; - allow_subscription -> Config#config{allow_subscription = Value}; - allow_user_invites -> Config#config{allow_user_invites = Value}; - allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value}; - allow_visitor_status -> Config#config{allow_visitor_status = Value}; - allow_voice_requests -> Config#config{allow_voice_requests = Value}; - anonymous -> Config#config{anonymous = Value}; - captcha_protected -> Config#config{captcha_protected = Value}; - description -> Config#config{description = Value}; - enable_hats -> Config#config{enable_hats = Value}; - lang -> Config#config{lang = Value}; - logging -> Config#config{logging = Value}; - mam -> Config#config{mam = Value}; - max_users -> Config#config{max_users = Value}; - members_by_default -> Config#config{members_by_default = Value}; - members_only -> Config#config{members_only = Value}; - moderated -> Config#config{moderated = Value}; - password -> Config#config{password = Value}; - password_protected -> Config#config{password_protected = Value}; - persistent -> Config#config{persistent = Value}; - presence_broadcast -> Config#config{presence_broadcast = Value}; - public -> Config#config{public = Value}; - public_list -> Config#config{public_list = Value}; - pubsub -> Config#config{pubsub = Value}; - title -> Config#config{title = Value}; - vcard -> Config#config{vcard = Value}; - vcard_xupdate -> Config#config{vcard_xupdate = Value}; - voice_request_min_interval -> Config#config{voice_request_min_interval = Value} + allow_change_subj -> Config#config{allow_change_subj = Value}; + allowpm -> Config#config{allowpm = Value}; + allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value}; + allow_query_users -> Config#config{allow_query_users = Value}; + allow_subscription -> Config#config{allow_subscription = Value}; + allow_user_invites -> Config#config{allow_user_invites = Value}; + allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value}; + allow_visitor_status -> Config#config{allow_visitor_status = Value}; + allow_voice_requests -> Config#config{allow_voice_requests = Value}; + anonymous -> Config#config{anonymous = Value}; + captcha_protected -> Config#config{captcha_protected = Value}; + description -> Config#config{description = Value}; + enable_hats -> Config#config{enable_hats = Value}; + lang -> Config#config{lang = Value}; + logging -> Config#config{logging = Value}; + mam -> Config#config{mam = Value}; + max_users -> Config#config{max_users = Value}; + members_by_default -> Config#config{members_by_default = Value}; + members_only -> Config#config{members_only = Value}; + moderated -> Config#config{moderated = Value}; + password -> Config#config{password = Value}; + password_protected -> Config#config{password_protected = Value}; + persistent -> Config#config{persistent = Value}; + presence_broadcast -> Config#config{presence_broadcast = Value}; + public -> Config#config{public = Value}; + public_list -> Config#config{public_list = Value}; + pubsub -> Config#config{pubsub = Value}; + title -> Config#config{title = Value}; + vcard -> Config#config{vcard = Value}; + vcard_xupdate -> Config#config{vcard_xupdate = Value}; + voice_request_min_interval -> Config#config{voice_request_min_interval = Value} end. + %%---------------------------- %% Get Room Options %%---------------------------- + get_room_options(Name, Service) -> case get_room_pid_validate(Name, Service, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> get_room_options(Pid); - _ -> [] + {Pid, _, _} when is_pid(Pid) -> get_room_options(Pid); + _ -> [] end. + get_room_options(Pid) -> Config = get_room_config(Pid), get_options(Config). + get_options(Config) -> - Fields = [misc:atom_to_binary(Field) || Field <- record_info(fields, config)], + Fields = [ misc:atom_to_binary(Field) || Field <- record_info(fields, config) ], [config | ValuesRaw] = tuple_to_list(Config), Values = lists:map(fun(V) when is_atom(V) -> misc:atom_to_binary(V); (V) when is_integer(V) -> integer_to_binary(V); (V) when is_tuple(V); is_list(V) -> list_to_binary(hd(io_lib:format("~w", [V]))); - (V) -> V end, ValuesRaw), + (V) -> V + end, + ValuesRaw), lists:zip(Fields, Values). + %%---------------------------- %% Get Room Affiliations %%---------------------------- + %% @spec(Name::binary(), Service::binary()) -> %% [{Username::string(), Domain::string(), Role::string(), Reason::string()}] %% @doc Get the affiliations of the room Name@Service. get_room_affiliations(Name, Service) -> case get_room_pid_validate(Name, Service, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - %% Get the PID of the online room, then request its state - {ok, StateData} = mod_muc_room:get_state(Pid), - Affiliations = maps:to_list(StateData#state.affiliations), - lists:map( - fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> - {Uname, Domain, Aff, Reason}; - ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> - {Uname, Domain, Aff, <<>>} - end, Affiliations); - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist."}) + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID of the online room, then request its state + {ok, StateData} = mod_muc_room:get_state(Pid), + Affiliations = maps:to_list(StateData#state.affiliations), + lists:map( + fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff) -> + {Uname, Domain, Aff, Reason}; + ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff) -> + {Uname, Domain, Aff, <<>>} + end, + Affiliations); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) end. + %% @spec(Name::binary(), Service::binary()) -> %% [{JID::string(), Role::string(), Reason::string()}] %% @doc Get the affiliations of the room Name@Service. get_room_affiliations_v3(Name, Service) -> case get_room_pid_validate(Name, Service, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - %% Get the PID of the online room, then request its state - {ok, StateData} = mod_muc_room:get_state(Pid), - Affiliations = maps:to_list(StateData#state.affiliations), - lists:map( - fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> - Jid = makeencode(Uname, Domain), - {Jid, Aff, Reason}; - ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> - Jid = makeencode(Uname, Domain), - {Jid, Aff, <<>>} - end, Affiliations); - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist."}) + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID of the online room, then request its state + {ok, StateData} = mod_muc_room:get_state(Pid), + Affiliations = maps:to_list(StateData#state.affiliations), + lists:map( + fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff) -> + Jid = makeencode(Uname, Domain), + {Jid, Aff, Reason}; + ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff) -> + Jid = makeencode(Uname, Domain), + {Jid, Aff, <<>>} + end, + Affiliations); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) end. + get_room_history(Name, Service) -> case get_room_pid_validate(Name, Service, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - case mod_muc_room:get_state(Pid) of - {ok, StateData} -> - History = p1_queue:to_list((StateData#state.history)#lqueue.queue), - lists:map( - fun({_Nick, Packet, _HaveSubject, TimeStamp, _Size}) -> - {xmpp_util:encode_timestamp(TimeStamp), - ejabberd_web_admin:pretty_print_xml(xmpp:encode(Packet))} - end, History); - _ -> - throw({error, "Unable to fetch room state."}) - end; - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist."}) + {Pid, _, _} when is_pid(Pid) -> + case mod_muc_room:get_state(Pid) of + {ok, StateData} -> + History = p1_queue:to_list((StateData#state.history)#lqueue.queue), + lists:map( + fun({_Nick, Packet, _HaveSubject, TimeStamp, _Size}) -> + {xmpp_util:encode_timestamp(TimeStamp), + ejabberd_web_admin:pretty_print_xml(xmpp:encode(Packet))} + end, + History); + _ -> + throw({error, "Unable to fetch room state."}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) end. + %%---------------------------- %% Get Room Affiliation %%---------------------------- @@ -2021,26 +2358,30 @@ get_room_history(Name, Service) -> %% {Affiliation::string()} %% @doc Get affiliation of a user in the room Name@Service. + get_room_affiliation(Name, Service, JID) -> case get_room_pid_validate(Name, Service, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - %% Get the PID of the online room, then request its state - {ok, StateData} = mod_muc_room:get_state(Pid), - UserJID = jid:decode(JID), - mod_muc_room:get_affiliation(UserJID, StateData); - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist."}) + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID of the online room, then request its state + {ok, StateData} = mod_muc_room:get_state(Pid), + UserJID = jid:decode(JID), + mod_muc_room:get_affiliation(UserJID, StateData); + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist."}) end. + %%---------------------------- %% Change Room Affiliation %%---------------------------- + set_room_affiliation(Name, Service, User, Host, AffiliationString) -> set_room_affiliation(Name, Service, makeencode(User, Host), AffiliationString). + %% @spec(Name, Service, JID, AffiliationString) -> ok | {error, Error} %% Name = binary() %% Service = binary() @@ -2060,29 +2401,34 @@ set_room_affiliation(Name, Service, JID, AffiliationString) -> throw({error, "Invalid affiliation"}) end, case get_room_pid_validate(Name, Service, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - %% Get the PID for the online room so we can get the state of the room - case mod_muc_room:change_item(Pid, jid:decode(JID), affiliation, Affiliation, <<"">>) of - {ok, _} -> - ok; - {error, notfound} -> - throw({error, "Room doesn't exists"}); - {error, _} -> - throw({error, "Unable to perform change"}) - end; - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "Room doesn't exists"}) + {Pid, _, _} when is_pid(Pid) -> + %% Get the PID for the online room so we can get the state of the room + case mod_muc_room:change_item(Pid, jid:decode(JID), affiliation, Affiliation, <<"">>) of + {ok, _} -> + ok; + {error, notfound} -> + throw({error, "Room doesn't exists"}); + {error, _} -> + throw({error, "Unable to perform change"}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "Room doesn't exists"}) end. + %%% %%% MUC Subscription %%% + subscribe_room(Username, Host, Nick, Name, Service, Nodes) -> - subscribe_room(makeencode(Username, Host), Nick, - makeencode(Name, Service), Nodes). + subscribe_room(makeencode(Username, Host), + Nick, + makeencode(Name, Service), + Nodes). + subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> -> throw({error, "Nickname must be set"}); @@ -2091,37 +2437,41 @@ subscribe_room(User, Nick, Room, Nodes) when is_binary(Nodes) -> subscribe_room(User, Nick, Room, NodeList); subscribe_room(User, Nick, Room, NodeList) -> try jid:decode(Room) of - #jid{luser = Name, lserver = Host} when Name /= <<"">> -> - try jid:decode(User) of - UserJID1 -> - UserJID = jid:replace_resource(UserJID1, <<"modmucadmin">>), - case get_room_pid_validate(Name, Host, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - case mod_muc_room:subscribe( - Pid, UserJID, Nick, NodeList) of - {ok, SubscribedNodes} -> - SubscribedNodes; - {error, Reason} -> - throw({error, binary_to_list(Reason)}) - end; - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist"}) - end - catch _:{bad_jid, _} -> - throw({error, "Malformed user JID"}) - end; - _ -> - throw({error, "Malformed room JID"}) - catch _:{bad_jid, _} -> - throw({error, "Malformed room JID"}) + #jid{luser = Name, lserver = Host} when Name /= <<"">> -> + try jid:decode(User) of + UserJID1 -> + UserJID = jid:replace_resource(UserJID1, <<"modmucadmin">>), + case get_room_pid_validate(Name, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + case mod_muc_room:subscribe( + Pid, UserJID, Nick, NodeList) of + {ok, SubscribedNodes} -> + SubscribedNodes; + {error, Reason} -> + throw({error, binary_to_list(Reason)}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist"}) + end + catch + _:{bad_jid, _} -> + throw({error, "Malformed user JID"}) + end; + _ -> + throw({error, "Malformed room JID"}) + catch + _:{bad_jid, _} -> + throw({error, "Malformed room JID"}) end. + subscribe_room_many_v3(List, Name, Service, Nodes) -> - List2 = [{makeencode(User, Host), Nick} || {User, Host, Nick} <- List], + List2 = [ {makeencode(User, Host), Nick} || {User, Host, Nick} <- List ], subscribe_room_many(List2, makeencode(Name, Service), Nodes). + subscribe_room_many(Users, Room, Nodes) -> MaxUsers = mod_muc_admin_opt:subscribe_room_many_max_users(global), if @@ -2131,190 +2481,219 @@ subscribe_room_many(Users, Room, Nodes) -> lists:foreach( fun({User, Nick}) -> subscribe_room(User, Nick, Room, Nodes) - end, Users) + end, + Users) end. + unsubscribe_room(User, Host, Name, Service) -> unsubscribe_room(makeencode(User, Host), makeencode(Name, Service)). + unsubscribe_room(User, Room) -> try jid:decode(Room) of - #jid{luser = Name, lserver = Host} when Name /= <<"">> -> - try jid:decode(User) of - UserJID -> - case get_room_pid_validate(Name, Host, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - case mod_muc_room:unsubscribe(Pid, UserJID) of - ok -> - ok; - {error, Reason} -> - throw({error, binary_to_list(Reason)}) - end; - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist"}) - end - catch _:{bad_jid, _} -> - throw({error, "Malformed user JID"}) - end; - _ -> - throw({error, "Malformed room JID"}) - catch _:{bad_jid, _} -> - throw({error, "Malformed room JID"}) + #jid{luser = Name, lserver = Host} when Name /= <<"">> -> + try jid:decode(User) of + UserJID -> + case get_room_pid_validate(Name, Host, <<"service">>) of + {Pid, _, _} when is_pid(Pid) -> + case mod_muc_room:unsubscribe(Pid, UserJID) of + ok -> + ok; + {error, Reason} -> + throw({error, binary_to_list(Reason)}) + end; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist"}) + end + catch + _:{bad_jid, _} -> + throw({error, "Malformed user JID"}) + end; + _ -> + throw({error, "Malformed room JID"}) + catch + _:{bad_jid, _} -> + throw({error, "Malformed room JID"}) end. + get_subscribers(Name, Host) -> case get_room_pid_validate(Name, Host, <<"service">>) of - {Pid, _, _} when is_pid(Pid) -> - {ok, JIDList} = mod_muc_room:get_subscribers(Pid), - [jid:encode(jid:remove_resource(J)) || J <- JIDList]; - {db_failure, _Name, _Host} -> - throw({error, "Database error"}); - _ -> - throw({error, "The room does not exist"}) + {Pid, _, _} when is_pid(Pid) -> + {ok, JIDList} = mod_muc_room:get_subscribers(Pid), + [ jid:encode(jid:remove_resource(J)) || J <- JIDList ]; + {db_failure, _Name, _Host} -> + throw({error, "Database error"}); + _ -> + throw({error, "The room does not exist"}) end. + %%---------------------------- %% Utils %%---------------------------- + makeencode(User, Host) -> jid:encode(jid:make(User, Host)). --spec validate_host(Name :: binary(), ArgName::binary()) -> binary(). + +-spec validate_host(Name :: binary(), ArgName :: binary()) -> binary(). validate_host(Name, ArgName) -> case jid:nameprep(Name) of - error -> - throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); - Name2 -> - case lists:member(Name2, ejabberd_option:hosts()) of - false -> - throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}); - _ -> - Name2 - end + error -> + throw({error, <<"Invalid value of '", ArgName/binary, "'">>}); + Name2 -> + case lists:member(Name2, ejabberd_option:hosts()) of + false -> + throw({error, <<"Unknown host passed in '", ArgName/binary, "'">>}); + _ -> + Name2 + end end. --spec validate_user(Name :: binary(), ArgName::binary()) -> binary(). + +-spec validate_user(Name :: binary(), ArgName :: binary()) -> binary(). validate_user(Name, ArgName) -> case jid:nodeprep(Name) of - error -> - throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); - Name2 -> - Name2 + error -> + throw({error, <<"Invalid value of '", ArgName/binary, "'">>}); + Name2 -> + Name2 end. --spec validate_muc(Name :: binary(), ArgName::binary()) -> binary(). + +-spec validate_muc(Name :: binary(), ArgName :: binary()) -> binary(). validate_muc(Name, ArgName) -> case jid:nameprep(Name) of - error -> - throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); - Name2 -> - try get_room_serverhost(Name2) of - _ -> Name2 - catch - error:{invalid_domain, _} -> - throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}); - error:{unregistered_route, _} -> - throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}) - end + error -> + throw({error, <<"Invalid value of '", ArgName/binary, "'">>}); + Name2 -> + try get_room_serverhost(Name2) of + _ -> Name2 + catch + error:{invalid_domain, _} -> + throw({error, <<"Unknown host passed in '", ArgName/binary, "'">>}); + error:{unregistered_route, _} -> + throw({error, <<"Unknown host passed in '", ArgName/binary, "'">>}) + end end. --spec validate_muc2(Name :: binary(), ArgName::binary()) -> {binary(), binary()}. + +-spec validate_muc2(Name :: binary(), ArgName :: binary()) -> {binary(), binary()}. validate_muc2(Name, ArgName) -> case jid:nameprep(Name) of - error -> - throw({error, <<"Invalid value of '",ArgName/binary,"'">>}); - Name2 -> - try get_room_serverhost(Name2) of - Host -> {Host, Name2} - catch - error:{invalid_domain, _} -> - throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}); - error:{unregistered_route, _} -> - throw({error, <<"Unknown host passed in '",ArgName/binary,"'">>}) - end + error -> + throw({error, <<"Invalid value of '", ArgName/binary, "'">>}); + Name2 -> + try get_room_serverhost(Name2) of + Host -> {Host, Name2} + catch + error:{invalid_domain, _} -> + throw({error, <<"Unknown host passed in '", ArgName/binary, "'">>}); + error:{unregistered_route, _} -> + throw({error, <<"Unknown host passed in '", ArgName/binary, "'">>}) + end end. + -spec validate_room(Name :: binary()) -> binary(). validate_room(Name) -> case jid:nodeprep(Name) of - error -> - throw({error, <<"Invalid value of room name">>}); - Name2 -> - Name2 + error -> + throw({error, <<"Invalid value of room name">>}); + Name2 -> + Name2 end. + find_service(global) -> global; find_service(ServerHost) -> hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). + find_services_validate(Global, _Name) when Global == global; - Global == <<"global">> -> + Global == <<"global">> -> find_services(Global); find_services_validate(Service, Name) -> Service2 = validate_muc(Service, Name), find_services(Service2). + find_services(Global) when Global == global; - Global == <<"global">> -> + Global == <<"global">> -> lists:flatmap( fun(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_muc) of - true -> - [find_service(ServerHost)]; - false -> - [] - end - end, ejabberd_option:hosts()); + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + [find_service(ServerHost)]; + false -> + [] + end + end, + ejabberd_option:hosts()); find_services(Service) when is_binary(Service) -> [Service]. + get_room_serverhost(Service) when is_binary(Service) -> - ejabberd_router:host_of_route(Service). + ejabberd_router:host_of_route(Service). + find_host(ServerHost) -> hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). + find_hosts(Global) when Global == global; - Global == <<"global">> -> + Global == <<"global">> -> lists:flatmap( fun(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_muc) of - true -> - [find_host(ServerHost)]; - false -> - [] - end - end, ejabberd_option:hosts()); + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + [find_host(ServerHost)]; + false -> + [] + end + end, + ejabberd_option:hosts()); find_hosts(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of - true -> - [find_host(ServerHost)]; - false -> - [] + true -> + [find_host(ServerHost)]; + false -> + [] end. + 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`_.")], - opts => + #{ + 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`_.")], + opts => [{subscribe_room_many_max_users, - #{value => ?T("Number"), + #{ + value => ?T("Number"), note => "added in 22.05", desc => ?T("How many users can be subscribed to a room at once using " "the _`subscribe_room_many`_ API. " - "The default value is '50'.")}}]}. + "The default value is '50'.") + }}] + }. diff --git a/src/mod_muc_admin_opt.erl b/src/mod_muc_admin_opt.erl index 18ca64af7..334d206b8 100644 --- a/src/mod_muc_admin_opt.erl +++ b/src/mod_muc_admin_opt.erl @@ -5,9 +5,9 @@ -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_log.erl b/src/mod_muc_log.erl index 57a975b0b..142cd1243 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -34,36 +34,52 @@ -behaviour(gen_mod). %% API --export([start/2, stop/1, reload/3, get_url/2, - check_access_log/3, add_to_log/5]). +-export([start/2, + stop/1, + reload/3, + get_url/2, + check_access_log/3, + add_to_log/5]). --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3, - mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + mod_opt_type/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_muc_room.hrl"). -include("translate.hrl"). -record(room, {jid, title, subject, subject_author, config}). --define(PLAINTEXT_CO, <<"ZZCZZ">>). --define(PLAINTEXT_IN, <<"ZZIZZ">>). +-define(PLAINTEXT_CO, <<"ZZCZZ">>). +-define(PLAINTEXT_IN, <<"ZZIZZ">>). -define(PLAINTEXT_OUT, <<"ZZOZZ">>). --record(logstate, {host, - out_dir, - dir_type, - dir_name, - file_format, - file_permissions, - css_file, - access, - lang, - timezone, - spam_prevention, - top_link}). +-record(logstate, { + host, + out_dir, + dir_type, + dir_name, + file_format, + file_permissions, + css_file, + access, + lang, + timezone, + spam_prevention, + top_link + }). + %%==================================================================== %% API @@ -71,52 +87,58 @@ start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(Host, NewOpts, _OldOpts) -> Proc = get_proc_name(Host), gen_server:cast(Proc, {reload, NewOpts}). + add_to_log(Host, Type, Data, Room, Opts) -> gen_server:cast(get_proc_name(Host), - {add_to_log, Type, Data, Room, Opts}). + {add_to_log, Type, Data, Room, Opts}). + check_access_log(allow, _Host, _From) -> allow; check_access_log(_Acc, Host, From) -> case catch gen_server:call(get_proc_name(Host), - {check_access_log, Host, From}) - of - {'EXIT', _Error} -> deny; - Res -> Res + {check_access_log, Host, From}) of + {'EXIT', _Error} -> deny; + Res -> Res end. + -spec get_url(any(), #state{}) -> {ok, binary()} | error. get_url({ok, _} = Acc, _State) -> Acc; get_url(_Acc, #state{room = Room, host = Host, server_host = ServerHost}) -> try mod_muc_log_opt:url(ServerHost) of - undefined -> error; - URL -> - case mod_muc_log_opt:dirname(ServerHost) of - room_jid -> - {ok, <>}; - room_name -> - {ok, <>} - end + undefined -> error; + URL -> + case mod_muc_log_opt:dirname(ServerHost) of + room_jid -> + {ok, <>}; + room_name -> + {ok, <>} + end catch - error:{module_not_loaded, _, _} -> - error + error:{module_not_loaded, _, _} -> + error end. + depends(_Host, _Opts) -> [{mod_muc, hard}]. + %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), ejabberd_hooks:add(muc_log_add, Host, ?MODULE, add_to_log, 100), @@ -124,34 +146,40 @@ init([Host|_]) -> ejabberd_hooks:add(muc_log_get_url, Host, ?MODULE, get_url, 100), {ok, init_state(Host, Opts)}. + handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID), {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. + handle_cast({reload, Opts}, #logstate{host = Host}) -> {noreply, init_state(Host, Opts)}; handle_cast({add_to_log, Type, Data, Room, Opts}, State) -> case catch add_to_log2(Type, Data, Room, Opts, State) of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); - _ -> ok + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); + _ -> ok end, {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(_Info, State) -> {noreply, State}. + terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(muc_log_add, Host, ?MODULE, add_to_log, 100), ejabberd_hooks:delete(muc_log_check_access_log, Host, ?MODULE, check_access_log, 100), ejabberd_hooks:delete(muc_log_get_url, Host, ?MODULE, get_url, 100), ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- @@ -167,306 +195,402 @@ init_state(Host, Opts) -> Top_link = mod_muc_log_opt:top_link(Opts), NoFollow = mod_muc_log_opt:spam_prevention(Opts), Lang = ejabberd_option:language(Host), - #logstate{host = Host, out_dir = OutDir, - dir_type = DirType, dir_name = DirName, - file_format = FileFormat, css_file = CSSFile, - file_permissions = FilePermissions, - access = AccessLog, lang = Lang, timezone = Timezone, - spam_prevention = NoFollow, top_link = Top_link}. + #logstate{ + host = Host, + out_dir = OutDir, + dir_type = DirType, + dir_name = DirName, + file_format = FileFormat, + css_file = CSSFile, + file_permissions = FilePermissions, + access = AccessLog, + lang = Lang, + timezone = Timezone, + spam_prevention = NoFollow, + top_link = Top_link + }. + add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> case has_no_permanent_store_hint(Packet) of - false -> - case {Packet#message.subject, Packet#message.body} of - {[], []} -> ok; - {[], Body} -> - Message = {body, xmpp:get_text(Body)}, - add_message_to_log(Nick, Message, Room, Opts, State); - {Subj, _} -> - Message = {subject, xmpp:get_text(Subj)}, - add_message_to_log(Nick, Message, Room, Opts, State) - end; - true -> ok + false -> + case {Packet#message.subject, Packet#message.body} of + {[], []} -> ok; + {[], Body} -> + Message = {body, xmpp:get_text(Body)}, + add_message_to_log(Nick, Message, Room, Opts, State); + {Subj, _} -> + Message = {subject, xmpp:get_text(Subj)}, + add_message_to_log(Nick, Message, Room, Opts, State) + end; + true -> ok end; -add_to_log2(roomconfig_change, _Occupants, Room, Opts, - State) -> - add_message_to_log(<<"">>, roomconfig_change, Room, - Opts, State); -add_to_log2(roomconfig_change_enabledlogging, Occupants, - Room, Opts, State) -> +add_to_log2(roomconfig_change, + _Occupants, + Room, + Opts, + State) -> add_message_to_log(<<"">>, - {roomconfig_change, Occupants}, Room, Opts, State); -add_to_log2(room_existence, NewStatus, Room, Opts, - State) -> - add_message_to_log(<<"">>, {room_existence, NewStatus}, - Room, Opts, State); -add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts, - State) -> - add_message_to_log(NewNick, {nickchange, OldNick}, Room, - Opts, State); + roomconfig_change, + Room, + Opts, + State); +add_to_log2(roomconfig_change_enabledlogging, + Occupants, + Room, + Opts, + State) -> + add_message_to_log(<<"">>, + {roomconfig_change, Occupants}, + Room, + Opts, + State); +add_to_log2(room_existence, + NewStatus, + Room, + Opts, + State) -> + add_message_to_log(<<"">>, + {room_existence, NewStatus}, + Room, + Opts, + State); +add_to_log2(nickchange, + {OldNick, NewNick}, + Room, + Opts, + State) -> + add_message_to_log(NewNick, + {nickchange, OldNick}, + Room, + Opts, + State); add_to_log2(join, Nick, Room, Opts, State) -> add_message_to_log(Nick, join, Room, Opts, State); add_to_log2(leave, {Nick, Reason}, Room, Opts, State) -> case Reason of - <<"">> -> - add_message_to_log(Nick, leave, Room, Opts, State); - _ -> - add_message_to_log(Nick, {leave, Reason}, Room, Opts, - State) + <<"">> -> + add_message_to_log(Nick, leave, Room, Opts, State); + _ -> + add_message_to_log(Nick, + {leave, Reason}, + Room, + Opts, + State) end; -add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, - State) -> - add_message_to_log(Nick, {kickban, Code, Reason}, Room, - Opts, State). +add_to_log2(kickban, + {Nick, Reason, Code}, + Room, + Opts, + State) -> + add_message_to_log(Nick, + {kickban, Code, Reason}, + Room, + Opts, + State). + %%---------------------------------------------------------------------- %% Core -build_filename_string(TimeStamp, OutDir, RoomJID, - DirType, DirName, FileFormat) -> + +build_filename_string(TimeStamp, + OutDir, + RoomJID, + DirType, + DirName, + FileFormat) -> {{Year, Month, Day}, _Time} = TimeStamp, {Dir, Filename, Rel} = case DirType of - subdirs -> - SYear = - (str:format("~4..0w", - [Year])), - SMonth = - (str:format("~2..0w", - [Month])), - SDay = (str:format("~2..0w", - [Day])), - {fjoin([SYear, SMonth]), SDay, - <<"../..">>}; - plain -> - Date = - (str:format("~4..0w-~2..0w-~2..0w", - [Year, - Month, - Day])), - {<<"">>, Date, <<".">>} - end, + subdirs -> + SYear = + (str:format("~4..0w", + [Year])), + SMonth = + (str:format("~2..0w", + [Month])), + SDay = (str:format("~2..0w", + [Day])), + {fjoin([SYear, SMonth]), + SDay, + <<"../..">>}; + plain -> + Date = + (str:format("~4..0w-~2..0w-~2..0w", + [Year, + Month, + Day])), + {<<"">>, Date, <<".">>} + end, RoomString = case DirName of - room_jid -> RoomJID; - room_name -> get_room_name(RoomJID) - end, + room_jid -> RoomJID; + room_name -> get_room_name(RoomJID) + end, Extension = case FileFormat of - html -> <<".html">>; - plaintext -> <<".txt">> - end, + html -> <<".html">>; + plaintext -> <<".txt">> + end, Fd = fjoin([OutDir, RoomString, Dir]), Fn = fjoin([Fd, <>]), Fnrel = fjoin([Rel, Dir, <>]), {Fd, Fn, Fnrel}. + get_room_name(RoomJID) -> JID = jid:decode(RoomJID), JID#jid.user. + %% calculate day before get_timestamp_daydiff(TimeStamp, Daydiff) -> {Date1, HMS} = TimeStamp, Date2 = - calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date1) - + Daydiff), + calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date1) + + Daydiff), {Date2, HMS}. + %% Try to close the previous day log, if it exists close_previous_log(Fn, Images_dir, FileFormat) -> case file:read_file_info(Fn) of - {ok, _} -> - {ok, F} = file:open(Fn, [append]), - write_last_lines(F, Images_dir, FileFormat), - file:close(F); - _ -> ok + {ok, _} -> + {ok, F} = file:open(Fn, [append]), + write_last_lines(F, Images_dir, FileFormat), + file:close(F); + _ -> ok end. + write_last_lines(_, _, plaintext) -> ok; write_last_lines(F, Images_dir, _FileFormat) -> fw(F, <<"
">>), fw(F, <<" \"Powered">>, + "style=\"border:0\" src=\"~ts/powered-by-ejabbe" + "rd.png\" alt=\"Powered by ejabberd - robust, scalable and extensible XMPP server\"/>">>, [Images_dir]), fw(F, <<" \"Powered">>, + "style=\"border:0\" src=\"~ts/powered-by-erlang" + ".png\" alt=\"Powered by Erlang\"/>">>, [Images_dir]), fw(F, <<"">>), fw(F, <<" ">>, + "=referer\">">>, [Images_dir]), fw(F, <<" \"Valid">>, + "r/\">\"Valid">>, [Images_dir]), fw(F, <<"
">>). + set_filemode(Fn, {FileMode, FileGroup}) -> ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), ok = file:change_group(Fn, FileGroup). + htmlize_nick(Nick1, html) -> htmlize(<<"<", Nick1/binary, ">">>, html); htmlize_nick(Nick1, plaintext) -> htmlize(<>, plaintext). -add_message_to_log(Nick1, Message, RoomJID, Opts, - State) -> - #logstate{out_dir = OutDir, dir_type = DirType, - dir_name = DirName, file_format = FileFormat, - file_permissions = FilePermissions, - css_file = CSSFile, lang = Lang, timezone = Timezone, - spam_prevention = NoFollow, top_link = TopLink} = - State, + +add_message_to_log(Nick1, + Message, + RoomJID, + Opts, + State) -> + #logstate{ + out_dir = OutDir, + dir_type = DirType, + dir_name = DirName, + file_format = FileFormat, + file_permissions = FilePermissions, + css_file = CSSFile, + lang = Lang, + timezone = Timezone, + spam_prevention = NoFollow, + top_link = TopLink + } = + State, Room = get_room_info(RoomJID, Opts), Nick = htmlize(Nick1, FileFormat), Nick2 = htmlize_nick(Nick1, FileFormat), Now = erlang:timestamp(), TimeStamp = case Timezone of - local -> calendar:now_to_local_time(Now); - universal -> calendar:now_to_universal_time(Now) - end, + local -> calendar:now_to_local_time(Now); + universal -> calendar:now_to_universal_time(Now) + end, {Fd, Fn, _Dir} = build_filename_string(TimeStamp, - OutDir, Room#room.jid, DirType, - DirName, FileFormat), + OutDir, + Room#room.jid, + DirType, + DirName, + FileFormat), {Date, Time} = TimeStamp, case file:read_file_info(Fn) of - {ok, _} -> {ok, F} = file:open(Fn, [append]); - {error, enoent} -> - make_dir_rec(Fd), - {ok, F} = file:open(Fn, [append]), - catch set_filemode(Fn, FilePermissions), - Datestring = get_dateweek(Date, Lang), - TimeStampYesterday = get_timestamp_daydiff(TimeStamp, - -1), - {_FdYesterday, FnYesterday, DatePrev} = - build_filename_string(TimeStampYesterday, OutDir, - Room#room.jid, DirType, DirName, - FileFormat), - TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1), - {_FdTomorrow, _FnTomorrow, DateNext} = - build_filename_string(TimeStampTomorrow, OutDir, - Room#room.jid, DirType, DirName, - FileFormat), - HourOffset = calc_hour_offset(TimeStamp), - put_header(F, Room, Datestring, CSSFile, Lang, - HourOffset, DatePrev, DateNext, TopLink, FileFormat), - Images_dir = fjoin([OutDir, <<"images">>]), - file:make_dir(Images_dir), - create_image_files(Images_dir), - Images_url = case DirType of - subdirs -> <<"../../../images">>; - plain -> <<"../images">> - end, - close_previous_log(FnYesterday, Images_url, FileFormat) + {ok, _} -> {ok, F} = file:open(Fn, [append]); + {error, enoent} -> + make_dir_rec(Fd), + {ok, F} = file:open(Fn, [append]), + catch set_filemode(Fn, FilePermissions), + Datestring = get_dateweek(Date, Lang), + TimeStampYesterday = get_timestamp_daydiff(TimeStamp, + -1), + {_FdYesterday, FnYesterday, DatePrev} = + build_filename_string(TimeStampYesterday, + OutDir, + Room#room.jid, + DirType, + DirName, + FileFormat), + TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1), + {_FdTomorrow, _FnTomorrow, DateNext} = + build_filename_string(TimeStampTomorrow, + OutDir, + Room#room.jid, + DirType, + DirName, + FileFormat), + HourOffset = calc_hour_offset(TimeStamp), + put_header(F, + Room, + Datestring, + CSSFile, + Lang, + HourOffset, + DatePrev, + DateNext, + TopLink, + FileFormat), + Images_dir = fjoin([OutDir, <<"images">>]), + file:make_dir(Images_dir), + create_image_files(Images_dir), + Images_url = case DirType of + subdirs -> <<"../../../images">>; + plain -> <<"../images">> + end, + close_previous_log(FnYesterday, Images_url, FileFormat) end, Text = case Message of - roomconfig_change -> - RoomConfig = roomconfig_to_string(Room#room.config, - Lang, FileFormat), - put_room_config(F, RoomConfig, Lang, FileFormat), - io_lib:format("~ts
", - [tr(Lang, ?T("Chatroom configuration modified"))]); - {roomconfig_change, Occupants} -> - RoomConfig = roomconfig_to_string(Room#room.config, - Lang, FileFormat), - put_room_config(F, RoomConfig, Lang, FileFormat), - RoomOccupants = roomoccupants_to_string(Occupants, - FileFormat), - put_room_occupants(F, RoomOccupants, Lang, FileFormat), - io_lib:format("~ts
", - [tr(Lang, ?T("Chatroom configuration modified"))]); - join -> - io_lib:format("~ts ~ts
", - [Nick, tr(Lang, ?T("joins the room"))]); - leave -> - io_lib:format("~ts ~ts
", - [Nick, tr(Lang, ?T("leaves the room"))]); - {leave, Reason} -> - io_lib:format("~ts ~ts: ~ts
", - [Nick, tr(Lang, ?T("leaves the room")), - htmlize(Reason, NoFollow, FileFormat)]); - {kickban, 301, <<"">>} -> - io_lib:format("~ts ~ts
", - [Nick, tr(Lang, ?T("has been banned"))]); - {kickban, 301, Reason} -> - io_lib:format("~ts ~ts: ~ts
", - [Nick, tr(Lang, ?T("has been banned")), - htmlize(Reason, FileFormat)]); - {kickban, 307, <<"">>} -> - io_lib:format("~ts ~ts
", - [Nick, tr(Lang, ?T("has been kicked"))]); - {kickban, 307, Reason} -> - io_lib:format("~ts ~ts: ~ts
", - [Nick, tr(Lang, ?T("has been kicked")), - htmlize(Reason, FileFormat)]); - {kickban, 321, <<"">>} -> - io_lib:format("~ts ~ts
", - [Nick, - tr(Lang, ?T("has been kicked because of an affiliation " - "change"))]); - {kickban, 322, <<"">>} -> - io_lib:format("~ts ~ts
", - [Nick, - tr(Lang, ?T("has been kicked because the room has " - "been changed to members-only"))]); - {kickban, 332, <<"">>} -> - io_lib:format("~ts ~ts
", - [Nick, - tr(Lang, ?T("has been kicked because of a system " - "shutdown"))]); - {nickchange, OldNick} -> - io_lib:format("~ts ~ts ~ts
", - [htmlize(OldNick, FileFormat), - tr(Lang, ?T("is now known as")), Nick]); - {subject, T} -> - io_lib:format("~ts~ts~ts
", - [Nick, tr(Lang, ?T(" has set the subject to: ")), - htmlize(T, NoFollow, FileFormat)]); - {body, T} -> - case {ejabberd_regexp:run(T, <<"^/me ">>), Nick} of - {_, <<"">>} -> - io_lib:format("~ts
", - [htmlize(T, NoFollow, FileFormat)]); - {match, _} -> - io_lib:format("~ts ~ts
", - [Nick, - str:substr(htmlize(T, FileFormat), 5)]); - {nomatch, _} -> - io_lib:format("~ts ~ts
", - [Nick2, htmlize(T, NoFollow, FileFormat)]) - end; - {room_existence, RoomNewExistence} -> - io_lib:format("~ts
", - [get_room_existence_string(RoomNewExistence, - Lang)]) - end, + roomconfig_change -> + RoomConfig = roomconfig_to_string(Room#room.config, + Lang, + FileFormat), + put_room_config(F, RoomConfig, Lang, FileFormat), + io_lib:format("~ts
", + [tr(Lang, ?T("Chatroom configuration modified"))]); + {roomconfig_change, Occupants} -> + RoomConfig = roomconfig_to_string(Room#room.config, + Lang, + FileFormat), + put_room_config(F, RoomConfig, Lang, FileFormat), + RoomOccupants = roomoccupants_to_string(Occupants, + FileFormat), + put_room_occupants(F, RoomOccupants, Lang, FileFormat), + io_lib:format("~ts
", + [tr(Lang, ?T("Chatroom configuration modified"))]); + join -> + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("joins the room"))]); + leave -> + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("leaves the room"))]); + {leave, Reason} -> + io_lib:format("~ts ~ts: ~ts
", + [Nick, + tr(Lang, ?T("leaves the room")), + htmlize(Reason, NoFollow, FileFormat)]); + {kickban, 301, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("has been banned"))]); + {kickban, 301, Reason} -> + io_lib:format("~ts ~ts: ~ts
", + [Nick, + tr(Lang, ?T("has been banned")), + htmlize(Reason, FileFormat)]); + {kickban, 307, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, tr(Lang, ?T("has been kicked"))]); + {kickban, 307, Reason} -> + io_lib:format("~ts ~ts: ~ts
", + [Nick, + tr(Lang, ?T("has been kicked")), + htmlize(Reason, FileFormat)]); + {kickban, 321, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, + tr(Lang, + ?T("has been kicked because of an affiliation " + "change"))]); + {kickban, 322, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, + tr(Lang, + ?T("has been kicked because the room has " + "been changed to members-only"))]); + {kickban, 332, <<"">>} -> + io_lib:format("~ts ~ts
", + [Nick, + tr(Lang, + ?T("has been kicked because of a system " + "shutdown"))]); + {nickchange, OldNick} -> + io_lib:format("~ts ~ts ~ts
", + [htmlize(OldNick, FileFormat), + tr(Lang, ?T("is now known as")), + Nick]); + {subject, T} -> + io_lib:format("~ts~ts~ts
", + [Nick, + tr(Lang, ?T(" has set the subject to: ")), + htmlize(T, NoFollow, FileFormat)]); + {body, T} -> + case {ejabberd_regexp:run(T, <<"^/me ">>), Nick} of + {_, <<"">>} -> + io_lib:format("~ts
", + [htmlize(T, NoFollow, FileFormat)]); + {match, _} -> + io_lib:format("~ts ~ts
", + [Nick, + str:substr(htmlize(T, FileFormat), 5)]); + {nomatch, _} -> + io_lib:format("~ts ~ts
", + [Nick2, htmlize(T, NoFollow, FileFormat)]) + end; + {room_existence, RoomNewExistence} -> + io_lib:format("~ts
", + [get_room_existence_string(RoomNewExistence, + Lang)]) + end, {Hour, Minute, Second} = Time, STime = io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second]), {_, _, Microsecs} = Now, STimeUnique = io_lib:format("~ts.~w", - [STime, Microsecs]), + [STime, Microsecs]), maybe_print_jl(open, F, Message, FileFormat), - fw(F, io_lib:format("[~ts] ", - [STimeUnique, STimeUnique, STimeUnique, STime]) - ++ Text, + fw(F, + io_lib:format("[~ts] ", + [STimeUnique, STimeUnique, STimeUnique, STime]) ++ + Text, FileFormat), maybe_print_jl(close, F, Message, FileFormat), file:close(F), ok. + %%---------------------------------------------------------------------- %% Utilities + get_room_existence_string(created, Lang) -> tr(Lang, ?T("Chatroom is created")); get_room_existence_string(destroyed, Lang) -> @@ -476,31 +600,32 @@ get_room_existence_string(started, Lang) -> get_room_existence_string(stopped, Lang) -> tr(Lang, ?T("Chatroom is stopped")). + get_dateweek(Date, Lang) -> Weekday = case calendar:day_of_the_week(Date) of - 1 -> tr(Lang, ?T("Monday")); - 2 -> tr(Lang, ?T("Tuesday")); - 3 -> tr(Lang, ?T("Wednesday")); - 4 -> tr(Lang, ?T("Thursday")); - 5 -> tr(Lang, ?T("Friday")); - 6 -> tr(Lang, ?T("Saturday")); - 7 -> tr(Lang, ?T("Sunday")) - end, + 1 -> tr(Lang, ?T("Monday")); + 2 -> tr(Lang, ?T("Tuesday")); + 3 -> tr(Lang, ?T("Wednesday")); + 4 -> tr(Lang, ?T("Thursday")); + 5 -> tr(Lang, ?T("Friday")); + 6 -> tr(Lang, ?T("Saturday")); + 7 -> tr(Lang, ?T("Sunday")) + end, {Y, M, D} = Date, Month = case M of - 1 -> tr(Lang, ?T("January")); - 2 -> tr(Lang, ?T("February")); - 3 -> tr(Lang, ?T("March")); - 4 -> tr(Lang, ?T("April")); - 5 -> tr(Lang, ?T("May")); - 6 -> tr(Lang, ?T("June")); - 7 -> tr(Lang, ?T("July")); - 8 -> tr(Lang, ?T("August")); - 9 -> tr(Lang, ?T("September")); - 10 -> tr(Lang, ?T("October")); - 11 -> tr(Lang, ?T("November")); - 12 -> tr(Lang, ?T("December")) - end, + 1 -> tr(Lang, ?T("January")); + 2 -> tr(Lang, ?T("February")); + 3 -> tr(Lang, ?T("March")); + 4 -> tr(Lang, ?T("April")); + 5 -> tr(Lang, ?T("May")); + 6 -> tr(Lang, ?T("June")); + 7 -> tr(Lang, ?T("July")); + 8 -> tr(Lang, ?T("August")); + 9 -> tr(Lang, ?T("September")); + 10 -> tr(Lang, ?T("October")); + 11 -> tr(Lang, ?T("November")); + 12 -> tr(Lang, ?T("December")) + end, unicode:characters_to_binary( case Lang of <<"en">> -> @@ -512,65 +637,83 @@ get_dateweek(Date, Lang) -> io_lib:format("~ts, ~w ~ts ~w", [Weekday, D, Month, Y]) end). + make_dir_rec(Dir) -> filelib:ensure_dir(<>). + %% {ok, F1}=file:open("valid-xhtml10.png", [read]). %% {ok, F1b}=file:read(F1, 1000000). %% c("../../ejabberd/src/jlib.erl"). %% base64:encode(F1b). + create_image_files(Images_dir) -> Filenames = [<<"powered-by-ejabberd.png">>, - <<"powered-by-erlang.png">>, <<"valid-xhtml10.png">>, - <<"vcss.png">>], + <<"powered-by-erlang.png">>, + <<"valid-xhtml10.png">>, + <<"vcss.png">>], lists:foreach( fun(Filename) -> - Src = filename:join([misc:img_dir(), Filename]), - Dst = fjoin([Images_dir, Filename]), - case file:copy(Src, Dst) of - {ok, _} -> ok; - {error, Why} -> - ?ERROR_MSG("Failed to copy ~ts to ~ts: ~ts", - [Src, Dst, file:format_error(Why)]) - end - end, Filenames). + Src = filename:join([misc:img_dir(), Filename]), + Dst = fjoin([Images_dir, Filename]), + case file:copy(Src, Dst) of + {ok, _} -> ok; + {error, Why} -> + ?ERROR_MSG("Failed to copy ~ts to ~ts: ~ts", + [Src, Dst, file:format_error(Why)]) + end + end, + Filenames). + fw(F, S) -> fw(F, S, [], html). + fw(F, S, O) when is_list(O) -> fw(F, S, O, html); fw(F, S, FileFormat) when is_atom(FileFormat) -> fw(F, S, [], FileFormat). + fw(F, S, O, FileFormat) -> S1 = <<(str:format(S, O))/binary, "\n">>, S2 = case FileFormat of - html -> - S1; - plaintext -> - S1a = ejabberd_regexp:greplace(S1, <<"<[^<^>]*>">>, <<"">>), - S1x = ejabberd_regexp:greplace(S1a, ?PLAINTEXT_CO, <<"~~">>), - S1y = ejabberd_regexp:greplace(S1x, ?PLAINTEXT_IN, <<"<">>), - ejabberd_regexp:greplace(S1y, ?PLAINTEXT_OUT, <<">">>) - end, + html -> + S1; + plaintext -> + S1a = ejabberd_regexp:greplace(S1, <<"<[^<^>]*>">>, <<"">>), + S1x = ejabberd_regexp:greplace(S1a, ?PLAINTEXT_CO, <<"~~">>), + S1y = ejabberd_regexp:greplace(S1x, ?PLAINTEXT_IN, <<"<">>), + ejabberd_regexp:greplace(S1y, ?PLAINTEXT_OUT, <<">">>) + end, file:write(F, S2). + put_header(_, _, _, _, _, _, _, _, _, plaintext) -> ok; -put_header(F, Room, Date, CSSFile, Lang, Hour_offset, - Date_prev, Date_next, Top_link, FileFormat) -> +put_header(F, + Room, + Date, + CSSFile, + Lang, + Hour_offset, + Date_prev, + Date_next, + Top_link, + FileFormat) -> fw(F, <<"">>), + "XHTML 1.0 Transitional//EN\" \"http://www.w3." + "org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>), fw(F, <<"">>, + "xml:lang=\"~ts\" lang=\"~ts\">">>, [Lang, Lang]), fw(F, <<"">>), fw(F, <<"">>), - fw(F, <<"~ts - ~ts">>, + "ext/html; charset=utf-8\" />">>), + fw(F, + <<"~ts - ~ts">>, [htmlize(Room#room.title), Date]), put_header_css(F, CSSFile), put_header_script(F), @@ -579,69 +722,75 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, {Top_url, Top_text} = Top_link, fw(F, <<"">>, + "style=\"color: #AAAAAA; font-family: " + "monospace; text-decoration: none; font-weight" + ": bold;\" href=\"~ts\">~ts">>, [Top_url, Top_text]), - fw(F, <<"
~ts
">>, + fw(F, + <<"
~ts
">>, [htmlize(Room#room.title)]), fw(F, <<"~ts" - "">>, + "">>, [Room#room.jid, Room#room.jid]), fw(F, <<"
~ts" - "< " - "^ >">>, + "< " + "^ >">>, [Date, Date_prev, Date_next]), case {htmlize(prepare_subject_author(Room#room.subject_author)), - htmlize(Room#room.subject)} - of - {<<"">>, <<"">>} -> ok; - {SuA, Su} -> - fw(F, <<"
~ts~ts~ts
">>, - [SuA, tr(Lang, ?T(" has set the subject to: ")), Su]) + htmlize(Room#room.subject)} of + {<<"">>, <<"">>} -> ok; + {SuA, Su} -> + fw(F, + <<"
~ts~ts~ts
">>, + [SuA, tr(Lang, ?T(" has set the subject to: ")), Su]) end, RoomConfig = roomconfig_to_string(Room#room.config, - Lang, FileFormat), + Lang, + FileFormat), put_room_config(F, RoomConfig, Lang, FileFormat), Occupants = get_room_occupants(Room#room.jid), RoomOccupants = roomoccupants_to_string(Occupants, - FileFormat), + FileFormat), put_room_occupants(F, RoomOccupants, Lang, FileFormat), put_occupants_join_leave(F, Lang), Time_offset_str = case Hour_offset < 0 of - true -> io_lib:format("~p", [Hour_offset]); - false -> io_lib:format("+~p", [Hour_offset]) - end, - fw(F, <<"
GMT~ts
">>, + true -> io_lib:format("~p", [Hour_offset]); + false -> io_lib:format("+~p", [Hour_offset]) + end, + fw(F, + <<"
GMT~ts
">>, [Time_offset_str]). + put_header_css(F, {file, Path}) -> fw(F, <<"">>); put_header_css(F, {url, URL}) -> fw(F, <<"">>, + "href=\"~ts\" media=\"all\">">>, [URL]). + put_header_script(F) -> fw(F, <<"">>). + put_room_config(_F, _RoomConfig, _Lang, plaintext) -> ok; put_room_config(F, RoomConfig, Lang, _FileFormat) -> @@ -649,56 +798,64 @@ put_room_config(F, RoomConfig, Lang, _FileFormat) -> fw(F, <<"
">>), fw(F, <<"
~ts
">>, + "false;\">~ts
">>, [Now2, tr(Lang, ?T("Room Configuration"))]), fw(F, <<"

~ts
">>, + "y: none;\" >
~ts
">>, [Now2, RoomConfig]), fw(F, <<"">>). -put_room_occupants(_F, _RoomOccupants, _Lang, - plaintext) -> + +put_room_occupants(_F, + _RoomOccupants, + _Lang, + plaintext) -> ok; -put_room_occupants(F, RoomOccupants, Lang, - _FileFormat) -> +put_room_occupants(F, + RoomOccupants, + Lang, + _FileFormat) -> {_, Now2, _} = erlang:timestamp(), -%% htmlize -%% The default behaviour is to ignore the nofollow spam prevention on links -%% (NoFollow=false) + %% htmlize + %% The default behaviour is to ignore the nofollow spam prevention on links + %% (NoFollow=false) fw(F, <<"
">>), fw(F, <<"
~ts
">>, + "false;\">~ts
">>, [Now2, tr(Lang, ?T("Room Occupants"))]), fw(F, <<"

~ts
">>, + "y: none;\" >
~ts">>, [Now2, RoomOccupants]), fw(F, <<"">>). + put_occupants_join_leave(F, Lang) -> fw(F, <<"
">>), fw(F, <<"
~ts
">>, + "false;\">~ts
">>, [tr(Lang, ?T("Show Occupants Join/Leave"))]), fw(F, <<"">>). + maybe_print_jl(_Direction, _F, _Message, plaintext) -> ok; maybe_print_jl(Direction, F, Message, html) -> PrintJl = case Message of - join -> true; - leave -> true; - {leave, _} -> true; - _ -> false - end, + join -> true; + leave -> true; + {leave, _} -> true; + _ -> false + end, case PrintJl of true -> print_jl(Direction, F); false -> ok end. + print_jl(Direction, F) -> String = case Direction of open -> "
"; @@ -706,139 +863,165 @@ print_jl(Direction, F) -> end, fw(F, io_lib:format(String, [])). + htmlize(S1) -> htmlize(S1, html). + htmlize(S1, plaintext) -> ejabberd_regexp:greplace(S1, <<"~">>, ?PLAINTEXT_CO); htmlize(S1, FileFormat) -> htmlize(S1, false, FileFormat). + %% The NoFollow parameter tell if the spam prevention should be applied to the link found %% true means 'apply nofollow on links'. htmlize(S0, _NoFollow, plaintext) -> - S1 = ejabberd_regexp:greplace(S0, <<"~">>, ?PLAINTEXT_CO), + S1 = ejabberd_regexp:greplace(S0, <<"~">>, ?PLAINTEXT_CO), S1x = ejabberd_regexp:greplace(S1, <<"<">>, ?PLAINTEXT_IN), ejabberd_regexp:greplace(S1x, <<">">>, ?PLAINTEXT_OUT); htmlize(S1, NoFollow, _FileFormat) -> S2_list = str:tokens(S1, <<"\n">>), - lists:foldl(fun (Si, Res) -> - Si2 = htmlize2(Si, NoFollow), - case Res of - <<"">> -> Si2; - _ -> <", Si2/binary>> - end - end, - <<"">>, S2_list). + lists:foldl(fun(Si, Res) -> + Si2 = htmlize2(Si, NoFollow), + case Res of + <<"">> -> Si2; + _ -> <", Si2/binary>> + end + end, + <<"">>, + S2_list). + htmlize2(S1, NoFollow) -> -%% Regexp link -%% Add the nofollow rel attribute when required - S2 = ejabberd_regexp:greplace(S1, <<"\\&">>, - <<"\\&">>), - S3 = ejabberd_regexp:greplace(S2, <<"<">>, - <<"\\<">>), - S4 = ejabberd_regexp:greplace(S3, <<">">>, - <<"\\>">>), + %% Regexp link + %% Add the nofollow rel attribute when required + S2 = ejabberd_regexp:greplace(S1, + <<"\\&">>, + <<"\\&">>), + S3 = ejabberd_regexp:greplace(S2, + <<"<">>, + <<"\\<">>), + S4 = ejabberd_regexp:greplace(S3, + <<">">>, + <<"\\>">>), S5 = ejabberd_regexp:greplace(S4, - <<"((http|https|ftp)://|(mailto|xmpp):)[^] " - ")'\"}]+">>, - link_regexp(NoFollow)), - S6 = ejabberd_regexp:greplace(S5, <<" ">>, - <<"\\ \\ ">>), - S7 = ejabberd_regexp:greplace(S6, <<"\\t">>, - <<"\\ \\ \\ \\ ">>), - S8 = ejabberd_regexp:greplace(S7, <<"~">>, - <<"~~">>), - ejabberd_regexp:greplace(S8, <<226, 128, 174>>, - <<"[RLO]">>). + <<"((http|https|ftp)://|(mailto|xmpp):)[^] " + ")'\"}]+">>, + link_regexp(NoFollow)), + S6 = ejabberd_regexp:greplace(S5, + <<" ">>, + <<"\\ \\ ">>), + S7 = ejabberd_regexp:greplace(S6, + <<"\\t">>, + <<"\\ \\ \\ \\ ">>), + S8 = ejabberd_regexp:greplace(S7, + <<"~">>, + <<"~~">>), + ejabberd_regexp:greplace(S8, + <<226, 128, 174>>, + <<"[RLO]">>). + link_regexp(false) -> <<"&">>; link_regexp(true) -> <<"&">>. + get_room_info(RoomJID, Opts) -> Title = case lists:keysearch(title, 1, Opts) of - {value, {_, T}} -> T; - false -> <<"">> - end, + {value, {_, T}} -> T; + false -> <<"">> + end, Subject = case lists:keysearch(subject, 1, Opts) of - {value, {_, S}} -> xmpp:get_text(S); - false -> <<"">> - end, - SubjectAuthor = case lists:keysearch(subject_author, 1, - Opts) - of - {value, {_, SA}} -> SA; - false -> <<"">> - end, - #room{jid = jid:encode(RoomJID), title = Title, - subject = Subject, subject_author = SubjectAuthor, - config = Opts}. + {value, {_, S}} -> xmpp:get_text(S); + false -> <<"">> + end, + SubjectAuthor = case lists:keysearch(subject_author, + 1, + Opts) of + {value, {_, SA}} -> SA; + false -> <<"">> + end, + #room{ + jid = jid:encode(RoomJID), + title = Title, + subject = Subject, + subject_author = SubjectAuthor, + config = Opts + }. + roomconfig_to_string(Options, Lang, FileFormat) -> Title = case lists:keysearch(title, 1, Options) of - {value, Tuple} -> [Tuple]; - false -> [] - end, + {value, Tuple} -> [Tuple]; + false -> [] + end, Os1 = lists:keydelete(title, 1, Options), Os2 = lists:sort(Os1), Options2 = Title ++ Os2, - lists:foldl(fun ({Opt, Val}, R) -> - case get_roomconfig_text(Opt, Lang) of - undefined -> R; - OptText -> - R2 = case Val of - false -> - <<"
", - OptText/binary, "
">>; - true -> - <<"
", - OptText/binary, "
">>; - <<"">> -> - <<"
", - OptText/binary, "
">>; - T -> - case Opt of - password -> - <<"
", - OptText/binary, "
">>; - max_users -> - <<"
", - OptText/binary, ": \"", - (htmlize(integer_to_binary(T), - FileFormat))/binary, - "\"
">>; - title -> - <<"
", - OptText/binary, ": \"", - (htmlize(T, - FileFormat))/binary, - "\"
">>; - description -> - <<"
", - OptText/binary, ": \"", - (htmlize(T, - FileFormat))/binary, - "\"
">>; - allow_private_messages_from_visitors -> - <<"
", - OptText/binary, ": \"", - (htmlize(tr(Lang, misc:atom_to_binary(T)), - FileFormat))/binary, - "\"
">>; - allowpm -> - <<"
", - OptText/binary, ": \"", - (htmlize(tr(Lang, misc:atom_to_binary(T)), - FileFormat))/binary, - "\"
">>; - _ -> <<"\"", T/binary, "\"">> - end - end, - <> - end - end, - <<"">>, Options2). + lists:foldl(fun({Opt, Val}, R) -> + case get_roomconfig_text(Opt, Lang) of + undefined -> R; + OptText -> + R2 = case Val of + false -> + <<"
", + OptText/binary, "
">>; + true -> + <<"
", + OptText/binary, "
">>; + <<"">> -> + <<"
", + OptText/binary, "
">>; + T -> + case Opt of + password -> + <<"
", + OptText/binary, "
">>; + max_users -> + <<"
", + OptText/binary, + ": \"", + (htmlize(integer_to_binary(T), + FileFormat))/binary, + "\"
">>; + title -> + <<"
", + OptText/binary, + ": \"", + (htmlize(T, + FileFormat))/binary, + "\"
">>; + description -> + <<"
", + OptText/binary, + ": \"", + (htmlize(T, + FileFormat))/binary, + "\"
">>; + allow_private_messages_from_visitors -> + <<"
", + OptText/binary, + ": \"", + (htmlize(tr(Lang, misc:atom_to_binary(T)), + FileFormat))/binary, + "\"
">>; + allowpm -> + <<"
", + OptText/binary, + ": \"", + (htmlize(tr(Lang, misc:atom_to_binary(T)), + FileFormat))/binary, + "\"
">>; + _ -> <<"\"", T/binary, "\"">> + end + end, + <> + end + end, + <<"">>, + Options2). + get_roomconfig_text(title, Lang) -> tr(Lang, ?T("Room title")); get_roomconfig_text(persistent, Lang) -> @@ -883,119 +1066,136 @@ get_roomconfig_text(max_users, Lang) -> tr(Lang, ?T("Maximum Number of Occupants")); get_roomconfig_text(_, _) -> undefined. + %% Users = [{JID, Nick, Role}] roomoccupants_to_string(Users, _FileFormat) -> - Res = [role_users_to_string(RoleS, Users1) - || {RoleS, Users1} <- group_by_role(Users), - Users1 /= []], + Res = [ role_users_to_string(RoleS, Users1) + || {RoleS, Users1} <- group_by_role(Users), + Users1 /= [] ], iolist_to_binary([<<"
">>, Res, <<"
">>]). + group_by_role(Users) -> - {Ms, Ps, Vs, Ns} = lists:foldl(fun ({JID, Nick, - moderator}, - {Mod, Par, Vis, Non}) -> - {[{JID, Nick}] ++ Mod, Par, Vis, - Non}; - ({JID, Nick, participant}, - {Mod, Par, Vis, Non}) -> - {Mod, [{JID, Nick}] ++ Par, Vis, - Non}; - ({JID, Nick, visitor}, - {Mod, Par, Vis, Non}) -> - {Mod, Par, [{JID, Nick}] ++ Vis, - Non}; - ({JID, Nick, none}, - {Mod, Par, Vis, Non}) -> - {Mod, Par, Vis, [{JID, Nick}] ++ Non} - end, - {[], [], [], []}, Users), + {Ms, Ps, Vs, Ns} = lists:foldl(fun({JID, Nick, + moderator}, + {Mod, Par, Vis, Non}) -> + {[{JID, Nick}] ++ Mod, + Par, + Vis, + Non}; + ({JID, Nick, participant}, + {Mod, Par, Vis, Non}) -> + {Mod, + [{JID, Nick}] ++ Par, + Vis, + Non}; + ({JID, Nick, visitor}, + {Mod, Par, Vis, Non}) -> + {Mod, + Par, + [{JID, Nick}] ++ Vis, + Non}; + ({JID, Nick, none}, + {Mod, Par, Vis, Non}) -> + {Mod, Par, Vis, [{JID, Nick}] ++ Non} + end, + {[], [], [], []}, + Users), case Ms of - [] -> []; - _ -> [{<<"Moderator">>, Ms}] - end - ++ - case Ms of - [] -> []; - _ -> [{<<"Participant">>, Ps}] - end - ++ - case Ms of - [] -> []; - _ -> [{<<"Visitor">>, Vs}] - end - ++ - case Ms of - [] -> []; - _ -> [{<<"None">>, Ns}] - end. + [] -> []; + _ -> [{<<"Moderator">>, Ms}] + end ++ + case Ms of + [] -> []; + _ -> [{<<"Participant">>, Ps}] + end ++ + case Ms of + [] -> []; + _ -> [{<<"Visitor">>, Vs}] + end ++ + case Ms of + [] -> []; + _ -> [{<<"None">>, Ns}] + end. + role_users_to_string(RoleS, Users) -> SortedUsers = lists:keysort(2, Users), UsersString = << <">> - || {_JID, Nick} <- SortedUsers >>, + || {_JID, Nick} <- SortedUsers >>, <>. + get_room_occupants(RoomJIDString) -> RoomJID = jid:decode(RoomJIDString), RoomName = RoomJID#jid.luser, MucService = RoomJID#jid.lserver, case get_room_state(RoomName, MucService) of - {ok, StateData} -> - [{U#user.jid, U#user.nick, U#user.role} - || U <- maps:values(StateData#state.users)]; - error -> - [] + {ok, StateData} -> + [ {U#user.jid, U#user.nick, U#user.role} + || U <- maps:values(StateData#state.users) ]; + error -> + [] end. + prepare_subject_author({Nick, _}) -> Nick; prepare_subject_author(SA) -> SA. + -spec get_room_state(binary(), binary()) -> {ok, mod_muc_room:state()} | error. get_room_state(RoomName, MucService) -> case mod_muc:find_online_room(RoomName, MucService) of - {ok, RoomPid} -> - get_room_state(RoomPid); - error -> - error + {ok, RoomPid} -> + get_room_state(RoomPid); + error -> + error end. + -spec get_room_state(pid()) -> {ok, mod_muc_room:state()} | error. get_room_state(RoomPid) -> case mod_muc_room:get_state(RoomPid) of - {ok, State} -> {ok, State}; - {error, _} -> error + {ok, State} -> {ok, State}; + {error, _} -> error end. + get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE). + calc_hour_offset(TimeHere) -> TimeZero = calendar:universal_time(), TimeHereHour = - calendar:datetime_to_gregorian_seconds(TimeHere) div - 3600, + calendar:datetime_to_gregorian_seconds(TimeHere) div + 3600, TimeZeroHour = - calendar:datetime_to_gregorian_seconds(TimeZero) div - 3600, + calendar:datetime_to_gregorian_seconds(TimeZero) div + 3600, TimeHereHour - TimeZeroHour. + fjoin(FileList) -> - list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). + list_to_binary(filename:join([ binary_to_list(File) || File <- FileList ])). + -spec tr(binary(), binary()) -> binary(). tr(Lang, Text) -> translate:translate(Lang, Text). + has_no_permanent_store_hint(Packet) -> xmpp:has_subtag(Packet, #hint{type = 'no-store'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-storage'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-permanent-store'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-permanent-storage'}). + mod_opt_type(access_log) -> econf:acl(); mod_opt_type(cssfile) -> @@ -1009,11 +1209,13 @@ mod_opt_type(file_format) -> mod_opt_type(file_permissions) -> econf:and_then( econf:options( - #{mode => econf:non_neg_int(), - group => econf:non_neg_int()}), + #{ + mode => econf:non_neg_int(), + group => econf:non_neg_int() + }), fun(Opts) -> - {proplists:get_value(mode, Opts, 644), - proplists:get_value(group, Opts, 33)} + {proplists:get_value(mode, Opts, 644), + proplists:get_value(group, Opts, 33)} end); mod_opt_type(outdir) -> econf:directory(write); @@ -1026,13 +1228,14 @@ mod_opt_type(url) -> mod_opt_type(top_link) -> econf:and_then( econf:non_empty( - econf:map(econf:binary(), econf:binary())), + econf:map(econf:binary(), econf:binary())), fun hd/1). + -spec mod_options(binary()) -> [{top_link, {binary(), binary()}} | - {file_permissions, - {non_neg_integer(), non_neg_integer()}} | - {atom(), any()}]. + {file_permissions, + {non_neg_integer(), non_neg_integer()}} | + {atom(), any()}]. mod_options(_) -> [{access_log, muc_admin}, {cssfile, {file, filename:join(misc:css_dir(), <<"muc.css">>)}}, @@ -1046,8 +1249,10 @@ mod_options(_) -> {url, undefined}, {top_link, {<<"/">>, <<"Home">>}}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module enables optional logging " "of Multi-User Chat (MUC) public " "conversations to HTML. Once you enable " @@ -1055,69 +1260,93 @@ mod_doc() -> "MUC capable XMPP client, and if they have " "enough privileges, they can request the " "configuration form in which they can set " - "the option to enable room logging."), "", - ?T("Features:"), "", + "the option to enable room logging."), + "", + ?T("Features:"), + "", ?T("- Room details are added on top of each page: " - "room title, JID, author, subject and configuration."), "", + "room title, JID, author, subject and configuration."), + "", ?T("- The room JID in the generated HTML is a link " - "to join the room (using XMPP URI)."), "", + "to join the room (using XMPP URI)."), + "", ?T("- Subject and room configuration changes are tracked " - "and displayed."), "", + "and displayed."), + "", ?T("- Joins, leaves, nick changes, kicks, bans and '/me' " - "are tracked and displayed, including the reason if available."), "", + "are tracked and displayed, including the reason if available."), + "", ?T("- Generated HTML files are XHTML 1.0 Transitional and " - "CSS compliant."), "", - ?T("- Timestamps are self-referencing links."), "", + "CSS compliant."), + "", + ?T("- Timestamps are self-referencing links."), + "", ?T("- Links on top for quicker navigation: " - "Previous day, Next day, Up."), "", + "Previous day, Next day, Up."), + "", ?T("- CSS is used for style definition, and a custom " - "CSS file can be used."), "", - ?T("- URLs on messages and subjects are converted to hyperlinks."), "", - ?T("- Timezone used on timestamps is shown on the log files."), "", - ?T("- A custom link can be added on top of each page."), "", + "CSS file can be used."), + "", + ?T("- URLs on messages and subjects are converted to hyperlinks."), + "", + ?T("- Timezone used on timestamps is shown on the log files."), + "", + ?T("- A custom link can be added on top of each page."), + "", ?T("The module depends on _`mod_muc`_.")], opts => [{access_log, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option restricts which occupants are " "allowed to enable or disable room logging. " "The default value is 'muc_admin'. NOTE: " "for this default setting you need to have an " - "access rule for 'muc_admin' in order to take effect.")}}, + "access rule for 'muc_admin' in order to take effect.") + }}, {cssfile, - #{value => ?T("Path | URL"), + #{ + value => ?T("Path | URL"), desc => ?T("With this option you can set whether the HTML " "files should have a custom CSS file or if they " "need to use the embedded CSS. Allowed values " "are either 'Path' to local file or an 'URL' to " "a remote file. By default a predefined CSS will " - "be embedded into the HTML page.")}}, + "be embedded into the HTML page.") + }}, {dirname, - #{value => "room_jid | room_name", + #{ + value => "room_jid | room_name", desc => ?T("Configure the name of the room directory. " "If set to 'room_jid', the room directory name will " "be the full room JID. Otherwise, the room directory " "name will be only the room name, not including the " - "MUC service name. The default value is 'room_jid'.")}}, + "MUC service name. The default value is 'room_jid'.") + }}, {dirtype, - #{value => "subdirs | plain", + #{ + value => "subdirs | plain", desc => ?T("The type of the created directories can be specified " "with this option. If set to 'subdirs', subdirectories " "are created for each year and month. Otherwise, the " "names of the log files contain the full date, and " - "there are no subdirectories. The default value is 'subdirs'.")}}, + "there are no subdirectories. The default value is 'subdirs'.") + }}, {file_format, - #{value => "html | plaintext", + #{ + value => "html | plaintext", desc => ?T("Define the format of the log files: 'html' stores " "in HTML format, 'plaintext' stores in plain text. " - "The default value is 'html'.")}}, + "The default value is 'html'.") + }}, {file_permissions, - #{value => "{mode: Mode, group: Group}", + #{ + value => "{mode: Mode, group: Group}", desc => ?T("Define the permissions that must be used when " "creating the log files: the number of the mode, " @@ -1126,43 +1355,55 @@ mod_doc() -> example => ["file_permissions:", " mode: 644", - " group: 33"]}}, + " group: 33"] + }}, {outdir, - #{value => ?T("Path"), + #{ + value => ?T("Path"), desc => ?T("This option sets the full path to the directory " "in which the HTML files should be stored. " "Make sure the ejabberd daemon user has write " - "access on that directory. The default value is 'www/muc'.")}}, + "access on that directory. The default value is 'www/muc'.") + }}, {spam_prevention, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If set to 'true', a special attribute is added to links " "that prevent their indexation by search engines. " "The default value is 'true', which mean that 'nofollow' " - "attributes will be added to user submitted links.")}}, + "attributes will be added to user submitted links.") + }}, {timezone, - #{value => "local | universal", + #{ + value => "local | universal", desc => ?T("The time zone for the logs is configurable with " "this option. If set to 'local', the local time, as " "reported to Erlang emulator by the operating system, " "will be used. Otherwise, UTC time will be used. " - "The default value is 'local'.")}}, + "The default value is 'local'.") + }}, {url, - #{value => ?T("URL"), + #{ + value => ?T("URL"), desc => ?T("A top level 'URL' where a client can access " "logs of a particular conference. The conference name " "is appended to the URL if 'dirname' option is set to " "'room_name' or a conference JID is appended to the 'URL' " - "otherwise. There is no default value.")}}, + "otherwise. There is no default value.") + }}, {top_link, - #{value => "{URL: Text}", + #{ + value => "{URL: Text}", desc => ?T("With this option you can customize the link on " "the top right corner of each log file. " "The default value is shown in the example below:"), example => ["top_link:", - " /: Home"]}}]}. + " /: Home"] + }}] + }. diff --git a/src/mod_muc_log_opt.erl b/src/mod_muc_log_opt.erl index fb4d0266f..b09165fb2 100644 --- a/src/mod_muc_log_opt.erl +++ b/src/mod_muc_log_opt.erl @@ -15,69 +15,79 @@ -export([top_link/1]). -export([url/1]). + -spec access_log(gen_mod:opts() | global | binary()) -> 'muc_admin' | acl:acl(). access_log(Opts) when is_map(Opts) -> gen_mod:get_opt(access_log, Opts); access_log(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, access_log). --spec cssfile(gen_mod:opts() | global | binary()) -> {'file',binary()} | {'url',binary()}. + +-spec cssfile(gen_mod:opts() | global | binary()) -> {'file', binary()} | {'url', binary()}. cssfile(Opts) when is_map(Opts) -> gen_mod:get_opt(cssfile, Opts); cssfile(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, cssfile). + -spec dirname(gen_mod:opts() | global | binary()) -> 'room_jid' | 'room_name'. dirname(Opts) when is_map(Opts) -> gen_mod:get_opt(dirname, Opts); dirname(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, dirname). + -spec dirtype(gen_mod:opts() | global | binary()) -> 'plain' | 'subdirs'. dirtype(Opts) when is_map(Opts) -> gen_mod:get_opt(dirtype, Opts); dirtype(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, dirtype). + -spec file_format(gen_mod:opts() | global | binary()) -> 'html' | 'plaintext'. file_format(Opts) when is_map(Opts) -> gen_mod:get_opt(file_format, Opts); file_format(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, file_format). --spec file_permissions(gen_mod:opts() | global | binary()) -> {non_neg_integer(),non_neg_integer()}. + +-spec file_permissions(gen_mod:opts() | global | binary()) -> {non_neg_integer(), non_neg_integer()}. file_permissions(Opts) when is_map(Opts) -> gen_mod:get_opt(file_permissions, Opts); file_permissions(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, file_permissions). + -spec outdir(gen_mod:opts() | global | binary()) -> binary(). outdir(Opts) when is_map(Opts) -> gen_mod:get_opt(outdir, Opts); outdir(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, outdir). + -spec spam_prevention(gen_mod:opts() | global | binary()) -> boolean(). spam_prevention(Opts) when is_map(Opts) -> gen_mod:get_opt(spam_prevention, Opts); spam_prevention(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, spam_prevention). + -spec timezone(gen_mod:opts() | global | binary()) -> 'local' | 'universal'. timezone(Opts) when is_map(Opts) -> gen_mod:get_opt(timezone, Opts); timezone(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, timezone). --spec top_link(gen_mod:opts() | global | binary()) -> {binary(),binary()}. + +-spec top_link(gen_mod:opts() | global | binary()) -> {binary(), binary()}. top_link(Opts) when is_map(Opts) -> gen_mod:get_opt(top_link, Opts); top_link(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, top_link). + -spec url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). url(Opts) when is_map(Opts) -> gen_mod:get_opt(url, Opts); url(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, url). - diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl index 65c37a7ab..9f37b17ed 100644 --- a/src/mod_muc_mnesia.erl +++ b/src/mod_muc_mnesia.erl @@ -28,218 +28,279 @@ -behaviour(mod_muc_room). %% API --export([init/2, import/3, store_room/5, restore_room/3, forget_room/3, - can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). --export([register_online_room/4, unregister_online_room/4, find_online_room/3, - get_online_rooms/3, count_online_rooms/2, rsm_supported/0, - register_online_user/4, unregister_online_user/4, - count_online_rooms_by_user/3, get_online_rooms_by_user/3, - find_online_room_by_pid/2]). --export([set_affiliation/6, set_affiliations/4, get_affiliation/5, - get_affiliations/3, search_affiliation/4]). +-export([init/2, + import/3, + store_room/5, + restore_room/3, + forget_room/3, + can_use_nick/4, + get_rooms/2, + get_nick/3, + set_nick/4]). +-export([register_online_room/4, + unregister_online_room/4, + find_online_room/3, + get_online_rooms/3, + count_online_rooms/2, + rsm_supported/0, + register_online_user/4, + unregister_online_user/4, + count_online_rooms_by_user/3, + get_online_rooms_by_user/3, + find_online_room_by_pid/2]). +-export([set_affiliation/6, + set_affiliations/4, + get_affiliation/5, + get_affiliations/3, + search_affiliation/4]). %% gen_server callbacks --export([start_link/2, init/1, handle_cast/2, handle_call/3, handle_info/2, - terminate/2, code_change/3]). +-export([start_link/2, + init/1, + handle_cast/2, + handle_call/3, + handle_info/2, + terminate/2, + code_change/3]). -export([need_transform/1, transform/1]). -include("mod_muc.hrl"). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -record(state, {}). + %%%=================================================================== %%% API %%%=================================================================== init(Host, Opts) -> Spec = {?MODULE, {?MODULE, start_link, [Host, Opts]}, - transient, 5000, worker, [?MODULE]}, + transient, + 5000, + worker, + [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of - {ok, _Pid} -> ok; + {ok, _Pid} -> ok; %% Maybe started for a vhost which only wanted mnesia for ram %% and this vhost wants mnesia for persitent storage too {error, {already_started, _Pid}} -> init([Host, Opts]); - Err -> Err + Err -> Err end. + start_link(Host, Opts) -> Name = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Name}, ?MODULE, [Host, Opts], []). + store_room(_LServer, Host, Name, Opts, _) -> - F = fun () -> - mnesia:write(#muc_room{name_host = {Name, Host}, - opts = Opts}) - end, + F = fun() -> + mnesia:write(#muc_room{ + name_host = {Name, Host}, + opts = Opts + }) + end, mnesia:transaction(F). + restore_room(_LServer, Host, Name) -> try mnesia:dirty_read(muc_room, {Name, Host}) of - [#muc_room{opts = Opts}] -> Opts; - _ -> error + [#muc_room{opts = Opts}] -> Opts; + _ -> error catch - _:_ -> {error, db_failure} + _:_ -> {error, db_failure} end. + forget_room(_LServer, Host, Name) -> - F = fun () -> mnesia:delete({muc_room, {Name, Host}}) - end, + F = fun() -> mnesia:delete({muc_room, {Name, Host}}) + end, mnesia:transaction(F). + can_use_nick(_LServer, ServiceOrRoom, JID, Nick) -> {LUser, LServer, _} = jid:tolower(JID), LUS = {LUser, LServer}, MatchSpec = case (jid:decode(ServiceOrRoom))#jid.lserver of - ServiceOrRoom -> [{'==', {element, 2, '$1'}, ServiceOrRoom}]; - Service -> [{'orelse', - {'==', {element, 2, '$1'}, Service}, - {'==', {element, 2, '$1'}, ServiceOrRoom} }] + ServiceOrRoom -> [{'==', {element, 2, '$1'}, ServiceOrRoom}]; + Service -> + [{'orelse', + {'==', {element, 2, '$1'}, Service}, + {'==', {element, 2, '$1'}, ServiceOrRoom}}] end, case catch mnesia:dirty_select(muc_registered, - [{#muc_registered{us_host = '$1', - nick = Nick, _ = '_'}, - MatchSpec, - ['$_']}]) - of - {'EXIT', _Reason} -> true; - [] -> true; - [#muc_registered{us_host = {U, _Host}}] -> U == LUS + [{#muc_registered{ + us_host = '$1', + nick = Nick, + _ = '_' + }, + MatchSpec, + ['$_']}]) of + {'EXIT', _Reason} -> true; + [] -> true; + [#muc_registered{us_host = {U, _Host}}] -> U == LUS end. + get_rooms(_LServer, Host) -> mnesia:dirty_select(muc_room, - [{#muc_room{name_host = {'_', Host}, - _ = '_'}, - [], ['$_']}]). + [{#muc_room{ + name_host = {'_', Host}, + _ = '_' + }, + [], + ['$_']}]). + get_nick(_LServer, Host, From) -> {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, case mnesia:dirty_read(muc_registered, {LUS, Host}) of - [] -> error; - [#muc_registered{nick = Nick}] -> Nick + [] -> error; + [#muc_registered{nick = Nick}] -> Nick end. + set_nick(_LServer, ServiceOrRoom, From, Nick) -> {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, - F = fun () -> - case Nick of - <<"">> -> - mnesia:delete({muc_registered, {LUS, ServiceOrRoom}}), - ok; - _ -> + F = fun() -> + case Nick of + <<"">> -> + mnesia:delete({muc_registered, {LUS, ServiceOrRoom}}), + ok; + _ -> Service = (jid:decode(ServiceOrRoom))#jid.lserver, MatchSpec = case (ServiceOrRoom == Service) of - true -> [{'==', {element, 2, '$1'}, ServiceOrRoom}]; - false -> [{'orelse', - {'==', {element, 2, '$1'}, Service}, - {'==', {element, 2, '$1'}, ServiceOrRoom} }] + true -> [{'==', {element, 2, '$1'}, ServiceOrRoom}]; + false -> + [{'orelse', + {'==', {element, 2, '$1'}, Service}, + {'==', {element, 2, '$1'}, ServiceOrRoom}}] end, - Allow = case mnesia:select( - muc_registered, - [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, - MatchSpec, - ['$_']}]) of - [] when (ServiceOrRoom == Service) -> - NickRegistrations = mnesia:select( - muc_registered, - [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, - [], - ['$_']}]), + Allow = case mnesia:select( + muc_registered, + [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, + MatchSpec, + ['$_']}]) of + [] when (ServiceOrRoom == Service) -> + NickRegistrations = mnesia:select( + muc_registered, + [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, + [], + ['$_']}]), not lists:any(fun({_, {_NRUS, NRServiceOrRoom}, _Nick}) -> - Service == (jid:decode(NRServiceOrRoom))#jid.lserver end, + Service == (jid:decode(NRServiceOrRoom))#jid.lserver + end, NickRegistrations); - [] -> true; - [#muc_registered{us_host = {_U, Host}}] + [] -> true; + [#muc_registered{us_host = {_U, Host}}] when (Host == Service) and (ServiceOrRoom /= Service) -> - false; - [#muc_registered{us_host = {U, _Host}}] -> - U == LUS - end, - if Allow -> - mnesia:write(#muc_registered{ - us_host = {LUS, ServiceOrRoom}, - nick = Nick}), - ok; - true -> - false - end - end - end, + false; + [#muc_registered{us_host = {U, _Host}}] -> + U == LUS + end, + if + Allow -> + mnesia:write(#muc_registered{ + us_host = {LUS, ServiceOrRoom}, + nick = Nick + }), + ok; + true -> + false + end + end + end, mnesia:transaction(F). + set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) -> {error, not_implemented}. + set_affiliations(_ServerHost, _Room, _Host, _Affiliations) -> {error, not_implemented}. + get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) -> {error, not_implemented}. + get_affiliations(_ServerHost, _Room, _Host) -> {error, not_implemented}. + search_affiliation(_ServerHost, _Room, _Host, _Affiliation) -> {error, not_implemented}. + register_online_room(_ServerHost, Room, Host, Pid) -> F = fun() -> - mnesia:write( - #muc_online_room{name_host = {Room, Host}, pid = Pid}) - end, + mnesia:write( + #muc_online_room{name_host = {Room, Host}, pid = Pid}) + end, mnesia:transaction(F). + unregister_online_room(_ServerHost, Room, Host, Pid) -> - F = fun () -> - mnesia:delete_object( - #muc_online_room{name_host = {Room, Host}, pid = Pid}) - end, + F = fun() -> + mnesia:delete_object( + #muc_online_room{name_host = {Room, Host}, pid = Pid}) + end, mnesia:transaction(F). + find_online_room(_ServerHost, Room, Host) -> find_online_room(Room, Host). + find_online_room(Room, Host) -> case mnesia:dirty_read(muc_online_room, {Room, Host}) of - [] -> error; - [#muc_online_room{pid = Pid}] -> {ok, Pid} + [] -> error; + [#muc_online_room{pid = Pid}] -> {ok, Pid} end. + find_online_room_by_pid(_ServerHost, Pid) -> Res = - mnesia:dirty_select( - muc_online_room, - ets:fun2ms( - fun(#muc_online_room{name_host = {Name, Host}, pid = PidS}) - when PidS == Pid -> {Name, Host} - end)), + mnesia:dirty_select( + muc_online_room, + ets:fun2ms( + fun(#muc_online_room{name_host = {Name, Host}, pid = PidS}) + when PidS == Pid -> {Name, Host} + end)), case Res of - [{Name, Host}] -> {ok, Name, Host}; - _ -> error + [{Name, Host}] -> {ok, Name, Host}; + _ -> error end. + count_online_rooms(_ServerHost, Host) -> ets:select_count( muc_online_room, ets:fun2ms( - fun(#muc_online_room{name_host = {_, H}}) -> - H == Host - end)). + fun(#muc_online_room{name_host = {_, H}}) -> + H == Host + end)). -get_online_rooms(_ServerHost, Host, - #rsm_set{max = Max, 'after' = After, before = undefined}) + +get_online_rooms(_ServerHost, + Host, + #rsm_set{max = Max, 'after' = After, before = undefined}) when is_binary(After), After /= <<"">> -> lists:reverse(get_online_rooms(next, {After, Host}, Host, 0, Max, [])); -get_online_rooms(_ServerHost, Host, - #rsm_set{max = Max, 'after' = undefined, before = Before}) +get_online_rooms(_ServerHost, + Host, + #rsm_set{max = Max, 'after' = undefined, before = Before}) when is_binary(Before), Before /= <<"">> -> get_online_rooms(prev, {Before, Host}, Host, 0, Max, []); -get_online_rooms(_ServerHost, Host, - #rsm_set{max = Max, 'after' = undefined, before = <<"">>}) -> +get_online_rooms(_ServerHost, + Host, + #rsm_set{max = Max, 'after' = undefined, before = <<"">>}) -> get_online_rooms(last, {<<"">>, Host}, Host, 0, Max, []); get_online_rooms(_ServerHost, Host, #rsm_set{max = Max}) -> lists:reverse(get_online_rooms(first, {<<"">>, Host}, Host, 0, Max, [])); @@ -247,92 +308,125 @@ get_online_rooms(_ServerHost, Host, undefined) -> mnesia:dirty_select( muc_online_room, ets:fun2ms( - fun(#muc_online_room{name_host = {Name, H}, pid = Pid}) - when H == Host -> {Name, Host, Pid} - end)). + fun(#muc_online_room{name_host = {Name, H}, pid = Pid}) + when H == Host -> {Name, Host, Pid} + end)). + -spec get_online_rooms(prev | next | last | first, - {binary(), binary()}, binary(), - non_neg_integer(), non_neg_integer() | undefined, - [{binary(), binary(), pid()}]) -> - [{binary(), binary(), pid()}]. + {binary(), binary()}, + binary(), + non_neg_integer(), + non_neg_integer() | undefined, + [{binary(), binary(), pid()}]) -> + [{binary(), binary(), pid()}]. get_online_rooms(_Action, _Key, _Host, Count, Max, Items) when Count >= Max -> Items; get_online_rooms(Action, Key, Host, Count, Max, Items) -> Call = fun() -> - case Action of - prev -> mnesia:dirty_prev(muc_online_room, Key); - next -> mnesia:dirty_next(muc_online_room, Key); - last -> mnesia:dirty_last(muc_online_room); - first -> mnesia:dirty_first(muc_online_room) - end - end, + case Action of + prev -> mnesia:dirty_prev(muc_online_room, Key); + next -> mnesia:dirty_next(muc_online_room, Key); + last -> mnesia:dirty_last(muc_online_room); + first -> mnesia:dirty_first(muc_online_room) + end + end, NewAction = case Action of - last -> prev; - first -> next; - _ -> Action - end, + last -> prev; + first -> next; + _ -> Action + end, try Call() of - '$end_of_table' -> - Items; - {Room, Host} = NewKey -> - case find_online_room(Room, Host) of - {ok, Pid} -> - get_online_rooms(NewAction, NewKey, Host, - Count + 1, Max, [{Room, Host, Pid}|Items]); - error -> - get_online_rooms(NewAction, NewKey, Host, - Count, Max, Items) - end; - NewKey -> - get_online_rooms(NewAction, NewKey, Host, Count, Max, Items) - catch _:{aborted, {badarg, _}} -> - Items + '$end_of_table' -> + Items; + {Room, Host} = NewKey -> + case find_online_room(Room, Host) of + {ok, Pid} -> + get_online_rooms(NewAction, + NewKey, + Host, + Count + 1, + Max, + [{Room, Host, Pid} | Items]); + error -> + get_online_rooms(NewAction, + NewKey, + Host, + Count, + Max, + Items) + end; + NewKey -> + get_online_rooms(NewAction, NewKey, Host, Count, Max, Items) + catch + _:{aborted, {badarg, _}} -> + Items end. + rsm_supported() -> true. + register_online_user(_ServerHost, {U, S, R}, Room, Host) -> ets:insert(muc_online_users, - #muc_online_users{us = {U, S}, resource = R, - room = Room, host = Host}). + #muc_online_users{ + us = {U, S}, + resource = R, + room = Room, + host = Host + }). + unregister_online_user(_ServerHost, {U, S, R}, Room, Host) -> ets:delete_object(muc_online_users, - #muc_online_users{us = {U, S}, resource = R, - room = Room, host = Host}). + #muc_online_users{ + us = {U, S}, + resource = R, + room = Room, + host = Host + }). + count_online_rooms_by_user(ServerHost, U, S) -> MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)), ets:select_count( muc_online_users, ets:fun2ms( - fun(#muc_online_users{us = {U1, S1}, host = Host}) -> - U == U1 andalso S == S1 andalso MucHost == Host - end)). + fun(#muc_online_users{us = {U1, S1}, host = Host}) -> + U == U1 andalso S == S1 andalso MucHost == Host + end)). + get_online_rooms_by_user(ServerHost, U, S) -> MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)), ets:select( muc_online_users, ets:fun2ms( - fun(#muc_online_users{us = {U1, S1}, room = Room, host = Host}) - when U == U1 andalso S == S1 andalso MucHost == Host -> {Room, Host} - end)). + fun(#muc_online_users{us = {U1, S1}, room = Room, host = Host}) + when U == U1 andalso S == S1 andalso MucHost == Host -> {Room, Host} + end)). -import(_LServer, <<"muc_room">>, + +import(_LServer, + <<"muc_room">>, [Name, RoomHost, SOpts, _TimeStamp]) -> Opts = mod_muc:opts_to_binary(ejabberd_sql:decode_term(SOpts)), mnesia:dirty_write( - #muc_room{name_host = {Name, RoomHost}, - opts = Opts}); -import(_LServer, <<"muc_registered">>, + #muc_room{ + name_host = {Name, RoomHost}, + opts = Opts + }); +import(_LServer, + <<"muc_registered">>, [J, RoomHost, Nick, _TimeStamp]) -> #jid{user = U, server = S} = jid:decode(J), mnesia:dirty_write( - #muc_registered{us_host = {{U, S}, RoomHost}, - nick = Nick}). + #muc_registered{ + us_host = {{U, S}, RoomHost}, + nick = Nick + }). + %%%=================================================================== %%% gen_server callbacks @@ -340,44 +434,51 @@ import(_LServer, <<"muc_registered">>, init([_Host, Opts]) -> MyHosts = mod_muc_opt:hosts(Opts), case gen_mod:db_mod(Opts, mod_muc) of - ?MODULE -> - ejabberd_mnesia:create(?MODULE, muc_room, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, muc_room)}]), - ejabberd_mnesia:create(?MODULE, muc_registered, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, muc_registered)}, - {index, [nick]}]); - _ -> - ok + ?MODULE -> + ejabberd_mnesia:create(?MODULE, + muc_room, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_room)}]), + ejabberd_mnesia:create(?MODULE, + muc_registered, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_registered)}, + {index, [nick]}]); + _ -> + ok end, case gen_mod:ram_db_mod(Opts, mod_muc) of - ?MODULE -> - ejabberd_mnesia:create(?MODULE, muc_online_room, - [{ram_copies, [node()]}, - {type, ordered_set}, - {attributes, record_info(fields, muc_online_room)}]), - catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), - lists:foreach( - fun(MyHost) -> - clean_table_from_bad_node(node(), MyHost) - end, MyHosts), - mnesia:subscribe(system); - _ -> - ok + ?MODULE -> + ejabberd_mnesia:create(?MODULE, + muc_online_room, + [{ram_copies, [node()]}, + {type, ordered_set}, + {attributes, record_info(fields, muc_online_room)}]), + catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), + lists:foreach( + fun(MyHost) -> + clean_table_from_bad_node(node(), MyHost) + end, + MyHosts), + mnesia:subscribe(system); + _ -> + ok end, {ok, #state{}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> clean_table_from_bad_node(Node), {noreply, State}; @@ -387,43 +488,52 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== clean_table_from_bad_node(Node) -> F = fun() -> - Es = mnesia:select( - muc_online_room, - [{#muc_online_room{pid = '$1', _ = '_'}, - [{'==', {node, '$1'}, Node}], - ['$_']}]), - lists:foreach(fun(E) -> - mnesia:delete_object(E) - end, Es) + Es = mnesia:select( + muc_online_room, + [{#muc_online_room{pid = '$1', _ = '_'}, + [{'==', {node, '$1'}, Node}], + ['$_']}]), + lists:foreach(fun(E) -> + mnesia:delete_object(E) + end, + Es) end, mnesia:async_dirty(F). + clean_table_from_bad_node(Node, Host) -> F = fun() -> - Es = mnesia:select( - muc_online_room, - [{#muc_online_room{pid = '$1', - name_host = {'_', Host}, - _ = '_'}, - [{'==', {node, '$1'}, Node}], - ['$_']}]), - lists:foreach(fun(E) -> - mnesia:delete_object(E) - end, Es) + Es = mnesia:select( + muc_online_room, + [{#muc_online_room{ + pid = '$1', + name_host = {'_', Host}, + _ = '_' + }, + [{'==', {node, '$1'}, Node}], + ['$_']}]), + lists:foreach(fun(E) -> + mnesia:delete_object(E) + end, + Es) end, mnesia:async_dirty(F). + need_transform({muc_room, {N, H}, _}) when is_list(N) orelse is_list(H) -> ?INFO_MSG("Mnesia table 'muc_room' will be converted to binary", []), @@ -444,23 +554,28 @@ need_transform({muc_registered, {{U, S}, H}, Nick}) need_transform(_) -> false. + transform({muc_room, {N, H}, Opts} = R) when is_list(N) orelse is_list(H) -> - R#muc_room{name_host = {iolist_to_binary(N), iolist_to_binary(H)}, - opts = mod_muc:opts_to_binary(Opts)}; + R#muc_room{ + name_host = {iolist_to_binary(N), iolist_to_binary(H)}, + opts = mod_muc:opts_to_binary(Opts) + }; transform(#muc_room{opts = Opts} = R) -> Opts2 = case lists:keyfind(allow_private_messages, 1, Opts) of - {_, Value} when is_boolean(Value) -> - Value2 = case Value of - true -> anyone; - false -> none - end, - lists:keyreplace(allow_private_messages, 1, Opts, {allowpm, Value2}); - _ -> - Opts - end, + {_, Value} when is_boolean(Value) -> + Value2 = case Value of + true -> anyone; + false -> none + end, + lists:keyreplace(allow_private_messages, 1, Opts, {allowpm, Value2}); + _ -> + Opts + end, R#muc_room{opts = Opts2}; transform(#muc_registered{us_host = {{U, S}, H}, nick = Nick} = R) -> - R#muc_registered{us_host = {{iolist_to_binary(U), iolist_to_binary(S)}, - iolist_to_binary(H)}, - nick = iolist_to_binary(Nick)}. + R#muc_registered{ + us_host = {{iolist_to_binary(U), iolist_to_binary(S)}, + iolist_to_binary(H)}, + nick = iolist_to_binary(Nick) + }. diff --git a/src/mod_muc_occupantid.erl b/src/mod_muc_occupantid.erl index 1e8eabee2..f4e5daecc 100644 --- a/src/mod_muc_occupantid.erl +++ b/src/mod_muc_occupantid.erl @@ -32,63 +32,78 @@ -behaviour(gen_mod). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("translate.hrl"). -include("mod_muc_room.hrl"). --export([start/2, stop/1, - mod_options/1, mod_doc/0, depends/2]). +-export([start/2, + stop/1, + mod_options/1, + mod_doc/0, + depends/2]). -export([filter_packet/3, remove_room/3]). %%% %%% gen_mod %%% + start(_Host, _Opts) -> create_table(), {ok, [{hook, muc_filter_presence, filter_packet, 10}, {hook, muc_filter_message, filter_packet, 10}, {hook, remove_room, remove_room, 50}]}. + stop(_Host) -> ok. + %%% %%% Hooks %%% + filter_packet(Packet, State, _Nick) -> add_occupantid_packet(Packet, State#state.jid). + remove_room(_LServer, Name, Host) -> delete_salt(jid:make(Name, Host)). + %%% %%% XEP-0421 Occupant-id %%% + add_occupantid_packet(Packet, RoomJid) -> From = xmpp:get_from(Packet), OccupantId = calculate_occupantid(From, RoomJid), OccupantElement = #occupant_id{id = OccupantId}, xmpp:append_subtags(xmpp:remove_subtag(Packet, OccupantElement), [OccupantElement]). + calculate_occupantid(From, RoomJid) -> Term = {jid:remove_resource(From), get_salt(RoomJid)}, misc:term_to_base64(crypto:hash(sha256, io_lib:format("~p", [Term]))). + %%% %%% Table storing rooms' salt %%% -record(muc_occupant_id, {room_jid, salt}). + create_table() -> - ejabberd_mnesia:create(?MODULE, muc_occupant_id, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, muc_occupant_id)}, - {type, set}]). + ejabberd_mnesia:create(?MODULE, + muc_occupant_id, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, muc_occupant_id)}, + {type, set}]). get_salt(RoomJid) -> @@ -101,28 +116,36 @@ get_salt(RoomJid) -> Salt end. + write_salt(RoomJid, Salt) -> mnesia:dirty_write(#muc_occupant_id{room_jid = RoomJid, salt = Salt}). + delete_salt(RoomJid) -> mnesia:dirty_delete(muc_occupant_id, RoomJid). + %%% %%% Doc %%% + mod_options(_Host) -> []. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0421.html" - "[XEP-0421: Anonymous unique occupant identifiers for MUCs]."), "", + "[XEP-0421: Anonymous unique occupant identifiers for MUCs]."), + "", ?T("When the module is enabled, the feature is enabled " "in all semi-anonymous rooms.")], note => "added in 23.10" }. + depends(_, _) -> [{mod_muc, hard}]. diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl index d4550da1a..48cfb8c72 100644 --- a/src/mod_muc_opt.erl +++ b/src/mod_muc_opt.erl @@ -38,207 +38,240 @@ -export([user_presence_shaper/1]). -export([vcard/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_muc, access). + -spec access_admin(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access_admin(Opts) when is_map(Opts) -> gen_mod:get_opt(access_admin, Opts); access_admin(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_admin). + -spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_create(Opts) when is_map(Opts) -> gen_mod:get_opt(access_create, Opts); access_create(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_create). + -spec access_mam(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_mam(Opts) when is_map(Opts) -> gen_mod:get_opt(access_mam, Opts); access_mam(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_mam). + -spec access_persistent(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_persistent(Opts) when is_map(Opts) -> gen_mod:get_opt(access_persistent, Opts); access_persistent(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_persistent). + -spec access_register(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_register(Opts) when is_map(Opts) -> gen_mod:get_opt(access_register, 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); db_type(Host) -> gen_mod:get_module_opt(Host, mod_muc, db_type). --spec default_room_options(gen_mod:opts() | global | binary()) -> [{atom(),'anyone' | 'false' | 'moderators' | 'nobody' | 'none' | 'participants' | 'true' | 'undefined' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer() | tuple()}]. + +-spec default_room_options(gen_mod:opts() | global | binary()) -> [{atom(), 'anyone' | 'false' | 'moderators' | 'nobody' | 'none' | 'participants' | 'true' | 'undefined' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer() | tuple()}]. default_room_options(Opts) when is_map(Opts) -> gen_mod:get_opt(default_room_options, Opts); default_room_options(Host) -> gen_mod:get_module_opt(Host, mod_muc, default_room_options). + -spec hibernation_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). hibernation_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(hibernation_timeout, Opts); hibernation_timeout(Host) -> gen_mod:get_module_opt(Host, mod_muc, hibernation_timeout). + -spec history_size(gen_mod:opts() | global | binary()) -> non_neg_integer(). history_size(Opts) when is_map(Opts) -> gen_mod:get_opt(history_size, Opts); history_size(Host) -> gen_mod:get_module_opt(Host, mod_muc, history_size). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_muc, host). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_muc, hosts). + -spec max_captcha_whitelist(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_captcha_whitelist(Opts) when is_map(Opts) -> gen_mod:get_opt(max_captcha_whitelist, Opts); max_captcha_whitelist(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_captcha_whitelist). + -spec max_password(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_password(Opts) when is_map(Opts) -> gen_mod:get_opt(max_password, Opts); max_password(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_password). + -spec max_room_desc(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_room_desc(Opts) when is_map(Opts) -> gen_mod:get_opt(max_room_desc, Opts); max_room_desc(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_room_desc). + -spec max_room_id(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_room_id(Opts) when is_map(Opts) -> gen_mod:get_opt(max_room_id, Opts); max_room_id(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_room_id). + -spec max_room_name(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_room_name(Opts) when is_map(Opts) -> gen_mod:get_opt(max_room_name, Opts); max_room_name(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_room_name). + -spec max_rooms_discoitems(gen_mod:opts() | global | binary()) -> non_neg_integer(). max_rooms_discoitems(Opts) when is_map(Opts) -> gen_mod:get_opt(max_rooms_discoitems, Opts); max_rooms_discoitems(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_rooms_discoitems). + -spec max_user_conferences(gen_mod:opts() | global | binary()) -> pos_integer(). max_user_conferences(Opts) when is_map(Opts) -> gen_mod:get_opt(max_user_conferences, Opts); max_user_conferences(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_user_conferences). + -spec max_users(gen_mod:opts() | global | binary()) -> pos_integer(). max_users(Opts) when is_map(Opts) -> gen_mod:get_opt(max_users, Opts); max_users(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_users). + -spec max_users_admin_threshold(gen_mod:opts() | global | binary()) -> pos_integer(). max_users_admin_threshold(Opts) when is_map(Opts) -> gen_mod:get_opt(max_users_admin_threshold, Opts); max_users_admin_threshold(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_users_admin_threshold). + -spec max_users_presence(gen_mod:opts() | global | binary()) -> integer(). max_users_presence(Opts) when is_map(Opts) -> gen_mod:get_opt(max_users_presence, Opts); max_users_presence(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_users_presence). + -spec min_message_interval(gen_mod:opts() | global | binary()) -> number(). min_message_interval(Opts) when is_map(Opts) -> gen_mod:get_opt(min_message_interval, Opts); min_message_interval(Host) -> gen_mod:get_module_opt(Host, mod_muc, min_message_interval). + -spec min_presence_interval(gen_mod:opts() | global | binary()) -> number(). min_presence_interval(Opts) when is_map(Opts) -> gen_mod:get_opt(min_presence_interval, Opts); min_presence_interval(Host) -> gen_mod:get_module_opt(Host, mod_muc, min_presence_interval). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_muc, name). + -spec preload_rooms(gen_mod:opts() | global | binary()) -> boolean(). preload_rooms(Opts) when is_map(Opts) -> gen_mod:get_opt(preload_rooms, Opts); preload_rooms(Host) -> gen_mod:get_module_opt(Host, mod_muc, preload_rooms). + -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_muc, queue_type). + -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_muc, ram_db_type). + -spec regexp_room_id(gen_mod:opts() | global | binary()) -> <<>> | misc:re_mp(). regexp_room_id(Opts) when is_map(Opts) -> gen_mod:get_opt(regexp_room_id, Opts); regexp_room_id(Host) -> gen_mod:get_module_opt(Host, mod_muc, regexp_room_id). + -spec room_shaper(gen_mod:opts() | global | binary()) -> atom(). room_shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(room_shaper, Opts); room_shaper(Host) -> gen_mod:get_module_opt(Host, mod_muc, room_shaper). + -spec user_message_shaper(gen_mod:opts() | global | binary()) -> atom(). user_message_shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(user_message_shaper, Opts); user_message_shaper(Host) -> gen_mod:get_module_opt(Host, mod_muc, user_message_shaper). + -spec user_presence_shaper(gen_mod:opts() | global | binary()) -> atom(). user_presence_shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(user_presence_shaper, Opts); user_presence_shaper(Host) -> gen_mod:get_module_opt(Host, mod_muc, user_presence_shaper). + -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_muc, vcard). - diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index b4148b33f..98c2e112f 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -33,57 +33,56 @@ -behaviour(p1_fsm). %% External exports --export([start_link/10, - start_link/8, - start/10, - start/8, - supervisor/1, - get_role/2, - get_affiliation/2, - is_occupant_or_admin/2, - route/2, - expand_opts/1, - config_fields/0, - destroy/1, - destroy/2, - shutdown/1, - get_config/1, - set_config/2, - get_state/1, - get_info/1, - change_item/5, - change_item_async/5, - config_reloaded/1, - subscribe/4, - unsubscribe/2, - is_subscribed/2, - get_subscribers/1, - service_message/2, - get_disco_item/4]). +-export([start_link/10, start_link/8, + start/10, start/8, + supervisor/1, + get_role/2, + get_affiliation/2, + is_occupant_or_admin/2, + route/2, + expand_opts/1, + config_fields/0, + destroy/1, destroy/2, + shutdown/1, + get_config/1, + set_config/2, + get_state/1, + get_info/1, + change_item/5, + change_item_async/5, + config_reloaded/1, + subscribe/4, + unsubscribe/2, + is_subscribed/2, + get_subscribers/1, + service_message/2, + get_disco_item/4]). %% gen_fsm callbacks -export([init/1, - normal_state/2, - handle_event/3, - handle_sync_event/4, - handle_info/3, - terminate/3, - code_change/4]). + normal_state/2, + handle_event/3, + handle_sync_event/4, + handle_info/3, + terminate/3, + code_change/4]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_stacktrace.hrl"). -define(MAX_USERS_DEFAULT_LIST, - [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). + [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). --define(MUC_HAT_ADD_CMD, <<"urn:xmpp:hats:commands:don">>). +-define(MUC_HAT_ADD_CMD, <<"urn:xmpp:hats:commands:don">>). -define(MUC_HAT_REMOVE_CMD, <<"urn:xmpp:hats:commands:doff">>). --define(MUC_HAT_LIST_CMD, <<"urn:xmpp:hats:commands:dlist">>). --define(MAX_HATS_USERS, 100). --define(MAX_HATS_PER_USER, 10). +-define(MUC_HAT_LIST_CMD, <<"urn:xmpp:hats:commands:dlist">>). +-define(MAX_HATS_USERS, 100). +-define(MAX_HATS_PER_USER, 10). -define(CLEAN_ROOM_TIMEOUT, 30000). %-define(DBGFSM, true). @@ -102,197 +101,294 @@ -type fsm_stop() :: {stop, normal, state()}. -type fsm_next() :: {next_state, normal_state, state()}. -type fsm_transition() :: fsm_stop() | fsm_next(). --type disco_item_filter() :: only_non_empty | all | non_neg_integer(). +-type disco_item_filter() :: only_non_empty | all | non_neg_integer(). -type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}. -export_type([state/0, disco_item_filter/0]). --callback set_affiliation(binary(), binary(), binary(), jid(), affiliation(), - binary()) -> ok | {error, any()}. --callback set_affiliations(binary(), binary(), binary(), - affiliations()) -> ok | {error, any()}. --callback get_affiliation(binary(), binary(), binary(), - binary(), binary()) -> {ok, affiliation()} | {error, any()}. + +-callback set_affiliation(binary(), + binary(), + binary(), + jid(), + affiliation(), + binary()) -> ok | {error, any()}. +-callback set_affiliations(binary(), + binary(), + binary(), + affiliations()) -> ok | {error, any()}. +-callback get_affiliation(binary(), + binary(), + binary(), + binary(), + binary()) -> {ok, affiliation()} | {error, any()}. -callback get_affiliations(binary(), binary(), binary()) -> {ok, affiliations()} | {error, any()}. -callback search_affiliation(binary(), binary(), binary(), affiliation()) -> - {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}. + {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}. -ifndef(OTP_BELOW_28). -dialyzer([no_opaque_union]). -endif. + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- --spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), - atom(), jid(), binary(), [{atom(), term()}], ram | file) -> - {ok, pid()} | {error, any()}. -start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Creator, Nick, DefRoomOpts, QueueType) -> +-spec start(binary(), + binary(), + mod_muc:access(), + binary(), + non_neg_integer(), + atom(), + jid(), + binary(), + [{atom(), term()}], + ram | file) -> + {ok, pid()} | {error, any()}. +start(Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + Creator, + Nick, + DefRoomOpts, + QueueType) -> supervisor:start_child( supervisor(ServerHost), [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType]). --spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), - atom(), [{atom(), term()}], ram | file) -> - {ok, pid()} | {error, any()}. + +-spec start(binary(), + binary(), + mod_muc:access(), + binary(), + non_neg_integer(), + atom(), + [{atom(), term()}], + ram | file) -> + {ok, pid()} | {error, any()}. start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> supervisor:start_child( supervisor(ServerHost), [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]). --spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), - atom(), jid(), binary(), [{atom(), term()}], ram | file) -> - {ok, pid()} | {error, any()}. -start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Creator, Nick, DefRoomOpts, QueueType) -> - p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Creator, Nick, DefRoomOpts, QueueType], - ?FSMOPTS). --spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), - atom(), [{atom(), term()}], ram | file) -> - {ok, pid()} | {error, any()}. +-spec start_link(binary(), + binary(), + mod_muc:access(), + binary(), + non_neg_integer(), + atom(), + jid(), + binary(), + [{atom(), term()}], + ram | file) -> + {ok, pid()} | {error, any()}. +start_link(Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + Creator, + Nick, + DefRoomOpts, + QueueType) -> + p1_fsm:start_link(?MODULE, + [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts, QueueType], + ?FSMOPTS). + + +-spec start_link(binary(), + binary(), + mod_muc:access(), + binary(), + non_neg_integer(), + atom(), + [{atom(), term()}], + ram | file) -> + {ok, pid()} | {error, any()}. start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> - p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Opts, QueueType], - ?FSMOPTS). + p1_fsm:start_link(?MODULE, + [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts, QueueType], + ?FSMOPTS). + -spec supervisor(binary()) -> atom(). supervisor(Host) -> gen_mod:get_module_proc(Host, mod_muc_room_sup). + -spec destroy(pid()) -> ok. destroy(Pid) -> p1_fsm:send_all_state_event(Pid, destroy). + -spec destroy(pid(), binary()) -> ok. destroy(Pid, Reason) -> p1_fsm:send_all_state_event(Pid, {destroy, Reason}). + -spec shutdown(pid()) -> boolean(). shutdown(Pid) -> ejabberd_cluster:send(Pid, shutdown). + -spec config_reloaded(pid()) -> boolean(). config_reloaded(Pid) -> ejabberd_cluster:send(Pid, config_reloaded). + -spec get_config(pid()) -> {ok, config()} | {error, notfound | timeout}. get_config(Pid) -> - try p1_fsm:sync_send_all_state_event(Pid, get_config) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + try + p1_fsm:sync_send_all_state_event(Pid, get_config) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} end. + -spec set_config(pid(), config()) -> {ok, config()} | {error, notfound | timeout}. set_config(Pid, Config) -> - try p1_fsm:sync_send_all_state_event(Pid, {change_config, Config}) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + try + p1_fsm:sync_send_all_state_event(Pid, {change_config, Config}) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} end. + -spec change_item(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> - {ok, state()} | {error, notfound | timeout}. + {ok, state()} | {error, notfound | timeout}. change_item(Pid, JID, Type, AffiliationOrRole, Reason) -> - try p1_fsm:sync_send_all_state_event( - Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + try + p1_fsm:sync_send_all_state_event( + Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {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) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + try + p1_fsm:sync_send_all_state_event(Pid, get_state) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} end. + -spec get_info(pid()) -> {ok, #{occupants_number => integer()}} | {error, notfound | timeout}. get_info(Pid) -> try {ok, p1_fsm:sync_send_all_state_event(Pid, get_info)} - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} end. + -spec subscribe(pid(), jid(), binary(), [binary()]) -> {ok, [binary()]} | {error, binary()}. subscribe(Pid, JID, Nick, Nodes) -> - try p1_fsm:sync_send_all_state_event(Pid, {muc_subscribe, JID, Nick, Nodes}) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, ?T("Request has timed out")}; - _:{_, {p1_fsm, _, _}} -> - {error, ?T("Conference room does not exist")} + try + p1_fsm:sync_send_all_state_event(Pid, {muc_subscribe, JID, Nick, Nodes}) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, ?T("Request has timed out")}; + _:{_, {p1_fsm, _, _}} -> + {error, ?T("Conference room does not exist")} end. + -spec unsubscribe(pid(), jid()) -> ok | {error, binary()}. unsubscribe(Pid, JID) -> - try p1_fsm:sync_send_all_state_event(Pid, {muc_unsubscribe, JID}) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, ?T("Request has timed out")}; - exit:{normal, {p1_fsm, _, _}} -> - ok; - _:{_, {p1_fsm, _, _}} -> - {error, ?T("Conference room does not exist")} + try + p1_fsm:sync_send_all_state_event(Pid, {muc_unsubscribe, JID}) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, ?T("Request has timed out")}; + exit:{normal, {p1_fsm, _, _}} -> + ok; + _:{_, {p1_fsm, _, _}} -> + {error, ?T("Conference room does not exist")} end. + -spec is_subscribed(pid(), jid()) -> {true, binary(), [binary()]} | false. is_subscribed(Pid, JID) -> - try p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, JID}) - catch _:{_, {p1_fsm, _, _}} -> false + try + p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, JID}) + catch + _:{_, {p1_fsm, _, _}} -> false end. + -spec get_subscribers(pid()) -> {ok, [jid()]} | {error, notfound | timeout}. get_subscribers(Pid) -> - try p1_fsm:sync_send_all_state_event(Pid, get_subscribers) - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + try + p1_fsm:sync_send_all_state_event(Pid, get_subscribers) + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} end. + -spec service_message(pid(), binary()) -> ok. service_message(Pid, Text) -> p1_fsm:send_all_state_event(Pid, {service_message, Text}). + -spec get_disco_item(pid(), disco_item_filter(), jid(), binary()) -> - {ok, binary()} | {error, notfound | timeout}. + {ok, binary()} | {error, notfound | timeout}. get_disco_item(Pid, Filter, JID, Lang) -> Timeout = 100, Time = erlang:system_time(millisecond), - Query = {get_disco_item, Filter, JID, Lang, Time+Timeout}, + Query = {get_disco_item, Filter, JID, Lang, Time + Timeout}, try p1_fsm:sync_send_all_state_event(Pid, Query, Timeout) of - {item, Desc} -> - {ok, Desc}; - false -> - {error, notfound} - catch _:{timeout, {p1_fsm, _, _}} -> - {error, timeout}; - _:{_, {p1_fsm, _, _}} -> - {error, notfound} + {item, Desc} -> + {ok, Desc}; + false -> + {error, notfound} + catch + _:{timeout, {p1_fsm, _, _}} -> + {error, timeout}; + _:{_, {p1_fsm, _, _}} -> + {error, notfound} end. + %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- + init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts, QueueType]) -> process_flag(trap_exit, true), @@ -300,21 +396,26 @@ init([Host, ServerHost, Access, Room, HistorySize, Shaper = ejabberd_shaper:new(RoomShaper), RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), State = set_opts(DefRoomOpts, - #state{host = Host, server_host = ServerHost, - access = Access, room = Room, - history = lqueue_new(HistorySize, QueueType), - jid = jid:make(Room, Host), - just_created = true, - room_queue = RoomQueue, - room_shaper = Shaper}), + #state{ + host = Host, + server_host = ServerHost, + access = Access, + room = Room, + history = lqueue_new(HistorySize, QueueType), + jid = jid:make(Room, Host), + just_created = true, + room_queue = RoomQueue, + room_shaper = Shaper + }), State1 = set_affiliation(Creator, owner, State), store_room(State1), ?INFO_MSG("Created MUC room ~ts@~ts by ~ts", - [Room, Host, jid:encode(Creator)]), + [Room, Host, jid:encode(Creator)]), add_to_log(room_existence, created, State1), add_to_log(room_existence, started, State1), ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), - erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(), + erlang:send_after(?CLEAN_ROOM_TIMEOUT, + self(), close_room_if_temporary_and_empty), {ok, normal_state, reset_hibernate_timer(State1)}; init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) -> @@ -323,319 +424,355 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) Shaper = ejabberd_shaper:new(RoomShaper), RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), Jid = jid:make(Room, Host), - State = set_opts(Opts, #state{host = Host, - server_host = ServerHost, - access = Access, - room = Room, - history = lqueue_new(HistorySize, QueueType), - jid = Jid, - room_queue = RoomQueue, - room_shaper = Shaper}), + State = set_opts(Opts, + #state{ + host = Host, + server_host = ServerHost, + access = Access, + room = Room, + history = lqueue_new(HistorySize, QueueType), + jid = Jid, + room_queue = RoomQueue, + room_shaper = Shaper + }), add_to_log(room_existence, started, State), ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), State1 = cleanup_affiliations(State), State2 = - case {lists:keyfind(hibernation_time, 1, Opts), - (State1#state.config)#config.mam, - (State1#state.history)#lqueue.max} of - {{_, V}, true, L} when is_integer(V), L > 0 -> - {Msgs, _, _} = mod_mam:select(ServerHost, Jid, Jid, [], - #rsm_set{max = L, before = <<"9999999999999999">>}, - groupchat, only_messages), - Hist2 = - lists:foldl( - fun({_, TS, #forwarded{sub_els = [#message{meta = #{archive_nick := Nick}} = Msg]}}, Hist) -> - Pkt = xmpp:set_from_to(Msg, jid:replace_resource(Jid, Nick), Jid), - Size = element_size(Pkt), - lqueue_in({Nick, Pkt, false, misc:usec_to_now(TS), Size}, Hist) - end, State1#state.history, Msgs), - State1#state{history = Hist2}; - _ -> - State1 - end, - erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(), + case {lists:keyfind(hibernation_time, 1, Opts), + (State1#state.config)#config.mam, + (State1#state.history)#lqueue.max} of + {{_, V}, true, L} when is_integer(V), L > 0 -> + {Msgs, _, _} = mod_mam:select(ServerHost, + Jid, + Jid, + [], + #rsm_set{max = L, before = <<"9999999999999999">>}, + groupchat, + only_messages), + Hist2 = + lists:foldl( + fun({_, TS, #forwarded{sub_els = [#message{meta = #{archive_nick := Nick}} = Msg]}}, Hist) -> + Pkt = xmpp:set_from_to(Msg, jid:replace_resource(Jid, Nick), Jid), + Size = element_size(Pkt), + lqueue_in({Nick, Pkt, false, misc:usec_to_now(TS), Size}, Hist) + end, + State1#state.history, + Msgs), + State1#state{history = Hist2}; + _ -> + State1 + end, + erlang:send_after(?CLEAN_ROOM_TIMEOUT, + self(), close_room_if_temporary_and_empty), {ok, normal_state, reset_hibernate_timer(State2)}. + normal_state({route, <<"">>, - #message{from = From, type = Type, lang = Lang} = Packet}, - StateData) -> + #message{from = From, type = Type, lang = Lang} = Packet}, + StateData) -> case is_user_online(From, StateData) orelse - is_subscriber(From, StateData) orelse - is_user_allowed_message_nonparticipant(From, StateData) of - true when Type == groupchat -> - Activity = get_user_activity(From, StateData), - Now = erlang:system_time(microsecond), - MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000000), - Size = element_size(Packet), - {MessageShaper, MessageShaperInterval} = - ejabberd_shaper:update(Activity#activity.message_shaper, Size), - if Activity#activity.message /= undefined -> - ErrText = ?T("Traffic rate limit is exceeded"), - Err = xmpp:err_resource_constraint(ErrText, Lang), - ejabberd_router:route_error(Packet, Err), - {next_state, normal_state, StateData}; - Now >= Activity#activity.message_time + MinMessageInterval, - MessageShaperInterval == 0 -> - {RoomShaper, RoomShaperInterval} = - ejabberd_shaper:update(StateData#state.room_shaper, Size), - RoomQueueEmpty = case StateData#state.room_queue of - undefined -> true; - RQ -> p1_queue:is_empty(RQ) - end, - if RoomShaperInterval == 0, RoomQueueEmpty -> - NewActivity = Activity#activity{ - message_time = Now, - message_shaper = MessageShaper}, - StateData1 = store_user_activity(From, - NewActivity, - StateData), - StateData2 = StateData1#state{room_shaper = - RoomShaper}, - process_groupchat_message(Packet, - StateData2); - true -> - StateData1 = if RoomQueueEmpty -> - erlang:send_after(RoomShaperInterval, - self(), - process_room_queue), - StateData#state{room_shaper = - RoomShaper}; - true -> StateData - end, - NewActivity = Activity#activity{ - message_time = Now, - message_shaper = MessageShaper, - message = Packet}, - RoomQueue = p1_queue:in({message, From}, - StateData#state.room_queue), - StateData2 = store_user_activity(From, - NewActivity, - StateData1), - StateData3 = StateData2#state{room_queue = RoomQueue}, - {next_state, normal_state, StateData3} - end; - true -> - MessageInterval = (Activity#activity.message_time + - MinMessageInterval - Now) div 1000, - Interval = lists:max([MessageInterval, - MessageShaperInterval]), - erlang:send_after(Interval, self(), - {process_user_message, From}), - NewActivity = Activity#activity{ - message = Packet, - message_shaper = MessageShaper}, - StateData1 = store_user_activity(From, NewActivity, StateData), - {next_state, normal_state, StateData1} - end; - true when Type == error -> - case is_user_online(From, StateData) of - true -> - ErrorText = ?T("It is not allowed to send error messages to the" - " room. The participant (~s) has sent an error " - "message (~s) and got kicked from the room"), - NewState = expulse_participant(Packet, From, StateData, - translate:translate(Lang, - ErrorText)), - close_room_if_temporary_and_empty(NewState); - _ -> - {next_state, normal_state, StateData} - end; - true when Type == chat -> - ErrText = ?T("It is not allowed to send private messages " - "to the conference"), - Err = xmpp:err_not_acceptable(ErrText, Lang), - ejabberd_router:route_error(Packet, Err), - {next_state, normal_state, StateData}; - true when Type == normal -> - {next_state, normal_state, - try xmpp:decode_els(Packet) of - Pkt -> process_normal_message(From, Pkt, StateData) - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Packet, Err), - StateData - end}; - true -> - ErrText = ?T("Improper message type"), - Err = xmpp:err_not_acceptable(ErrText, Lang), - ejabberd_router:route_error(Packet, Err), - {next_state, normal_state, StateData}; - false when Type /= error -> - handle_roommessage_from_nonparticipant(Packet, StateData, From), - {next_state, normal_state, StateData}; - false -> - {next_state, normal_state, StateData} + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of + true when Type == groupchat -> + Activity = get_user_activity(From, StateData), + Now = erlang:system_time(microsecond), + MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000000), + Size = element_size(Packet), + {MessageShaper, MessageShaperInterval} = + ejabberd_shaper:update(Activity#activity.message_shaper, Size), + if + Activity#activity.message /= undefined -> + ErrText = ?T("Traffic rate limit is exceeded"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + Now >= Activity#activity.message_time + MinMessageInterval, + MessageShaperInterval == 0 -> + {RoomShaper, RoomShaperInterval} = + ejabberd_shaper:update(StateData#state.room_shaper, Size), + RoomQueueEmpty = case StateData#state.room_queue of + undefined -> true; + RQ -> p1_queue:is_empty(RQ) + end, + if + RoomShaperInterval == 0, RoomQueueEmpty -> + NewActivity = Activity#activity{ + message_time = Now, + message_shaper = MessageShaper + }, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + StateData2 = StateData1#state{ + room_shaper = + RoomShaper + }, + process_groupchat_message(Packet, + StateData2); + true -> + StateData1 = if + RoomQueueEmpty -> + erlang:send_after(RoomShaperInterval, + self(), + process_room_queue), + StateData#state{ + room_shaper = + RoomShaper + }; + true -> StateData + end, + NewActivity = Activity#activity{ + message_time = Now, + message_shaper = MessageShaper, + message = Packet + }, + RoomQueue = p1_queue:in({message, From}, + StateData#state.room_queue), + StateData2 = store_user_activity(From, + NewActivity, + StateData1), + StateData3 = StateData2#state{room_queue = RoomQueue}, + {next_state, normal_state, StateData3} + end; + true -> + MessageInterval = (Activity#activity.message_time + + MinMessageInterval - Now) div 1000, + Interval = lists:max([MessageInterval, + MessageShaperInterval]), + erlang:send_after(Interval, + self(), + {process_user_message, From}), + NewActivity = Activity#activity{ + message = Packet, + message_shaper = MessageShaper + }, + StateData1 = store_user_activity(From, NewActivity, StateData), + {next_state, normal_state, StateData1} + end; + true when Type == error -> + case is_user_online(From, StateData) of + true -> + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room"), + NewState = expulse_participant(Packet, + From, + StateData, + translate:translate(Lang, + ErrorText)), + close_room_if_temporary_and_empty(NewState); + _ -> + {next_state, normal_state, StateData} + end; + true when Type == chat -> + ErrText = ?T("It is not allowed to send private messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + true when Type == normal -> + {next_state, normal_state, + try xmpp:decode_els(Packet) of + Pkt -> process_normal_message(From, Pkt, StateData) + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + StateData + end}; + true -> + ErrText = ?T("Improper message type"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData}; + false when Type /= error -> + handle_roommessage_from_nonparticipant(Packet, StateData, From), + {next_state, normal_state, StateData}; + false -> + {next_state, normal_state, StateData} end; normal_state({route, <<"">>, - #iq{from = From, type = Type, lang = Lang, sub_els = [_]} = IQ0}, - StateData) when Type == get; Type == set -> + #iq{from = From, type = Type, lang = Lang, sub_els = [_]} = IQ0}, + StateData) when Type == get; Type == set -> try - case ejabberd_hooks:run_fold( - muc_process_iq, - StateData#state.server_host, - xmpp:set_from_to(xmpp:decode_els(IQ0), - From, StateData#state.jid), - [StateData]) of - ignore -> - {next_state, normal_state, StateData}; + case ejabberd_hooks:run_fold( + muc_process_iq, + StateData#state.server_host, + xmpp:set_from_to(xmpp:decode_els(IQ0), + From, + StateData#state.jid), + [StateData]) of + ignore -> + {next_state, normal_state, StateData}; {ignore, StateData2} -> - {next_state, normal_state, StateData2}; - #iq{type = T} = IQRes when T == error; T == result -> - ejabberd_router:route(IQRes), - {next_state, normal_state, StateData}; - #iq{sub_els = [SubEl]} = IQ -> - Res1 = case SubEl of - #muc_admin{} -> - process_iq_admin(From, IQ, StateData); - #muc_owner{} -> - process_iq_owner(From, IQ, StateData); - #disco_info{} -> - process_iq_disco_info(From, IQ, StateData); - #disco_items{} -> - process_iq_disco_items(From, IQ, StateData); - #vcard_temp{} -> - process_iq_vcard(From, IQ, StateData); - #muc_subscribe{} -> - process_iq_mucsub(From, IQ, StateData); - #muc_unsubscribe{} -> - process_iq_mucsub(From, IQ, StateData); - #muc_subscriptions{} -> - process_iq_mucsub(From, IQ, StateData); - #xcaptcha{} -> - process_iq_captcha(From, IQ, StateData); - #adhoc_command{} -> - process_iq_adhoc(From, IQ, StateData); - #register{} -> + {next_state, normal_state, StateData2}; + #iq{type = T} = IQRes when T == error; T == result -> + ejabberd_router:route(IQRes), + {next_state, normal_state, StateData}; + #iq{sub_els = [SubEl]} = IQ -> + Res1 = case SubEl of + #muc_admin{} -> + process_iq_admin(From, IQ, StateData); + #muc_owner{} -> + process_iq_owner(From, IQ, StateData); + #disco_info{} -> + process_iq_disco_info(From, IQ, StateData); + #disco_items{} -> + process_iq_disco_items(From, IQ, StateData); + #vcard_temp{} -> + process_iq_vcard(From, IQ, StateData); + #muc_subscribe{} -> + process_iq_mucsub(From, IQ, StateData); + #muc_unsubscribe{} -> + process_iq_mucsub(From, IQ, StateData); + #muc_subscriptions{} -> + process_iq_mucsub(From, IQ, StateData); + #xcaptcha{} -> + process_iq_captcha(From, IQ, StateData); + #adhoc_command{} -> + process_iq_adhoc(From, IQ, StateData); + #register{} -> mod_muc:process_iq_register(IQ); - #message_moderate{id = Id, reason = Reason} -> % moderate:1 - process_iq_moderate(From, IQ, Id, Reason, StateData); - #fasten_apply_to{id = ModerateId} = ApplyTo -> - case xmpp:get_subtag(ApplyTo, #message_moderate_21{}) of - #message_moderate_21{reason = Reason} -> % moderate:0 - process_iq_moderate(From, IQ, ModerateId, Reason, StateData); - _ -> - Txt = ?T("The feature requested is not " - "supported by the conference"), - {error, xmpp:err_service_unavailable(Txt, Lang)} - end; - _ -> - Txt = ?T("The feature requested is not " - "supported by the conference"), - {error, xmpp:err_service_unavailable(Txt, Lang)} - end, - {IQRes, NewStateData} = - case Res1 of - {result, Res, SD} -> - {xmpp:make_iq_result(IQ, Res), SD}; - {result, Res} -> - {xmpp:make_iq_result(IQ, Res), StateData}; - {ignore, SD} -> - {ignore, SD}; - {error, Error} -> - {xmpp:make_error(IQ0, Error), StateData} - end, - if IQRes /= ignore -> - ejabberd_router:route(IQRes); - true -> - ok - end, - case NewStateData of - stop -> - Conf = StateData#state.config, - {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; - _ when NewStateData#state.just_created -> - close_room_if_temporary_and_empty(NewStateData); - _ -> - {next_state, normal_state, NewStateData} - end - end - catch _:{xmpp_codec, Why} -> - ErrTxt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(ErrTxt, Lang), - ejabberd_router:route_error(IQ0, Err), - {next_state, normal_state, StateData} + #message_moderate{id = Id, reason = Reason} -> % moderate:1 + process_iq_moderate(From, IQ, Id, Reason, StateData); + #fasten_apply_to{id = ModerateId} = ApplyTo -> + case xmpp:get_subtag(ApplyTo, #message_moderate_21{}) of + #message_moderate_21{reason = Reason} -> % moderate:0 + process_iq_moderate(From, IQ, ModerateId, Reason, StateData); + _ -> + Txt = ?T("The feature requested is not " + "supported by the conference"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + end; + _ -> + Txt = ?T("The feature requested is not " + "supported by the conference"), + {error, xmpp:err_service_unavailable(Txt, Lang)} + end, + {IQRes, NewStateData} = + case Res1 of + {result, Res, SD} -> + {xmpp:make_iq_result(IQ, Res), SD}; + {result, Res} -> + {xmpp:make_iq_result(IQ, Res), StateData}; + {ignore, SD} -> + {ignore, SD}; + {error, Error} -> + {xmpp:make_error(IQ0, Error), StateData} + end, + if + IQRes /= ignore -> + ejabberd_router:route(IQRes); + true -> + ok + end, + case NewStateData of + stop -> + Conf = StateData#state.config, + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; + _ when NewStateData#state.just_created -> + close_room_if_temporary_and_empty(NewStateData); + _ -> + {next_state, normal_state, NewStateData} + end + end + catch + _:{xmpp_codec, Why} -> + ErrTxt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(ErrTxt, Lang), + ejabberd_router:route_error(IQ0, Err), + {next_state, normal_state, StateData} end; normal_state({route, <<"">>, #iq{} = IQ}, StateData) -> Err = xmpp:err_bad_request(), ejabberd_router:route_error(IQ, Err), case StateData#state.just_created of - true -> {stop, normal, StateData}; - _ -> {next_state, normal_state, StateData} + true -> {stop, normal, StateData}; + _ -> {next_state, normal_state, StateData} end; normal_state({route, Nick, #presence{from = From} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), Now = erlang:system_time(microsecond), MinPresenceInterval = - trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000000), - if (Now >= Activity#activity.presence_time + MinPresenceInterval) - and (Activity#activity.presence == undefined) -> - NewActivity = Activity#activity{presence_time = Now}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - process_presence(Nick, Packet, StateData1); - true -> - if Activity#activity.presence == undefined -> - Interval = (Activity#activity.presence_time + - MinPresenceInterval - Now) div 1000, - erlang:send_after(Interval, self(), - {process_user_presence, From}); - true -> ok - end, - NewActivity = Activity#activity{presence = {Nick, Packet}}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - {next_state, normal_state, StateData1} + trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000000), + if + (Now >= Activity#activity.presence_time + MinPresenceInterval) and + (Activity#activity.presence == undefined) -> + NewActivity = Activity#activity{presence_time = Now}, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + process_presence(Nick, Packet, StateData1); + true -> + if + Activity#activity.presence == undefined -> + Interval = (Activity#activity.presence_time + + MinPresenceInterval - Now) div 1000, + erlang:send_after(Interval, + self(), + {process_user_presence, From}); + true -> ok + end, + NewActivity = Activity#activity{presence = {Nick, Packet}}, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + {next_state, normal_state, StateData1} end; normal_state({route, ToNick, - #message{from = From, type = Type, lang = Lang} = Packet}, - StateData) -> + #message{from = From, type = Type, lang = Lang} = Packet}, + StateData) -> case decide_fate_message(Packet, From, StateData) of - {expulse_sender, Reason} -> - ?DEBUG(Reason, []), - ErrorText = ?T("It is not allowed to send error messages to the" - " room. The participant (~s) has sent an error " - "message (~s) and got kicked from the room"), - NewState = expulse_participant(Packet, From, StateData, - translate:translate(Lang, ErrorText)), - {next_state, normal_state, NewState}; - forget_message -> - {next_state, normal_state, StateData}; - continue_delivery -> - case {is_user_allowed_private_message(From, StateData), - is_user_online(From, StateData) orelse - is_subscriber(From, StateData) orelse - is_user_allowed_message_nonparticipant(From, StateData)} of - {true, true} when Type == groupchat -> - ErrText = ?T("It is not allowed to send private messages " - "of type \"groupchat\""), - Err = xmpp:err_bad_request(ErrText, Lang), - ejabberd_router:route_error(Packet, Err); - {true, true} -> - case find_jids_by_nick(ToNick, StateData) of - [] -> - ErrText = ?T("Recipient is not in the conference room"), - Err = xmpp:err_item_not_found(ErrText, Lang), - ejabberd_router:route_error(Packet, Err); - ToJIDs -> - SrcIsVisitor = is_visitor(From, StateData), - DstIsModerator = is_moderator(hd(ToJIDs), StateData), - PmFromVisitors = - (StateData#state.config)#config.allow_private_messages_from_visitors, - if SrcIsVisitor == false; - PmFromVisitors == anyone; - (PmFromVisitors == moderators) and - DstIsModerator -> - {FromNick, _} = get_participant_data(From, StateData), - FromNickJID = - jid:replace_resource(StateData#state.jid, - FromNick), - X = #muc_user{}, + {expulse_sender, Reason} -> + ?DEBUG(Reason, []), + ErrorText = ?T("It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room"), + NewState = expulse_participant(Packet, + From, + StateData, + translate:translate(Lang, ErrorText)), + {next_state, normal_state, NewState}; + forget_message -> + {next_state, normal_state, StateData}; + continue_delivery -> + case {is_user_allowed_private_message(From, StateData), + is_user_online(From, StateData) orelse + is_subscriber(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData)} of + {true, true} when Type == groupchat -> + ErrText = ?T("It is not allowed to send private messages " + "of type \"groupchat\""), + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {true, true} -> + case find_jids_by_nick(ToNick, StateData) of + [] -> + ErrText = ?T("Recipient is not in the conference room"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + ToJIDs -> + SrcIsVisitor = is_visitor(From, StateData), + DstIsModerator = is_moderator(hd(ToJIDs), StateData), + PmFromVisitors = + (StateData#state.config)#config.allow_private_messages_from_visitors, + if + SrcIsVisitor == false; + PmFromVisitors == anyone; + (PmFromVisitors == moderators) and + DstIsModerator -> + {FromNick, _} = get_participant_data(From, StateData), + FromNickJID = + jid:replace_resource(StateData#state.jid, + FromNick), + X = #muc_user{}, Packet2 = xmpp:set_subtag(Packet, X), case ejabberd_hooks:run_fold(muc_filter_message, StateData#state.server_host, - xmpp:put_meta(Packet2, mam_ignore, true), + xmpp:put_meta(Packet2, mam_ignore, true), [StateData, FromNick]) of drop -> ok; @@ -644,85 +781,91 @@ normal_state({route, ToNick, lists:foreach( fun(ToJID) -> ejabberd_router:route(xmpp:set_to(PrivMsg, ToJID)) - end, ToJIDs) + end, + ToJIDs) end; - true -> - ErrText = ?T("You are not allowed to send private messages"), - Err = xmpp:err_forbidden(ErrText, Lang), - ejabberd_router:route_error(Packet, Err) - end - end; - {true, false} -> - ErrText = ?T("Only occupants are allowed to send messages " - "to the conference"), - Err = xmpp:err_not_acceptable(ErrText, Lang), - ejabberd_router:route_error(Packet, Err); - {false, _} -> - ErrText = ?T("You are not allowed to send private messages"), - Err = xmpp:err_forbidden(ErrText, Lang), - ejabberd_router:route_error(Packet, Err) - end, - {next_state, normal_state, StateData} + true -> + ErrText = ?T("You are not allowed to send private messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + end + end; + {true, false} -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + {false, _} -> + ErrText = ?T("You are not allowed to send private messages"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + end, + {next_state, normal_state, StateData} end; normal_state({route, ToNick, - #iq{from = From, lang = Lang} = Packet}, - #state{config = #config{allow_query_users = AllowQuery}} = StateData) -> + #iq{from = From, lang = Lang} = Packet}, + #state{config = #config{allow_query_users = AllowQuery}} = StateData) -> try maps:get(jid:tolower(From), StateData#state.users) of - #user{nick = FromNick} when AllowQuery orelse ToNick == FromNick -> - case find_jid_by_nick(ToNick, StateData) of - false -> - ErrText = ?T("Recipient is not in the conference room"), - Err = xmpp:err_item_not_found(ErrText, Lang), - ejabberd_router:route_error(Packet, Err); - To -> - FromJID = jid:replace_resource(StateData#state.jid, FromNick), - case direct_iq_type(Packet) of - vcard -> - ejabberd_router:route_iq( - xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), - Packet, self()); - pubsub -> - ejabberd_router:route_iq( - xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), - Packet, self()); - ping when ToNick == FromNick -> - %% Self-ping optimization from XEP-0410 - ejabberd_router:route(xmpp:make_iq_result(Packet)); - response -> - ejabberd_router:route(xmpp:set_from_to(Packet, FromJID, To)); - #stanza_error{} = Err -> - ejabberd_router:route_error(Packet, Err); - _OtherRequest -> - ejabberd_router:route_iq( - xmpp:set_from_to(Packet, FromJID, To), Packet, self()) - end - end; - _ -> - ErrText = ?T("Queries to the conference members are " - "not allowed in this room"), - Err = xmpp:err_not_allowed(ErrText, Lang), - ejabberd_router:route_error(Packet, Err) - catch _:{badkey, _} -> - ErrText = ?T("Only occupants are allowed to send queries " - "to the conference"), - Err = xmpp:err_not_acceptable(ErrText, Lang), - ejabberd_router:route_error(Packet, Err) + #user{nick = FromNick} when AllowQuery orelse ToNick == FromNick -> + case find_jid_by_nick(ToNick, StateData) of + false -> + ErrText = ?T("Recipient is not in the conference room"), + Err = xmpp:err_item_not_found(ErrText, Lang), + ejabberd_router:route_error(Packet, Err); + To -> + FromJID = jid:replace_resource(StateData#state.jid, FromNick), + case direct_iq_type(Packet) of + vcard -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), + Packet, + self()); + pubsub -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), + Packet, + self()); + ping when ToNick == FromNick -> + %% Self-ping optimization from XEP-0410 + ejabberd_router:route(xmpp:make_iq_result(Packet)); + response -> + ejabberd_router:route(xmpp:set_from_to(Packet, FromJID, To)); + #stanza_error{} = Err -> + ejabberd_router:route_error(Packet, Err); + _OtherRequest -> + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, To), Packet, self()) + end + end; + _ -> + ErrText = ?T("Queries to the conference members are " + "not allowed in this room"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) + catch + _:{badkey, _} -> + ErrText = ?T("Only occupants are allowed to send queries " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err) end, {next_state, normal_state, StateData}; normal_state(hibernate, StateData) -> case maps:size(StateData#state.users) of - 0 -> - store_room_no_checks(StateData, [], true), - ?INFO_MSG("Hibernating room ~ts@~ts", [StateData#state.room, StateData#state.host]), - {stop, normal, StateData#state{hibernate_timer = hibernating}}; - _ -> - {next_state, normal_state, StateData} + 0 -> + store_room_no_checks(StateData, [], true), + ?INFO_MSG("Hibernating room ~ts@~ts", [StateData#state.room, StateData#state.host]), + {stop, normal, StateData#state{hibernate_timer = hibernating}}; + _ -> + {next_state, normal_state, StateData} end; normal_state(_Event, StateData) -> {next_state, normal_state, StateData}. -handle_event({service_message, Msg}, _StateName, - StateData) -> + +handle_event({service_message, Msg}, + _StateName, + StateData) -> MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)}, send_wrapped_multiple( StateData#state.jid, @@ -731,236 +874,293 @@ handle_event({service_message, Msg}, _StateName, ?NS_MUCSUB_NODES_MESSAGES, StateData), NSD = add_message_to_history(<<"">>, - StateData#state.jid, MessagePkt, StateData), + StateData#state.jid, + MessagePkt, + StateData), {next_state, normal_state, NSD}; -handle_event({destroy, Reason}, _StateName, - StateData) -> +handle_event({destroy, Reason}, + _StateName, + StateData) -> _ = destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, StateData), ?INFO_MSG("Destroyed MUC room ~ts with reason: ~p", - [jid:encode(StateData#state.jid), Reason]), + [jid:encode(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), Conf = StateData#state.config, {stop, shutdown, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; handle_event(destroy, StateName, StateData) -> ?INFO_MSG("Destroyed MUC room ~ts", - [jid:encode(StateData#state.jid)]), + [jid:encode(StateData#state.jid)]), handle_event({destroy, <<"">>}, StateName, StateData); handle_event({set_affiliations, Affiliations}, - StateName, StateData) -> + 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, _} -> + {error, _} -> {next_state, StateName, StateData}; StateData -> {next_state, StateName, StateData}; - NSD -> - store_room(NSD), + NSD -> + store_room(NSD), {next_state, StateName, NSD} end; handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. + handle_sync_event({get_disco_item, Filter, JID, Lang, Time}, _From, StateName, StateData) -> Len = maps:size(StateData#state.nicks), Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of - true -> - get_roomdesc_reply(JID, StateData, - get_roomdesc_tail(StateData, Lang)); - false -> - false - end, + true -> + get_roomdesc_reply(JID, + StateData, + get_roomdesc_tail(StateData, Lang)); + false -> + false + end, CurrentTime = erlang:system_time(millisecond), - if CurrentTime < Time -> - {reply, Reply, StateName, StateData}; - true -> - {next_state, StateName, StateData} + if + CurrentTime < Time -> + {reply, Reply, StateName, StateData}; + true -> + {next_state, StateName, StateData} end; %% These two clauses are only for backward compatibility with nodes running old code handle_sync_event({get_disco_item, JID, Lang}, From, StateName, StateData) -> handle_sync_event({get_disco_item, any, JID, Lang}, From, StateName, StateData); handle_sync_event({get_disco_item, Filter, JID, Lang}, From, StateName, StateData) -> handle_sync_event({get_disco_item, Filter, JID, Lang, infinity}, From, StateName, StateData); -handle_sync_event(get_config, _From, StateName, - StateData) -> - {reply, {ok, StateData#state.config}, StateName, - StateData}; -handle_sync_event(get_state, _From, StateName, - StateData) -> +handle_sync_event(get_config, + _From, + StateName, + StateData) -> + {reply, {ok, StateData#state.config}, + StateName, + StateData}; +handle_sync_event(get_state, + _From, + StateName, + StateData) -> {reply, {ok, StateData}, StateName, StateData}; -handle_sync_event(get_info, _From, StateName, - StateData) -> +handle_sync_event(get_info, + _From, + StateName, + StateData) -> Result = #{occupants_number => maps:size(StateData#state.users)}, {reply, Result, StateName, StateData}; -handle_sync_event({change_config, Config}, _From, - StateName, StateData) -> +handle_sync_event({change_config, Config}, + _From, + StateName, + StateData) -> {result, undefined, NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; -handle_sync_event({change_state, NewStateData}, _From, - StateName, _StateData) -> +handle_sync_event({change_state, NewStateData}, + _From, + StateName, + _StateData) -> Mod = gen_mod:db_mod(NewStateData#state.server_host, mod_muc), case erlang:function_exported(Mod, get_subscribed_rooms, 3) of - true -> - ok; - _ -> - erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers) + true -> + ok; + _ -> + erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers) end, {reply, {ok, NewStateData}, StateName, NewStateData}; handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> case process_item_change(Item, StateData, UJID) of - {error, _} = Err -> - {reply, Err, StateName, StateData}; + {error, _} = Err -> + {reply, Err, StateName, StateData}; StateData -> {reply, {ok, StateData}, StateName, StateData}; - NSD -> - store_room(NSD), - {reply, {ok, NSD}, StateName, NSD} + NSD -> + store_room(NSD), + {reply, {ok, NSD}, StateName, NSD} end; handle_sync_event(get_subscribers, _From, StateName, StateData) -> JIDs = muc_subscribers_fold( fun(_LBareJID, #subscriber{jid = JID}, Acc) -> [JID | Acc] - end, [], StateData#state.muc_subscribers), + end, + [], + StateData#state.muc_subscribers), {reply, {ok, JIDs}, StateName, StateData}; -handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, - StateName, StateData) -> - IQ = #iq{type = set, id = p1_rand:get_string(), - from = From, sub_els = [#muc_subscribe{nick = Nick, - events = Nodes}]}, +handle_sync_event({muc_subscribe, From, Nick, Nodes}, + _From, + StateName, + StateData) -> + IQ = #iq{ + type = set, + id = p1_rand:get_string(), + from = From, + sub_els = [#muc_subscribe{ + nick = Nick, + events = Nodes + }] + }, Config = StateData#state.config, CaptchaRequired = Config#config.captcha_protected, PasswordProtected = Config#config.password_protected, MembersOnly = Config#config.members_only, - TmpConfig = Config#config{captcha_protected = false, - password_protected = false, - members_only = false}, + TmpConfig = Config#config{ + captcha_protected = false, + password_protected = false, + members_only = false + }, TmpState = StateData#state{config = TmpConfig}, case process_iq_mucsub(From, IQ, TmpState) of - {result, #muc_subscribe{events = NewNodes}, NewState} -> - NewConfig = (NewState#state.config)#config{ - captcha_protected = CaptchaRequired, - password_protected = PasswordProtected, - members_only = MembersOnly}, - {reply, {ok, NewNodes}, StateName, - NewState#state{config = NewConfig}}; - {ignore, NewState} -> - NewConfig = (NewState#state.config)#config{ - captcha_protected = CaptchaRequired, - password_protected = PasswordProtected, - members_only = MembersOnly}, - {reply, {error, ?T("Request is ignored")}, - NewState#state{config = NewConfig}}; - {error, Err} -> - {reply, {error, get_error_text(Err)}, StateName, StateData} + {result, #muc_subscribe{events = NewNodes}, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected, + members_only = MembersOnly + }, + {reply, {ok, NewNodes}, + StateName, + NewState#state{config = NewConfig}}; + {ignore, NewState} -> + NewConfig = (NewState#state.config)#config{ + captcha_protected = CaptchaRequired, + password_protected = PasswordProtected, + members_only = MembersOnly + }, + {reply, {error, ?T("Request is ignored")}, + NewState#state{config = NewConfig}}; + {error, Err} -> + {reply, {error, get_error_text(Err)}, StateName, StateData} end; -handle_sync_event({muc_unsubscribe, From}, _From, StateName, - #state{config = Conf} = StateData) -> - IQ = #iq{type = set, id = p1_rand:get_string(), - from = From, sub_els = [#muc_unsubscribe{}]}, +handle_sync_event({muc_unsubscribe, From}, + _From, + StateName, + #state{config = Conf} = StateData) -> + IQ = #iq{ + type = set, + id = p1_rand:get_string(), + from = From, + sub_els = [#muc_unsubscribe{}] + }, case process_iq_mucsub(From, IQ, StateData) of - {result, _, stop} -> - {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; - {result, _, NewState} -> - {reply, ok, StateName, NewState}; - {ignore, NewState} -> - {reply, {error, ?T("Request is ignored")}, NewState}; - {error, Err} -> - {reply, {error, get_error_text(Err)}, StateName, StateData} + {result, _, stop} -> + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; + {result, _, NewState} -> + {reply, ok, StateName, NewState}; + {ignore, NewState} -> + {reply, {error, ?T("Request is ignored")}, NewState}; + {error, Err} -> + {reply, {error, get_error_text(Err)}, StateName, StateData} end; handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> IsSubs = try muc_subscribers_get( jid:split(From), StateData#state.muc_subscribers) of - #subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes} - catch _:{badkey, _} -> false - end, + #subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes} + catch + _:{badkey, _} -> false + end, {reply, IsSubs, StateName, StateData}; -handle_sync_event(_Event, _From, StateName, - StateData) -> +handle_sync_event(_Event, + _From, + StateName, + StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. + code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. + handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> RoomQueueEmpty = p1_queue:is_empty(StateData#state.room_queue), RoomQueue = p1_queue:in({presence, From}, StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, - if RoomQueueEmpty -> - StateData2 = prepare_room_queue(StateData1), - {next_state, normal_state, StateData2}; - true -> {next_state, normal_state, StateData1} + if + RoomQueueEmpty -> + StateData2 = prepare_room_queue(StateData1), + {next_state, normal_state, StateData2}; + true -> {next_state, normal_state, StateData1} end; handle_info({process_user_message, From}, - normal_state = _StateName, StateData) -> + normal_state = _StateName, + StateData) -> RoomQueueEmpty = - p1_queue:is_empty(StateData#state.room_queue), + p1_queue:is_empty(StateData#state.room_queue), RoomQueue = p1_queue:in({message, From}, - StateData#state.room_queue), + StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, - if RoomQueueEmpty -> - StateData2 = prepare_room_queue(StateData1), - {next_state, normal_state, StateData2}; - true -> {next_state, normal_state, StateData1} + if + RoomQueueEmpty -> + StateData2 = prepare_room_queue(StateData1), + {next_state, normal_state, StateData2}; + true -> {next_state, normal_state, StateData1} end; handle_info(process_room_queue, - normal_state = StateName, StateData) -> + normal_state = StateName, + StateData) -> case p1_queue:out(StateData#state.room_queue) of - {{value, {message, From}}, RoomQueue} -> - Activity = get_user_activity(From, StateData), - Packet = Activity#activity.message, - NewActivity = Activity#activity{message = undefined}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - StateData2 = StateData1#state{room_queue = RoomQueue}, - StateData3 = prepare_room_queue(StateData2), - process_groupchat_message(Packet, StateData3); - {{value, {presence, From}}, RoomQueue} -> - Activity = get_user_activity(From, StateData), - {Nick, Packet} = Activity#activity.presence, - NewActivity = Activity#activity{presence = undefined}, - StateData1 = store_user_activity(From, NewActivity, - StateData), - StateData2 = StateData1#state{room_queue = RoomQueue}, - StateData3 = prepare_room_queue(StateData2), - process_presence(Nick, Packet, StateData3); - {empty, _} -> {next_state, StateName, StateData} + {{value, {message, From}}, RoomQueue} -> + Activity = get_user_activity(From, StateData), + Packet = Activity#activity.message, + NewActivity = Activity#activity{message = undefined}, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + StateData2 = StateData1#state{room_queue = RoomQueue}, + StateData3 = prepare_room_queue(StateData2), + process_groupchat_message(Packet, StateData3); + {{value, {presence, From}}, RoomQueue} -> + Activity = get_user_activity(From, StateData), + {Nick, Packet} = Activity#activity.presence, + NewActivity = Activity#activity{presence = undefined}, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + StateData2 = StateData1#state{room_queue = RoomQueue}, + StateData3 = prepare_room_queue(StateData2), + process_presence(Nick, Packet, StateData3); + {empty, _} -> {next_state, StateName, StateData} end; -handle_info({captcha_succeed, From}, normal_state, - StateData) -> +handle_info({captcha_succeed, From}, + normal_state, + StateData) -> NewState = case maps:get(From, StateData#state.robots, passed) of - {Nick, Packet} -> - Robots = maps:put(From, passed, StateData#state.robots), - add_new_user(From, Nick, Packet, - StateData#state{robots = Robots}); - passed -> - StateData - end, + {Nick, Packet} -> + Robots = maps:put(From, passed, StateData#state.robots), + add_new_user(From, + Nick, + Packet, + StateData#state{robots = Robots}); + passed -> + StateData + end, {next_state, normal_state, NewState}; -handle_info({captcha_failed, From}, normal_state, - StateData) -> +handle_info({captcha_failed, From}, + normal_state, + StateData) -> NewState = case maps:get(From, StateData#state.robots, passed) of - {_Nick, Packet} -> - Robots = maps:remove(From, StateData#state.robots), - Txt = ?T("The CAPTCHA verification has failed"), - Lang = xmpp:get_lang(Packet), - Err = xmpp:err_not_authorized(Txt, Lang), - ejabberd_router:route_error(Packet, Err), - StateData#state{robots = Robots}; - passed -> - StateData - end, + {_Nick, Packet} -> + Robots = maps:remove(From, StateData#state.robots), + Txt = ?T("The CAPTCHA verification has failed"), + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_not_authorized(Txt, Lang), + ejabberd_router:route_error(Packet, Err), + StateData#state{robots = Robots}; + passed -> + StateData + end, {next_state, normal_state, NewState}; handle_info(close_room_if_temporary_and_empty, _StateName, StateData) -> close_room_if_temporary_and_empty(StateData); handle_info(shutdown, _StateName, StateData) -> {stop, shutdown, StateData}; handle_info({iq_reply, #iq{type = Type, sub_els = Els}, - #iq{from = From, to = To} = IQ}, StateName, StateData) -> + #iq{from = From, to = To} = IQ}, + StateName, + StateData) -> ejabberd_router:route( xmpp:set_from_to( - IQ#iq{type = Type, sub_els = Els}, - To, From)), + IQ#iq{type = Type, sub_els = Els}, + To, + From)), {next_state, StateName, StateData}; handle_info({iq_reply, timeout, IQ}, StateName, StateData) -> Txt = ?T("Request has timed out"), @@ -972,67 +1172,79 @@ handle_info(config_reloaded, StateName, StateData) -> History1 = StateData#state.history, Q1 = History1#lqueue.queue, Q2 = case p1_queue:len(Q1) of - Len when Len > Max -> - lqueue_cut(Q1, Len-Max); - _ -> - Q1 - end, + Len when Len > Max -> + lqueue_cut(Q1, Len - Max); + _ -> + Q1 + end, History2 = History1#lqueue{queue = Q2, max = Max}, {next_state, StateName, StateData#state{history = History2}}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. -terminate(Reason, _StateName, - #state{server_host = LServer, host = Host, room = Room} = StateData) -> - try - ?INFO_MSG("Stopping MUC room ~ts@~ts", [Room, Host]), - ReasonT = case Reason of - shutdown -> - ?T("You are being removed from the room " - "because of a system shutdown"); - _ -> ?T("Room terminates") - end, - Packet = #presence{ - type = unavailable, - sub_els = [#muc_user{items = [#muc_item{affiliation = none, - reason = ReasonT, - role = none}], - status_codes = [332,110]}]}, - maps:fold( - fun(_, #user{nick = Nick, jid = JID}, _) -> - case Reason of - shutdown -> - send_wrapped(jid:replace_resource(StateData#state.jid, Nick), - JID, Packet, - ?NS_MUCSUB_NODES_PARTICIPANTS, - StateData); - _ -> ok - end, - tab_remove_online_user(JID, StateData) - end, [], get_users_and_subscribers_with_node( - ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), - disable_hibernate_timer(StateData), - case StateData#state.hibernate_timer of - hibernating -> - ok; - _ -> - add_to_log(room_existence, stopped, StateData), - case (StateData#state.config)#config.persistent of +terminate(Reason, + _StateName, + #state{server_host = LServer, host = Host, room = Room} = StateData) -> + try + ?INFO_MSG("Stopping MUC room ~ts@~ts", [Room, Host]), + ReasonT = case Reason of + shutdown -> + ?T("You are being removed from the room " + "because of a system shutdown"); + _ -> ?T("Room terminates") + end, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [#muc_item{ + affiliation = none, + reason = ReasonT, + role = none + }], + status_codes = [332, 110] + }] + }, + maps:fold( + fun(_, #user{nick = Nick, jid = JID}, _) -> + case Reason of + shutdown -> + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + JID, + Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, + StateData); + _ -> ok + end, + tab_remove_online_user(JID, StateData) + end, + [], + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), + + disable_hibernate_timer(StateData), + case StateData#state.hibernate_timer of + hibernating -> + ok; + _ -> + add_to_log(room_existence, stopped, StateData), + case (StateData#state.config)#config.persistent of false -> - ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, false]); + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, false]); {destroying, Persistent} -> - ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, Persistent]); - _ -> - ok - end - end - catch ?EX_RULE(E, R, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Got exception on room termination:~n** ~ts", - [misc:format_exception(2, E, R, StackTrace)]) + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, Persistent]); + _ -> + ok + end + end + catch + ?EX_RULE(E, R, St) -> + StackTrace = ?EX_STACK(St), + ?ERROR_MSG("Got exception on room termination:~n** ~ts", + [misc:format_exception(2, E, R, StackTrace)]) end. + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- @@ -1042,302 +1254,329 @@ route(Pid, Packet) -> #jid{lresource = Nick} = xmpp:get_to(Packet), p1_fsm:send_event(Pid, {route, Nick, Packet}). + -spec process_groupchat_message(message(), state()) -> fsm_next(). process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData) -> IsSubscriber = is_subscriber(From, StateData), case is_user_online(From, StateData) orelse IsSubscriber orelse - is_user_allowed_message_nonparticipant(From, StateData) - of - true -> - {FromNick, Role} = get_participant_data(From, StateData), - #config{moderated = Moderated} = StateData#state.config, - AllowedByModerationRules = - case {Role == moderator orelse Role == participant orelse - not Moderated, IsSubscriber} of - {true, _} -> true; - {_, true} -> - % We assume all subscribers are at least members - true; - _ -> - false - end, - if AllowedByModerationRules -> - Subject = check_subject(Packet), - {NewStateData1, IsAllowed} = - case Subject of - [] -> - {StateData, true}; - _ -> - case - can_change_subject(Role, - IsSubscriber, - StateData) - of - true -> - NSD = - StateData#state{subject = Subject, - subject_author = {FromNick, From}}, - store_room(NSD), - {NSD, true}; - _ -> {StateData, false} - end - end, - case IsAllowed of - true -> - case - ejabberd_hooks:run_fold(muc_filter_message, - StateData#state.server_host, - Packet, - [StateData, FromNick]) - of - drop -> - {next_state, normal_state, StateData}; - NewPacket1 -> - NewPacket = xmpp:put_meta(xmpp:remove_subtag( - add_stanza_id(NewPacket1, StateData), #nick{}), - muc_sender_real_jid, From), - Node = if Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES; - true -> ?NS_MUCSUB_NODES_SUBJECT - end, - NewStateData2 = check_message_for_retractions(NewPacket1, NewStateData1), - send_wrapped_multiple( - jid:replace_resource(StateData#state.jid, FromNick), - get_users_and_subscribers_with_node(Node, StateData), - NewPacket, Node, NewStateData2), - NewStateData3 = case has_body_or_subject(NewPacket) of - true -> - add_message_to_history(FromNick, From, - NewPacket, - NewStateData2); - false -> - NewStateData2 - end, - {next_state, normal_state, NewStateData3} - end; - _ -> - Err = case (StateData#state.config)#config.allow_change_subj of - true -> - xmpp:err_forbidden( - ?T("Only moderators and participants are " - "allowed to change the subject in this " - "room"), Lang); - _ -> - xmpp:err_forbidden( - ?T("Only moderators are allowed to change " - "the subject in this room"), Lang) - end, - ejabberd_router:route_error(Packet, Err), - {next_state, normal_state, StateData} - end; - true -> - ErrText = ?T("Visitors are not allowed to send messages " - "to all occupants"), - Err = xmpp:err_forbidden(ErrText, Lang), - ejabberd_router:route_error(Packet, Err), - {next_state, normal_state, StateData} - end; - false -> - ErrText = ?T("Only occupants are allowed to send messages " - "to the conference"), - Err = xmpp:err_not_acceptable(ErrText, Lang), - ejabberd_router:route_error(Packet, Err), - {next_state, normal_state, StateData} + is_user_allowed_message_nonparticipant(From, StateData) of + true -> + {FromNick, Role} = get_participant_data(From, StateData), + #config{moderated = Moderated} = StateData#state.config, + AllowedByModerationRules = + case {Role == moderator orelse Role == participant orelse + not Moderated, + IsSubscriber} of + {true, _} -> true; + {_, true} -> + % We assume all subscribers are at least members + true; + _ -> + false + end, + if + AllowedByModerationRules -> + Subject = check_subject(Packet), + {NewStateData1, IsAllowed} = + case Subject of + [] -> + {StateData, true}; + _ -> + case can_change_subject(Role, + IsSubscriber, + StateData) of + true -> + NSD = + StateData#state{ + subject = Subject, + subject_author = {FromNick, From} + }, + store_room(NSD), + {NSD, true}; + _ -> {StateData, false} + end + end, + case IsAllowed of + true -> + case ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + Packet, + [StateData, FromNick]) of + drop -> + {next_state, normal_state, StateData}; + NewPacket1 -> + NewPacket = xmpp:put_meta(xmpp:remove_subtag( + add_stanza_id(NewPacket1, StateData), #nick{}), + muc_sender_real_jid, + From), + Node = if + Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES; + true -> ?NS_MUCSUB_NODES_SUBJECT + end, + NewStateData2 = check_message_for_retractions(NewPacket1, NewStateData1), + send_wrapped_multiple( + jid:replace_resource(StateData#state.jid, FromNick), + get_users_and_subscribers_with_node(Node, StateData), + NewPacket, + Node, + NewStateData2), + NewStateData3 = case has_body_or_subject(NewPacket) of + true -> + add_message_to_history(FromNick, + From, + NewPacket, + NewStateData2); + false -> + NewStateData2 + end, + {next_state, normal_state, NewStateData3} + end; + _ -> + Err = case (StateData#state.config)#config.allow_change_subj of + true -> + xmpp:err_forbidden( + ?T("Only moderators and participants are " + "allowed to change the subject in this " + "room"), + Lang); + _ -> + xmpp:err_forbidden( + ?T("Only moderators are allowed to change " + "the subject in this room"), + Lang) + end, + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData} + end; + true -> + ErrText = ?T("Visitors are not allowed to send messages " + "to all occupants"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData} + end; + false -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + {next_state, normal_state, StateData} end. + -spec check_message_for_retractions(Packet :: message(), State :: state()) -> state(). check_message_for_retractions(Packet, - #state{config = Config, room = Room, host = Host, - server_host = Server} = State) -> + #state{ + config = Config, + room = Room, + host = Host, + server_host = Server + } = State) -> case xmpp:get_subtag(Packet, #fasten_apply_to{}) of - #fasten_apply_to{id = ID} = F -> - case xmpp:get_subtag(F, #message_retract{}) of - #message_retract{} -> - #jid{luser = U, lserver = S} = xmpp:get_from(Packet), - case remove_from_history({U, S}, ID, State) of - {NewState, StanzaId} when is_integer(StanzaId) -> - case Config#config.mam of - true -> - mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId), - NewState; - _ -> - NewState - end; - {NewState, _} -> - NewState - end; - _ -> - State - end; - _ -> - State + #fasten_apply_to{id = ID} = F -> + case xmpp:get_subtag(F, #message_retract{}) of + #message_retract{} -> + #jid{luser = U, lserver = S} = xmpp:get_from(Packet), + case remove_from_history({U, S}, ID, State) of + {NewState, StanzaId} when is_integer(StanzaId) -> + case Config#config.mam of + true -> + mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId), + NewState; + _ -> + NewState + end; + {NewState, _} -> + NewState + end; + _ -> + State + end; + _ -> + State end. + -spec add_stanza_id(Packet :: message(), State :: state()) -> message(). add_stanza_id(Packet, #state{jid = JID}) -> {AddId, NewPacket} = - case xmpp:get_meta(Packet, stanza_id, false) of - false -> - GenID = erlang:system_time(microsecond), - {true, xmpp:put_meta(Packet, stanza_id, GenID)}; - _ -> - StanzaIds = xmpp:get_subtags(Packet, #stanza_id{by = #jid{}}), - HasOurStanzaId = lists:any( - fun(#stanza_id{by = JID2}) when JID == JID2 -> true; - (_) -> false - end, StanzaIds), - {not HasOurStanzaId, Packet} - end, + case xmpp:get_meta(Packet, stanza_id, false) of + false -> + GenID = erlang:system_time(microsecond), + {true, xmpp:put_meta(Packet, stanza_id, GenID)}; + _ -> + StanzaIds = xmpp:get_subtags(Packet, #stanza_id{by = #jid{}}), + HasOurStanzaId = lists:any( + fun(#stanza_id{by = JID2}) when JID == JID2 -> true; + (_) -> false + end, + StanzaIds), + {not HasOurStanzaId, Packet} + end, if - AddId -> - ID = xmpp:get_meta(NewPacket, stanza_id), - IDs = integer_to_binary(ID), - xmpp:append_subtags(NewPacket, [#stanza_id{by = JID, id = IDs}]); - true -> - Packet + AddId -> + ID = xmpp:get_meta(NewPacket, stanza_id), + IDs = integer_to_binary(ID), + xmpp:append_subtags(NewPacket, [#stanza_id{by = JID, id = IDs}]); + true -> + Packet end. + -spec process_normal_message(jid(), message(), state()) -> state(). process_normal_message(From, #message{lang = Lang} = Pkt, StateData) -> Action = lists:foldl( - fun(_, {error, _} = Err) -> - Err; - (_, {ok, _} = Result) -> - Result; - (#muc_user{invites = [_|_] = Invites}, _) -> - case check_invitation(From, Invites, Lang, StateData) of - ok -> - {ok, Invites}; - {error, _} = Err -> - Err - end; - (#xdata{type = submit, fields = Fs}, _) -> - try {ok, muc_request:decode(Fs)} - catch _:{muc_request, Why} -> - Txt = muc_request:format_error(Why), - {error, xmpp:err_bad_request(Txt, Lang)} - end; - (_, Acc) -> - Acc - end, ok, xmpp:get_els(Pkt)), + fun(_, {error, _} = Err) -> + Err; + (_, {ok, _} = Result) -> + Result; + (#muc_user{invites = [_ | _] = Invites}, _) -> + case check_invitation(From, Invites, Lang, StateData) of + ok -> + {ok, Invites}; + {error, _} = Err -> + Err + end; + (#xdata{type = submit, fields = Fs}, _) -> + try + {ok, muc_request:decode(Fs)} + catch + _:{muc_request, Why} -> + Txt = muc_request:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + (_, Acc) -> + Acc + end, + ok, + xmpp:get_els(Pkt)), case Action of - {ok, [#muc_invite{}|_] = Invitations} -> - lists:foldl( - fun(Invitation, AccState) -> - process_invitation(From, Pkt, Invitation, Lang, AccState) - end, StateData, Invitations); - {ok, [{role, participant}]} -> - process_voice_request(From, Pkt, StateData); - {ok, VoiceApproval} -> - process_voice_approval(From, Pkt, VoiceApproval, StateData); - {error, Err} -> - ejabberd_router:route_error(Pkt, Err), - StateData; - ok -> - StateData + {ok, [#muc_invite{} | _] = Invitations} -> + lists:foldl( + fun(Invitation, AccState) -> + process_invitation(From, Pkt, Invitation, Lang, AccState) + end, + StateData, + Invitations); + {ok, [{role, participant}]} -> + process_voice_request(From, Pkt, StateData); + {ok, VoiceApproval} -> + process_voice_approval(From, Pkt, VoiceApproval, StateData); + {error, Err} -> + ejabberd_router:route_error(Pkt, Err), + StateData; + ok -> + StateData end. + -spec process_invitation(jid(), message(), muc_invite(), binary(), state()) -> state(). process_invitation(From, Pkt, Invitation, Lang, StateData) -> IJID = route_invitation(From, Pkt, Invitation, Lang, StateData), Config = StateData#state.config, case Config#config.members_only of - true -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation(IJID, member, StateData), - send_affiliation(IJID, member, StateData), - store_room(NSD), - NSD; - _ -> - StateData - end; - false -> - StateData + true -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation(IJID, member, StateData), + send_affiliation(IJID, member, StateData), + store_room(NSD), + NSD; + _ -> + StateData + end; + false -> + StateData end. + -spec process_voice_request(jid(), message(), state()) -> state(). process_voice_request(From, Pkt, StateData) -> Lang = xmpp:get_lang(Pkt), case (StateData#state.config)#config.allow_voice_requests of - true -> - MinInterval = (StateData#state.config)#config.voice_request_min_interval, - BareFrom = jid:remove_resource(jid:tolower(From)), - NowPriority = -erlang:system_time(microsecond), - CleanPriority = NowPriority + MinInterval * 1000000, - Times = clean_treap(StateData#state.last_voice_request_time, - CleanPriority), - case treap:lookup(BareFrom, Times) of - error -> - Times1 = treap:insert(BareFrom, - NowPriority, - true, Times), - NSD = StateData#state{last_voice_request_time = Times1}, - send_voice_request(From, Lang, NSD), - NSD; - {ok, _, _} -> - ErrText = ?T("Please, wait for a while before sending " - "new voice request"), - Err = xmpp:err_resource_constraint(ErrText, Lang), - ejabberd_router:route_error(Pkt, Err), - StateData#state{last_voice_request_time = Times} - end; - false -> - ErrText = ?T("Voice requests are disabled in this conference"), - Err = xmpp:err_forbidden(ErrText, Lang), - ejabberd_router:route_error(Pkt, Err), - StateData + true -> + MinInterval = (StateData#state.config)#config.voice_request_min_interval, + BareFrom = jid:remove_resource(jid:tolower(From)), + NowPriority = -erlang:system_time(microsecond), + CleanPriority = NowPriority + MinInterval * 1000000, + Times = clean_treap(StateData#state.last_voice_request_time, + CleanPriority), + case treap:lookup(BareFrom, Times) of + error -> + Times1 = treap:insert(BareFrom, + NowPriority, + true, + Times), + NSD = StateData#state{last_voice_request_time = Times1}, + send_voice_request(From, Lang, NSD), + NSD; + {ok, _, _} -> + ErrText = ?T("Please, wait for a while before sending " + "new voice request"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData#state{last_voice_request_time = Times} + end; + false -> + ErrText = ?T("Voice requests are disabled in this conference"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData end. + -spec process_voice_approval(jid(), message(), [muc_request:property()], state()) -> state(). process_voice_approval(From, Pkt, VoiceApproval, StateData) -> Lang = xmpp:get_lang(Pkt), case is_moderator(From, StateData) of - true -> - case lists:keyfind(jid, 1, VoiceApproval) of - {_, TargetJid} -> - Allow = proplists:get_bool(request_allow, VoiceApproval), - case is_visitor(TargetJid, StateData) of - true when Allow -> - Reason = <<>>, - NSD = set_role(TargetJid, participant, StateData), - catch send_new_presence( - TargetJid, Reason, NSD, StateData), - NSD; - _ -> - StateData - end; - false -> - ErrText = ?T("Failed to extract JID from your voice " - "request approval"), - Err = xmpp:err_bad_request(ErrText, Lang), - ejabberd_router:route_error(Pkt, Err), - StateData - end; - false -> - ErrText = ?T("Only moderators can approve voice requests"), - Err = xmpp:err_not_allowed(ErrText, Lang), - ejabberd_router:route_error(Pkt, Err), - StateData + true -> + case lists:keyfind(jid, 1, VoiceApproval) of + {_, TargetJid} -> + Allow = proplists:get_bool(request_allow, VoiceApproval), + case is_visitor(TargetJid, StateData) of + true when Allow -> + Reason = <<>>, + NSD = set_role(TargetJid, participant, StateData), + catch send_new_presence( + TargetJid, Reason, NSD, StateData), + NSD; + _ -> + StateData + end; + false -> + ErrText = ?T("Failed to extract JID from your voice " + "request approval"), + Err = xmpp:err_bad_request(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData + end; + false -> + ErrText = ?T("Only moderators can approve voice requests"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Pkt, Err), + StateData end. + -spec direct_iq_type(iq()) -> vcard | ping | request | response | pubsub | stanza_error(). direct_iq_type(#iq{type = T, sub_els = SubEls, lang = Lang}) when T == get; T == set -> case SubEls of - [El] -> - case xmpp:get_ns(El) of - ?NS_VCARD when T == get -> vcard; - ?NS_PUBSUB when T == get -> pubsub; - ?NS_PING when T == get -> ping; - _ -> request - end; - [] -> - xmpp:err_bad_request(?T("No child elements found"), Lang); - [_|_] -> - xmpp:err_bad_request(?T("Too many child elements"), Lang) + [El] -> + case xmpp:get_ns(El) of + ?NS_VCARD when T == get -> vcard; + ?NS_PUBSUB when T == get -> pubsub; + ?NS_PING when T == get -> ping; + _ -> request + end; + [] -> + xmpp:err_bad_request(?T("No child elements found"), Lang); + [_ | _] -> + xmpp:err_bad_request(?T("Too many child elements"), Lang) end; direct_iq_type(#iq{}) -> response. + %% @doc Check if this non participant can send message to room. %% %% XEP-0045 v1.23: @@ -1347,16 +1586,17 @@ direct_iq_type(#iq{}) -> %% to send messages to the room even if those users are not occupants. -spec is_user_allowed_message_nonparticipant(jid(), state()) -> boolean(). is_user_allowed_message_nonparticipant(JID, - StateData) -> + StateData) -> case get_service_affiliation(JID, StateData) of - owner -> true; - _ -> false + owner -> true; + _ -> false end. + -spec is_user_allowed_private_message(jid(), state()) -> boolean(). is_user_allowed_private_message(JID, StateData) -> case {(StateData#state.config)#config.allowpm, - get_role(JID, StateData)} of + get_role(JID, StateData)} of {anyone, _} -> true; {participants, moderator} -> @@ -1371,285 +1611,327 @@ is_user_allowed_private_message(JID, StateData) -> false end. + %% @doc Get information of this participant, or default values. %% If the JID is not a participant, return values for a service message. -spec get_participant_data(jid(), state()) -> {binary(), role()}. get_participant_data(From, StateData) -> try maps:get(jid:tolower(From), StateData#state.users) of - #user{nick = FromNick, role = Role} -> - {FromNick, Role} - catch _:{badkey, _} -> - try muc_subscribers_get(jid:tolower(jid:remove_resource(From)), + #user{nick = FromNick, role = Role} -> + {FromNick, Role} + catch + _:{badkey, _} -> + try muc_subscribers_get(jid:tolower(jid:remove_resource(From)), StateData#state.muc_subscribers) of - #subscriber{nick = FromNick} -> - {FromNick, none} - catch _:{badkey, _} -> - {From#jid.luser, moderator} - end + #subscriber{nick = FromNick} -> + {FromNick, none} + catch + _:{badkey, _} -> + {From#jid.luser, moderator} + end end. + -spec process_presence(binary(), presence(), state()) -> fsm_transition(). process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData) -> IsOnline = is_user_online(From, StateData), - if Type0 == available; - IsOnline and ((Type0 == unavailable) or (Type0 == error)) -> - case ejabberd_hooks:run_fold(muc_filter_presence, - StateData#state.server_host, - Packet0, - [StateData, Nick]) of - drop -> - {next_state, normal_state, StateData}; - #presence{} = Packet -> - close_room_if_temporary_and_empty( - do_process_presence(Nick, Packet, StateData)) - end; - true -> - {next_state, normal_state, StateData} + if + Type0 == available; + IsOnline and ((Type0 == unavailable) or (Type0 == error)) -> + case ejabberd_hooks:run_fold(muc_filter_presence, + StateData#state.server_host, + Packet0, + [StateData, Nick]) of + drop -> + {next_state, normal_state, StateData}; + #presence{} = Packet -> + close_room_if_temporary_and_empty( + do_process_presence(Nick, Packet, StateData)) + end; + true -> + {next_state, normal_state, StateData} end. + -spec do_process_presence(binary(), presence(), state()) -> state(). -do_process_presence(Nick, #presence{from = From, type = available, lang = Lang} = Packet, - StateData) -> +do_process_presence(Nick, + #presence{from = From, type = available, lang = Lang} = Packet, + StateData) -> case is_user_online(From, StateData) of - false -> - add_new_user(From, Nick, Packet, StateData); - true -> - case is_nick_change(From, Nick, StateData) of - true -> - case {nick_collision(From, Nick, StateData), - mod_muc:can_use_nick(StateData#state.server_host, - jid:encode(StateData#state.jid), - From, Nick), - {(StateData#state.config)#config.allow_visitor_nickchange, - is_visitor(From, StateData)}} of - {_, _, {false, true}} -> - Packet1 = Packet#presence{sub_els = [#muc{}]}, - ErrText = ?T("Visitors are not allowed to change their " - "nicknames in this room"), - Err = xmpp:err_not_allowed(ErrText, Lang), - ejabberd_router:route_error(Packet1, Err), - StateData; - {true, _, _} -> - Packet1 = Packet#presence{sub_els = [#muc{}]}, - ErrText = ?T("That nickname is already in use by another " - "occupant"), - Err = xmpp:err_conflict(ErrText, Lang), - ejabberd_router:route_error(Packet1, Err), - StateData; - {_, false, _} -> - Packet1 = Packet#presence{sub_els = [#muc{}]}, - Err = case Nick of - <<>> -> - xmpp:err_jid_malformed(?T("Nickname can't be empty"), - Lang); - _ -> - xmpp:err_conflict(?T("That nickname is registered" - " by another person"), Lang) - end, - ejabberd_router:route_error(Packet1, Err), - StateData; - _ -> - change_nick(From, Nick, StateData) - end; - false -> - Stanza = maybe_strip_status_from_presence( - From, Packet, StateData), - NewState = add_user_presence(From, Stanza, - StateData), - case xmpp:has_subtag(Packet, #muc{}) of - true -> - send_initial_presences_and_messages( - From, Nick, Packet, NewState, StateData); - false -> - send_new_presence(From, NewState, StateData) - end, - NewState - end + false -> + add_new_user(From, Nick, Packet, StateData); + true -> + case is_nick_change(From, Nick, StateData) of + true -> + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + jid:encode(StateData#state.jid), + From, + Nick), + {(StateData#state.config)#config.allow_visitor_nickchange, + is_visitor(From, StateData)}} of + {_, _, {false, true}} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + ErrText = ?T("Visitors are not allowed to change their " + "nicknames in this room"), + Err = xmpp:err_not_allowed(ErrText, Lang), + ejabberd_router:route_error(Packet1, Err), + StateData; + {true, _, _} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + ErrText = ?T("That nickname is already in use by another " + "occupant"), + Err = xmpp:err_conflict(ErrText, Lang), + ejabberd_router:route_error(Packet1, Err), + StateData; + {_, false, _} -> + Packet1 = Packet#presence{sub_els = [#muc{}]}, + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), + Lang) + end, + ejabberd_router:route_error(Packet1, Err), + StateData; + _ -> + change_nick(From, Nick, StateData) + end; + false -> + Stanza = maybe_strip_status_from_presence( + From, Packet, StateData), + NewState = add_user_presence(From, + Stanza, + StateData), + case xmpp:has_subtag(Packet, #muc{}) of + true -> + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData); + false -> + send_new_presence(From, NewState, StateData) + end, + NewState + end end; -do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet, - StateData) -> +do_process_presence(Nick, + #presence{from = From, type = unavailable} = Packet, + StateData) -> NewPacket = case {(StateData#state.config)#config.allow_visitor_status, - is_visitor(From, StateData)} of - {false, true} -> - strip_status(Packet); - _ -> Packet - end, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _ -> Packet + end, NewState = add_user_presence_un(From, NewPacket, StateData), case maps:get(Nick, StateData#state.nicks, []) of - [_, _ | _] -> - Aff = get_affiliation(From, StateData), - Item = #muc_item{affiliation = Aff, role = none, jid = From}, - Pres = xmpp:set_subtag( - Packet, #muc_user{items = [Item], - status_codes = [110]}), - send_wrapped(jid:replace_resource(StateData#state.jid, Nick), - From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); - _ -> - send_new_presence(From, NewState, StateData) + [_, _ | _] -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, + #muc_user{ + items = [Item], + status_codes = [110] + }), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, + Pres, + ?NS_MUCSUB_NODES_PRESENCE, + StateData); + _ -> + send_new_presence(From, NewState, StateData) end, Reason = xmpp:get_text(NewPacket#presence.status), remove_online_user(From, NewState, Reason); -do_process_presence(_Nick, #presence{from = From, type = error, lang = Lang} = Packet, - StateData) -> +do_process_presence(_Nick, + #presence{from = From, type = error, lang = Lang} = Packet, + StateData) -> ErrorText = ?T("It is not allowed to send error messages to the" - " room. The participant (~s) has sent an error " - "message (~s) and got kicked from the room"), - expulse_participant(Packet, From, StateData, - translate:translate(Lang, ErrorText)). + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room"), + expulse_participant(Packet, + From, + StateData, + translate:translate(Lang, ErrorText)). --spec maybe_strip_status_from_presence(jid(), presence(), - state()) -> presence(). + +-spec maybe_strip_status_from_presence(jid(), + presence(), + state()) -> presence(). maybe_strip_status_from_presence(From, Packet, StateData) -> case {(StateData#state.config)#config.allow_visitor_status, - is_visitor(From, StateData)} of - {false, true} -> - strip_status(Packet); - _Allowed -> Packet + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _Allowed -> Packet end. + -spec close_room_if_temporary_and_empty(state()) -> fsm_transition(). close_room_if_temporary_and_empty(StateData1) -> - case not (StateData1#state.config)#config.persistent - andalso maps:size(StateData1#state.users) == 0 - andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of - true -> - ?INFO_MSG("Destroyed MUC room ~ts because it's temporary " - "and empty", - [jid:encode(StateData1#state.jid)]), - add_to_log(room_existence, destroyed, StateData1), - forget_room(StateData1), - {stop, normal, StateData1}; - _ -> {next_state, normal_state, StateData1} + case not (StateData1#state.config)#config.persistent andalso + maps:size(StateData1#state.users) == 0 andalso + muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of + true -> + ?INFO_MSG("Destroyed MUC room ~ts because it's temporary " + "and empty", + [jid:encode(StateData1#state.jid)]), + add_to_log(room_existence, destroyed, StateData1), + forget_room(StateData1), + {stop, normal, StateData1}; + _ -> {next_state, normal_state, StateData1} end. + -spec get_users_and_subscribers(state()) -> users(). get_users_and_subscribers(StateData) -> get_users_and_subscribers_aux( StateData#state.muc_subscribers#muc_subscribers.subscribers, StateData). + -spec get_users_and_subscribers_with_node(binary(), state()) -> users(). get_users_and_subscribers_with_node(Node, StateData) -> get_users_and_subscribers_aux( muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers), StateData). + get_users_and_subscribers_aux(Subscribers, StateData) -> OnlineSubscribers = maps:fold( - fun(LJID, _, Acc) -> - LBareJID = jid:remove_resource(LJID), - case is_subscriber(LBareJID, StateData) of - true -> - ?SETS:add_element(LBareJID, Acc); - false -> - Acc - end - end, ?SETS:new(), StateData#state.users), + fun(LJID, _, Acc) -> + LBareJID = jid:remove_resource(LJID), + case is_subscriber(LBareJID, StateData) of + true -> + ?SETS:add_element(LBareJID, Acc); + false -> + Acc + end + end, + ?SETS:new(), + StateData#state.users), maps:fold( - fun(LBareJID, #subscriber{nick = Nick}, Acc) -> - case ?SETS:is_element(LBareJID, OnlineSubscribers) of - false -> - maps:put(LBareJID, - #user{jid = jid:make(LBareJID), - nick = Nick, - role = none, - last_presence = undefined}, - Acc); - true -> - Acc - end - end, StateData#state.users, Subscribers). + fun(LBareJID, #subscriber{nick = Nick}, Acc) -> + case ?SETS:is_element(LBareJID, OnlineSubscribers) of + false -> + maps:put(LBareJID, + #user{ + jid = jid:make(LBareJID), + nick = Nick, + role = none, + last_presence = undefined + }, + Acc); + true -> + Acc + end + end, + StateData#state.users, + Subscribers). + -spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> LJID = jid:tolower(JID), maps:is_key(LJID, StateData#state.users). + -spec is_subscriber(jid(), state()) -> boolean(). is_subscriber(JID, StateData) -> LJID = jid:tolower(jid:remove_resource(JID)), muc_subscribers_is_key(LJID, StateData#state.muc_subscribers). + %% Check if the user is occupant of the room, or at least is an admin or owner. -spec is_occupant_or_admin(jid(), state()) -> boolean(). is_occupant_or_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), FRole = get_role(JID, StateData), case FRole /= none orelse - FAffiliation == member orelse - FAffiliation == admin orelse FAffiliation == owner - of - true -> true; - _ -> false + FAffiliation == member orelse + FAffiliation == admin orelse FAffiliation == owner of + true -> true; + _ -> 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()) -> - continue_delivery | forget_message | - {expulse_sender, binary()}. + continue_delivery | + forget_message | + {expulse_sender, binary()}. decide_fate_message(#message{type = error} = Msg, - From, StateData) -> + From, + StateData) -> Err = xmpp:get_error(Msg), PD = case check_error_kick(Err) of - %% If this is an error stanza and its condition matches a criteria - true -> - Reason = str:format("This participant is considered a ghost " - "and is expulsed: ~s", - [jid:encode(From)]), - {expulse_sender, Reason}; - false -> continue_delivery - end, + %% If this is an error stanza and its condition matches a criteria + true -> + Reason = str:format("This participant is considered a ghost " + "and is expulsed: ~s", + [jid:encode(From)]), + {expulse_sender, Reason}; + false -> continue_delivery + end, case PD of - {expulse_sender, R} -> - case is_user_online(From, StateData) of - true -> {expulse_sender, R}; - false -> forget_message - end; - Other -> Other + {expulse_sender, R} -> + case is_user_online(From, StateData) of + true -> {expulse_sender, R}; + false -> forget_message + end; + Other -> Other end; decide_fate_message(_, _, _) -> continue_delivery. + %% Check if the elements of this error stanza indicate %% that the sender is a dead participant. %% If so, return true to kick the participant. -spec check_error_kick(stanza_error()) -> boolean(). check_error_kick(#stanza_error{reason = Reason}) -> case Reason of - #gone{} -> true; - 'internal-server-error' -> true; - 'item-not-found' -> true; - 'jid-malformed' -> true; - 'recipient-unavailable' -> true; - #redirect{} -> true; - 'remote-server-not-found' -> true; - 'remote-server-timeout' -> true; - 'service-unavailable' -> true; - _ -> false + #gone{} -> true; + 'internal-server-error' -> true; + 'item-not-found' -> true; + 'jid-malformed' -> true; + 'recipient-unavailable' -> true; + #redirect{} -> true; + 'remote-server-not-found' -> true; + 'remote-server-timeout' -> true; + 'service-unavailable' -> true; + _ -> false end; check_error_kick(undefined) -> false. + -spec get_error_condition(stanza_error()) -> string(). get_error_condition(#stanza_error{reason = Reason}) -> case Reason of - #gone{} -> "gone"; - #redirect{} -> "redirect"; - Atom -> atom_to_list(Atom) + #gone{} -> "gone"; + #redirect{} -> "redirect"; + Atom -> atom_to_list(Atom) end; get_error_condition(undefined) -> "undefined". + -spec get_error_text(stanza_error()) -> binary(). get_error_text(#stanza_error{text = Txt}) -> xmpp:get_text(Txt). + -spec make_reason(stanza(), jid(), state(), binary()) -> binary(). make_reason(Packet, From, StateData, Reason1) -> #user{nick = FromNick} = maps:get(jid:tolower(From), StateData#state.users), @@ -1657,38 +1939,50 @@ make_reason(Packet, From, StateData, Reason1) -> Reason2 = unicode:characters_to_list(Reason1), str:format(Reason2, [FromNick, Condition]). + -spec expulse_participant(stanza(), jid(), state(), binary()) -> - state(). + state(). expulse_participant(Packet, From, StateData, Reason1) -> Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, - #presence{type = unavailable, - status = xmpp:mk_text(Reason2)}, - StateData), + #presence{ + type = unavailable, + status = xmpp:mk_text(Reason2) + }, + StateData), LJID = jid:tolower(From), #user{nick = Nick} = maps:get(LJID, StateData#state.users), case maps:get(Nick, StateData#state.nicks, []) of - [_, _ | _] -> - Aff = get_affiliation(From, StateData), - Item = #muc_item{affiliation = Aff, role = none, jid = From}, - Pres = xmpp:set_subtag( - Packet, #muc_user{items = [Item], - status_codes = [110]}), - send_wrapped(jid:replace_resource(StateData#state.jid, Nick), - From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); - _ -> - send_new_presence(From, NewState, StateData) + [_, _ | _] -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, + #muc_user{ + items = [Item], + status_codes = [110] + }), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, + Pres, + ?NS_MUCSUB_NODES_PRESENCE, + StateData); + _ -> + send_new_presence(From, NewState, StateData) end, remove_online_user(From, NewState). + -spec set_affiliation(jid(), affiliation(), state()) -> state(). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). + -spec set_affiliation(jid(), affiliation(), state(), binary()) -> state(). -set_affiliation(JID, Affiliation, - #state{config = #config{persistent = false}} = StateData, - Reason) -> +set_affiliation(JID, + Affiliation, + #state{config = #config{persistent = false}} = StateData, + Reason) -> set_affiliation_fallback(JID, Affiliation, StateData, Reason); set_affiliation(JID, Affiliation, StateData, Reason) -> ServerHost = StateData#state.server_host, @@ -1696,24 +1990,27 @@ set_affiliation(JID, Affiliation, StateData, Reason) -> Host = StateData#state.host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:set_affiliation(ServerHost, Room, Host, JID, Affiliation, Reason) of - ok -> - StateData; - {error, _} -> - set_affiliation_fallback(JID, Affiliation, StateData, Reason) + ok -> + StateData; + {error, _} -> + set_affiliation_fallback(JID, Affiliation, StateData, Reason) end. + -spec set_affiliation_fallback(jid(), affiliation(), state(), binary()) -> state(). set_affiliation_fallback(JID, Affiliation, StateData, Reason) -> LJID = jid:remove_resource(jid:tolower(JID)), Affiliations = case Affiliation of - none -> - maps:remove(LJID, StateData#state.affiliations); - _ -> - maps:put(LJID, {Affiliation, Reason}, - StateData#state.affiliations) - end, + none -> + maps:remove(LJID, StateData#state.affiliations); + _ -> + maps:put(LJID, + {Affiliation, Reason}, + StateData#state.affiliations) + end, StateData#state{affiliations = Affiliations}. + -spec set_affiliations(affiliations(), state()) -> state(). set_affiliations(Affiliations, #state{config = #config{persistent = false}} = StateData) -> @@ -1724,40 +2021,43 @@ set_affiliations(Affiliations, StateData) -> ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:set_affiliations(ServerHost, Room, Host, Affiliations) of - ok -> - StateData; - {error, _} -> - set_affiliations_fallback(Affiliations, StateData) + ok -> + StateData; + {error, _} -> + set_affiliations_fallback(Affiliations, StateData) end. + -spec set_affiliations_fallback(affiliations(), state()) -> state(). set_affiliations_fallback(Affiliations, StateData) -> StateData#state{affiliations = Affiliations}. + -spec get_affiliation(ljid() | jid(), state()) -> affiliation(). get_affiliation(#jid{} = JID, StateData) -> case get_service_affiliation(JID, StateData) of - owner -> - owner; - none -> - Aff = case do_get_affiliation(JID, StateData) of - {Affiliation, _Reason} -> Affiliation; - Affiliation -> Affiliation - end, - case {Aff, (StateData#state.config)#config.members_only} of - % Subscribers should be have members affiliation in this case - {none, true} -> - case is_subscriber(JID, StateData) of - true -> member; - _ -> none - end; - _ -> - Aff - end + owner -> + owner; + none -> + Aff = case do_get_affiliation(JID, StateData) of + {Affiliation, _Reason} -> Affiliation; + Affiliation -> Affiliation + end, + case {Aff, (StateData#state.config)#config.members_only} of + % Subscribers should be have members affiliation in this case + {none, true} -> + case is_subscriber(JID, StateData) of + true -> member; + _ -> none + end; + _ -> + Aff + end end; get_affiliation(LJID, StateData) -> get_affiliation(jid:make(LJID), StateData). + -spec do_get_affiliation(jid(), state()) -> affiliation() | {affiliation(), binary()}. do_get_affiliation(JID, #state{config = #config{persistent = false}} = StateData) -> do_get_affiliation_fallback(JID, StateData); @@ -1769,31 +2069,41 @@ do_get_affiliation(JID, StateData) -> ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:get_affiliation(ServerHost, Room, Host, LUser, LServer) of - {error, _} -> - do_get_affiliation_fallback(JID, StateData); - {ok, Affiliation} -> - Affiliation + {error, _} -> + do_get_affiliation_fallback(JID, StateData); + {ok, Affiliation} -> + Affiliation end. --spec do_get_affiliation_fallback(jid(), state()) -> affiliation() | {affiliation(), binary()}. + +-spec do_get_affiliation_fallback(jid(), state()) -> affiliation() | {affiliation(), binary()}. do_get_affiliation_fallback(JID, StateData) -> LJID = jid:tolower(JID), - try maps:get(LJID, StateData#state.affiliations) - catch _:{badkey, _} -> + try + maps:get(LJID, StateData#state.affiliations) + catch + _:{badkey, _} -> BareLJID = jid:remove_resource(LJID), - try maps:get(BareLJID, StateData#state.affiliations) - catch _:{badkey, _} -> + try + maps:get(BareLJID, StateData#state.affiliations) + catch + _:{badkey, _} -> DomainLJID = setelement(1, LJID, <<"">>), - try maps:get(DomainLJID, StateData#state.affiliations) - catch _:{badkey, _} -> + try + maps:get(DomainLJID, StateData#state.affiliations) + catch + _:{badkey, _} -> DomainBareLJID = jid:remove_resource(DomainLJID), - try maps:get(DomainBareLJID, StateData#state.affiliations) - catch _:{badkey, _} -> none + try + maps:get(DomainBareLJID, StateData#state.affiliations) + catch + _:{badkey, _} -> none end end end end. + -spec get_affiliations(state()) -> affiliations(). get_affiliations(#state{config = #config{persistent = false}} = StateData) -> get_affiliations_fallback(StateData); @@ -1803,136 +2113,155 @@ get_affiliations(StateData) -> ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:get_affiliations(ServerHost, Room, Host) of - {error, _} -> - get_affiliations_fallback(StateData); - {ok, Affiliations} -> - Affiliations + {error, _} -> + get_affiliations_fallback(StateData); + {ok, Affiliations} -> + Affiliations end. + -spec get_affiliations_fallback(state()) -> affiliations(). get_affiliations_fallback(StateData) -> StateData#state.affiliations. + -spec get_service_affiliation(jid(), state()) -> owner | none. get_service_affiliation(JID, StateData) -> {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent, _AccessMam} = - StateData#state.access, + StateData#state.access, case acl:match_rule(StateData#state.server_host, - AccessAdmin, JID) - of - allow -> owner; - _ -> none + AccessAdmin, + JID) of + allow -> owner; + _ -> none end. + -spec set_role(jid(), role(), state()) -> state(). set_role(JID, Role, StateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of - {U, S, <<"">>} -> - maps:fold(fun (J, _, Js) -> - case J of - {U, S, _} -> [J | Js]; - _ -> Js - end - end, [], StateData#state.users); - _ -> - case maps:is_key(LJID, StateData#state.users) of - true -> [LJID]; - _ -> [] - end - end, + {U, S, <<"">>} -> + maps:fold(fun(J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], + StateData#state.users); + _ -> + case maps:is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end + end, {Users, Nicks} = - case Role of - none -> - lists:foldl( - fun (J, {Us, Ns}) -> - NewNs = try maps:get(J, Us) of - #user{nick = Nick} -> - maps:remove(Nick, Ns) - catch _:{badkey, _} -> - Ns - end, - {maps:remove(J, Us), NewNs} - end, - {StateData#state.users, StateData#state.nicks}, LJIDs); - _ -> - {lists:foldl( - fun (J, Us) -> - User = maps:get(J, Us), - if User#user.last_presence == undefined -> - Us; - true -> - maps:put(J, User#user{role = Role}, Us) - end - end, StateData#state.users, LJIDs), - StateData#state.nicks} - end, + case Role of + none -> + lists:foldl( + fun(J, {Us, Ns}) -> + NewNs = try maps:get(J, Us) of + #user{nick = Nick} -> + maps:remove(Nick, Ns) + catch + _:{badkey, _} -> + Ns + end, + {maps:remove(J, Us), NewNs} + end, + {StateData#state.users, StateData#state.nicks}, + LJIDs); + _ -> + {lists:foldl( + fun(J, Us) -> + User = maps:get(J, Us), + if + User#user.last_presence == undefined -> + Us; + true -> + maps:put(J, User#user{role = Role}, Us) + end + end, + StateData#state.users, + LJIDs), + StateData#state.nicks} + end, Affiliation = get_affiliation(JID, StateData), Roles = case Role of %% Don't persist 'none' role: if someone is kicked, they will %% maintain the same role they had *before* they were kicked, %% unless they were banned none when Affiliation /= outcast -> - maps:remove(jid:remove_resource(LJID), StateData#state.roles); + maps:remove(jid:remove_resource(LJID), StateData#state.roles); NewRole -> maps:put(jid:remove_resource(LJID), NewRole, StateData#state.roles) - end, + end, StateData#state{users = Users, nicks = Nicks, roles = Roles}. + -spec get_role(jid(), state()) -> role(). get_role(JID, StateData) -> LJID = jid:tolower(JID), try maps:get(LJID, StateData#state.users) of - #user{role = Role} -> Role - catch _:{badkey, _} -> none + #user{role = Role} -> Role + catch + _:{badkey, _} -> none end. + -spec get_default_role(affiliation(), state()) -> role(). get_default_role(Affiliation, StateData) -> case Affiliation of - owner -> moderator; - admin -> moderator; - member -> participant; - outcast -> none; - none -> - case (StateData#state.config)#config.members_only of - true -> none; - _ -> - case (StateData#state.config)#config.members_by_default - of - true -> participant; - _ -> visitor - end - end + owner -> moderator; + admin -> moderator; + member -> participant; + outcast -> none; + none -> + case (StateData#state.config)#config.members_only of + true -> none; + _ -> + case (StateData#state.config)#config.members_by_default of + true -> participant; + _ -> visitor + end + end end. + -spec is_visitor(jid(), state()) -> boolean(). is_visitor(Jid, StateData) -> get_role(Jid, StateData) =:= visitor. + -spec is_moderator(jid(), state()) -> boolean(). is_moderator(Jid, StateData) -> get_role(Jid, StateData) =:= moderator. + -spec get_max_users(state()) -> non_neg_integer(). get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), - if MaxUsers =< ServiceMaxUsers -> MaxUsers; - true -> ServiceMaxUsers + if + MaxUsers =< ServiceMaxUsers -> MaxUsers; + true -> ServiceMaxUsers end. + -spec get_service_max_users(state()) -> pos_integer(). get_service_max_users(StateData) -> mod_muc_opt:max_users(StateData#state.server_host). + -spec get_max_users_admin_threshold(state()) -> pos_integer(). get_max_users_admin_threshold(StateData) -> mod_muc_opt:max_users_admin_threshold(StateData#state.server_host). + -spec room_queue_new(binary(), ejabberd_shaper:shaper(), _) -> p1_queue:queue({message | presence, jid()}) | undefined. room_queue_new(ServerHost, Shaper, QueueType) -> HaveRoomShaper = Shaper /= none, @@ -1940,198 +2269,233 @@ room_queue_new(ServerHost, Shaper, QueueType) -> HavePresenceShaper = mod_muc_opt:user_presence_shaper(ServerHost) /= none, HaveMinMessageInterval = mod_muc_opt:min_message_interval(ServerHost) /= 0, HaveMinPresenceInterval = mod_muc_opt:min_presence_interval(ServerHost) /= 0, - if HaveRoomShaper or HaveMessageShaper or HavePresenceShaper - or HaveMinMessageInterval or HaveMinPresenceInterval -> - p1_queue:new(QueueType); - true -> - undefined + if + HaveRoomShaper or HaveMessageShaper or HavePresenceShaper or + HaveMinMessageInterval or HaveMinPresenceInterval -> + p1_queue:new(QueueType); + true -> + undefined end. + -spec get_user_activity(jid(), state()) -> #activity{}. get_user_activity(JID, StateData) -> case treap:lookup(jid:tolower(JID), - StateData#state.activity) - of - {ok, _P, A} -> A; - error -> - MessageShaper = - ejabberd_shaper:new(mod_muc_opt:user_message_shaper(StateData#state.server_host)), - PresenceShaper = - ejabberd_shaper:new(mod_muc_opt:user_presence_shaper(StateData#state.server_host)), - #activity{message_shaper = MessageShaper, - presence_shaper = PresenceShaper} + StateData#state.activity) of + {ok, _P, A} -> A; + error -> + MessageShaper = + ejabberd_shaper:new(mod_muc_opt:user_message_shaper(StateData#state.server_host)), + PresenceShaper = + ejabberd_shaper:new(mod_muc_opt:user_presence_shaper(StateData#state.server_host)), + #activity{ + message_shaper = MessageShaper, + presence_shaper = PresenceShaper + } end. + -spec store_user_activity(jid(), #activity{}, state()) -> state(). store_user_activity(JID, UserActivity, StateData) -> MinMessageInterval = - trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000), + trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000), MinPresenceInterval = - trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000), + trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000), Key = jid:tolower(JID), Now = erlang:system_time(microsecond), Activity1 = clean_treap(StateData#state.activity, - {1, -Now}), + {1, -Now}), Activity = case treap:lookup(Key, Activity1) of - {ok, _P, _A} -> treap:delete(Key, Activity1); - error -> Activity1 - end, + {ok, _P, _A} -> treap:delete(Key, Activity1); + error -> Activity1 + end, StateData1 = case MinMessageInterval == 0 andalso - MinPresenceInterval == 0 andalso - UserActivity#activity.message_shaper == none andalso - UserActivity#activity.presence_shaper == none - andalso - UserActivity#activity.message == undefined andalso - UserActivity#activity.presence == undefined - of - true -> StateData#state{activity = Activity}; - false -> - case UserActivity#activity.message == undefined andalso - UserActivity#activity.presence == undefined - of - true -> - {_, MessageShaperInterval} = - ejabberd_shaper:update(UserActivity#activity.message_shaper, - 100000), - {_, PresenceShaperInterval} = - ejabberd_shaper:update(UserActivity#activity.presence_shaper, - 100000), - Delay = lists:max([MessageShaperInterval, - PresenceShaperInterval, - MinMessageInterval, - MinPresenceInterval]) - * 1000, - Priority = {1, -(Now + Delay)}, - StateData#state{activity = - treap:insert(Key, Priority, - UserActivity, - Activity)}; - false -> - Priority = {0, 0}, - StateData#state{activity = - treap:insert(Key, Priority, - UserActivity, - Activity)} - end - end, + MinPresenceInterval == 0 andalso + UserActivity#activity.message_shaper == none andalso + UserActivity#activity.presence_shaper == none andalso + UserActivity#activity.message == undefined andalso + UserActivity#activity.presence == undefined of + true -> StateData#state{activity = Activity}; + false -> + case UserActivity#activity.message == undefined andalso + UserActivity#activity.presence == undefined of + true -> + {_, MessageShaperInterval} = + ejabberd_shaper:update(UserActivity#activity.message_shaper, + 100000), + {_, PresenceShaperInterval} = + ejabberd_shaper:update(UserActivity#activity.presence_shaper, + 100000), + Delay = lists:max([MessageShaperInterval, + PresenceShaperInterval, + MinMessageInterval, + MinPresenceInterval]) * + 1000, + Priority = {1, -(Now + Delay)}, + StateData#state{ + activity = + treap:insert(Key, + Priority, + UserActivity, + Activity) + }; + false -> + Priority = {0, 0}, + StateData#state{ + activity = + treap:insert(Key, + Priority, + UserActivity, + Activity) + } + end + end, reset_hibernate_timer(StateData1). + -spec clean_treap(treap:treap(), integer() | {1, integer()}) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of - true -> Treap; - false -> - {_Key, Priority, _Value} = treap:get_root(Treap), - if Priority > CleanPriority -> - clean_treap(treap:delete_root(Treap), CleanPriority); - true -> Treap - end + true -> Treap; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if + Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority); + true -> Treap + end end. + -spec prepare_room_queue(state()) -> state(). prepare_room_queue(StateData) -> case p1_queue:out(StateData#state.room_queue) of - {{value, {message, From}}, _RoomQueue} -> - Activity = get_user_activity(From, StateData), - Packet = Activity#activity.message, - Size = element_size(Packet), - {RoomShaper, RoomShaperInterval} = - ejabberd_shaper:update(StateData#state.room_shaper, Size), - erlang:send_after(RoomShaperInterval, self(), - process_room_queue), - StateData#state{room_shaper = RoomShaper}; - {{value, {presence, From}}, _RoomQueue} -> - Activity = get_user_activity(From, StateData), - {_Nick, Packet} = Activity#activity.presence, - Size = element_size(Packet), - {RoomShaper, RoomShaperInterval} = - ejabberd_shaper:update(StateData#state.room_shaper, Size), - erlang:send_after(RoomShaperInterval, self(), - process_room_queue), - StateData#state{room_shaper = RoomShaper}; - {empty, _} -> StateData + {{value, {message, From}}, _RoomQueue} -> + Activity = get_user_activity(From, StateData), + Packet = Activity#activity.message, + Size = element_size(Packet), + {RoomShaper, RoomShaperInterval} = + ejabberd_shaper:update(StateData#state.room_shaper, Size), + erlang:send_after(RoomShaperInterval, + self(), + process_room_queue), + StateData#state{room_shaper = RoomShaper}; + {{value, {presence, From}}, _RoomQueue} -> + Activity = get_user_activity(From, StateData), + {_Nick, Packet} = Activity#activity.presence, + Size = element_size(Packet), + {RoomShaper, RoomShaperInterval} = + ejabberd_shaper:update(StateData#state.room_shaper, Size), + erlang:send_after(RoomShaperInterval, + self(), + process_room_queue), + StateData#state{room_shaper = RoomShaper}; + {empty, _} -> StateData end. + -spec update_online_user(jid(), #user{}, state()) -> state(). update_online_user(JID, #user{nick = Nick} = User, StateData) -> LJID = jid:tolower(JID), add_to_log(join, Nick, StateData), Nicks1 = try maps:get(LJID, StateData#state.users) of - #user{nick = OldNick} -> - case lists:delete( - LJID, maps:get(OldNick, StateData#state.nicks)) of - [] -> - maps:remove(OldNick, StateData#state.nicks); - LJIDs -> - maps:put(OldNick, LJIDs, StateData#state.nicks) - end - catch _:{badkey, _} -> - StateData#state.nicks - end, + #user{nick = OldNick} -> + case lists:delete( + LJID, maps:get(OldNick, StateData#state.nicks)) of + [] -> + maps:remove(OldNick, StateData#state.nicks); + LJIDs -> + maps:put(OldNick, LJIDs, StateData#state.nicks) + end + catch + _:{badkey, _} -> + StateData#state.nicks + end, Nicks = maps:update_with(Nick, - fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, - [LJID], Nicks1), + fun(LJIDs) -> [LJID | LJIDs -- [LJID]] end, + [LJID], + Nicks1), Users = maps:update_with(LJID, - fun(U) -> - U#user{nick = Nick} - end, User, StateData#state.users), + fun(U) -> + U#user{nick = Nick} + end, + User, + StateData#state.users), NewStateData = StateData#state{users = Users, nicks = Nicks}, case {maps:get(LJID, StateData#state.users, error), - maps:get(LJID, NewStateData#state.users, error)} of - {#user{nick = Old}, #user{nick = New}} when Old /= New -> - send_nick_changing(JID, Old, NewStateData, true, true); - _ -> - ok + maps:get(LJID, NewStateData#state.users, error)} of + {#user{nick = Old}, #user{nick = New}} when Old /= New -> + send_nick_changing(JID, Old, NewStateData, true, true); + _ -> + ok end, NewStateData. + -spec set_subscriber(jid(), binary(), [binary()], state()) -> state(). -set_subscriber(JID, Nick, Nodes, - #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> +set_subscriber(JID, + Nick, + Nodes, + #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> BareJID = jid:remove_resource(JID), LBareJID = jid:tolower(BareJID), MUCSubscribers = muc_subscribers_put( - #subscriber{jid = BareJID, - nick = Nick, - nodes = Nodes}, + #subscriber{ + jid = BareJID, + nick = Nick, + nodes = Nodes + }, StateData#state.muc_subscribers), NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]), case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of - true -> - Packet1a = #message{ - sub_els = [#ps_event{ - items = #ps_items{ - node = ?NS_MUCSUB_NODES_SUBSCRIBERS, - items = [#ps_item{ - id = p1_rand:get_string(), - sub_els = [#muc_subscribe{jid = BareJID, nick = Nick}]}]}}]}, - Packet1b = #message{ - sub_els = [#ps_event{ - items = #ps_items{ - node = ?NS_MUCSUB_NODES_SUBSCRIBERS, - items = [#ps_item{ - id = p1_rand:get_string(), - sub_els = [#muc_subscribe{nick = Nick}]}]}}]}, - {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_subscribed, ServerHost, {Packet1a, Packet1b}, - [ServerHost, Room, Host, BareJID, StateData]), - send_subscriptions_change_notifications(Packet2a, Packet2b, NewStateData); - _ -> - ok + true -> + Packet1a = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_subscribe{jid = BareJID, nick = Nick}] + }] + } + }] + }, + Packet1b = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_subscribe{nick = Nick}] + }] + } + }] + }, + {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_subscribed, + ServerHost, + {Packet1a, Packet1b}, + [ServerHost, Room, Host, BareJID, StateData]), + send_subscriptions_change_notifications(Packet2a, Packet2b, NewStateData); + _ -> + ok end, NewStateData. + -spec add_online_user(jid(), binary(), role(), state()) -> state(). add_online_user(JID, Nick, Role, StateData) -> tab_add_online_user(JID, StateData), User = #user{jid = JID, nick = Nick, role = Role}, reset_hibernate_timer(update_online_user(JID, User, StateData)). + -spec remove_online_user(jid(), state()) -> state(). remove_online_user(JID, StateData) -> remove_online_user(JID, StateData, <<"">>). + -spec remove_online_user(jid(), state(), binary()) -> state(). remove_online_user(JID, StateData, Reason) -> LJID = jid:tolower(JID), @@ -2140,88 +2504,105 @@ remove_online_user(JID, StateData, Reason) -> tab_remove_online_user(JID, StateData), Users = maps:remove(LJID, StateData#state.users), Nicks = try maps:get(Nick, StateData#state.nicks) of - [LJID] -> - maps:remove(Nick, StateData#state.nicks); - U -> - maps:put(Nick, U -- [LJID], StateData#state.nicks) - catch _:{badkey, _} -> - StateData#state.nicks - end, + [LJID] -> + maps:remove(Nick, StateData#state.nicks); + U -> + maps:put(Nick, U -- [LJID], StateData#state.nicks) + catch + _:{badkey, _} -> + StateData#state.nicks + end, reset_hibernate_timer(StateData#state{users = Users, nicks = Nicks}). + -spec filter_presence(presence()) -> presence(). filter_presence(Presence) -> Els = lists:filter( - fun(El) -> - XMLNS = xmpp:get_ns(El), - case catch binary:part(XMLNS, 0, size(?NS_MUC)) of - ?NS_MUC -> false; - _ -> XMLNS /= ?NS_HATS - end - end, xmpp:get_els(Presence)), + fun(El) -> + XMLNS = xmpp:get_ns(El), + case catch binary:part(XMLNS, 0, size(?NS_MUC)) of + ?NS_MUC -> false; + _ -> XMLNS /= ?NS_HATS + end + end, + xmpp:get_els(Presence)), xmpp:set_els(Presence, Els). + -spec strip_status(presence()) -> presence(). strip_status(Presence) -> Presence#presence{status = []}. + -spec add_user_presence(jid(), presence(), state()) -> state(). add_user_presence(JID, Presence, StateData) -> LJID = jid:tolower(JID), FPresence = filter_presence(Presence), Users = maps:update_with(LJID, - fun (#user{} = User) -> - User#user{last_presence = FPresence} - end, StateData#state.users), + fun(#user{} = User) -> + User#user{last_presence = FPresence} + end, + StateData#state.users), StateData#state{users = Users}. + -spec add_user_presence_un(jid(), presence(), state()) -> state(). add_user_presence_un(JID, Presence, StateData) -> LJID = jid:tolower(JID), FPresence = filter_presence(Presence), Users = maps:update_with(LJID, - fun (#user{} = User) -> - User#user{last_presence = FPresence, - role = none} - end, StateData#state.users), + fun(#user{} = User) -> + User#user{ + last_presence = FPresence, + role = none + } + end, + StateData#state.users), StateData#state{users = Users}. + %% Find and return a list of the full JIDs of the users of Nick. %% Return jid record. -spec find_jids_by_nick(binary(), state()) -> [jid()]. find_jids_by_nick(Nick, StateData) -> Users = case maps:get(Nick, StateData#state.nicks, []) of - [] -> muc_subscribers_get_by_nick( - Nick, StateData#state.muc_subscribers); - Us -> Us - end, - [jid:make(LJID) || LJID <- Users]. + [] -> + muc_subscribers_get_by_nick( + Nick, StateData#state.muc_subscribers); + Us -> Us + end, + [ jid:make(LJID) || LJID <- Users ]. + %% Find and return the full JID of the user of Nick with %% highest-priority presence. Return jid record. -spec find_jid_by_nick(binary(), state()) -> jid() | false. find_jid_by_nick(Nick, StateData) -> try maps:get(Nick, StateData#state.nicks) of - [User] -> jid:make(User); - [FirstUser | Users] -> - #user{last_presence = FirstPresence} = - maps:get(FirstUser, StateData#state.users), - {LJID, _} = lists:foldl( - fun(Compare, {HighestUser, HighestPresence}) -> - #user{last_presence = P1} = - maps:get(Compare, StateData#state.users), - case higher_presence(P1, HighestPresence) of - true -> {Compare, P1}; - false -> {HighestUser, HighestPresence} - end - end, {FirstUser, FirstPresence}, Users), - jid:make(LJID) - catch _:{badkey, _} -> - false + [User] -> jid:make(User); + [FirstUser | Users] -> + #user{last_presence = FirstPresence} = + maps:get(FirstUser, StateData#state.users), + {LJID, _} = lists:foldl( + fun(Compare, {HighestUser, HighestPresence}) -> + #user{last_presence = P1} = + maps:get(Compare, StateData#state.users), + case higher_presence(P1, HighestPresence) of + true -> {Compare, P1}; + false -> {HighestUser, HighestPresence} + end + end, + {FirstUser, FirstPresence}, + Users), + jid:make(LJID) + catch + _:{badkey, _} -> + false end. + -spec higher_presence(undefined | presence(), - undefined | presence()) -> boolean(). + undefined | presence()) -> boolean(). higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), @@ -2229,6 +2610,7 @@ higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> higher_presence(Pres1, Pres2) -> Pres1 > Pres2. + -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of @@ -2236,347 +2618,400 @@ get_priority_from_presence(#presence{priority = Prio}) -> _ -> Prio end. + -spec find_nick_by_jid(jid() | undefined, state()) -> binary(). find_nick_by_jid(undefined, _StateData) -> <<>>; find_nick_by_jid(JID, StateData) -> LJID = jid:tolower(JID), case maps:find(LJID, StateData#state.users) of - {ok, #user{nick = Nick}} -> - Nick; - _ -> - case maps:find(LJID, (StateData#state.muc_subscribers)#muc_subscribers.subscribers) of - {ok, #subscriber{nick = Nick}} -> - Nick; - _ -> - <<>> - end + {ok, #user{nick = Nick}} -> + Nick; + _ -> + case maps:find(LJID, (StateData#state.muc_subscribers)#muc_subscribers.subscribers) of + {ok, #subscriber{nick = Nick}} -> + Nick; + _ -> + <<>> + end end. + -spec is_nick_change(jid(), binary(), state()) -> boolean(). is_nick_change(JID, Nick, StateData) -> LJID = jid:tolower(JID), case Nick of - <<"">> -> false; - _ -> - #user{nick = OldNick} = maps:get(LJID, StateData#state.users), - Nick /= OldNick + <<"">> -> false; + _ -> + #user{nick = OldNick} = maps:get(LJID, StateData#state.users), + Nick /= OldNick end. + -spec nick_collision(jid(), binary(), state()) -> boolean(). nick_collision(User, Nick, StateData) -> UserOfNick = case find_jid_by_nick(Nick, StateData) of - false -> + false -> case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of [J] -> J; [] -> false end; - J -> J - end, + J -> J + end, (UserOfNick /= false andalso - jid:remove_resource(jid:tolower(UserOfNick)) - /= jid:remove_resource(jid:tolower(User))). + jid:remove_resource(jid:tolower(UserOfNick)) /= + jid:remove_resource(jid:tolower(User))). + -spec add_new_user(jid(), binary(), presence(), state()) -> state(); - (jid(), binary(), iq(), state()) -> {error, stanza_error()} | - {ignore, state()} | - {result, muc_subscribe(), state()}. + (jid(), binary(), iq(), state()) -> {error, stanza_error()} | + {ignore, state()} | + {result, muc_subscribe(), state()}. add_new_user(From, Nick, Packet, StateData) -> Lang = xmpp:get_lang(Packet), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + - get_max_users_admin_threshold(StateData), + get_max_users_admin_threshold(StateData), NUsers = maps:size(StateData#state.users), Affiliation = get_affiliation(From, StateData), ServiceAffiliation = get_service_affiliation(From, - StateData), + StateData), NConferences = tab_count_user(From, StateData), MaxConferences = - mod_muc_opt:max_user_conferences(StateData#state.server_host), + mod_muc_opt:max_user_conferences(StateData#state.server_host), Collision = nick_collision(From, Nick, StateData), IsSubscribeRequest = not is_record(Packet, presence), case {ServiceAffiliation == owner orelse - ((((Affiliation == admin orelse Affiliation == owner) - andalso NUsers < MaxAdminUsers) - orelse NUsers < MaxUsers) - andalso NConferences < MaxConferences), - Collision, - mod_muc:can_use_nick(StateData#state.server_host, - jid:encode(StateData#state.jid), From, Nick), - get_occupant_initial_role(From, Affiliation, StateData)} - of - {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> - Txt = ?T("Too many users in this conference"), - Err = xmpp:err_resource_constraint(Txt, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - {false, _, _, _} when NConferences >= MaxConferences -> - Txt = ?T("You have joined too many conferences"), - Err = xmpp:err_resource_constraint(Txt, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - {false, _, _, _} -> - Err = xmpp:err_service_unavailable(), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - {_, _, _, none} -> - Err = case Affiliation of - outcast -> - ErrText = ?T("You have been banned from this room"), - xmpp:err_forbidden(ErrText, Lang); - _ -> - ErrText = ?T("Membership is required to enter this room"), - xmpp:err_registration_required(ErrText, Lang) - end, - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - {_, true, _, _} -> - ErrText = ?T("That nickname is already in use by another occupant"), - Err = xmpp:err_conflict(ErrText, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - {_, _, false, _} -> - Err = case Nick of - <<>> -> - xmpp:err_jid_malformed(?T("Nickname can't be empty"), - Lang); - _ -> - xmpp:err_conflict(?T("That nickname is registered" - " by another person"), Lang) - end, - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - {_, _, _, Role} -> - case check_password(ServiceAffiliation, Affiliation, - Packet, From, StateData) - of - true -> - Nodes = get_subscription_nodes(Packet), - NewStateData = - if not IsSubscribeRequest -> - NewState = add_user_presence( - From, Packet, - add_online_user(From, Nick, Role, - StateData)), - send_initial_presences_and_messages( - From, Nick, Packet, NewState, StateData), - NewState; - true -> - set_subscriber(From, Nick, Nodes, StateData) - end, - ResultState = - case NewStateData#state.just_created of - true -> - NewStateData#state{just_created = erlang:system_time(microsecond)}; - _ -> - Robots = maps:remove(From, StateData#state.robots), - NewStateData#state{robots = Robots} - end, - if not IsSubscribeRequest -> ResultState; - true -> {result, subscribe_result(Packet), ResultState} - end; - need_password -> - ErrText = ?T("A password is required to enter this room"), - Err = xmpp:err_not_authorized(ErrText, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - captcha_required -> - SID = xmpp:get_id(Packet), - RoomJID = StateData#state.jid, - To = jid:replace_resource(RoomJID, Nick), - Limiter = {From#jid.luser, From#jid.lserver}, - case ejabberd_captcha:create_captcha(SID, RoomJID, To, - Lang, Limiter, From) - of - {ok, ID, Body, CaptchaEls} -> - MsgPkt = #message{from = RoomJID, - to = From, - id = ID, body = Body, - sub_els = CaptchaEls}, - Robots = maps:put(From, {Nick, Packet}, - StateData#state.robots), - ejabberd_router:route(MsgPkt), - NewState = StateData#state{robots = Robots}, - if not IsSubscribeRequest -> - NewState; - true -> - {ignore, NewState} - end; - {error, limit} -> - ErrText = ?T("Too many CAPTCHA requests"), - Err = xmpp:err_resource_constraint(ErrText, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end; - _ -> - ErrText = ?T("Unable to generate a CAPTCHA"), - Err = xmpp:err_internal_server_error(ErrText, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end - end; - _ -> - ErrText = ?T("Incorrect password"), - Err = xmpp:err_not_authorized(ErrText, Lang), - if not IsSubscribeRequest -> - ejabberd_router:route_error(Packet, Err), - StateData; - true -> - {error, Err} - end - end + ((((Affiliation == admin orelse Affiliation == owner) andalso + NUsers < MaxAdminUsers) orelse + NUsers < MaxUsers) andalso + NConferences < MaxConferences), + Collision, + mod_muc:can_use_nick(StateData#state.server_host, + jid:encode(StateData#state.jid), + From, + Nick), + get_occupant_initial_role(From, Affiliation, StateData)} of + {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> + Txt = ?T("Too many users in this conference"), + Err = xmpp:err_resource_constraint(Txt, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {false, _, _, _} when NConferences >= MaxConferences -> + Txt = ?T("You have joined too many conferences"), + Err = xmpp:err_resource_constraint(Txt, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {false, _, _, _} -> + Err = xmpp:err_service_unavailable(), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {_, _, _, none} -> + Err = case Affiliation of + outcast -> + ErrText = ?T("You have been banned from this room"), + xmpp:err_forbidden(ErrText, Lang); + _ -> + ErrText = ?T("Membership is required to enter this room"), + xmpp:err_registration_required(ErrText, Lang) + end, + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {_, true, _, _} -> + ErrText = ?T("That nickname is already in use by another occupant"), + Err = xmpp:err_conflict(ErrText, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {_, _, false, _} -> + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed(?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict(?T("That nickname is registered" + " by another person"), + Lang) + end, + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + {_, _, _, Role} -> + case check_password(ServiceAffiliation, + Affiliation, + Packet, + From, + StateData) of + true -> + Nodes = get_subscription_nodes(Packet), + NewStateData = + if + not IsSubscribeRequest -> + NewState = add_user_presence( + From, + Packet, + add_online_user(From, + Nick, + Role, + StateData)), + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData), + NewState; + true -> + set_subscriber(From, Nick, Nodes, StateData) + end, + ResultState = + case NewStateData#state.just_created of + true -> + NewStateData#state{just_created = erlang:system_time(microsecond)}; + _ -> + Robots = maps:remove(From, StateData#state.robots), + NewStateData#state{robots = Robots} + end, + if + not IsSubscribeRequest -> ResultState; + true -> {result, subscribe_result(Packet), ResultState} + end; + need_password -> + ErrText = ?T("A password is required to enter this room"), + Err = xmpp:err_not_authorized(ErrText, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + captcha_required -> + SID = xmpp:get_id(Packet), + RoomJID = StateData#state.jid, + To = jid:replace_resource(RoomJID, Nick), + Limiter = {From#jid.luser, From#jid.lserver}, + case ejabberd_captcha:create_captcha(SID, + RoomJID, + To, + Lang, + Limiter, + From) of + {ok, ID, Body, CaptchaEls} -> + MsgPkt = #message{ + from = RoomJID, + to = From, + id = ID, + body = Body, + sub_els = CaptchaEls + }, + Robots = maps:put(From, + {Nick, Packet}, + StateData#state.robots), + ejabberd_router:route(MsgPkt), + NewState = StateData#state{robots = Robots}, + if + not IsSubscribeRequest -> + NewState; + true -> + {ignore, NewState} + end; + {error, limit} -> + ErrText = ?T("Too many CAPTCHA requests"), + Err = xmpp:err_resource_constraint(ErrText, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end; + _ -> + ErrText = ?T("Unable to generate a CAPTCHA"), + Err = xmpp:err_internal_server_error(ErrText, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end + end; + _ -> + ErrText = ?T("Incorrect password"), + Err = xmpp:err_not_authorized(ErrText, Lang), + if + not IsSubscribeRequest -> + ejabberd_router:route_error(Packet, Err), + StateData; + true -> + {error, Err} + end + end end. --spec check_password(affiliation(), affiliation(), - presence() | iq(), jid(), state()) -> - boolean() | need_password | captcha_required. -check_password(owner, _Affiliation, _Packet, _From, - _StateData) -> + +-spec check_password(affiliation(), + affiliation(), + presence() | iq(), + jid(), + state()) -> + boolean() | need_password | captcha_required. +check_password(owner, + _Affiliation, + _Packet, + _From, + _StateData) -> %% Don't check pass if user is owner in MUC service (access_admin option) true; -check_password(_ServiceAffiliation, Affiliation, Packet, - From, StateData) -> - case (StateData#state.config)#config.password_protected - of - false -> check_captcha(Affiliation, From, StateData); - true -> - Pass = extract_password(Packet), - case Pass of - false -> need_password; - _ -> - case (StateData#state.config)#config.password of - Pass -> true; - _ -> false - end - end +check_password(_ServiceAffiliation, + Affiliation, + Packet, + From, + StateData) -> + case (StateData#state.config)#config.password_protected of + false -> check_captcha(Affiliation, From, StateData); + true -> + Pass = extract_password(Packet), + case Pass of + false -> need_password; + _ -> + case (StateData#state.config)#config.password of + Pass -> true; + _ -> false + end + end end. + -spec check_captcha(affiliation(), jid(), state()) -> true | captcha_required. check_captcha(Affiliation, From, StateData) -> - case (StateData#state.config)#config.captcha_protected - andalso ejabberd_captcha:is_feature_available() - of - true when Affiliation == none -> - case maps:get(From, StateData#state.robots, error) of - passed -> true; - _ -> - WList = - (StateData#state.config)#config.captcha_whitelist, - #jid{luser = U, lserver = S, lresource = R} = From, - case (?SETS):is_element({U, S, R}, WList) of - true -> true; - false -> - case (?SETS):is_element({U, S, <<"">>}, WList) of - true -> true; - false -> - case (?SETS):is_element({<<"">>, S, <<"">>}, WList) - of - true -> true; - false -> captcha_required - end - end - end - end; - _ -> true + case (StateData#state.config)#config.captcha_protected andalso + ejabberd_captcha:is_feature_available() of + true when Affiliation == none -> + case maps:get(From, StateData#state.robots, error) of + passed -> true; + _ -> + WList = + (StateData#state.config)#config.captcha_whitelist, + #jid{luser = U, lserver = S, lresource = R} = From, + case (?SETS):is_element({U, S, R}, WList) of + true -> true; + false -> + case (?SETS):is_element({U, S, <<"">>}, WList) of + true -> true; + false -> + case (?SETS):is_element({<<"">>, S, <<"">>}, WList) of + true -> true; + false -> captcha_required + end + end + end + end; + _ -> true end. + -spec extract_password(presence() | iq()) -> binary() | false. extract_password(#presence{} = Pres) -> case xmpp:get_subtag(Pres, #muc{}) of - #muc{password = Password} when is_binary(Password) -> - Password; - _ -> - false + #muc{password = Password} when is_binary(Password) -> + Password; + _ -> + false end; extract_password(#iq{} = IQ) -> case xmpp:get_subtag(IQ, #muc_subscribe{}) of - #muc_subscribe{password = Password} when Password /= <<"">> -> - Password; - _ -> - false + #muc_subscribe{password = Password} when Password /= <<"">> -> + Password; + _ -> + false end. + -spec get_history(binary(), stanza(), state()) -> [lqueue_elem()]. get_history(Nick, Packet, #state{history = History}) -> case xmpp:get_subtag(Packet, #muc{}) of - #muc{history = #muc_history{} = MUCHistory} -> - Now = erlang:timestamp(), - Q = History#lqueue.queue, - filter_history(Q, Now, Nick, MUCHistory); - _ -> - p1_queue:to_list(History#lqueue.queue) + #muc{history = #muc_history{} = MUCHistory} -> + Now = erlang:timestamp(), + Q = History#lqueue.queue, + filter_history(Q, Now, Nick, MUCHistory); + _ -> + p1_queue:to_list(History#lqueue.queue) end. --spec filter_history(p1_queue:queue(lqueue_elem()), erlang:timestamp(), - binary(), muc_history()) -> [lqueue_elem()]. -filter_history(Queue, Now, Nick, - #muc_history{since = Since, - seconds = Seconds, - maxstanzas = MaxStanzas, - maxchars = MaxChars}) -> + +-spec filter_history(p1_queue:queue(lqueue_elem()), + erlang:timestamp(), + binary(), + muc_history()) -> [lqueue_elem()]. +filter_history(Queue, + Now, + Nick, + #muc_history{ + since = Since, + seconds = Seconds, + maxstanzas = MaxStanzas, + maxchars = MaxChars + }) -> {History, _, _} = - lists:foldr( - fun({_, _, _, TimeStamp, Size} = Elem, - {Elems, NumStanzas, NumChars} = Acc) -> - NowDiff = timer:now_diff(Now, TimeStamp) div 1000000, - Chars = Size + byte_size(Nick) + 1, - if (NumStanzas < MaxStanzas) andalso - (TimeStamp > Since) andalso - (NowDiff =< Seconds) andalso - (NumChars + Chars =< MaxChars) -> - {[Elem|Elems], NumStanzas + 1, NumChars + Chars}; - true -> - Acc - end - end, {[], 0, 0}, p1_queue:to_list(Queue)), + lists:foldr( + fun({_, _, _, TimeStamp, Size} = Elem, + {Elems, NumStanzas, NumChars} = Acc) -> + NowDiff = timer:now_diff(Now, TimeStamp) div 1000000, + Chars = Size + byte_size(Nick) + 1, + if + (NumStanzas < MaxStanzas) andalso + (TimeStamp > Since) andalso + (NowDiff =< Seconds) andalso + (NumChars + Chars =< MaxChars) -> + {[Elem | Elems], NumStanzas + 1, NumChars + Chars}; + true -> + Acc + end + end, + {[], 0, 0}, + p1_queue:to_list(Queue)), History. + -spec is_room_overcrowded(state()) -> boolean(). is_room_overcrowded(StateData) -> MaxUsersPresence = mod_muc_opt:max_users_presence(StateData#state.server_host), maps:size(StateData#state.users) > MaxUsersPresence. + -spec presence_broadcast_allowed(jid(), state()) -> boolean(). presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). --spec send_initial_presences_and_messages( - jid(), binary(), presence(), state(), state()) -> ok. + +-spec send_initial_presences_and_messages(jid(), binary(), presence(), state(), state()) -> ok. send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> advertise_entity_capabilities(From, NewState), send_existing_presences(From, NewState), @@ -2585,6 +3020,7 @@ send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> send_history(From, History, NewState), send_subject(From, OldState). + -spec advertise_entity_capabilities(jid(), state()) -> ok. advertise_entity_capabilities(JID, State) -> AvatarHash = (State#state.config)#config.vcard_xupdate, @@ -2592,64 +3028,82 @@ advertise_entity_capabilities(JID, State) -> Extras = iq_disco_info_extras(<<"en">>, State, true), DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, DiscoHash = mod_caps:compute_disco_hash(DiscoInfo1, sha), - Els1 = [#caps{hash = <<"sha-1">>, - node = ejabberd_config:get_uri(), - version = DiscoHash}], - Els2 = if is_binary(AvatarHash) -> - [#vcard_xupdate{hash = AvatarHash}|Els1]; - true -> - Els1 - end, - ejabberd_router:route(#presence{from = State#state.jid, to = JID, - id = p1_rand:get_string(), - sub_els = Els2}). + Els1 = [#caps{ + hash = <<"sha-1">>, + node = ejabberd_config:get_uri(), + version = DiscoHash + }], + Els2 = if + is_binary(AvatarHash) -> + [#vcard_xupdate{hash = AvatarHash} | Els1]; + true -> + Els1 + end, + ejabberd_router:route(#presence{ + from = State#state.jid, + to = JID, + id = p1_rand:get_string(), + sub_els = Els2 + }). + -spec send_self_presence(jid(), state(), state()) -> ok. send_self_presence(NJID, StateData, OldStateData) -> send_new_presence(NJID, <<"">>, true, StateData, OldStateData). + -spec send_update_presence(jid(), state(), state()) -> ok. send_update_presence(JID, StateData, OldStateData) -> send_update_presence(JID, <<"">>, StateData, OldStateData). + -spec send_update_presence(jid(), binary(), state(), state()) -> ok. send_update_presence(JID, Reason, StateData, OldStateData) -> case is_room_overcrowded(StateData) of - true -> ok; - false -> send_update_presence1(JID, Reason, StateData, OldStateData) + true -> ok; + false -> send_update_presence1(JID, Reason, StateData, OldStateData) end. + -spec send_update_presence1(jid(), binary(), state(), state()) -> ok. send_update_presence1(JID, Reason, StateData, OldStateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of - {U, S, <<"">>} -> - maps:fold(fun (J, _, Js) -> - case J of - {U, S, _} -> [J | Js]; - _ -> Js - end - end, [], StateData#state.users); - _ -> - case maps:is_key(LJID, StateData#state.users) of - true -> [LJID]; - _ -> [] - end - end, - lists:foreach(fun (J) -> - send_new_presence(J, Reason, false, StateData, - OldStateData) - end, - LJIDs). + {U, S, <<"">>} -> + maps:fold(fun(J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], + StateData#state.users); + _ -> + case maps:is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end + end, + lists:foreach(fun(J) -> + send_new_presence(J, + Reason, + false, + StateData, + OldStateData) + end, + LJIDs). + -spec send_new_presence(jid(), state(), state()) -> ok. send_new_presence(NJID, StateData, OldStateData) -> send_new_presence(NJID, <<"">>, false, StateData, OldStateData). + -spec send_new_presence(jid(), binary(), state(), state()) -> ok. send_new_presence(NJID, Reason, StateData, OldStateData) -> send_new_presence(NJID, Reason, false, StateData, OldStateData). + -spec is_ra_changed(jid(), boolean(), state(), state()) -> boolean(). is_ra_changed(_, _IsInitialPresence = true, _, _) -> false; @@ -2658,24 +3112,29 @@ is_ra_changed(JID, _IsInitialPresence = false, NewStateData, OldStateData) -> NewAff = get_affiliation(JID, NewStateData), OldRole = get_role(JID, OldStateData), OldAff = get_affiliation(JID, OldStateData), - if (NewRole == none) and (NewAff == OldAff) -> - %% A user is leaving the room; - false; - true -> - (NewRole /= OldRole) or (NewAff /= OldAff) + if + (NewRole == none) and (NewAff == OldAff) -> + %% A user is leaving the room; + false; + true -> + (NewRole /= OldRole) or (NewAff /= OldAff) end. + -spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok. send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), #user{nick = Nick} = maps:get(LNJID, StateData#state.users), LJID = find_jid_by_nick(Nick, StateData), - #user{jid = RealJID, role = Role0, - last_presence = Presence0} = UserInfo = - maps:get(jid:tolower(LJID), StateData#state.users), + #user{ + jid = RealJID, + role = Role0, + last_presence = Presence0 + } = UserInfo = + maps:get(jid:tolower(LJID), StateData#state.users), {Role1, Presence1} = case (presence_broadcast_allowed(NJID, StateData) orelse - presence_broadcast_allowed(NJID, OldStateData)) of + presence_broadcast_allowed(NJID, OldStateData)) of true -> {Role0, Presence0}; false -> {none, #presence{type = unavailable}} end, @@ -2687,8 +3146,8 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, UserMap = case is_room_overcrowded(StateData) orelse - (not (presence_broadcast_allowed(NJID, StateData) orelse - presence_broadcast_allowed(NJID, OldStateData))) of + (not (presence_broadcast_allowed(NJID, StateData) orelse + presence_broadcast_allowed(NJID, OldStateData))) of true -> #{LNJID => UserInfo}; false -> @@ -2699,104 +3158,137 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> end, maps:fold( fun(LUJID, Info, _) -> - IsSelfPresence = LNJID == LUJID, - {Role, Presence} = if IsSelfPresence -> {Role0, Presence0}; - true -> {Role1, Presence1} - end, - Item0 = #muc_item{affiliation = Affiliation, - role = Role}, - Item1 = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false orelse IsSelfPresence of - true -> Item0#muc_item{jid = RealJID}; - false -> Item0 - end, - Item = Item1#muc_item{reason = Reason}, - StatusCodes = status_codes(IsInitialPresence, IsSelfPresence, - StateData), - Pres = if Presence == undefined -> #presence{}; - true -> Presence - end, + IsSelfPresence = LNJID == LUJID, + {Role, Presence} = if + IsSelfPresence -> {Role0, Presence0}; + true -> {Role1, Presence1} + end, + Item0 = #muc_item{ + affiliation = Affiliation, + role = Role + }, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous == + false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item = Item1#muc_item{reason = Reason}, + StatusCodes = status_codes(IsInitialPresence, + IsSelfPresence, + StateData), + Pres = if + Presence == undefined -> #presence{}; + true -> Presence + end, Packet = xmpp:set_subtag( add_presence_hats(NJID, Pres, StateData), - #muc_user{items = [Item], - status_codes = StatusCodes}), - send_wrapped(jid:replace_resource(StateData#state.jid, Nick), - Info#user.jid, Packet, Node1, StateData), - Type = xmpp:get_type(Packet), - IsSubscriber = is_subscriber(Info#user.jid, StateData), - IsOccupant = Info#user.last_presence /= undefined, - if (IsSubscriber and not IsOccupant) and - (IsInitialPresence or (Type == unavailable)) -> - send_wrapped(jid:replace_resource(StateData#state.jid, Nick), - Info#user.jid, Packet, Node2, StateData); - true -> - ok - end - end, ok, UserMap). + #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), + IsSubscriber = is_subscriber(Info#user.jid, StateData), + IsOccupant = Info#user.last_presence /= undefined, + if + (IsSubscriber and not IsOccupant) and + (IsInitialPresence or (Type == unavailable)) -> + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet, + Node2, + StateData); + true -> + ok + end + end, + ok, + UserMap). + -spec send_existing_presences(jid(), state()) -> ok. send_existing_presences(ToJID, StateData) -> case is_room_overcrowded(StateData) of - true -> ok; - false -> send_existing_presences1(ToJID, StateData) + true -> ok; + false -> send_existing_presences1(ToJID, StateData) end. + -spec send_existing_presences1(jid(), state()) -> ok. send_existing_presences1(ToJID, StateData) -> LToJID = jid:tolower(ToJID), #user{jid = RealToJID, role = Role} = maps:get(LToJID, StateData#state.users), maps:fold( fun(FromNick, _Users, _) -> - LJID = find_jid_by_nick(FromNick, StateData), - #user{jid = FromJID, role = FromRole, - last_presence = Presence} = - maps:get(jid:tolower(LJID), StateData#state.users), - PresenceBroadcast = - lists:member( - FromRole, (StateData#state.config)#config.presence_broadcast), - case {RealToJID, PresenceBroadcast} of - {FromJID, _} -> ok; - {_, false} -> ok; - _ -> - FromAffiliation = get_affiliation(LJID, StateData), - Item0 = #muc_item{affiliation = FromAffiliation, - role = FromRole}, - Item = case Role == moderator orelse - (StateData#state.config)#config.anonymous - == false of - true -> Item0#muc_item{jid = FromJID}; - false -> Item0 - end, - Packet = xmpp:set_subtag( + LJID = find_jid_by_nick(FromNick, StateData), + #user{ + jid = FromJID, + role = FromRole, + last_presence = Presence + } = + maps:get(jid:tolower(LJID), StateData#state.users), + PresenceBroadcast = + lists:member( + FromRole, (StateData#state.config)#config.presence_broadcast), + case {RealToJID, PresenceBroadcast} of + {FromJID, _} -> ok; + {_, false} -> ok; + _ -> + FromAffiliation = get_affiliation(LJID, StateData), + Item0 = #muc_item{ + affiliation = FromAffiliation, + role = FromRole + }, + Item = case Role == moderator orelse + (StateData#state.config)#config.anonymous == + false of + true -> Item0#muc_item{jid = FromJID}; + false -> Item0 + end, + Packet = xmpp:set_subtag( 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 - end, ok, StateData#state.nicks). + send_wrapped(jid:replace_resource(StateData#state.jid, FromNick), + RealToJID, + Packet, + ?NS_MUCSUB_NODES_PRESENCE, + StateData) + end + end, + ok, + StateData#state.nicks). + -spec set_nick(jid(), binary(), state()) -> state(). set_nick(JID, Nick, State) -> LJID = jid:tolower(JID), #user{nick = OldNick} = maps:get(LJID, State#state.users), Users = maps:update_with(LJID, - fun (#user{} = User) -> User#user{nick = Nick} end, - State#state.users), + fun(#user{} = User) -> User#user{nick = Nick} end, + State#state.users), OldNickUsers = maps:get(OldNick, State#state.nicks), NewNickUsers = maps:get(Nick, State#state.nicks, []), Nicks = case OldNickUsers of - [LJID] -> - maps:put(Nick, [LJID | NewNickUsers -- [LJID]], - maps:remove(OldNick, State#state.nicks)); - [_ | _] -> - maps:put(Nick, [LJID | NewNickUsers -- [LJID]], - maps:put(OldNick, OldNickUsers -- [LJID], - State#state.nicks)) - end, + [LJID] -> + maps:put(Nick, + [LJID | NewNickUsers -- [LJID]], + maps:remove(OldNick, State#state.nicks)); + [_ | _] -> + maps:put(Nick, + [LJID | NewNickUsers -- [LJID]], + maps:put(OldNick, + OldNickUsers -- [LJID], + State#state.nicks)) + end, State#state{users = Users, nicks = Nicks}. + -spec change_nick(jid(), binary(), state()) -> state(). change_nick(JID, Nick, StateData) -> LJID = jid:tolower(JID), @@ -2808,60 +3300,84 @@ change_nick(JID, Nick, StateData) -> NewStateData = set_nick(JID, Nick, StateData), case presence_broadcast_allowed(JID, NewStateData) of true -> - send_nick_changing(JID, OldNick, NewStateData, - SendOldUnavailable, SendNewAvailable); + send_nick_changing(JID, + OldNick, + NewStateData, + SendOldUnavailable, + SendNewAvailable); false -> ok end, add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. + -spec send_nick_changing(jid(), binary(), state(), boolean(), boolean()) -> ok. -send_nick_changing(JID, OldNick, StateData, - SendOldUnavailable, SendNewAvailable) -> - #user{jid = RealJID, nick = Nick, role = Role, - last_presence = Presence} = - maps:get(jid:tolower(JID), StateData#state.users), +send_nick_changing(JID, + OldNick, + StateData, + SendOldUnavailable, + SendNewAvailable) -> + #user{ + jid = RealJID, + nick = Nick, + role = Role, + last_presence = Presence + } = + maps:get(jid:tolower(JID), StateData#state.users), Affiliation = get_affiliation(JID, StateData), maps:fold( fun(LJID, Info, _) when Presence /= undefined -> - IsSelfPresence = LJID == jid:tolower(JID), - Item0 = #muc_item{affiliation = Affiliation, role = Role}, - Item = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false orelse IsSelfPresence of - true -> Item0#muc_item{jid = RealJID}; - false -> Item0 - end, - Status110 = case IsSelfPresence of - true -> [110]; - false -> [] - end, - Packet1 = #presence{ - type = unavailable, - sub_els = [#muc_user{ - items = [Item#muc_item{nick = Nick}], - status_codes = [303|Status110]}]}, - Packet2 = xmpp:set_subtag(Presence, - #muc_user{items = [Item], - status_codes = Status110}), - if SendOldUnavailable -> - send_wrapped( - jid:replace_resource(StateData#state.jid, OldNick), - Info#user.jid, Packet1, ?NS_MUCSUB_NODES_PRESENCE, - StateData); - true -> ok - end, - if SendNewAvailable -> - send_wrapped( - jid:replace_resource(StateData#state.jid, Nick), - Info#user.jid, Packet2, ?NS_MUCSUB_NODES_PRESENCE, - StateData); - true -> ok - end; - (_, _, _) -> - ok - end, ok, get_users_and_subscribers_with_node( - ?NS_MUCSUB_NODES_PRESENCE, StateData)). + IsSelfPresence = LJID == jid:tolower(JID), + Item0 = #muc_item{affiliation = Affiliation, role = Role}, + Item = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous == + false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Status110 = case IsSelfPresence of + true -> [110]; + false -> [] + end, + Packet1 = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [Item#muc_item{nick = Nick}], + status_codes = [303 | Status110] + }] + }, + Packet2 = xmpp:set_subtag(Presence, + #muc_user{ + items = [Item], + status_codes = Status110 + }), + if + SendOldUnavailable -> + send_wrapped( + jid:replace_resource(StateData#state.jid, OldNick), + Info#user.jid, + Packet1, + ?NS_MUCSUB_NODES_PRESENCE, + StateData); + true -> ok + end, + if + SendNewAvailable -> + send_wrapped( + jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet2, + ?NS_MUCSUB_NODES_PRESENCE, + StateData); + true -> ok + end; + (_, _, _) -> + ok + end, + ok, + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PRESENCE, StateData)). + -spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. maybe_send_affiliation(JID, Affiliation, StateData) -> @@ -2869,70 +3385,83 @@ maybe_send_affiliation(JID, Affiliation, StateData) -> %% TODO: there should be a better way to check IsOccupant Users = get_users_and_subscribers(StateData), IsOccupant = case LJID of - {LUser, LServer, <<"">>} -> - #{} /= maps:filter( - fun({U, S, _}, _) -> - U == LUser andalso - S == LServer - end, Users); - {_LUser, _LServer, _LResource} -> - maps:is_key(LJID, Users) - end, + {LUser, LServer, <<"">>} -> + #{} /= maps:filter( + fun({U, S, _}, _) -> + U == LUser andalso + S == LServer + end, + Users); + {_LUser, _LServer, _LResource} -> + maps:is_key(LJID, Users) + end, case IsOccupant of - true -> - ok; % The new affiliation is published via presence. - false -> - send_affiliation(JID, Affiliation, StateData) + true -> + ok; % The new affiliation is published via presence. + false -> + send_affiliation(JID, Affiliation, StateData) end. + -spec send_affiliation(jid(), affiliation(), state()) -> ok. send_affiliation(JID, Affiliation, StateData) -> - Item = #muc_item{jid = JID, - affiliation = Affiliation, - role = none}, - Message = #message{id = p1_rand:get_string(), - sub_els = [#muc_user{items = [Item]}]}, + Item = #muc_item{ + jid = JID, + affiliation = Affiliation, + role = none + }, + Message = #message{ + id = p1_rand:get_string(), + sub_els = [#muc_user{items = [Item]}] + }, Users = get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), Recipients = case (StateData#state.config)#config.anonymous of - true -> - maps:filter(fun(_, #user{role = moderator}) -> - true; - (_, _) -> - false - end, Users); - false -> - Users - end, - send_wrapped_multiple(StateData#state.jid, Recipients, Message, - ?NS_MUCSUB_NODES_AFFILIATIONS, StateData). + true -> + maps:filter(fun(_, #user{role = moderator}) -> + true; + (_, _) -> + false + end, + Users); + false -> + Users + end, + send_wrapped_multiple(StateData#state.jid, + Recipients, + Message, + ?NS_MUCSUB_NODES_AFFILIATIONS, + StateData). + -spec status_codes(boolean(), boolean(), state()) -> [pos_integer()]. status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) -> S0 = [110], case IsInitialPresence of - true -> - S1 = case StateData#state.just_created of - true -> [201|S0]; - _ -> S0 - end, - S2 = case (StateData#state.config)#config.anonymous of - true -> S1; - false -> [100|S1] - end, - S3 = case (StateData#state.config)#config.logging of - true -> [170|S2]; - false -> S2 - end, - S3; - false -> S0 + true -> + S1 = case StateData#state.just_created of + true -> [201 | S0]; + _ -> S0 + end, + S2 = case (StateData#state.config)#config.anonymous of + true -> S1; + false -> [100 | S1] + end, + S3 = case (StateData#state.config)#config.logging of + true -> [170 | S2]; + false -> S2 + end, + S3; + false -> S0 end; status_codes(_IsInitialPresence, _IsSelfPresence = false, _StateData) -> []. + -spec lqueue_new(non_neg_integer(), ram | file) -> lqueue(). lqueue_new(Max, Type) -> #lqueue{queue = p1_queue:new(Type), max = Max}. + -spec lqueue_in(lqueue_elem(), lqueue()) -> lqueue(). %% If the message queue limit is set to 0, do not store messages. lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ; @@ -2940,97 +3469,116 @@ lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ; lqueue_in(Item, #lqueue{queue = Q1, max = Max}) -> Len = p1_queue:len(Q1), Q2 = p1_queue:in(Item, Q1), - if Len >= Max -> - Q3 = lqueue_cut(Q2, Len - Max + 1), - #lqueue{queue = Q3, max = Max}; - true -> #lqueue{queue = Q2, max = Max} + if + Len >= Max -> + Q3 = lqueue_cut(Q2, Len - Max + 1), + #lqueue{queue = Q3, max = Max}; + true -> #lqueue{queue = Q2, max = Max} end. + -spec lqueue_cut(p1_queue:queue(lqueue_elem()), non_neg_integer()) -> p1_queue:queue(lqueue_elem()). lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> {_, Q1} = p1_queue:out(Q), lqueue_cut(Q1, N - 1). + -spec add_message_to_history(binary(), jid(), message(), state()) -> state(). add_message_to_history(FromNick, FromJID, Packet, StateData) -> add_to_log(text, {FromNick, Packet}, StateData), case check_subject(Packet) of - [] -> - TimeStamp = erlang:timestamp(), - AddrPacket = case (StateData#state.config)#config.anonymous of - true -> Packet; - false -> - Addresses = #addresses{ - list = [#address{type = ofrom, - jid = FromJID}]}, - xmpp:set_subtag(Packet, Addresses) - end, - TSPacket = misc:add_delay_info( - AddrPacket, StateData#state.jid, TimeStamp), - SPacket = xmpp:set_from_to( - TSPacket, - jid:replace_resource(StateData#state.jid, FromNick), - StateData#state.jid), - Size = element_size(SPacket), - Q1 = lqueue_in({FromNick, TSPacket, false, - TimeStamp, Size}, - StateData#state.history), - StateData#state{history = Q1, just_created = erlang:system_time(microsecond)}; - _ -> - StateData#state{just_created = erlang:system_time(microsecond)} + [] -> + TimeStamp = erlang:timestamp(), + AddrPacket = case (StateData#state.config)#config.anonymous of + true -> Packet; + false -> + Addresses = #addresses{ + list = [#address{ + type = ofrom, + jid = FromJID + }] + }, + xmpp:set_subtag(Packet, Addresses) + end, + TSPacket = misc:add_delay_info( + AddrPacket, StateData#state.jid, TimeStamp), + SPacket = xmpp:set_from_to( + TSPacket, + jid:replace_resource(StateData#state.jid, FromNick), + StateData#state.jid), + Size = element_size(SPacket), + Q1 = lqueue_in({FromNick, TSPacket, false, + TimeStamp, Size}, + StateData#state.history), + StateData#state{history = Q1, just_created = erlang:system_time(microsecond)}; + _ -> + StateData#state{just_created = erlang:system_time(microsecond)} end. + remove_from_history(StanzaId, #state{history = #lqueue{queue = Queue} = LQueue} = StateData) -> NewQ = p1_queue:foldl( - fun({_, Pkt, _, _, _} = Entry, Acc) -> - case xmpp:get_meta(Pkt, stanza_id, missing) of - V when V == StanzaId -> - Acc; - _ -> - p1_queue:in(Entry, Acc) - end - end, p1_queue:new(), Queue), + fun({_, Pkt, _, _, _} = Entry, Acc) -> + case xmpp:get_meta(Pkt, stanza_id, missing) of + V when V == StanzaId -> + Acc; + _ -> + p1_queue:in(Entry, Acc) + end + end, + p1_queue:new(), + Queue), StateData#state{history = LQueue#lqueue{queue = NewQ}}. + remove_from_history({U1, S1}, OriginId, #state{history = #lqueue{queue = Queue} = LQueue} = StateData) -> {NewQ, StanzaId} = p1_queue:foldl( - fun({_, Pkt, _, _, _} = Entry, {Q, none}) -> - case jid:tolower(xmpp:get_from(Pkt)) of - {U2, S2, _} when U1 == U2, S1 == S2 -> - case xmpp:get_subtag(Pkt, #origin_id{}) of - #origin_id{id = V} when V == OriginId -> - {Q, xmpp:get_meta(Pkt, stanza_id, missing)}; - _ -> - {p1_queue:in(Entry, Q), none} - end; - _ -> - {p1_queue:in(Entry, Q), none} - end; - (Entry, {Q, S}) -> - {p1_queue:in(Entry, Q), S} - end, {p1_queue:new(), none}, Queue), + fun({_, Pkt, _, _, _} = Entry, {Q, none}) -> + case jid:tolower(xmpp:get_from(Pkt)) of + {U2, S2, _} when U1 == U2, S1 == S2 -> + case xmpp:get_subtag(Pkt, #origin_id{}) of + #origin_id{id = V} when V == OriginId -> + {Q, xmpp:get_meta(Pkt, stanza_id, missing)}; + _ -> + {p1_queue:in(Entry, Q), none} + end; + _ -> + {p1_queue:in(Entry, Q), none} + end; + (Entry, {Q, S}) -> + {p1_queue:in(Entry, Q), S} + end, + {p1_queue:new(), none}, + Queue), {StateData#state{history = LQueue#lqueue{queue = NewQ}}, StanzaId}. + -spec send_history(jid(), [lqueue_elem()], state()) -> ok. send_history(JID, History, StateData) -> lists:foreach( fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) -> - ejabberd_router:route( - xmpp:set_from_to( - Packet, - jid:replace_resource(StateData#state.jid, Nick), - JID)) - end, History). + ejabberd_router:route( + xmpp:set_from_to( + Packet, + jid:replace_resource(StateData#state.jid, Nick), + JID)) + end, + History). + -spec send_subject(jid(), state()) -> ok. send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) -> Subject = case StateData#state.subject of - [] -> [#text{}]; - [_|_] = S -> S - end, - Packet = #message{from = AuthorJID, - to = JID, type = groupchat, subject = Subject}, + [] -> [#text{}]; + [_ | _] = S -> S + end, + Packet = #message{ + from = AuthorJID, + to = JID, + type = groupchat, + subject = Subject + }, case ejabberd_hooks:run_fold(muc_filter_message, StateData#state.server_host, xmpp:put_meta(Packet, mam_ignore, true), @@ -3043,105 +3591,136 @@ send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) -> ejabberd_router:route(NewPacket2) end. + -spec check_subject(message()) -> [text()]. -check_subject(#message{subject = [_|_] = Subj, body = [], - thread = undefined}) -> +check_subject(#message{ + subject = [_ | _] = Subj, + body = [], + thread = undefined + }) -> Subj; check_subject(_) -> []. + -spec can_change_subject(role(), boolean(), state()) -> boolean(). can_change_subject(Role, IsSubscriber, StateData) -> - case (StateData#state.config)#config.allow_change_subj - of - true -> Role == moderator orelse Role == participant orelse IsSubscriber == true; - _ -> Role == moderator + case (StateData#state.config)#config.allow_change_subj of + true -> Role == moderator orelse Role == participant orelse IsSubscriber == true; + _ -> Role == moderator end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Admin stuff + -spec process_iq_admin(jid(), iq(), #state{}) -> {error, stanza_error()} | - {result, undefined, #state{}} | - {result, muc_admin()}. -process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, - _StateData) -> + {result, undefined, #state{}} | + {result, muc_admin()}. +process_iq_admin(_From, + #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, + _StateData) -> Txt = ?T("No 'item' element found"), {error, xmpp:err_bad_request(Txt, Lang)}; -process_iq_admin(_From, #iq{type = get, lang = Lang, - sub_els = [#muc_admin{items = [_, _|_]}]}, - _StateData) -> +process_iq_admin(_From, + #iq{ + type = get, + lang = Lang, + sub_els = [#muc_admin{items = [_, _ | _]}] + }, + _StateData) -> ErrText = ?T("Too many elements"), {error, xmpp:err_bad_request(ErrText, Lang)}; -process_iq_admin(From, #iq{type = set, lang = Lang, - sub_els = [#muc_admin{items = Items}]}, - StateData) -> +process_iq_admin(From, + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_admin{items = Items}] + }, + StateData) -> process_admin_items_set(From, Items, Lang, StateData); -process_iq_admin(From, #iq{type = get, lang = Lang, - sub_els = [#muc_admin{items = [Item]}]}, - StateData) -> +process_iq_admin(From, + #iq{ + type = get, + lang = Lang, + sub_els = [#muc_admin{items = [Item]}] + }, + StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), case Item of - #muc_item{role = undefined, affiliation = undefined} -> - Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), - {error, xmpp:err_bad_request(Txt, Lang)}; - #muc_item{role = undefined, affiliation = Affiliation} -> - if (FAffiliation == owner) or - (FAffiliation == admin) or - ((FAffiliation == member) and - not (StateData#state.config)#config.anonymous) -> - Items = items_with_affiliation(Affiliation, StateData), - {result, #muc_admin{items = Items}}; - true -> - ErrText = ?T("Administrator privileges required"), - {error, xmpp:err_forbidden(ErrText, Lang)} - end; - #muc_item{role = Role} -> - if FRole == moderator -> - Items = items_with_role(Role, StateData), - {result, #muc_admin{items = Items}}; - true -> - ErrText = ?T("Moderator privileges required"), - {error, xmpp:err_forbidden(ErrText, Lang)} - end + #muc_item{role = undefined, affiliation = undefined} -> + Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), + {error, xmpp:err_bad_request(Txt, Lang)}; + #muc_item{role = undefined, affiliation = Affiliation} -> + if + (FAffiliation == owner) or + (FAffiliation == admin) or + ((FAffiliation == member) and + not (StateData#state.config)#config.anonymous) -> + Items = items_with_affiliation(Affiliation, StateData), + {result, #muc_admin{items = Items}}; + true -> + ErrText = ?T("Administrator privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} + end; + #muc_item{role = Role} -> + if + FRole == moderator -> + Items = items_with_role(Role, StateData), + {result, #muc_admin{items = Items}}; + true -> + ErrText = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} + end end. + -spec items_with_role(role(), state()) -> [muc_item()]. items_with_role(SRole, StateData) -> - lists:map(fun ({_, U}) -> user_to_item(U, StateData) - end, - search_role(SRole, StateData)). + lists:map(fun({_, U}) -> user_to_item(U, StateData) + end, + search_role(SRole, StateData)). + -spec items_with_affiliation(affiliation(), state()) -> [muc_item()]. items_with_affiliation(SAffiliation, StateData) -> lists:map( fun({JID, {Affiliation, Reason}}) -> - #muc_item{affiliation = Affiliation, jid = jid:make(JID), - reason = Reason}; - ({JID, Affiliation}) -> - #muc_item{affiliation = Affiliation, jid = jid:make(JID)} + #muc_item{ + affiliation = Affiliation, + jid = jid:make(JID), + reason = Reason + }; + ({JID, Affiliation}) -> + #muc_item{affiliation = Affiliation, jid = jid:make(JID)} end, search_affiliation(SAffiliation, StateData)). + -spec user_to_item(#user{}, state()) -> muc_item(). user_to_item(#user{role = Role, nick = Nick, jid = JID}, - StateData) -> + StateData) -> Affiliation = get_affiliation(JID, StateData), - #muc_item{role = Role, - affiliation = Affiliation, - nick = Nick, - jid = JID}. + #muc_item{ + role = Role, + affiliation = Affiliation, + nick = Nick, + jid = JID + }. + -spec search_role(role(), state()) -> [{ljid(), #user{}}]. search_role(Role, StateData) -> - lists:filter(fun ({_, #user{role = R}}) -> Role == R - end, - maps:to_list(StateData#state.users)). + lists:filter(fun({_, #user{role = R}}) -> Role == R + end, + maps:to_list(StateData#state.users)). + -spec search_affiliation(affiliation(), state()) -> - [{ljid(), - affiliation() | {affiliation(), binary()}}]. + [{ljid(), + affiliation() | {affiliation(), binary()}}]. search_affiliation(Affiliation, #state{config = #config{persistent = false}} = StateData) -> search_affiliation_fallback(Affiliation, StateData); @@ -3151,69 +3730,83 @@ search_affiliation(Affiliation, StateData) -> ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:search_affiliation(ServerHost, Room, Host, Affiliation) of - {ok, AffiliationList} -> - AffiliationList; - {error, _} -> - search_affiliation_fallback(Affiliation, StateData) + {ok, AffiliationList} -> + AffiliationList; + {error, _} -> + search_affiliation_fallback(Affiliation, StateData) end. + -spec search_affiliation_fallback(affiliation(), state()) -> - [{ljid(), - affiliation() | {affiliation(), binary()}}]. + [{ljid(), + affiliation() | {affiliation(), binary()}}]. search_affiliation_fallback(Affiliation, StateData) -> lists:filter( fun({_, A}) -> - case A of - {A1, _Reason} -> Affiliation == A1; - _ -> Affiliation == A - end - end, maps:to_list(StateData#state.affiliations)). + case A of + {A1, _Reason} -> Affiliation == A1; + _ -> Affiliation == A + end + end, + maps:to_list(StateData#state.affiliations)). --spec process_admin_items_set(jid(), [muc_item()], binary(), - #state{}) -> {result, undefined, #state{}} | - {error, stanza_error()}. + +-spec process_admin_items_set(jid(), + [muc_item()], + binary(), + #state{}) -> {result, undefined, #state{}} | + {error, stanza_error()}. process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), - case catch find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, []) - of - {result, Res} -> - ?INFO_MSG("Processing MUC admin query from ~ts in " - "room ~ts:~n ~p", - [jid:encode(UJID), - jid:encode(StateData#state.jid), Res]), - case lists:foldl(process_item_change(UJID), - StateData, lists:flatten(Res)) of - {error, _} = Err -> - Err; - NSD -> - store_room(NSD), - {result, undefined, NSD} - end; - {error, Err} -> {error, Err} + case catch find_changed_items(UJID, + UAffiliation, + URole, + Items, + Lang, + StateData, + []) of + {result, Res} -> + ?INFO_MSG("Processing MUC admin query from ~ts in " + "room ~ts:~n ~p", + [jid:encode(UJID), + jid:encode(StateData#state.jid), + Res]), + case lists:foldl(process_item_change(UJID), + StateData, + lists:flatten(Res)) of + {error, _} = Err -> + Err; + NSD -> + store_room(NSD), + {result, undefined, NSD} + end; + {error, Err} -> {error, Err} end. + -spec process_item_change(jid()) -> fun((admin_action(), state() | {error, stanza_error()}) -> - state() | {error, stanza_error()}). + state() | {error, stanza_error()}). process_item_change(UJID) -> fun(_, {error, _} = Err) -> - Err; + Err; (Item, SD) -> - process_item_change(Item, SD, UJID) + process_item_change(Item, SD, UJID) end. + -spec process_item_change(admin_action(), state(), undefined | jid()) -> state() | {error, stanza_error()}. process_item_change(Item, SD, UJID) -> - try case Item of - {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> - %% If the provided JID does not have username, - %% forget the affiliation completely - SD; - {JID, role, none, Reason} -> - send_kickban_presence(UJID, JID, Reason, 307, SD), - set_role(JID, none, SD); - {JID, affiliation, none, Reason} -> + try + case Item of + {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> + %% If the provided JID does not have username, + %% forget the affiliation completely + SD; + {JID, role, none, Reason} -> + send_kickban_presence(UJID, JID, Reason, 307, SD), + set_role(JID, none, SD); + {JID, affiliation, none, Reason} -> case get_affiliation(JID, SD) of none -> SD; _ -> @@ -3235,52 +3828,58 @@ process_item_change(Item, SD, UJID) -> SD2 end end; - {JID, affiliation, outcast, Reason} -> - send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), - maybe_send_affiliation(JID, outcast, SD), - unsubscribe_from_room(JID, SD), + {JID, affiliation, outcast, Reason} -> + send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), + maybe_send_affiliation(JID, outcast, SD), + unsubscribe_from_room(JID, SD), {result, undefined, SD2} = process_iq_mucsub(JID, - #iq{type = set, - sub_els = [#muc_unsubscribe{}]}, SD), - set_role(JID, none, set_affiliation(JID, outcast, SD2, Reason)); - {JID, affiliation, A, Reason} when (A == admin) or (A == owner) -> - SD1 = set_affiliation(JID, A, SD, Reason), - SD2 = set_role(JID, moderator, SD1), - send_update_presence(JID, Reason, SD2, SD), - maybe_send_affiliation(JID, A, SD2), - SD2; - {JID, affiliation, member, Reason} -> - SD1 = set_affiliation(JID, member, SD, Reason), - SD2 = set_role(JID, participant, SD1), - send_update_presence(JID, Reason, SD2, SD), - maybe_send_affiliation(JID, member, SD2), - SD2; - {JID, role, Role, Reason} -> - SD1 = set_role(JID, Role, SD), - send_new_presence(JID, Reason, SD1, SD), - SD1; - {JID, affiliation, A, _Reason} -> - SD1 = set_affiliation(JID, A, SD), - send_update_presence(JID, SD1, SD), - maybe_send_affiliation(JID, A, SD1), - SD1 - end - catch ?EX_RULE(E, R, St) -> - StackTrace = ?EX_STACK(St), - FromSuffix = case UJID of - #jid{} -> - JidString = jid:encode(UJID), - <<" from ", JidString/binary>>; - undefined -> - <<"">> - end, - ?ERROR_MSG("Failed to set item ~p~ts:~n** ~ts", - [Item, FromSuffix, - misc:format_exception(2, E, R, StackTrace)]), - {error, xmpp:err_internal_server_error()} + #iq{ + type = set, + sub_els = [#muc_unsubscribe{}] + }, + SD), + set_role(JID, none, set_affiliation(JID, outcast, SD2, Reason)); + {JID, affiliation, A, Reason} when (A == admin) or (A == owner) -> + SD1 = set_affiliation(JID, A, SD, Reason), + SD2 = set_role(JID, moderator, SD1), + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, A, SD2), + SD2; + {JID, affiliation, member, Reason} -> + SD1 = set_affiliation(JID, member, SD, Reason), + SD2 = set_role(JID, participant, SD1), + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, member, SD2), + SD2; + {JID, role, Role, Reason} -> + SD1 = set_role(JID, Role, SD), + send_new_presence(JID, Reason, SD1, SD), + SD1; + {JID, affiliation, A, _Reason} -> + SD1 = set_affiliation(JID, A, SD), + send_update_presence(JID, SD1, SD), + maybe_send_affiliation(JID, A, SD1), + SD1 + end + catch + ?EX_RULE(E, R, St) -> + StackTrace = ?EX_STACK(St), + FromSuffix = case UJID of + #jid{} -> + JidString = jid:encode(UJID), + <<" from ", JidString/binary>>; + undefined -> + <<"">> + end, + ?ERROR_MSG("Failed to set item ~p~ts:~n** ~ts", + [Item, + FromSuffix, + misc:format_exception(2, E, R, StackTrace)]), + {error, xmpp:err_internal_server_error()} end. + -spec unsubscribe_from_room(jid(), state()) -> ok | error. unsubscribe_from_room(JID, SD) -> case SD#state.config#config.members_only of @@ -3293,309 +3892,603 @@ unsubscribe_from_room(JID, SD) -> {ok, Pid} -> _UnsubPid = spawn(fun() -> - case unsubscribe(Pid, JID) of - ok -> - ok; - {error, Reason} -> - ?WARNING_MSG("Failed to automatically unsubscribe expelled member from room: ~ts", - [Reason]), - error - end + case unsubscribe(Pid, JID) of + ok -> + ok; + {error, Reason} -> + ?WARNING_MSG("Failed to automatically unsubscribe expelled member from room: ~ts", + [Reason]), + error + end end) end end. --spec find_changed_items(jid(), affiliation(), role(), - [muc_item()], binary(), state(), [admin_action()]) -> - {result, [admin_action()]}. -find_changed_items(_UJID, _UAffiliation, _URole, [], - _Lang, _StateData, Res) -> + +-spec find_changed_items(jid(), + affiliation(), + role(), + [muc_item()], + binary(), + state(), + [admin_action()]) -> + {result, [admin_action()]}. +find_changed_items(_UJID, + _UAffiliation, + _URole, + [], + _Lang, + _StateData, + Res) -> {result, Res}; -find_changed_items(_UJID, _UAffiliation, _URole, - [#muc_item{jid = undefined, nick = <<"">>}|_], - Lang, _StateData, _Res) -> +find_changed_items(_UJID, + _UAffiliation, + _URole, + [#muc_item{jid = undefined, nick = <<"">>} | _], + Lang, + _StateData, + _Res) -> Txt = ?T("Neither 'jid' nor 'nick' attribute found"), throw({error, xmpp:err_bad_request(Txt, Lang)}); -find_changed_items(_UJID, _UAffiliation, _URole, - [#muc_item{role = undefined, affiliation = undefined}|_], - Lang, _StateData, _Res) -> +find_changed_items(_UJID, + _UAffiliation, + _URole, + [#muc_item{role = undefined, affiliation = undefined} | _], + Lang, + _StateData, + _Res) -> Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), throw({error, xmpp:err_bad_request(Txt, Lang)}); -find_changed_items(UJID, UAffiliation, URole, - [#muc_item{jid = J, nick = Nick, reason = Reason, - role = Role, affiliation = Affiliation}|Items], - Lang, StateData, Res) -> +find_changed_items(UJID, + UAffiliation, + URole, + [#muc_item{ + jid = J, + nick = Nick, + reason = Reason, + role = Role, + affiliation = Affiliation + } | Items], + Lang, + StateData, + Res) -> [JID | _] = JIDs = - if J /= undefined -> - [J]; - Nick /= <<"">> -> - case find_jids_by_nick(Nick, StateData) of - [] -> - ErrText = {?T("Nickname ~s does not exist in the room"), - [Nick]}, - throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); - JIDList -> - JIDList - end - end, - {RoleOrAff, RoleOrAffValue} = if Role == undefined -> - {affiliation, Affiliation}; - true -> - {role, Role} - end, + if + J /= undefined -> + [J]; + Nick /= <<"">> -> + case find_jids_by_nick(Nick, StateData) of + [] -> + ErrText = {?T("Nickname ~s does not exist in the room"), + [Nick]}, + throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); + JIDList -> + JIDList + end + end, + {RoleOrAff, RoleOrAffValue} = if + Role == undefined -> + {affiliation, Affiliation}; + true -> + {role, Role} + end, TAffiliation = get_affiliation(JID, StateData), TRole = get_role(JID, StateData), ServiceAf = get_service_affiliation(JID, StateData), UIsSubscriber = is_subscriber(UJID, StateData), URole1 = case {URole, UIsSubscriber} of - {none, true} -> subscriber; - {UR, _} -> UR - end, + {none, true} -> subscriber; + {UR, _} -> UR + end, CanChangeRA = case can_change_ra(UAffiliation, - URole1, - TAffiliation, - TRole, RoleOrAff, RoleOrAffValue, - ServiceAf) of - nothing -> nothing; - true -> true; - check_owner -> - case search_affiliation(owner, StateData) of - [{OJID, _}] -> - jid:remove_resource(OJID) - /= - jid:tolower(jid:remove_resource(UJID)); - _ -> true - end; - _ -> false - end, + URole1, + TAffiliation, + TRole, + RoleOrAff, + RoleOrAffValue, + ServiceAf) of + nothing -> nothing; + true -> true; + check_owner -> + case search_affiliation(owner, StateData) of + [{OJID, _}] -> + jid:remove_resource(OJID) /= + jid:tolower(jid:remove_resource(UJID)); + _ -> true + end; + _ -> false + end, case CanChangeRA of - nothing -> - find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, - Res); - true -> - MoreRes = case RoleOrAff of - affiliation -> - [{jid:remove_resource(Jidx), - RoleOrAff, RoleOrAffValue, Reason} - || Jidx <- JIDs]; - role -> - [{Jidx, RoleOrAff, RoleOrAffValue, Reason} - || Jidx <- JIDs] - end, - find_changed_items(UJID, UAffiliation, URole, - Items, Lang, StateData, - MoreRes ++ Res); - false -> - Txt = ?T("Changing role/affiliation is not allowed"), - throw({error, xmpp:err_not_allowed(Txt, Lang)}) + nothing -> + find_changed_items(UJID, + UAffiliation, + URole, + Items, + Lang, + StateData, + Res); + true -> + MoreRes = case RoleOrAff of + affiliation -> + [ {jid:remove_resource(Jidx), + RoleOrAff, + RoleOrAffValue, + Reason} + || Jidx <- JIDs ]; + role -> + [ {Jidx, RoleOrAff, RoleOrAffValue, Reason} + || Jidx <- JIDs ] + end, + find_changed_items(UJID, + UAffiliation, + URole, + Items, + Lang, + StateData, + MoreRes ++ Res); + false -> + Txt = ?T("Changing role/affiliation is not allowed"), + throw({error, xmpp:err_not_allowed(Txt, Lang)}) end. --spec can_change_ra(affiliation(), role(), affiliation(), role(), - affiliation, affiliation(), affiliation()) -> boolean() | nothing | check_owner; - (affiliation(), role(), affiliation(), role(), - role, role(), affiliation()) -> boolean() | nothing | check_owner. -can_change_ra(_FAffiliation, _FRole, owner, _TRole, - affiliation, owner, owner) -> + +-spec can_change_ra(affiliation(), + role(), + affiliation(), + role(), + affiliation, + affiliation(), + affiliation()) -> boolean() | nothing | check_owner; + (affiliation(), + role(), + affiliation(), + role(), + role, + role(), + affiliation()) -> boolean() | nothing | check_owner. +can_change_ra(_FAffiliation, + _FRole, + owner, + _TRole, + affiliation, + owner, + owner) -> %% A room owner tries to add as persistent owner a %% participant that is already owner because he is MUC admin true; -can_change_ra(_FAffiliation, _FRole, _TAffiliation, - _TRole, _RoleorAffiliation, _Value, owner) -> +can_change_ra(_FAffiliation, + _FRole, + _TAffiliation, + _TRole, + _RoleorAffiliation, + _Value, + owner) -> %% Nobody can decrease MUC admin's role/affiliation false; -can_change_ra(_FAffiliation, _FRole, TAffiliation, - _TRole, affiliation, Value, _ServiceAf) - when TAffiliation == Value -> +can_change_ra(_FAffiliation, + _FRole, + TAffiliation, + _TRole, + affiliation, + Value, + _ServiceAf) + when TAffiliation == Value -> nothing; -can_change_ra(_FAffiliation, _FRole, _TAffiliation, - TRole, role, Value, _ServiceAf) - when TRole == Value -> +can_change_ra(_FAffiliation, + _FRole, + _TAffiliation, + TRole, + role, + Value, + _ServiceAf) + when TRole == Value -> nothing; -can_change_ra(FAffiliation, _FRole, outcast, _TRole, - affiliation, none, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + outcast, + _TRole, + affiliation, + none, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, outcast, _TRole, - affiliation, member, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + outcast, + _TRole, + affiliation, + member, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, outcast, _TRole, - affiliation, admin, _ServiceAf) -> +can_change_ra(owner, + _FRole, + outcast, + _TRole, + affiliation, + admin, + _ServiceAf) -> true; -can_change_ra(owner, _FRole, outcast, _TRole, - affiliation, owner, _ServiceAf) -> +can_change_ra(owner, + _FRole, + outcast, + _TRole, + affiliation, + owner, + _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, none, _TRole, - affiliation, outcast, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + none, + _TRole, + affiliation, + outcast, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, none, _TRole, - affiliation, member, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + none, + _TRole, + affiliation, + member, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, none, _TRole, affiliation, - admin, _ServiceAf) -> +can_change_ra(owner, + _FRole, + none, + _TRole, + affiliation, + admin, + _ServiceAf) -> true; -can_change_ra(owner, _FRole, none, _TRole, affiliation, - owner, _ServiceAf) -> +can_change_ra(owner, + _FRole, + none, + _TRole, + affiliation, + owner, + _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, member, _TRole, - affiliation, outcast, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + member, + _TRole, + affiliation, + outcast, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, member, _TRole, - affiliation, none, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + member, + _TRole, + affiliation, + none, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, member, _TRole, - affiliation, admin, _ServiceAf) -> +can_change_ra(owner, + _FRole, + member, + _TRole, + affiliation, + admin, + _ServiceAf) -> true; -can_change_ra(owner, _FRole, member, _TRole, - affiliation, owner, _ServiceAf) -> +can_change_ra(owner, + _FRole, + member, + _TRole, + affiliation, + owner, + _ServiceAf) -> true; -can_change_ra(owner, _FRole, admin, _TRole, affiliation, - _Affiliation, _ServiceAf) -> +can_change_ra(owner, + _FRole, + admin, + _TRole, + affiliation, + _Affiliation, + _ServiceAf) -> true; -can_change_ra(owner, _FRole, owner, _TRole, affiliation, - _Affiliation, _ServiceAf) -> +can_change_ra(owner, + _FRole, + owner, + _TRole, + affiliation, + _Affiliation, + _ServiceAf) -> check_owner; -can_change_ra(_FAffiliation, _FRole, _TAffiliation, - _TRole, affiliation, _Value, _ServiceAf) -> +can_change_ra(_FAffiliation, + _FRole, + _TAffiliation, + _TRole, + affiliation, + _Value, + _ServiceAf) -> false; -can_change_ra(_FAffiliation, moderator, _TAffiliation, - visitor, role, none, _ServiceAf) -> +can_change_ra(_FAffiliation, + moderator, + _TAffiliation, + visitor, + role, + none, + _ServiceAf) -> true; -can_change_ra(FAffiliation, subscriber, _TAffiliation, - visitor, role, none, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + subscriber, + _TAffiliation, + visitor, + role, + none, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, moderator, _TAffiliation, - visitor, role, participant, _ServiceAf) -> +can_change_ra(_FAffiliation, + moderator, + _TAffiliation, + visitor, + role, + participant, + _ServiceAf) -> true; -can_change_ra(FAffiliation, subscriber, _TAffiliation, - visitor, role, participant, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + subscriber, + _TAffiliation, + visitor, + role, + participant, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, _TAffiliation, - visitor, role, moderator, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + _TAffiliation, + visitor, + role, + moderator, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, moderator, _TAffiliation, - participant, role, none, _ServiceAf) -> +can_change_ra(_FAffiliation, + moderator, + _TAffiliation, + participant, + role, + none, + _ServiceAf) -> true; -can_change_ra(FAffiliation, subscriber, _TAffiliation, - participant, role, none, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + subscriber, + _TAffiliation, + participant, + role, + none, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, moderator, _TAffiliation, - participant, role, visitor, _ServiceAf) -> +can_change_ra(_FAffiliation, + moderator, + _TAffiliation, + participant, + role, + visitor, + _ServiceAf) -> true; -can_change_ra(FAffiliation, subscriber, _TAffiliation, - participant, role, visitor, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + subscriber, + _TAffiliation, + participant, + role, + visitor, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, _TAffiliation, - participant, role, moderator, _ServiceAf) - when (FAffiliation == owner) or - (FAffiliation == admin) -> +can_change_ra(FAffiliation, + _FRole, + _TAffiliation, + participant, + role, + moderator, + _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, _FRole, owner, moderator, - role, visitor, _ServiceAf) -> +can_change_ra(_FAffiliation, + _FRole, + owner, + moderator, + role, + visitor, + _ServiceAf) -> false; -can_change_ra(owner, _FRole, _TAffiliation, moderator, - role, visitor, _ServiceAf) -> +can_change_ra(owner, + _FRole, + _TAffiliation, + moderator, + role, + visitor, + _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, admin, moderator, - role, visitor, _ServiceAf) -> +can_change_ra(_FAffiliation, + _FRole, + admin, + moderator, + role, + visitor, + _ServiceAf) -> false; -can_change_ra(admin, _FRole, _TAffiliation, moderator, - role, visitor, _ServiceAf) -> +can_change_ra(admin, + _FRole, + _TAffiliation, + moderator, + role, + visitor, + _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, owner, moderator, - role, participant, _ServiceAf) -> +can_change_ra(_FAffiliation, + _FRole, + owner, + moderator, + role, + participant, + _ServiceAf) -> false; -can_change_ra(owner, _FRole, _TAffiliation, moderator, - role, participant, _ServiceAf) -> +can_change_ra(owner, + _FRole, + _TAffiliation, + moderator, + role, + participant, + _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, admin, moderator, - role, participant, _ServiceAf) -> +can_change_ra(_FAffiliation, + _FRole, + admin, + moderator, + role, + participant, + _ServiceAf) -> false; -can_change_ra(admin, _FRole, _TAffiliation, moderator, - role, participant, _ServiceAf) -> +can_change_ra(admin, + _FRole, + _TAffiliation, + moderator, + role, + participant, + _ServiceAf) -> true; -can_change_ra(owner, moderator, TAffiliation, - moderator, role, none, _ServiceAf) - when TAffiliation /= owner -> +can_change_ra(owner, + moderator, + TAffiliation, + moderator, + role, + none, + _ServiceAf) + when TAffiliation /= owner -> true; -can_change_ra(owner, subscriber, TAffiliation, - moderator, role, none, _ServiceAf) - when TAffiliation /= owner -> +can_change_ra(owner, + subscriber, + TAffiliation, + moderator, + role, + none, + _ServiceAf) + when TAffiliation /= owner -> true; -can_change_ra(admin, moderator, TAffiliation, - moderator, role, none, _ServiceAf) - when (TAffiliation /= owner) and - (TAffiliation /= admin) -> +can_change_ra(admin, + moderator, + TAffiliation, + moderator, + role, + none, + _ServiceAf) + when (TAffiliation /= owner) and + (TAffiliation /= admin) -> true; -can_change_ra(admin, subscriber, TAffiliation, - moderator, role, none, _ServiceAf) - when (TAffiliation /= owner) and - (TAffiliation /= admin) -> +can_change_ra(admin, + subscriber, + TAffiliation, + moderator, + role, + none, + _ServiceAf) + when (TAffiliation /= owner) and + (TAffiliation /= admin) -> true; -can_change_ra(_FAffiliation, _FRole, _TAffiliation, - _TRole, role, _Value, _ServiceAf) -> +can_change_ra(_FAffiliation, + _FRole, + _TAffiliation, + _TRole, + role, + _Value, + _ServiceAf) -> false. --spec send_kickban_presence(undefined | jid(), jid(), binary(), - pos_integer(), state()) -> ok. + +-spec send_kickban_presence(undefined | jid(), + jid(), + binary(), + pos_integer(), + state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, StateData) -> NewAffiliation = get_affiliation(JID, StateData), - send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, - StateData). + send_kickban_presence(UJID, + JID, + Reason, + Code, + NewAffiliation, + StateData). --spec send_kickban_presence(undefined | jid(), jid(), binary(), pos_integer(), - affiliation(), state()) -> ok. -send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, - StateData) -> + +-spec send_kickban_presence(undefined | jid(), + jid(), + binary(), + pos_integer(), + affiliation(), + state()) -> ok. +send_kickban_presence(UJID, + JID, + Reason, + Code, + NewAffiliation, + StateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of - {U, S, <<"">>} -> - maps:fold(fun (J, _, Js) -> - case J of - {U, S, _} -> [J | Js]; - _ -> Js - end - end, [], StateData#state.users); - _ -> - case maps:is_key(LJID, StateData#state.users) of - true -> [LJID]; - _ -> [] - end - end, - lists:foreach(fun (LJ) -> - #user{nick = Nick, jid = J} = maps:get(LJ, StateData#state.users), - add_to_log(kickban, {Nick, Reason, Code}, StateData), - tab_remove_online_user(J, StateData), - send_kickban_presence1(UJID, J, Reason, Code, - NewAffiliation, StateData) - end, - LJIDs). + {U, S, <<"">>} -> + maps:fold(fun(J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], + StateData#state.users); + _ -> + case maps:is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end + end, + lists:foreach(fun(LJ) -> + #user{nick = Nick, jid = J} = maps:get(LJ, StateData#state.users), + add_to_log(kickban, {Nick, Reason, Code}, StateData), + tab_remove_online_user(J, StateData), + send_kickban_presence1(UJID, + J, + Reason, + Code, + NewAffiliation, + StateData) + end, + LJIDs). --spec send_kickban_presence1(undefined | jid(), jid(), binary(), pos_integer(), - affiliation(), state()) -> ok. -send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, - StateData) -> + +-spec send_kickban_presence1(undefined | jid(), + jid(), + binary(), + pos_integer(), + affiliation(), + state()) -> ok. +send_kickban_presence1(MJID, + UJID, + Reason, + Code, + Affiliation, + StateData) -> #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users), ActorNick = find_nick_by_jid(MJID, StateData), %% TODO: optimize further @@ -3607,181 +4500,221 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), maps:fold( fun(LJID, Info, _) -> - IsSelfPresence = jid:tolower(UJID) == LJID, - Item0 = #muc_item{affiliation = Affiliation, - role = none}, - Item1 = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false orelse IsSelfPresence of - true -> Item0#muc_item{jid = RealJID}; - false -> Item0 - end, - Item2 = Item1#muc_item{reason = Reason}, - Item = case ActorNick of - <<"">> -> Item2; - _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}} - end, - Codes = if IsSelfPresence -> [110, Code]; - true -> [Code] - end, - Packet = #presence{type = unavailable, - sub_els = [#muc_user{items = [Item], - status_codes = Codes}]}, - RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), - send_wrapped(RoomJIDNick, Info#user.jid, Packet, - ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), - IsSubscriber = is_subscriber(Info#user.jid, StateData), - IsOccupant = Info#user.last_presence /= undefined, - if (IsSubscriber and not IsOccupant) -> - send_wrapped(RoomJIDNick, Info#user.jid, Packet, - ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); - true -> - ok - end - end, ok, UserMap). + IsSelfPresence = jid:tolower(UJID) == LJID, + Item0 = #muc_item{ + affiliation = Affiliation, + role = none + }, + Item1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous == + false orelse IsSelfPresence of + true -> Item0#muc_item{jid = RealJID}; + false -> Item0 + end, + Item2 = Item1#muc_item{reason = Reason}, + Item = case ActorNick of + <<"">> -> Item2; + _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}} + end, + Codes = if + IsSelfPresence -> [110, Code]; + true -> [Code] + end, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [Item], + status_codes = Codes + }] + }, + RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), + send_wrapped(RoomJIDNick, + Info#user.jid, + Packet, + ?NS_MUCSUB_NODES_AFFILIATIONS, + StateData), + IsSubscriber = is_subscriber(Info#user.jid, StateData), + IsOccupant = Info#user.last_presence /= undefined, + if + (IsSubscriber and not IsOccupant) -> + send_wrapped(RoomJIDNick, + Info#user.jid, + Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, + StateData); + true -> + ok + end + end, + ok, + UserMap). + -spec convert_legacy_fields([xdata_field()]) -> [xdata_field()]. convert_legacy_fields(Fs) -> lists:map( fun(#xdata_field{var = Var} = F) -> - NewVar = case Var of - <<"muc#roomconfig_allowvisitorstatus">> -> - <<"allow_visitor_status">>; - <<"muc#roomconfig_allowvisitornickchange">> -> - <<"allow_visitor_nickchange">>; - <<"muc#roomconfig_allowvoicerequests">> -> - <<"allow_voice_requests">>; - <<"muc#roomconfig_allow_subscription">> -> - <<"allow_subscription">>; - <<"muc#roomconfig_voicerequestmininterval">> -> - <<"voice_request_min_interval">>; - <<"muc#roomconfig_captcha_whitelist">> -> - <<"captcha_whitelist">>; - <<"muc#roomconfig_mam">> -> - <<"mam">>; - _ -> - Var - end, - F#xdata_field{var = NewVar} - end, Fs). + NewVar = case Var of + <<"muc#roomconfig_allowvisitorstatus">> -> + <<"allow_visitor_status">>; + <<"muc#roomconfig_allowvisitornickchange">> -> + <<"allow_visitor_nickchange">>; + <<"muc#roomconfig_allowvoicerequests">> -> + <<"allow_voice_requests">>; + <<"muc#roomconfig_allow_subscription">> -> + <<"allow_subscription">>; + <<"muc#roomconfig_voicerequestmininterval">> -> + <<"voice_request_min_interval">>; + <<"muc#roomconfig_captcha_whitelist">> -> + <<"captcha_whitelist">>; + <<"muc#roomconfig_mam">> -> + <<"mam">>; + _ -> + Var + end, + F#xdata_field{var = NewVar} + end, + Fs). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff -spec process_iq_owner(jid(), iq(), state()) -> - {result, undefined | muc_owner()} | - {result, undefined | muc_owner(), state() | stop} | - {error, stanza_error()}. -process_iq_owner(From, #iq{type = set, lang = Lang, - sub_els = [#muc_owner{destroy = Destroy, - config = Config, - items = Items}]}, - StateData) -> + {result, undefined | muc_owner()} | + {result, undefined | muc_owner(), state() | stop} | + {error, stanza_error()}. +process_iq_owner(From, + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_owner{ + destroy = Destroy, + config = Config, + items = Items + }] + }, + StateData) -> FAffiliation = get_affiliation(From, StateData), - if FAffiliation /= owner -> - ErrText = ?T("Owner privileges required"), - {error, xmpp:err_forbidden(ErrText, Lang)}; - Destroy /= undefined, Config == undefined, Items == [] -> - ?INFO_MSG("Destroyed MUC room ~ts by the owner ~ts", - [jid:encode(StateData#state.jid), jid:encode(From)]), - add_to_log(room_existence, destroyed, StateData), - destroy_room(Destroy, StateData); - Config /= undefined, Destroy == undefined, Items == [] -> - case Config of - #xdata{type = cancel} -> - {result, undefined}; - #xdata{type = submit, fields = Fs} -> - Fs1 = convert_legacy_fields(Fs), - try muc_roomconfig:decode(Fs1) of - Options -> - case is_allowed_log_change(Options, StateData, From) andalso - is_allowed_persistent_change(Options, StateData, From) andalso - is_allowed_mam_change(Options, StateData, From) andalso - is_allowed_string_limits(Options, StateData) andalso - is_password_settings_correct(Options, StateData) of - true -> - set_config(Options, StateData, Lang); - false -> - {error, xmpp:err_not_acceptable()} - end - catch _:{muc_roomconfig, Why} -> - Txt = muc_roomconfig:format_error(Why), - {error, xmpp:err_bad_request(Txt, Lang)} - end; - _ -> - Txt = ?T("Incorrect data form"), - {error, xmpp:err_bad_request(Txt, Lang)} - end; - Items /= [], Config == undefined, Destroy == undefined -> - process_admin_items_set(From, Items, Lang, StateData); - true -> - {error, xmpp:err_bad_request()} + if + FAffiliation /= owner -> + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy /= undefined, Config == undefined, Items == [] -> + ?INFO_MSG("Destroyed MUC room ~ts by the owner ~ts", + [jid:encode(StateData#state.jid), jid:encode(From)]), + add_to_log(room_existence, destroyed, StateData), + destroy_room(Destroy, StateData); + Config /= undefined, Destroy == undefined, Items == [] -> + case Config of + #xdata{type = cancel} -> + {result, undefined}; + #xdata{type = submit, fields = Fs} -> + Fs1 = convert_legacy_fields(Fs), + try muc_roomconfig:decode(Fs1) of + Options -> + case is_allowed_log_change(Options, StateData, From) andalso + is_allowed_persistent_change(Options, StateData, From) andalso + is_allowed_mam_change(Options, StateData, From) andalso + is_allowed_string_limits(Options, StateData) andalso + is_password_settings_correct(Options, StateData) of + true -> + set_config(Options, StateData, Lang); + false -> + {error, xmpp:err_not_acceptable()} + end + catch + _:{muc_roomconfig, Why} -> + Txt = muc_roomconfig:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + _ -> + Txt = ?T("Incorrect data form"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + Items /= [], Config == undefined, Destroy == undefined -> + process_admin_items_set(From, Items, Lang, StateData); + true -> + {error, xmpp:err_bad_request()} end; -process_iq_owner(From, #iq{type = get, lang = Lang, - sub_els = [#muc_owner{destroy = Destroy, - config = Config, - items = Items}]}, - StateData) -> +process_iq_owner(From, + #iq{ + type = get, + lang = Lang, + sub_els = [#muc_owner{ + destroy = Destroy, + config = Config, + items = Items + }] + }, + StateData) -> FAffiliation = get_affiliation(From, StateData), - if FAffiliation /= owner -> - ErrText = ?T("Owner privileges required"), - {error, xmpp:err_forbidden(ErrText, Lang)}; - Destroy == undefined, Config == undefined -> - case Items of - [] -> - {result, - #muc_owner{config = get_config(Lang, StateData, From)}}; - [#muc_item{affiliation = undefined}] -> - Txt = ?T("No 'affiliation' attribute found"), - {error, xmpp:err_bad_request(Txt, Lang)}; - [#muc_item{affiliation = Affiliation}] -> - Items = items_with_affiliation(Affiliation, StateData), - {result, #muc_owner{items = Items}}; - [_|_] -> - Txt = ?T("Too many elements"), - {error, xmpp:err_bad_request(Txt, Lang)} - end; - true -> - {error, xmpp:err_bad_request()} + if + FAffiliation /= owner -> + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)}; + Destroy == undefined, Config == undefined -> + case Items of + [] -> + {result, + #muc_owner{config = get_config(Lang, StateData, From)}}; + [#muc_item{affiliation = undefined}] -> + Txt = ?T("No 'affiliation' attribute found"), + {error, xmpp:err_bad_request(Txt, Lang)}; + [#muc_item{affiliation = Affiliation}] -> + Items = items_with_affiliation(Affiliation, StateData), + {result, #muc_owner{items = Items}}; + [_ | _] -> + Txt = ?T("Too many elements"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + true -> + {error, xmpp:err_bad_request()} end. + -spec is_allowed_log_change(muc_roomconfig:result(), state(), jid()) -> boolean(). is_allowed_log_change(Options, StateData, From) -> case proplists:is_defined(enablelogging, Options) of - false -> true; - true -> - allow == - ejabberd_hooks:run_fold(muc_log_check_access_log, - StateData#state.server_host, - deny, - [StateData#state.server_host, From]) + false -> true; + true -> + allow == + ejabberd_hooks:run_fold(muc_log_check_access_log, + StateData#state.server_host, + deny, + [StateData#state.server_host, From]) end. + -spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean(). is_allowed_persistent_change(Options, StateData, From) -> case proplists:is_defined(persistentroom, Options) of - false -> true; - true -> - {_AccessRoute, _AccessCreate, _AccessAdmin, - AccessPersistent, _AccessMam} = - StateData#state.access, - allow == - acl:match_rule(StateData#state.server_host, - AccessPersistent, From) + false -> true; + true -> + {_AccessRoute, _AccessCreate, _AccessAdmin, + AccessPersistent, _AccessMam} = + StateData#state.access, + allow == + acl:match_rule(StateData#state.server_host, + AccessPersistent, + From) end. + -spec is_allowed_mam_change(muc_roomconfig:result(), state(), jid()) -> boolean(). is_allowed_mam_change(Options, StateData, From) -> case proplists:is_defined(mam, Options) of - false -> true; - true -> - {_AccessRoute, _AccessCreate, _AccessAdmin, - _AccessPersistent, AccessMam} = - StateData#state.access, - allow == - acl:match_rule(StateData#state.server_host, - AccessMam, From) + false -> true; + true -> + {_AccessRoute, _AccessCreate, _AccessAdmin, + _AccessPersistent, AccessMam} = + StateData#state.access, + allow == + acl:match_rule(StateData#state.server_host, + AccessMam, + From) end. + %% Check if the string fields defined in the Data Form %% are conformant to the configured limits -spec is_allowed_string_limits(muc_roomconfig:result(), state()) -> boolean(). @@ -3791,16 +4724,18 @@ is_allowed_string_limits(Options, StateData) -> Password = proplists:get_value(roomsecret, Options, <<"">>), CaptchaWhitelist = proplists:get_value(captcha_whitelist, Options, []), CaptchaWhitelistSize = lists:foldl( - fun(Jid, Sum) -> byte_size(jid:encode(Jid)) + Sum end, - 0, CaptchaWhitelist), + fun(Jid, Sum) -> byte_size(jid:encode(Jid)) + Sum end, + 0, + CaptchaWhitelist), MaxRoomName = mod_muc_opt:max_room_name(StateData#state.server_host), MaxRoomDesc = mod_muc_opt:max_room_desc(StateData#state.server_host), MaxPassword = mod_muc_opt:max_password(StateData#state.server_host), MaxCaptchaWhitelist = mod_muc_opt:max_captcha_whitelist(StateData#state.server_host), - (byte_size(RoomName) =< MaxRoomName) - andalso (byte_size(RoomDesc) =< MaxRoomDesc) - andalso (byte_size(Password) =< MaxPassword) - andalso (CaptchaWhitelistSize =< MaxCaptchaWhitelist). + (byte_size(RoomName) =< MaxRoomName) andalso + (byte_size(RoomDesc) =< MaxRoomDesc) andalso + (byte_size(Password) =< MaxPassword) andalso + (CaptchaWhitelistSize =< MaxCaptchaWhitelist). + %% Return false if: %% "the password for a password-protected room is blank" @@ -3812,183 +4747,199 @@ is_password_settings_correct(Options, StateData) -> NewProtected = proplists:get_value(passwordprotectedroom, Options), NewPassword = proplists:get_value(roomsecret, Options), case {OldProtected, NewProtected, OldPassword, NewPassword} of - {true, undefined, <<"">>, undefined} -> false; - {true, undefined, _, <<"">>} -> false; - {_, true, <<"">>, undefined} -> false; - {_, true, _, <<"">>} -> false; - _ -> true + {true, undefined, <<"">>, undefined} -> false; + {true, undefined, _, <<"">>} -> false; + {_, true, <<"">>, undefined} -> false; + {_, true, _, <<"">>} -> false; + _ -> true end. + -spec get_default_room_maxusers(state()) -> non_neg_integer(). get_default_room_maxusers(RoomState) -> DefRoomOpts = - mod_muc_opt:default_room_options(RoomState#state.server_host), + mod_muc_opt:default_room_options(RoomState#state.server_host), RoomState2 = set_opts(DefRoomOpts, RoomState), (RoomState2#state.config)#config.max_users. + -spec get_config(binary(), state(), jid()) -> xdata(). get_config(Lang, StateData, From) -> {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent, _AccessMam} = - StateData#state.access, + StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), DefaultRoomMaxUsers = get_default_room_maxusers(StateData), Config = StateData#state.config, MaxUsersRoom = get_max_users(StateData), Title = str:translate_and_format( - Lang, ?T("Configuration of room ~s"), - [jid:encode(StateData#state.jid)]), + Lang, + ?T("Configuration of room ~s"), + [jid:encode(StateData#state.jid)]), Fs = [{roomname, Config#config.title}, - {roomdesc, Config#config.description}, - {lang, Config#config.lang}] ++ - case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of - allow -> [{persistentroom, Config#config.persistent}]; - deny -> [] - end ++ - [{publicroom, Config#config.public}, - {public_list, Config#config.public_list}, - {passwordprotectedroom, Config#config.password_protected}, - {roomsecret, case Config#config.password_protected of - true -> Config#config.password; - false -> <<"">> - end}, - {maxusers, MaxUsersRoom, - [if is_integer(ServiceMaxUsers) -> []; - true -> [{?T("No limit"), <<"none">>}] - end] ++ [{integer_to_binary(N), N} - || N <- lists:usort([ServiceMaxUsers, - DefaultRoomMaxUsers, - MaxUsersRoom - | ?MAX_USERS_DEFAULT_LIST]), - N =< ServiceMaxUsers]}, - {whois, if Config#config.anonymous -> moderators; - true -> anyone - end}, - {presencebroadcast, Config#config.presence_broadcast}, - {membersonly, Config#config.members_only}, - {moderatedroom, Config#config.moderated}, - {members_by_default, Config#config.members_by_default}, - {changesubject, Config#config.allow_change_subj}, - {allowpm, Config#config.allowpm}, - {allow_private_messages_from_visitors, - Config#config.allow_private_messages_from_visitors}, - {allow_query_users, Config#config.allow_query_users}, - {allowinvites, Config#config.allow_user_invites}, - {allow_visitor_status, Config#config.allow_visitor_status}, - {allow_visitor_nickchange, Config#config.allow_visitor_nickchange}, - {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}, - {enable_hats, Config#config.enable_hats}] - ++ - case ejabberd_captcha:is_feature_available() of - true -> - [{captcha_protected, Config#config.captcha_protected}, - {captcha_whitelist, - lists:map( - fun jid:make/1, - ?SETS:to_list(Config#config.captcha_whitelist))}]; - false -> - [] - end - ++ + {roomdesc, Config#config.description}, + {lang, Config#config.lang}] ++ + case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of + allow -> [{persistentroom, Config#config.persistent}]; + deny -> [] + end ++ + [{publicroom, Config#config.public}, + {public_list, Config#config.public_list}, + {passwordprotectedroom, Config#config.password_protected}, + {roomsecret, case Config#config.password_protected of + true -> Config#config.password; + false -> <<"">> + end}, + {maxusers, MaxUsersRoom, + [if + is_integer(ServiceMaxUsers) -> []; + true -> [{?T("No limit"), <<"none">>}] + end] ++ [ {integer_to_binary(N), N} + || N <- lists:usort([ServiceMaxUsers, + DefaultRoomMaxUsers, + MaxUsersRoom | ?MAX_USERS_DEFAULT_LIST]), + N =< ServiceMaxUsers ]}, + {whois, if + Config#config.anonymous -> moderators; + true -> anyone + end}, + {presencebroadcast, Config#config.presence_broadcast}, + {membersonly, Config#config.members_only}, + {moderatedroom, Config#config.moderated}, + {members_by_default, Config#config.members_by_default}, + {changesubject, Config#config.allow_change_subj}, + {allowpm, Config#config.allowpm}, + {allow_private_messages_from_visitors, + Config#config.allow_private_messages_from_visitors}, + {allow_query_users, Config#config.allow_query_users}, + {allowinvites, Config#config.allow_user_invites}, + {allow_visitor_status, Config#config.allow_visitor_status}, + {allow_visitor_nickchange, Config#config.allow_visitor_nickchange}, + {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}, + {enable_hats, Config#config.enable_hats}] ++ + case ejabberd_captcha:is_feature_available() of + true -> + [{captcha_protected, Config#config.captcha_protected}, + {captcha_whitelist, + lists:map( + fun jid:make/1, + ?SETS:to_list(Config#config.captcha_whitelist))}]; + false -> + [] + end ++ case ejabberd_hooks:run_fold(muc_log_check_access_log, StateData#state.server_host, deny, [StateData#state.server_host, From]) of - allow -> [{enablelogging, Config#config.logging}]; - deny -> [] - end, + allow -> [{enablelogging, Config#config.logging}]; + deny -> [] + end, Fields = ejabberd_hooks:run_fold(get_room_config, - StateData#state.server_host, - Fs, - [StateData, From, Lang]), - #xdata{type = form, title = Title, - fields = muc_roomconfig:encode(Fields, Lang)}. + StateData#state.server_host, + Fs, + [StateData, From, Lang]), + #xdata{ + type = form, + title = Title, + fields = muc_roomconfig:encode(Fields, Lang) + }. + -spec set_config(muc_roomconfig:result(), state(), binary()) -> - {error, stanza_error()} | {result, undefined, state()}. + {error, stanza_error()} | {result, undefined, state()}. set_config(Options, StateData, Lang) -> try - #config{} = Config = set_config(Options, StateData#state.config, - StateData#state.server_host, Lang), - {result, _, NSD} = Res = change_config(Config, StateData), - Type = case {(StateData#state.config)#config.logging, - Config#config.logging} - of - {true, false} -> roomconfig_change_disabledlogging; - {false, true} -> roomconfig_change_enabledlogging; - {_, _} -> roomconfig_change - end, - Users = [{U#user.jid, U#user.nick, U#user.role} - || U <- maps:values(StateData#state.users)], - add_to_log(Type, Users, NSD), - Res - catch _:{badmatch, {error, #stanza_error{}} = Err} -> - Err + #config{} = Config = set_config(Options, + StateData#state.config, + StateData#state.server_host, + Lang), + {result, _, NSD} = Res = change_config(Config, StateData), + Type = case {(StateData#state.config)#config.logging, + Config#config.logging} of + {true, false} -> roomconfig_change_disabledlogging; + {false, true} -> roomconfig_change_enabledlogging; + {_, _} -> roomconfig_change + end, + Users = [ {U#user.jid, U#user.nick, U#user.role} + || U <- maps:values(StateData#state.users) ], + add_to_log(Type, Users, NSD), + Res + catch + _:{badmatch, {error, #stanza_error{}} = Err} -> + Err end. + -spec get_config_opt_name(pos_integer()) -> atom(). get_config_opt_name(Pos) -> - Fs = [config|record_info(fields, config)], + Fs = [config | record_info(fields, config)], lists:nth(Pos, Fs). --spec set_config([muc_roomconfig:property()], #config{}, - binary(), binary()) -> #config{} | {error, stanza_error()}. + +-spec set_config([muc_roomconfig:property()], + #config{}, + binary(), + binary()) -> #config{} | {error, stanza_error()}. set_config(Opts, Config, ServerHost, Lang) -> lists:foldl( fun(_, {error, _} = Err) -> Err; - ({roomname, Title}, C) -> C#config{title = Title}; - ({roomdesc, Desc}, C) -> C#config{description = Desc}; - ({changesubject, V}, C) -> C#config{allow_change_subj = V}; - ({allow_query_users, V}, C) -> C#config{allow_query_users = V}; - ({allowpm, V}, C) -> - C#config{allowpm = V}; - ({allow_private_messages_from_visitors, V}, C) -> - C#config{allow_private_messages_from_visitors = V}; - ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V}; - ({allow_visitor_nickchange, V}, C) -> - C#config{allow_visitor_nickchange = V}; - ({publicroom, V}, C) -> C#config{public = V}; - ({public_list, V}, C) -> C#config{public_list = V}; - ({persistentroom, V}, C) -> C#config{persistent = V}; - ({moderatedroom, V}, C) -> C#config{moderated = V}; - ({members_by_default, V}, C) -> C#config{members_by_default = V}; - ({membersonly, V}, C) -> C#config{members_only = V}; - ({captcha_protected, V}, C) -> C#config{captcha_protected = V}; - ({allowinvites, V}, C) -> C#config{allow_user_invites = V}; - ({allow_subscription, V}, C) -> C#config{allow_subscription = V}; - ({passwordprotectedroom, V}, C) -> C#config{password_protected = V}; - ({roomsecret, V}, C) -> C#config{password = V}; - ({anonymous, V}, C) -> C#config{anonymous = V}; - ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V}; - ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V}; - ({voice_request_min_interval, V}, C) -> - C#config{voice_request_min_interval = V}; - ({whois, moderators}, C) -> C#config{anonymous = true}; - ({whois, anyone}, C) -> C#config{anonymous = false}; - ({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], - C#config{captcha_whitelist = ?SETS:from_list(LJIDs)}; - ({O, V} = Opt, C) -> - case ejabberd_hooks:run_fold(set_room_option, - ServerHost, - {0, undefined}, - [Opt, Lang]) of - {0, undefined} -> - ?ERROR_MSG("set_room_option hook failed for " - "option '~ts' with value ~p", [O, V]), - Txt = {?T("Failed to process option '~s'"), [O]}, - {error, xmpp:err_internal_server_error(Txt, Lang)}; - {Pos, Val} -> - setelement(Pos, C, Val) - end - end, Config, Opts). + ({roomname, Title}, C) -> C#config{title = Title}; + ({roomdesc, Desc}, C) -> C#config{description = Desc}; + ({changesubject, V}, C) -> C#config{allow_change_subj = V}; + ({allow_query_users, V}, C) -> C#config{allow_query_users = V}; + ({allowpm, V}, C) -> + C#config{allowpm = V}; + ({allow_private_messages_from_visitors, V}, C) -> + C#config{allow_private_messages_from_visitors = V}; + ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V}; + ({allow_visitor_nickchange, V}, C) -> + C#config{allow_visitor_nickchange = V}; + ({publicroom, V}, C) -> C#config{public = V}; + ({public_list, V}, C) -> C#config{public_list = V}; + ({persistentroom, V}, C) -> C#config{persistent = V}; + ({moderatedroom, V}, C) -> C#config{moderated = V}; + ({members_by_default, V}, C) -> C#config{members_by_default = V}; + ({membersonly, V}, C) -> C#config{members_only = V}; + ({captcha_protected, V}, C) -> C#config{captcha_protected = V}; + ({allowinvites, V}, C) -> C#config{allow_user_invites = V}; + ({allow_subscription, V}, C) -> C#config{allow_subscription = V}; + ({passwordprotectedroom, V}, C) -> C#config{password_protected = V}; + ({roomsecret, V}, C) -> C#config{password = V}; + ({anonymous, V}, C) -> C#config{anonymous = V}; + ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V}; + ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V}; + ({voice_request_min_interval, V}, C) -> + C#config{voice_request_min_interval = V}; + ({whois, moderators}, C) -> C#config{anonymous = true}; + ({whois, anyone}, C) -> C#config{anonymous = false}; + ({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 ], + C#config{captcha_whitelist = ?SETS:from_list(LJIDs)}; + ({O, V} = Opt, C) -> + case ejabberd_hooks:run_fold(set_room_option, + ServerHost, + {0, undefined}, + [Opt, Lang]) of + {0, undefined} -> + ?ERROR_MSG("set_room_option hook failed for " + "option '~ts' with value ~p", + [O, V]), + Txt = {?T("Failed to process option '~s'"), [O]}, + {error, xmpp:err_internal_server_error(Txt, Lang)}; + {Pos, Val} -> + setelement(Pos, C, Val) + end + end, + Config, + Opts). + -spec change_config(#config{}, state()) -> {result, undefined, state()}. change_config(Config, StateData) -> @@ -3999,23 +4950,24 @@ change_config(Config, StateData) -> case {(StateData#state.config)#config.persistent, Config#config.persistent} of {WasPersistent, true} -> - if not WasPersistent -> + if + not WasPersistent -> set_affiliations(StateData1#state.affiliations, StateData1); - true -> + true -> ok end, store_room(StateData1), StateData1; {true, false} -> - Affiliations = get_affiliations(StateData), - maybe_forget_room(StateData), - StateData1#state{affiliations = Affiliations}; - _ -> - StateData1 + Affiliations = get_affiliations(StateData), + maybe_forget_room(StateData), + StateData1#state{affiliations = Affiliations}; + _ -> + StateData1 end, case {(StateData#state.config)#config.members_only, - Config#config.members_only} of + Config#config.members_only} of {false, true} -> StateData3 = remove_nonmembers(StateData2), {result, undefined, StateData3}; @@ -4023,66 +4975,77 @@ change_config(Config, StateData) -> {result, undefined, StateData2} end. + -spec send_config_change_info(#config{}, state()) -> ok. send_config_change_info(Config, #state{config = Config}) -> ok; send_config_change_info(New, #state{config = Old} = StateData) -> Codes = case {Old#config.logging, New#config.logging} of - {false, true} -> [170]; - {true, false} -> [171]; - _ -> [] - end - ++ - case {Old#config.anonymous, New#config.anonymous} of - {true, false} -> [172]; - {false, true} -> [173]; - _ -> [] - end - ++ - case Old#config{anonymous = New#config.anonymous, - logging = New#config.logging} of - New -> []; - _ -> [104] - end, - if Codes /= [] -> - maps:fold( - fun(_LJID, #user{jid = JID}, _) -> - advertise_entity_capabilities(JID, StateData#state{config = New}) - end, ok, StateData#state.users), - Message = #message{type = groupchat, - id = p1_rand:get_string(), - sub_els = [#muc_user{status_codes = Codes}]}, - send_wrapped_multiple(StateData#state.jid, - get_users_and_subscribers_with_node( + {false, true} -> [170]; + {true, false} -> [171]; + _ -> [] + end ++ + case {Old#config.anonymous, New#config.anonymous} of + {true, false} -> [172]; + {false, true} -> [173]; + _ -> [] + end ++ + case Old#config{ + anonymous = New#config.anonymous, + logging = New#config.logging + } of + New -> []; + _ -> [104] + end, + if + Codes /= [] -> + maps:fold( + fun(_LJID, #user{jid = JID}, _) -> + advertise_entity_capabilities(JID, StateData#state{config = New}) + end, + ok, + StateData#state.users), + Message = #message{ + type = groupchat, + id = p1_rand:get_string(), + sub_els = [#muc_user{status_codes = Codes}] + }, + send_wrapped_multiple(StateData#state.jid, + get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_CONFIG, StateData), - Message, - ?NS_MUCSUB_NODES_CONFIG, - StateData); - true -> - ok + Message, + ?NS_MUCSUB_NODES_CONFIG, + StateData); + true -> + ok end. + -spec remove_nonmembers(state()) -> state(). remove_nonmembers(StateData) -> maps:fold( fun(_LJID, #user{jid = JID}, SD) -> - Affiliation = get_affiliation(JID, SD), - case Affiliation of - none -> - catch send_kickban_presence(undefined, JID, <<"">>, 322, SD), - set_role(JID, none, SD); - _ -> SD - end - end, StateData, get_users_and_subscribers(StateData)). + Affiliation = get_affiliation(JID, SD), + case Affiliation of + none -> + catch send_kickban_presence(undefined, JID, <<"">>, 322, SD), + set_role(JID, none, SD); + _ -> SD + end + end, + StateData, + get_users_and_subscribers(StateData)). + -spec set_opts([{atom(), any()}], state()) -> state(). set_opts(Opts, StateData) -> case lists:keytake(persistent, 1, Opts) of - false -> - set_opts2(Opts, StateData); - {value, Tuple, Rest} -> - set_opts2([Tuple | Rest], StateData) + false -> + set_opts2(Opts, StateData); + {value, Tuple, Rest} -> + set_opts2([Tuple | Rest], StateData) end. + -spec set_opts2([{atom(), any()}], state()) -> state(). set_opts2([], StateData) -> set_vcard_xupdate(StateData); @@ -4093,135 +5056,253 @@ set_opts2([{vcard, Val} | Opts], StateData) set_opts2([{vcard, ValRaw} | Opts], StateData); set_opts2([{Opt, Val} | Opts], StateData) -> NSD = case Opt of - title -> - StateData#state{config = - (StateData#state.config)#config{title = - Val}}; - description -> - StateData#state{config = - (StateData#state.config)#config{description - = Val}}; - allow_change_subj -> - StateData#state{config = - (StateData#state.config)#config{allow_change_subj - = Val}}; - allow_query_users -> - StateData#state{config = - (StateData#state.config)#config{allow_query_users - = Val}}; - allowpm -> - StateData#state{config = - (StateData#state.config)#config{allowpm - = Val}}; - allow_private_messages_from_visitors -> - StateData#state{config = - (StateData#state.config)#config{allow_private_messages_from_visitors - = Val}}; - allow_visitor_nickchange -> - StateData#state{config = - (StateData#state.config)#config{allow_visitor_nickchange - = Val}}; - allow_visitor_status -> - StateData#state{config = - (StateData#state.config)#config{allow_visitor_status - = Val}}; - public -> - StateData#state{config = - (StateData#state.config)#config{public = - Val}}; - public_list -> - StateData#state{config = - (StateData#state.config)#config{public_list - = Val}}; - persistent -> - StateData#state{config = - (StateData#state.config)#config{persistent = - Val}}; - moderated -> - StateData#state{config = - (StateData#state.config)#config{moderated = - Val}}; - members_by_default -> - StateData#state{config = - (StateData#state.config)#config{members_by_default - = Val}}; - members_only -> - StateData#state{config = - (StateData#state.config)#config{members_only - = Val}}; - allow_user_invites -> - StateData#state{config = - (StateData#state.config)#config{allow_user_invites - = Val}}; - password_protected -> - StateData#state{config = - (StateData#state.config)#config{password_protected - = Val}}; - captcha_protected -> - StateData#state{config = - (StateData#state.config)#config{captcha_protected - = Val}}; - password -> - StateData#state{config = - (StateData#state.config)#config{password = - Val}}; - anonymous -> - StateData#state{config = - (StateData#state.config)#config{anonymous = - Val}}; - presence_broadcast -> - StateData#state{config = - (StateData#state.config)#config{presence_broadcast = - Val}}; - logging -> - StateData#state{config = - (StateData#state.config)#config{logging = - Val}}; - mam -> - StateData#state{config = - (StateData#state.config)#config{mam = Val}}; - captcha_whitelist -> - StateData#state{config = - (StateData#state.config)#config{captcha_whitelist - = - (?SETS):from_list(Val)}}; - allow_voice_requests -> - StateData#state{config = - (StateData#state.config)#config{allow_voice_requests - = Val}}; - voice_request_min_interval -> - StateData#state{config = - (StateData#state.config)#config{voice_request_min_interval - = Val}}; - max_users -> - ServiceMaxUsers = get_service_max_users(StateData), - MaxUsers = if Val =< ServiceMaxUsers -> Val; - true -> ServiceMaxUsers - end, - StateData#state{config = - (StateData#state.config)#config{max_users = - MaxUsers}}; - vcard -> - StateData#state{config = - (StateData#state.config)#config{vcard = - Val}}; - vcard_xupdate -> - StateData#state{config = - (StateData#state.config)#config{vcard_xupdate = - Val}}; - pubsub -> - StateData#state{config = - (StateData#state.config)#config{pubsub = Val}}; - allow_subscription -> - StateData#state{config = - (StateData#state.config)#config{allow_subscription = Val}}; - enable_hats -> - StateData#state{config = - (StateData#state.config)#config{enable_hats = Val}}; - lang -> - StateData#state{config = - (StateData#state.config)#config{lang = Val}}; - subscribers -> + title -> + StateData#state{ + config = + (StateData#state.config)#config{ + title = + Val + } + }; + description -> + StateData#state{ + config = + (StateData#state.config)#config{ + description = + Val + } + }; + allow_change_subj -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_change_subj = + Val + } + }; + allow_query_users -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_query_users = + Val + } + }; + allowpm -> + StateData#state{ + config = + (StateData#state.config)#config{ + allowpm = + Val + } + }; + allow_private_messages_from_visitors -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_private_messages_from_visitors = + Val + } + }; + allow_visitor_nickchange -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_visitor_nickchange = + Val + } + }; + allow_visitor_status -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_visitor_status = + Val + } + }; + public -> + StateData#state{ + config = + (StateData#state.config)#config{ + public = + Val + } + }; + public_list -> + StateData#state{ + config = + (StateData#state.config)#config{ + public_list = + Val + } + }; + persistent -> + StateData#state{ + config = + (StateData#state.config)#config{ + persistent = + Val + } + }; + moderated -> + StateData#state{ + config = + (StateData#state.config)#config{ + moderated = + Val + } + }; + members_by_default -> + StateData#state{ + config = + (StateData#state.config)#config{ + members_by_default = + Val + } + }; + members_only -> + StateData#state{ + config = + (StateData#state.config)#config{ + members_only = + Val + } + }; + allow_user_invites -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_user_invites = + Val + } + }; + password_protected -> + StateData#state{ + config = + (StateData#state.config)#config{ + password_protected = + Val + } + }; + captcha_protected -> + StateData#state{ + config = + (StateData#state.config)#config{ + captcha_protected = + Val + } + }; + password -> + StateData#state{ + config = + (StateData#state.config)#config{ + password = + Val + } + }; + anonymous -> + StateData#state{ + config = + (StateData#state.config)#config{ + anonymous = + Val + } + }; + presence_broadcast -> + StateData#state{ + config = + (StateData#state.config)#config{ + presence_broadcast = + Val + } + }; + logging -> + StateData#state{ + config = + (StateData#state.config)#config{ + logging = + Val + } + }; + mam -> + StateData#state{ + config = + (StateData#state.config)#config{mam = Val} + }; + captcha_whitelist -> + StateData#state{ + config = + (StateData#state.config)#config{ + captcha_whitelist = + (?SETS):from_list(Val) + } + }; + allow_voice_requests -> + StateData#state{ + config = + (StateData#state.config)#config{ + allow_voice_requests = + Val + } + }; + voice_request_min_interval -> + StateData#state{ + config = + (StateData#state.config)#config{ + voice_request_min_interval = + Val + } + }; + max_users -> + ServiceMaxUsers = get_service_max_users(StateData), + MaxUsers = if + Val =< ServiceMaxUsers -> Val; + true -> ServiceMaxUsers + end, + StateData#state{ + config = + (StateData#state.config)#config{ + max_users = + MaxUsers + } + }; + vcard -> + StateData#state{ + config = + (StateData#state.config)#config{ + vcard = + Val + } + }; + vcard_xupdate -> + StateData#state{ + config = + (StateData#state.config)#config{ + vcard_xupdate = + Val + } + }; + pubsub -> + StateData#state{ + config = + (StateData#state.config)#config{pubsub = Val} + }; + allow_subscription -> + StateData#state{ + config = + (StateData#state.config)#config{allow_subscription = Val} + }; + enable_hats -> + StateData#state{ + config = + (StateData#state.config)#config{enable_hats = Val} + }; + lang -> + StateData#state{ + config = + (StateData#state.config)#config{lang = Val} + }; + subscribers -> MUCSubscribers = lists:foldl( fun({JID, Nick, Nodes}, MUCSubs) -> @@ -4233,53 +5314,64 @@ set_opts2([{Opt, Val} | Opts], StateData) -> jid:remove_resource(jid:make(JID)) end, muc_subscribers_put( - #subscriber{jid = BareJID, - nick = Nick, - nodes = Nodes}, + #subscriber{ + jid = BareJID, + nick = Nick, + nodes = Nodes + }, MUCSubs) - end, muc_subscribers_new(), Val), + end, + muc_subscribers_new(), + Val), StateData#state{muc_subscribers = MUCSubscribers}; - affiliations -> - set_affiliations(maps:from_list(Val), StateData); - roles -> - StateData#state{roles = maps:from_list(Val)}; - subject -> - Subj = if Val == <<"">> -> []; - is_binary(Val) -> [#text{data = Val}]; - is_list(Val) -> Val - end, - StateData#state{subject = Subj}; - subject_author when is_tuple(Val) -> + affiliations -> + set_affiliations(maps:from_list(Val), StateData); + roles -> + StateData#state{roles = maps:from_list(Val)}; + subject -> + Subj = if + Val == <<"">> -> []; + is_binary(Val) -> [#text{data = Val}]; + is_list(Val) -> Val + end, + StateData#state{subject = Subj}; + subject_author when is_tuple(Val) -> StateData#state{subject_author = Val}; - subject_author when is_binary(Val) -> % ejabberd 23.04 or older + subject_author when is_binary(Val) -> % ejabberd 23.04 or older StateData#state{subject_author = {Val, #jid{}}}; - hats_users -> + hats_users -> Hats = maps:from_list( lists:map(fun({U, H}) -> {U, maps:from_list(H)} end, Val)), StateData#state{hats_users = Hats}; - hibernation_time -> StateData; - Other -> + hibernation_time -> StateData; + Other -> ?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]), StateData - end, + end, set_opts2(Opts, NSD). + -spec set_vcard_xupdate(state()) -> state(). -set_vcard_xupdate(#state{config = - #config{vcard = VCardRaw, - vcard_xupdate = undefined} = Config} = State) +set_vcard_xupdate(#state{ + config = + #config{ + vcard = VCardRaw, + vcard_xupdate = undefined + } = Config + } = State) when VCardRaw /= <<"">> -> case fxml_stream:parse_element(VCardRaw) of - {error, _} -> - State; - El -> - Hash = mod_vcard_xupdate:compute_hash(El), - State#state{config = Config#config{vcard_xupdate = Hash}} + {error, _} -> + State; + El -> + Hash = mod_vcard_xupdate:compute_hash(El), + State#state{config = Config#config{vcard_xupdate = Hash}} end; set_vcard_xupdate(State) -> State. + get_occupant_initial_role(Jid, Affiliation, #state{roles = Roles} = StateData) -> DefaultRole = get_default_role(Affiliation, StateData), case (StateData#state.config)#config.moderated of @@ -4289,29 +5381,36 @@ get_occupant_initial_role(Jid, Affiliation, #state{roles = Roles} = StateData) - DefaultRole end. + get_occupant_stored_role(Jid, Roles, DefaultRole) -> maps:get(jid:split(jid:remove_resource(Jid)), Roles, DefaultRole). + -define(MAKE_CONFIG_OPT(Opt), - {get_config_opt_name(Opt), element(Opt, Config)}). + {get_config_opt_name(Opt), element(Opt, Config)}). + -spec make_opts(state(), boolean()) -> [{atom(), any()}]. make_opts(StateData, Hibernation) -> Config = StateData#state.config, Subscribers = muc_subscribers_fold( - fun(_LJID, Sub, Acc) -> - [{Sub#subscriber.jid, - Sub#subscriber.nick, - Sub#subscriber.nodes}|Acc] - end, [], StateData#state.muc_subscribers), - [?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description), + fun(_LJID, Sub, Acc) -> + [{Sub#subscriber.jid, + Sub#subscriber.nick, + Sub#subscriber.nodes} | Acc] + end, + [], + StateData#state.muc_subscribers), + [?MAKE_CONFIG_OPT(#config.title), + ?MAKE_CONFIG_OPT(#config.description), ?MAKE_CONFIG_OPT(#config.allow_change_subj), ?MAKE_CONFIG_OPT(#config.allow_query_users), ?MAKE_CONFIG_OPT(#config.allowpm), ?MAKE_CONFIG_OPT(#config.allow_private_messages_from_visitors), ?MAKE_CONFIG_OPT(#config.allow_visitor_status), ?MAKE_CONFIG_OPT(#config.allow_visitor_nickchange), - ?MAKE_CONFIG_OPT(#config.public), ?MAKE_CONFIG_OPT(#config.public_list), + ?MAKE_CONFIG_OPT(#config.public), + ?MAKE_CONFIG_OPT(#config.public_list), ?MAKE_CONFIG_OPT(#config.persistent), ?MAKE_CONFIG_OPT(#config.moderated), ?MAKE_CONFIG_OPT(#config.members_by_default), @@ -4319,8 +5418,10 @@ make_opts(StateData, Hibernation) -> ?MAKE_CONFIG_OPT(#config.allow_user_invites), ?MAKE_CONFIG_OPT(#config.password_protected), ?MAKE_CONFIG_OPT(#config.captcha_protected), - ?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous), - ?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users), + ?MAKE_CONFIG_OPT(#config.password), + ?MAKE_CONFIG_OPT(#config.anonymous), + ?MAKE_CONFIG_OPT(#config.logging), + ?MAKE_CONFIG_OPT(#config.max_users), ?MAKE_CONFIG_OPT(#config.allow_voice_requests), ?MAKE_CONFIG_OPT(#config.allow_subscription), ?MAKE_CONFIG_OPT(#config.mam), @@ -4344,6 +5445,7 @@ make_opts(StateData, Hibernation) -> {hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end}, {subscribers, Subscribers}]. + expand_opts(CompactOpts) -> DefConfig = #config{}, Fields = record_info(fields, config), @@ -4357,11 +5459,13 @@ expand_opts(CompactOpts) -> true -> (?SETS):to_list(DefV); false -> DefV end, - {Pos+1, [{Field, DefVal}|Opts]}; + {Pos + 1, [{Field, DefVal} | Opts]}; {_, Val} -> - {Pos+1, [{Field, Val}|Opts]} + {Pos + 1, [{Field, Val} | Opts]} end - end, {2, []}, Fields), + end, + {2, []}, + Fields), SubjectAuthor = proplists:get_value(subject_author, CompactOpts, {<<"">>, #jid{}}), Subject = proplists:get_value(subject, CompactOpts, <<"">>), Subscribers = proplists:get_value(subscribers, CompactOpts, []), @@ -4369,313 +5473,393 @@ expand_opts(CompactOpts) -> [{subject, Subject}, {subject_author, SubjectAuthor}, {subscribers, Subscribers}, - {hibernation_time, HibernationTime} - | lists:reverse(Opts1)]. + {hibernation_time, HibernationTime} | lists:reverse(Opts1)]. + config_fields() -> [subject, subject_author, subscribers, hibernate_time | record_info(fields, config)]. + -spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}. destroy_room(DEl, StateData) -> Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER}, maps:fold( fun(_LJID, Info, _) -> - Nick = Info#user.nick, - Item = #muc_item{affiliation = none, - role = none}, - Packet = #presence{ - type = unavailable, - sub_els = [#muc_user{items = [Item], - destroy = Destroy}]}, - send_wrapped(jid:replace_resource(StateData#state.jid, Nick), - Info#user.jid, Packet, - ?NS_MUCSUB_NODES_CONFIG, StateData) - end, ok, get_users_and_subscribers_with_node( - ?NS_MUCSUB_NODES_CONFIG, StateData)), + Nick = Info#user.nick, + Item = #muc_item{ + affiliation = none, + role = none + }, + Packet = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [Item], + destroy = Destroy + }] + }, + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet, + ?NS_MUCSUB_NODES_CONFIG, + StateData) + end, + ok, + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_CONFIG, StateData)), forget_room(StateData), {result, undefined, stop}. + -spec forget_room(state()) -> state(). forget_room(StateData) -> mod_muc:forget_room(StateData#state.server_host, - StateData#state.host, - StateData#state.room), + StateData#state.host, + StateData#state.room), StateData. + -spec maybe_forget_room(state()) -> state(). maybe_forget_room(StateData) -> Forget = case (StateData#state.config)#config.persistent of - true -> - true; - _ -> - Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), - erlang:function_exported(Mod, get_subscribed_rooms, 3) - end, + true -> + true; + _ -> + Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), + erlang:function_exported(Mod, get_subscribed_rooms, 3) + end, case Forget of - true -> - forget_room(StateData); - _ -> - StateData + true -> + forget_room(StateData); + _ -> + StateData end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), - case Opt of - true -> Fiftrue; - false -> Fiffalse - end). + case Opt of + true -> Fiftrue; + false -> Fiffalse + end). + -spec make_disco_info(jid(), state()) -> disco_info(). make_disco_info(From, StateData) -> Config = StateData#state.config, ServerHost = StateData#state.server_host, AccessRegister = mod_muc_opt:access_register(ServerHost), - Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + Feats = [?NS_VCARD, + ?NS_MUC, + ?NS_DISCO_INFO, + ?NS_DISCO_ITEMS, ?NS_COMMANDS, - ?NS_MESSAGE_MODERATE_0, ?NS_MESSAGE_MODERATE_1, + ?NS_MESSAGE_MODERATE_0, + ?NS_MESSAGE_MODERATE_1, ?NS_MESSAGE_RETRACT, - ?CONFIG_OPT_TO_FEATURE((Config#config.public), - <<"muc_public">>, <<"muc_hidden">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), - <<"muc_persistent">>, <<"muc_temporary">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), - <<"muc_membersonly">>, <<"muc_open">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), - <<"muc_semianonymous">>, <<"muc_nonanonymous">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), - <<"muc_moderated">>, <<"muc_unmoderated">>), - ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), - <<"muc_passwordprotected">>, <<"muc_unsecured">>)] - ++ case acl:match_rule(ServerHost, AccessRegister, From) of - allow -> [?NS_REGISTER]; - deny -> [] - end - ++ case Config#config.allow_subscription of - true -> [?NS_MUCSUB]; - false -> [] - end - ++ case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of - true -> - [?NS_OCCUPANT_ID]; - _ -> - [] - end - ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), - Config#config.mam} of - {true, true} -> - [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0]; - _ -> - [] - end, - #disco_info{identities = [#identity{category = <<"conference">>, - type = <<"text">>, - name = (StateData#state.config)#config.title}], - features = Feats}. + ?CONFIG_OPT_TO_FEATURE((Config#config.public), + <<"muc_public">>, + <<"muc_hidden">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), + <<"muc_persistent">>, + <<"muc_temporary">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), + <<"muc_membersonly">>, + <<"muc_open">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), + <<"muc_semianonymous">>, + <<"muc_nonanonymous">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), + <<"muc_moderated">>, + <<"muc_unmoderated">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), + <<"muc_passwordprotected">>, + <<"muc_unsecured">>)] ++ + case acl:match_rule(ServerHost, AccessRegister, From) of + allow -> [?NS_REGISTER]; + deny -> [] + end ++ + case Config#config.allow_subscription of + true -> [?NS_MUCSUB]; + false -> [] + end ++ + case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of + true -> + [?NS_OCCUPANT_ID]; + _ -> + [] + end ++ + case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), + Config#config.mam} of + {true, true} -> + [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0]; + _ -> + [] + end, + #disco_info{ + identities = [#identity{ + category = <<"conference">>, + type = <<"text">>, + name = (StateData#state.config)#config.title + }], + features = Feats + }. + -spec process_iq_disco_info(jid(), iq(), state()) -> - {result, disco_info()} | {error, stanza_error()}. + {result, disco_info()} | {error, stanza_error()}. process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), {error, xmpp:err_not_allowed(Txt, Lang)}; -process_iq_disco_info(From, #iq{type = get, lang = Lang, - sub_els = [#disco_info{node = <<>>}]}, - StateData) -> +process_iq_disco_info(From, + #iq{ + type = get, + lang = Lang, + sub_els = [#disco_info{node = <<>>}] + }, + StateData) -> 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) -> +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 + is_admin(From, StateData) of true -> {result, #disco_info{ - identities = [#identity{category = <<"automation">>, - type = <<"command-list">>, - name = translate:translate( - Lang, ?T("Commands"))}]}}; + 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) -> +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 + 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]}}; + 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) -> +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 + 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]}}; + 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) -> +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 + 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]}}; + 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) -> +process_iq_disco_info(From, + #iq{ + type = get, + lang = Lang, + sub_els = [#disco_info{node = Node}] + }, + StateData) -> try - true = mod_caps:is_valid_node(Node), - DiscoInfo = make_disco_info(From, StateData), - Extras = iq_disco_info_extras(Lang, StateData, true), - DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, - Hash = mod_caps:compute_disco_hash(DiscoInfo1, sha), - Node = <<(ejabberd_config:get_uri())/binary, $#, Hash/binary>>, - {result, DiscoInfo1#disco_info{node = Node}} - catch _:{badmatch, _} -> - Txt = ?T("Invalid node name"), - {error, xmpp:err_item_not_found(Txt, Lang)} + true = mod_caps:is_valid_node(Node), + DiscoInfo = make_disco_info(From, StateData), + Extras = iq_disco_info_extras(Lang, StateData, true), + DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, + Hash = mod_caps:compute_disco_hash(DiscoInfo1, sha), + Node = <<(ejabberd_config:get_uri())/binary, $#, Hash/binary>>, + {result, DiscoInfo1#disco_info{node = Node}} + catch + _:{badmatch, _} -> + Txt = ?T("Invalid node name"), + {error, xmpp:err_item_not_found(Txt, Lang)} end. + -spec iq_disco_info_extras(binary(), state(), boolean()) -> xdata(). iq_disco_info_extras(Lang, StateData, Static) -> Config = StateData#state.config, Fs1 = [{roomname, Config#config.title}, - {description, Config#config.description}, - {changesubject, Config#config.allow_change_subj}, - {allowinvites, Config#config.allow_user_invites}, - {allow_query_users, Config#config.allow_query_users}, - {allowpm, Config#config.allowpm}, - {lang, Config#config.lang}], + {description, Config#config.description}, + {changesubject, Config#config.allow_change_subj}, + {allowinvites, Config#config.allow_user_invites}, + {allow_query_users, Config#config.allow_query_users}, + {allowpm, Config#config.allowpm}, + {lang, Config#config.lang}], Fs2 = case Config#config.pubsub of - Node when is_binary(Node), Node /= <<"">> -> - [{pubsub, Node}|Fs1]; - _ -> - Fs1 - end, + Node when is_binary(Node), Node /= <<"">> -> + [{pubsub, Node} | Fs1]; + _ -> + Fs1 + end, Fs3 = case Static of - false -> - [{occupants, maps:size(StateData#state.nicks)}|Fs2]; - true -> - Fs2 - end, + false -> + [{occupants, maps:size(StateData#state.nicks)} | Fs2]; + true -> + Fs2 + end, Fs4 = case Config#config.logging of - true -> - case ejabberd_hooks:run_fold(muc_log_get_url, + true -> + case ejabberd_hooks:run_fold(muc_log_get_url, StateData#state.server_host, error, [StateData]) of - {ok, URL} -> - [{logs, URL}|Fs3]; - error -> - Fs3 - end; - false -> - Fs3 - end, + {ok, URL} -> + [{logs, URL} | Fs3]; + error -> + Fs3 + end; + false -> + Fs3 + end, Fs5 = case (StateData#state.config)#config.vcard_xupdate of - Hash when is_binary(Hash) -> - [{avatarhash, [Hash]} | Fs4]; - _ -> - Fs4 - end, + Hash when is_binary(Hash) -> + [{avatarhash, [Hash]} | Fs4]; + _ -> + Fs4 + end, Fs6 = ejabberd_hooks:run_fold(muc_disco_info_extras, - StateData#state.server_host, - Fs5, - [StateData]), - #xdata{type = result, - fields = muc_roominfo:encode(Fs6, Lang)}. + StateData#state.server_host, + Fs5, + [StateData]), + #xdata{ + type = result, + fields = muc_roominfo:encode(Fs6, Lang) + }. + -spec process_iq_disco_items(jid(), iq(), state()) -> - {error, stanza_error()} | {result, disco_items()}. + {error, stanza_error()} | {result, disco_items()}. process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), {error, xmpp:err_not_allowed(Txt, Lang)}; -process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>}]}, - StateData) -> +process_iq_disco_items(From, + #iq{type = get, sub_els = [#disco_items{node = <<>>}]}, + StateData) -> case (StateData#state.config)#config.public_list of - true -> - {result, get_mucroom_disco_items(StateData)}; - _ -> - case is_occupant_or_admin(From, StateData) of - true -> - {result, get_mucroom_disco_items(StateData)}; - _ -> - %% If the list of occupants is private, - %% the room MUST return an empty element - %% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems) - {result, #disco_items{}} - end + true -> + {result, get_mucroom_disco_items(StateData)}; + _ -> + case is_occupant_or_admin(From, StateData) of + true -> + {result, get_mucroom_disco_items(StateData)}; + _ -> + %% If the list of occupants is private, + %% the room MUST return an empty element + %% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems) + {result, #disco_items{}} + end end; -process_iq_disco_items(From, #iq{type = get, lang = Lang, - sub_els = [#disco_items{node = ?NS_COMMANDS}]}, - StateData) -> +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 + 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"))}]}}; + 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) +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 + is_admin(From, StateData) of true -> {result, #disco_items{}}; false -> @@ -4686,89 +5870,109 @@ process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)}. + -spec process_iq_captcha(jid(), iq(), state()) -> {error, stanza_error()} | - {result, undefined}. + {result, undefined}. process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), {error, xmpp:err_not_allowed(Txt, Lang)}; -process_iq_captcha(_From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, - _StateData) -> +process_iq_captcha(_From, + #iq{type = set, lang = Lang, sub_els = [SubEl]}, + _StateData) -> case ejabberd_captcha:process_reply(SubEl) of - ok -> {result, undefined}; - {error, malformed} -> - Txt = ?T("Incorrect CAPTCHA submit"), - {error, xmpp:err_bad_request(Txt, Lang)}; - _ -> - Txt = ?T("The CAPTCHA verification has failed"), - {error, xmpp:err_not_allowed(Txt, Lang)} + ok -> {result, undefined}; + {error, malformed} -> + Txt = ?T("Incorrect CAPTCHA submit"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ -> + Txt = ?T("The CAPTCHA verification has failed"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. + -spec process_iq_vcard(jid(), iq(), state()) -> - {result, vcard_temp() | xmlel()} | - {result, undefined, state()} | - {error, stanza_error()}. + {result, vcard_temp() | xmlel()} | + {result, undefined, state()} | + {error, stanza_error()}. process_iq_vcard(_From, #iq{type = get}, StateData) -> #state{config = #config{vcard = VCardRaw}} = StateData, case fxml_stream:parse_element(VCardRaw) of - #xmlel{} = VCard -> - {result, VCard}; - {error, _} -> - {error, xmpp:err_item_not_found()} + #xmlel{} = VCard -> + {result, VCard}; + {error, _} -> + {error, xmpp:err_item_not_found()} end; -process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]}, - StateData) -> +process_iq_vcard(From, + #iq{type = set, lang = Lang, sub_els = [Pkt]}, + StateData) -> case get_affiliation(From, StateData) of - owner -> - SubEl = xmpp:encode(Pkt), - VCardRaw = fxml:element_to_binary(SubEl), - Hash = mod_vcard_xupdate:compute_hash(SubEl), - Config = StateData#state.config, - NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash}, - change_config(NewConfig, StateData); - _ -> - ErrText = ?T("Owner privileges required"), - {error, xmpp:err_forbidden(ErrText, Lang)} + owner -> + SubEl = xmpp:encode(Pkt), + VCardRaw = fxml:element_to_binary(SubEl), + Hash = mod_vcard_xupdate:compute_hash(SubEl), + Config = StateData#state.config, + NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash}, + change_config(NewConfig, StateData); + _ -> + ErrText = ?T("Owner privileges required"), + {error, xmpp:err_forbidden(ErrText, Lang)} end. + -spec process_iq_mucsub(jid(), iq(), state()) -> - {error, stanza_error()} | - {result, undefined | muc_subscribe() | muc_subscriptions(), stop | state()} | - {ignore, state()}. -process_iq_mucsub(_From, #iq{type = set, lang = Lang, - sub_els = [#muc_subscribe{}]}, - #state{just_created = Just, config = #config{allow_subscription = false}}) when Just /= true -> + {error, stanza_error()} | + {result, undefined | muc_subscribe() | muc_subscriptions(), stop | state()} | + {ignore, state()}. +process_iq_mucsub(_From, + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_subscribe{}] + }, + #state{just_created = Just, config = #config{allow_subscription = false}}) when Just /= true -> {error, xmpp:err_not_allowed(?T("Subscriptions are not allowed"), Lang)}; process_iq_mucsub(From, - #iq{type = set, lang = Lang, - sub_els = [#muc_subscribe{jid = #jid{} = SubJid} = Mucsub]}, - StateData) -> + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_subscribe{jid = #jid{} = SubJid} = Mucsub] + }, + StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), - if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> - process_iq_mucsub(SubJid, - #iq{type = set, lang = Lang, - sub_els = [Mucsub#muc_subscribe{jid = undefined}]}, - StateData); - true -> - Txt = ?T("Moderator privileges required"), - {error, xmpp:err_forbidden(Txt, Lang)} + if + FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + process_iq_mucsub(SubJid, + #iq{ + type = set, + lang = Lang, + sub_els = [Mucsub#muc_subscribe{jid = undefined}] + }, + StateData); + true -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} end; process_iq_mucsub(From, - #iq{type = set, lang = Lang, - sub_els = [#muc_subscribe{nick = Nick}]} = Packet, - StateData) -> + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_subscribe{nick = Nick}] + } = Packet, + StateData) -> LBareJID = jid:tolower(jid:remove_resource(From)), try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of - #subscriber{nick = Nick1} when Nick1 /= Nick -> - Nodes = get_subscription_nodes(Packet), - case nick_collision(From, Nick, StateData) of + #subscriber{nick = Nick1} when Nick1 /= Nick -> + Nodes = get_subscription_nodes(Packet), + case nick_collision(From, Nick, StateData) of true -> - ErrText = ?T("That nickname is already in use by another occupant"), - {error, xmpp:err_conflict(ErrText, Lang)}; + ErrText = ?T("That nickname is already in use by another occupant"), + {error, xmpp:err_conflict(ErrText, Lang)}; false -> case mod_muc:can_use_nick(StateData#state.server_host, jid:encode(StateData#state.jid), - From, Nick) of + From, + Nick) of false -> Err = case Nick of <<>> -> @@ -4778,7 +5982,8 @@ process_iq_mucsub(From, _ -> xmpp:err_conflict( ?T("That nickname is registered" - " by another person"), Lang) + " by another person"), + Lang) end, {error, Err}; true -> @@ -4787,174 +5992,225 @@ process_iq_mucsub(From, {result, subscribe_result(Packet), NewStateData} end end; - #subscriber{} -> - Nodes = get_subscription_nodes(Packet), - NewStateData = set_subscriber(From, Nick, Nodes, StateData), - {result, subscribe_result(Packet), NewStateData} - catch _:{badkey, _} -> - SD2 = StateData#state{config = (StateData#state.config)#config{allow_subscription = true}}, - add_new_user(From, Nick, Packet, SD2) + #subscriber{} -> + Nodes = get_subscription_nodes(Packet), + NewStateData = set_subscriber(From, Nick, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData} + catch + _:{badkey, _} -> + SD2 = StateData#state{config = (StateData#state.config)#config{allow_subscription = true}}, + add_new_user(From, Nick, Packet, SD2) end; -process_iq_mucsub(From, #iq{type = set, lang = Lang, - sub_els = [#muc_unsubscribe{jid = #jid{} = UnsubJid}]}, - StateData) -> +process_iq_mucsub(From, + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_unsubscribe{jid = #jid{} = UnsubJid}] + }, + StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), - if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> - process_iq_mucsub(UnsubJid, - #iq{type = set, lang = Lang, - sub_els = [#muc_unsubscribe{jid = undefined}]}, - StateData); - true -> - Txt = ?T("Moderator privileges required"), - {error, xmpp:err_forbidden(Txt, Lang)} + if + FRole == moderator; FAffiliation == owner; FAffiliation == admin -> + process_iq_mucsub(UnsubJid, + #iq{ + type = set, + lang = Lang, + sub_els = [#muc_unsubscribe{jid = undefined}] + }, + StateData); + true -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} end; -process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, - #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> +process_iq_mucsub(From, + #iq{type = set, sub_els = [#muc_unsubscribe{}]}, + #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> BareJID = jid:remove_resource(From), LBareJID = jid:tolower(BareJID), try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of - {MUCSubscribers, #subscriber{nick = Nick}} -> - NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, - store_room(NewStateData, [{del_subscription, LBareJID}]), - Packet1a = #message{ - sub_els = [#ps_event{ - items = #ps_items{ - node = ?NS_MUCSUB_NODES_SUBSCRIBERS, - items = [#ps_item{ - id = p1_rand:get_string(), - sub_els = [#muc_unsubscribe{jid = BareJID, nick = Nick}]}]}}]}, - Packet1b = #message{ - sub_els = [#ps_event{ - items = #ps_items{ - node = ?NS_MUCSUB_NODES_SUBSCRIBERS, - items = [#ps_item{ - id = p1_rand:get_string(), - sub_els = [#muc_unsubscribe{nick = Nick}]}]}}]}, - {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_unsubscribed, ServerHost, {Packet1a, Packet1b}, - [ServerHost, Room, Host, BareJID, StateData]), - send_subscriptions_change_notifications(Packet2a, Packet2b, StateData), - NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of - {stop, normal, _} -> stop; - {next_state, normal_state, SD} -> SD - end, - {result, undefined, NewStateData2} - catch _:{badkey, _} -> - {result, undefined, StateData} + {MUCSubscribers, #subscriber{nick = Nick}} -> + NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, + store_room(NewStateData, [{del_subscription, LBareJID}]), + Packet1a = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_unsubscribe{jid = BareJID, nick = Nick}] + }] + } + }] + }, + Packet1b = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = p1_rand:get_string(), + sub_els = [#muc_unsubscribe{nick = Nick}] + }] + } + }] + }, + {Packet2a, Packet2b} = ejabberd_hooks:run_fold(muc_unsubscribed, + ServerHost, + {Packet1a, Packet1b}, + [ServerHost, Room, Host, BareJID, StateData]), + send_subscriptions_change_notifications(Packet2a, Packet2b, StateData), + NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of + {stop, normal, _} -> stop; + {next_state, normal_state, SD} -> SD + end, + {result, undefined, NewStateData2} + catch + _:{badkey, _} -> + {result, undefined, StateData} end; -process_iq_mucsub(From, #iq{type = get, lang = Lang, - sub_els = [#muc_subscriptions{}]}, - StateData) -> +process_iq_mucsub(From, + #iq{ + type = get, + lang = Lang, + sub_els = [#muc_subscriptions{}] + }, + StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), IsModerator = FRole == moderator orelse FAffiliation == owner orelse - FAffiliation == admin, + FAffiliation == admin, case IsModerator orelse is_subscriber(From, StateData) of - true -> - ShowJid = IsModerator orelse - (StateData#state.config)#config.anonymous == false, - Subs = muc_subscribers_fold( - fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) -> - case ShowJid of - true -> - [#muc_subscription{jid = J, nick = N, events = Nodes}|Acc]; - _ -> - [#muc_subscription{nick = N, events = Nodes}|Acc] - end - end, [], StateData#state.muc_subscribers), - {result, #muc_subscriptions{list = Subs}, StateData}; - _ -> - Txt = ?T("Moderator privileges required"), - {error, xmpp:err_forbidden(Txt, Lang)} + true -> + ShowJid = IsModerator orelse + (StateData#state.config)#config.anonymous == false, + Subs = muc_subscribers_fold( + fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) -> + case ShowJid of + true -> + [#muc_subscription{jid = J, nick = N, events = Nodes} | Acc]; + _ -> + [#muc_subscription{nick = N, events = Nodes} | Acc] + end + end, + [], + StateData#state.muc_subscribers), + {result, #muc_subscriptions{list = Subs}, StateData}; + _ -> + Txt = ?T("Moderator privileges required"), + {error, xmpp:err_forbidden(Txt, Lang)} end; process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), {error, xmpp:err_bad_request(Txt, Lang)}. + -spec remove_subscriptions(state()) -> state(). remove_subscriptions(StateData) -> - if not (StateData#state.config)#config.allow_subscription -> - StateData#state{muc_subscribers = muc_subscribers_new()}; - true -> - StateData + if + not (StateData#state.config)#config.allow_subscription -> + StateData#state{muc_subscribers = muc_subscribers_new()}; + true -> + StateData end. + -spec get_subscription_nodes(stanza()) -> [binary()]. get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) -> lists:filter( fun(Node) -> - lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE, - ?NS_MUCSUB_NODES_MESSAGES, - ?NS_MUCSUB_NODES_AFFILIATIONS, - ?NS_MUCSUB_NODES_SUBJECT, - ?NS_MUCSUB_NODES_CONFIG, - ?NS_MUCSUB_NODES_PARTICIPANTS, - ?NS_MUCSUB_NODES_SUBSCRIBERS]) - end, Nodes); + lists:member(Node, + [?NS_MUCSUB_NODES_PRESENCE, + ?NS_MUCSUB_NODES_MESSAGES, + ?NS_MUCSUB_NODES_AFFILIATIONS, + ?NS_MUCSUB_NODES_SUBJECT, + ?NS_MUCSUB_NODES_CONFIG, + ?NS_MUCSUB_NODES_PARTICIPANTS, + ?NS_MUCSUB_NODES_SUBSCRIBERS]) + end, + Nodes); get_subscription_nodes(_) -> []. + -spec subscribe_result(iq()) -> muc_subscribe(). subscribe_result(#iq{sub_els = [#muc_subscribe{nick = Nick}]} = Packet) -> #muc_subscribe{nick = Nick, events = get_subscription_nodes(Packet)}. + -spec get_title(state()) -> binary(). get_title(StateData) -> case (StateData#state.config)#config.title of - <<"">> -> StateData#state.room; - Name -> Name + <<"">> -> StateData#state.room; + Name -> Name end. + -spec get_roomdesc_reply(jid(), state(), binary()) -> {item, binary()} | false. get_roomdesc_reply(JID, StateData, Tail) -> IsOccupantOrAdmin = is_occupant_or_admin(JID, - StateData), - if (StateData#state.config)#config.public or - IsOccupantOrAdmin -> - if (StateData#state.config)#config.public_list or - IsOccupantOrAdmin -> - {item, <<(get_title(StateData))/binary,Tail/binary>>}; - true -> {item, get_title(StateData)} - end; - true -> false + StateData), + if + (StateData#state.config)#config.public or + IsOccupantOrAdmin -> + if + (StateData#state.config)#config.public_list or + IsOccupantOrAdmin -> + {item, <<(get_title(StateData))/binary, Tail/binary>>}; + true -> {item, get_title(StateData)} + end; + true -> false end. + -spec get_roomdesc_tail(state(), binary()) -> binary(). get_roomdesc_tail(StateData, Lang) -> Desc = case (StateData#state.config)#config.public of - true -> <<"">>; - _ -> translate:translate(Lang, ?T("private, ")) - end, + true -> <<"">>; + _ -> translate:translate(Lang, ?T("private, ")) + end, Len = maps:size(StateData#state.nicks), <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>. + -spec get_mucroom_disco_items(state()) -> disco_items(). get_mucroom_disco_items(StateData) -> Items = maps:fold( - fun(Nick, _, Acc) -> - [#disco_item{jid = jid:make(StateData#state.room, - StateData#state.host, - Nick), - name = Nick}|Acc] - end, [], StateData#state.nicks), + fun(Nick, _, Acc) -> + [#disco_item{ + jid = jid:make(StateData#state.room, + StateData#state.host, + Nick), + name = Nick + } | Acc] + 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()}. + {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) -> +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 + is_admin(From, StateData) of true -> - #adhoc_command{lang = Lang2, node = Node, - action = Action, xdata = XData} = Request, + #adhoc_command{ + lang = Lang2, + node = Node, + action = Action, + xdata = XData + } = Request, Lang = case Lang2 of <<"">> -> Lang1; _ -> Lang2 @@ -4964,46 +6220,55 @@ process_iq_adhoc(From, #iq{type = set, lang = Lang1, {result, xmpp_util:make_adhoc_response( Request, - #adhoc_command{status = canceled, lang = Lang, - node = Node})}; + #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">>} - ]}, + 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})}; + 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 + catch + _:_ -> error end, URI = try hd(xmpp_util:get_xdata_values( <<"hat_uri">>, XData)) - catch _:_ -> error + catch + _:_ -> error end, Title = case xmpp_util:get_xdata_values( <<"hat_title">>, XData) of @@ -5037,37 +6302,42 @@ process_iq_adhoc(From, #iq{type = set, lang = Lang1, {?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">>} - ]}, + 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})}; + 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 + catch + _:_ -> error end, URI = try hd(xmpp_util:get_xdata_values( <<"hat_uri">>, XData)) - catch _:_ -> error + catch + _:_ -> error end, if (JID /= error) and (URI /= error) -> @@ -5094,49 +6364,59 @@ process_iq_adhoc(From, #iq{type = set, lang = Lang1, lists:map( fun({JID, URI, Title}) -> [#xdata_field{ - var = <<"jid">>, - values = [jid:encode(JID)]}, + var = <<"jid">>, + values = [jid:encode(JID)] + }, #xdata_field{ - var = <<"hat_title">>, - values = [URI]}, + var = <<"hat_title">>, + values = [URI] + }, #xdata_field{ - var = <<"hat_uri">>, - values = [Title]}] - end, Hats), + 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}, + 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})}; + 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()} + _ -> + {error, xmpp:err_forbidden()} end. + -spec add_hat(jid(), binary(), binary(), state()) -> - {ok, state()} | {error, size_limit}. + {ok, state()} | {error, size_limit}. add_hat(JID, URI, Title, StateData) -> Hats = StateData#state.hats_users, LJID = jid:remove_resource(jid:tolower(JID)), @@ -5157,6 +6437,7 @@ add_hat(JID, URI, Title, StateData) -> {error, size_limit} end. + -spec del_hat(jid(), binary(), state()) -> state(). del_hat(JID, URI, StateData) -> Hats = StateData#state.hats_users, @@ -5172,6 +6453,7 @@ del_hat(JID, URI, StateData) -> end, StateData#state{hats_users = Hats2}. + -spec get_all_hats(state()) -> list({jid(), binary(), binary()}). get_all_hats(StateData) -> lists:flatmap( @@ -5182,6 +6464,7 @@ get_all_hats(StateData) -> 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 @@ -5204,78 +6487,107 @@ add_presence_hats(JID, Pres, StateData) -> Pres end. + -spec process_iq_moderate(jid(), iq(), binary(), binary() | undefined, state()) -> - {result, undefined, state()} | - {error, stanza_error()}. + {result, undefined, state()} | + {error, stanza_error()}. process_iq_moderate(_From, #iq{type = get}, _Id, _Reason, _StateData) -> {error, xmpp:err_bad_request()}; -process_iq_moderate(From, #iq{type = set, lang = Lang}, Id, Reason, - #state{config = Config, room = Room, host = Host, - jid = JID, server_host = Server} = StateData) -> +process_iq_moderate(From, + #iq{type = set, lang = Lang}, + Id, + Reason, + #state{ + config = Config, + room = Room, + host = Host, + jid = JID, + server_host = Server + } = StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), IsModerator = FRole == moderator orelse FAffiliation == owner orelse - FAffiliation == admin, + FAffiliation == admin, case IsModerator of - false -> - {error, xmpp:err_forbidden( - ?T("Only moderators are allowed to retract messages"), Lang)}; - _ -> - try binary_to_integer(Id) of - StanzaId -> - case Config#config.mam of - true -> - mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId); - _ -> - ok - end, - By = jid:replace_resource(JID, find_nick_by_jid(From, StateData)), - Mod21 = #message_moderated_21{by = By, - reason = Reason, - sub_els = [#message_retract_30{}]}, - SubEl = [#fasten_apply_to{id = Id, - sub_els = [Mod21]}, - #message_retract{id = Id, - reason = Reason, - moderated = #message_moderated{by = By}}], - Packet0 = #message{type = groupchat, - from = From, - sub_els = SubEl}, - {FromNick, _Role} = get_participant_data(From, StateData), + false -> + {error, xmpp:err_forbidden( + ?T("Only moderators are allowed to retract messages"), Lang)}; + _ -> + try binary_to_integer(Id) of + StanzaId -> + case Config#config.mam of + true -> + mod_mam:remove_message_from_archive({Room, Host}, Server, StanzaId); + _ -> + ok + end, + By = jid:replace_resource(JID, find_nick_by_jid(From, StateData)), + Mod21 = #message_moderated_21{ + by = By, + reason = Reason, + sub_els = [#message_retract_30{}] + }, + SubEl = [#fasten_apply_to{ + id = Id, + sub_els = [Mod21] + }, + #message_retract{ + id = Id, + reason = Reason, + moderated = #message_moderated{by = By} + }], + Packet0 = #message{ + type = groupchat, + from = From, + sub_els = SubEl + }, + {FromNick, _Role} = get_participant_data(From, StateData), Packet = ejabberd_hooks:run_fold(muc_filter_message, - StateData#state.server_host, - xmpp:put_meta(Packet0, mam_ignore, true), - [StateData, FromNick]), - send_wrapped_multiple(JID, - get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), - Packet, ?NS_MUCSUB_NODES_MESSAGES, StateData), - NSD = add_message_to_history(<<"">>, - StateData#state.jid, Packet, StateData), - {result, undefined, remove_from_history(StanzaId, NSD)} - catch _:_ -> - {error, xmpp:err_bad_request( - ?T("Stanza id is not valid"), Lang)} - end + StateData#state.server_host, + xmpp:put_meta(Packet0, mam_ignore, true), + [StateData, FromNick]), + send_wrapped_multiple(JID, + get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), + Packet, + ?NS_MUCSUB_NODES_MESSAGES, + StateData), + NSD = add_message_to_history(<<"">>, + StateData#state.jid, + Packet, + StateData), + {result, undefined, remove_from_history(StanzaId, NSD)} + catch + _:_ -> + {error, xmpp:err_bad_request( + ?T("Stanza id is not valid"), Lang)} + end end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Voice request support + -spec prepare_request_form(jid(), binary(), binary()) -> message(). prepare_request_form(Requester, Nick, Lang) -> Title = translate:translate(Lang, ?T("Voice request")), Instruction = translate:translate( - Lang, ?T("Either approve or decline the voice request.")), + Lang, ?T("Either approve or decline the voice request.")), Fs = muc_request:encode([{role, participant}, - {jid, Requester}, - {roomnick, Nick}, - {request_allow, false}], - Lang), - #message{type = normal, - sub_els = [#xdata{type = form, - title = Title, - instructions = [Instruction], - fields = Fs}]}. + {jid, Requester}, + {roomnick, Nick}, + {request_allow, false}], + Lang), + #message{ + type = normal, + sub_els = [#xdata{ + type = form, + title = Title, + instructions = [Instruction], + fields = Fs + }] + }. + -spec send_voice_request(jid(), binary(), state()) -> ok. send_voice_request(From, Lang, StateData) -> @@ -5283,113 +6595,130 @@ send_voice_request(From, Lang, StateData) -> FromNick = find_nick_by_jid(From, StateData), lists:foreach( fun({_, User}) -> - ejabberd_router:route( - xmpp:set_from_to( - prepare_request_form(From, FromNick, Lang), - StateData#state.jid, User#user.jid)) - end, Moderators). + ejabberd_router:route( + xmpp:set_from_to( + prepare_request_form(From, FromNick, Lang), + StateData#state.jid, + User#user.jid)) + end, + Moderators). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support -spec check_invitation(jid(), [muc_invite()], binary(), state()) -> - ok | {error, stanza_error()}. + ok | {error, stanza_error()}. check_invitation(From, Invitations, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), CanInvite = (StateData#state.config)#config.allow_user_invites orelse - FAffiliation == admin orelse FAffiliation == owner, + FAffiliation == admin orelse FAffiliation == owner, case CanInvite of - true -> - case lists:all( - fun(#muc_invite{to = #jid{}}) -> true; - (_) -> false - end, Invitations) of - true -> - ok; - false -> - Txt = ?T("No 'to' attribute found in the invitation"), - {error, xmpp:err_bad_request(Txt, Lang)} - end; - false -> - Txt = ?T("Invitations are not allowed in this conference"), - {error, xmpp:err_not_allowed(Txt, Lang)} + true -> + case lists:all( + fun(#muc_invite{to = #jid{}}) -> true; + (_) -> false + end, + Invitations) of + true -> + ok; + false -> + Txt = ?T("No 'to' attribute found in the invitation"), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + false -> + Txt = ?T("Invitations are not allowed in this conference"), + {error, xmpp:err_not_allowed(Txt, Lang)} end. + -spec route_invitation(jid(), message(), muc_invite(), binary(), state()) -> jid(). route_invitation(From, Pkt, Invitation, Lang, StateData) -> #muc_invite{to = JID, reason = Reason} = Invitation, Invite = Invitation#muc_invite{to = undefined, from = From}, Password = case (StateData#state.config)#config.password_protected of - true -> - (StateData#state.config)#config.password; - false -> - undefined - end, + true -> + (StateData#state.config)#config.password; + false -> + undefined + end, XUser = #muc_user{password = Password, invites = [Invite]}, - XConference = #x_conference{jid = jid:make(StateData#state.room, - StateData#state.host), - reason = Reason}, + XConference = #x_conference{ + jid = jid:make(StateData#state.room, + StateData#state.host), + reason = Reason + }, Body = iolist_to_binary( - [io_lib:format( - translate:translate( - Lang, - ?T("~s invites you to the room ~s")), - [jid:encode(From), - jid:encode({StateData#state.room, StateData#state.host, <<"">>})]), - case (StateData#state.config)#config.password_protected of - true -> - <<", ", - (translate:translate( - Lang, ?T("the password is")))/binary, - " '", - ((StateData#state.config)#config.password)/binary, - "'">>; - _ -> <<"">> - end, - case Reason of - <<"">> -> <<"">>; - _ -> <<" (", Reason/binary, ") ">> - end]), - Msg = #message{from = StateData#state.jid, - to = JID, - type = normal, - body = xmpp:mk_text(Body), - sub_els = [XUser, XConference]}, + [io_lib:format( + translate:translate( + Lang, + ?T("~s invites you to the room ~s")), + [jid:encode(From), + jid:encode({StateData#state.room, StateData#state.host, <<"">>})]), + case (StateData#state.config)#config.password_protected of + true -> + <<", ", + (translate:translate( + Lang, ?T("the password is")))/binary, + " '", + ((StateData#state.config)#config.password)/binary, + "'">>; + _ -> <<"">> + end, + case Reason of + <<"">> -> <<"">>; + _ -> <<" (", Reason/binary, ") ">> + end]), + Msg = #message{ + from = StateData#state.jid, + to = JID, + type = normal, + body = xmpp:mk_text(Body), + sub_els = [XUser, XConference] + }, Msg2 = ejabberd_hooks:run_fold(muc_invite, - StateData#state.server_host, - Msg, - [StateData#state.jid, StateData#state.config, - From, JID, Reason, Pkt]), + StateData#state.server_host, + Msg, + [StateData#state.jid, + StateData#state.config, + From, + JID, + Reason, + Pkt]), ejabberd_router:route(Msg2), JID. + %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. %% Otherwise, an error message is sent to the sender. -spec handle_roommessage_from_nonparticipant(message(), state(), jid()) -> ok. handle_roommessage_from_nonparticipant(Packet, StateData, From) -> try xmpp:try_subtag(Packet, #muc_user{}) of - #muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser -> - NewDecline = Decline#muc_decline{to = undefined, from = From}, - NewXUser = XUser#muc_user{decline = NewDecline}, - NewPacket = xmpp:set_subtag(Packet, NewXUser), - ejabberd_router:route( - xmpp:set_from_to(NewPacket, StateData#state.jid, To)); - _ -> - ErrText = ?T("Only occupants are allowed to send messages " - "to the conference"), - Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), - ejabberd_router:route_error(Packet, Err) - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(Txt, xmpp:get_lang(Packet)), - ejabberd_router:route_error(Packet, Err) + #muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser -> + NewDecline = Decline#muc_decline{to = undefined, from = From}, + NewXUser = XUser#muc_user{decline = NewDecline}, + NewPacket = xmpp:set_subtag(Packet, NewXUser), + ejabberd_router:route( + xmpp:set_from_to(NewPacket, StateData#state.jid, To)); + _ -> + ErrText = ?T("Only occupants are allowed to send messages " + "to the conference"), + Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Logging + add_to_log(Type, Data, StateData) - when Type == roomconfig_change_disabledlogging -> + when Type == roomconfig_change_disabledlogging -> ejabberd_hooks:run(muc_log_add, StateData#state.server_host, [StateData#state.server_host, @@ -5399,20 +6728,22 @@ add_to_log(Type, Data, StateData) make_opts(StateData, false)]); add_to_log(Type, Data, StateData) -> case (StateData#state.config)#config.logging of - true -> - ejabberd_hooks:run(muc_log_add, - StateData#state.server_host, - [StateData#state.server_host, - Type, - Data, - StateData#state.jid, - make_opts(StateData, false)]); - false -> ok + true -> + ejabberd_hooks:run(muc_log_add, + StateData#state.server_host, + [StateData#state.server_host, + Type, + Data, + StateData#state.jid, + make_opts(StateData, false)]); + false -> ok end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Users number checking + -spec tab_add_online_user(jid(), state()) -> any(). tab_add_online_user(JID, StateData) -> Room = StateData#state.room, @@ -5421,6 +6752,7 @@ tab_add_online_user(JID, StateData) -> ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host). + -spec tab_remove_online_user(jid(), state()) -> any(). tab_remove_online_user(JID, StateData) -> Room = StateData#state.room, @@ -5429,61 +6761,71 @@ tab_remove_online_user(JID, StateData) -> ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host). + -spec tab_count_user(jid(), state()) -> non_neg_integer(). tab_count_user(JID, StateData) -> ServerHost = StateData#state.server_host, {LUser, LServer, _} = jid:tolower(JID), mod_muc:count_online_rooms_by_user(ServerHost, LUser, LServer). + -spec element_size(stanza()) -> non_neg_integer(). element_size(El) -> byte_size(fxml:element_to_binary(xmpp:encode(El, ?NS_CLIENT))). + -spec store_room(state()) -> ok. store_room(StateData) -> store_room(StateData, []). + + store_room(StateData, ChangesHints) -> % Let store persistent rooms or on those backends that have get_subscribed_rooms Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), HasGSR = erlang:function_exported(Mod, get_subscribed_rooms, 3), case HasGSR of - true -> - ok; - _ -> - erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers) + true -> + ok; + _ -> + erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers) end, ShouldStore = case (StateData#state.config)#config.persistent of - true -> - true; - _ -> - case ChangesHints of - [] -> - false; - _ -> - HasGSR - end - end, - if ShouldStore -> + true -> + true; + _ -> + case ChangesHints of + [] -> + false; + _ -> + HasGSR + end + end, + if + ShouldStore -> case erlang:function_exported(Mod, store_changes, 4) of true when ChangesHints /= [] -> mod_muc:store_changes( StateData#state.server_host, - StateData#state.host, StateData#state.room, + StateData#state.host, + StateData#state.room, ChangesHints); _ -> store_room_no_checks(StateData, ChangesHints, false), - ok + ok end; - true -> - ok + true -> + ok end. + -spec store_room_no_checks(state(), list(), boolean()) -> {atomic, any()}. store_room_no_checks(StateData, ChangesHints, Hibernation) -> mod_muc:store_room(StateData#state.server_host, - StateData#state.host, StateData#state.room, - make_opts(StateData, Hibernation), - ChangesHints). + StateData#state.host, + StateData#state.room, + make_opts(StateData, Hibernation), + ChangesHints). + -spec send_subscriptions_change_notifications(stanza(), stanza(), state()) -> ok. send_subscriptions_change_notifications(Packet, PacketWithoutJid, State) -> @@ -5491,208 +6833,259 @@ send_subscriptions_change_notifications(Packet, PacketWithoutJid, State) -> maps:fold( fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) -> case (State#state.config)#config.anonymous == false orelse - get_role(JID, State) == moderator orelse - get_default_role(get_affiliation(JID, State), State) == moderator of + get_role(JID, State) == moderator orelse + get_default_role(get_affiliation(JID, State), State) == moderator of true -> {[JID | WithJid], WithNick}; _ -> {WithJid, [JID | WithNick]} end - end, {[], []}, + end, + {[], []}, muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS, State#state.muc_subscribers)), - if WJ /= [] -> - ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, - WJ, Packet, false); - true -> ok + if + WJ /= [] -> + ejabberd_router_multicast:route_multicast(State#state.jid, + State#state.server_host, + WJ, + Packet, + false); + true -> ok end, - if WN /= [] -> - ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, - WN, PacketWithoutJid, false); - true -> ok + if + WN /= [] -> + ejabberd_router_multicast:route_multicast(State#state.jid, + State#state.server_host, + WN, + PacketWithoutJid, + false); + true -> ok end. + -spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok. send_wrapped(From, To, Packet, Node, State) -> LTo = jid:tolower(To), LBareTo = jid:tolower(jid:remove_resource(To)), IsOffline = case maps:get(LTo, State#state.users, error) of - #user{last_presence = undefined} -> true; - error -> true; - _ -> false - end, - if IsOffline -> - try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of - #subscriber{nodes = Nodes, jid = JID} -> - case lists:member(Node, Nodes) of - true -> - MamEnabled = (State#state.config)#config.mam, - Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of - #stanza_id{id = Id2} -> - Id2; - _ -> - p1_rand:get_string() - end, - NewPacket = wrap(From, JID, Packet, Node, Id), - NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), - ejabberd_router:route( - xmpp:set_from_to(NewPacket2, State#state.jid, JID)); - false -> - ok - end - catch _:{badkey, _} -> - ok - end; - true -> - case Packet of - #presence{type = unavailable} -> - case xmpp:get_subtag(Packet, #muc_user{}) of - #muc_user{destroy = Destroy, - status_codes = Codes} -> - case Destroy /= undefined orelse - (lists:member(110,Codes) andalso - not lists:member(303, Codes)) of - true -> - ejabberd_router:route( - #presence{from = State#state.jid, to = To, - id = p1_rand:get_string(), - type = unavailable}); - false -> - ok - end; - _ -> - false - end; - _ -> - ok - end, - ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) + #user{last_presence = undefined} -> true; + error -> true; + _ -> false + end, + if + IsOffline -> + try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of + #subscriber{nodes = Nodes, jid = JID} -> + case lists:member(Node, Nodes) of + true -> + MamEnabled = (State#state.config)#config.mam, + Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of + #stanza_id{id = Id2} -> + Id2; + _ -> + p1_rand:get_string() + end, + NewPacket = wrap(From, JID, Packet, Node, Id), + NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), + ejabberd_router:route( + xmpp:set_from_to(NewPacket2, State#state.jid, JID)); + false -> + ok + end + catch + _:{badkey, _} -> + ok + end; + true -> + case Packet of + #presence{type = unavailable} -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{ + destroy = Destroy, + status_codes = Codes + } -> + case Destroy /= undefined orelse + (lists:member(110, Codes) andalso + not lists:member(303, Codes)) of + true -> + ejabberd_router:route( + #presence{ + from = State#state.jid, + to = To, + id = p1_rand:get_string(), + type = unavailable + }); + false -> + ok + end; + _ -> + false + end; + _ -> + ok + end, + ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) end. + -spec wrap(jid(), undefined | jid(), stanza(), binary(), binary()) -> message(). wrap(From, To, Packet, Node, Id) -> El = xmpp:set_from_to(Packet, From, To), #message{ - id = Id, - sub_els = [#ps_event{ - items = #ps_items{ - node = Node, - items = [#ps_item{ - id = Id, - sub_els = [El]}]}}]}. + id = Id, + sub_els = [#ps_event{ + items = #ps_items{ + node = Node, + items = [#ps_item{ + id = Id, + sub_els = [El] + }] + } + }] + }. + -spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok. send_wrapped_multiple(From, Users, Packet, Node, State) -> {Dir, Wra} = - maps:fold( - fun(_, #user{jid = To, last_presence = LP}, {Direct, Wrapped} = Res) -> - IsOffline = LP == undefined, - if IsOffline -> - LBareTo = jid:tolower(jid:remove_resource(To)), - case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of - {ok, #subscriber{nodes = Nodes}} -> - case lists:member(Node, Nodes) of - true -> - {Direct, [To | Wrapped]}; - _ -> - %% TODO: check that this branch is never called - Res - end; - _ -> - Res - end; - true -> - {[To | Direct], Wrapped} - end - end, {[],[]}, Users), + maps:fold( + fun(_, #user{jid = To, last_presence = LP}, {Direct, Wrapped} = Res) -> + IsOffline = LP == undefined, + if + IsOffline -> + LBareTo = jid:tolower(jid:remove_resource(To)), + case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of + {ok, #subscriber{nodes = Nodes}} -> + case lists:member(Node, Nodes) of + true -> + {Direct, [To | Wrapped]}; + _ -> + %% TODO: check that this branch is never called + Res + end; + _ -> + Res + end; + true -> + {[To | Direct], Wrapped} + end + end, + {[], []}, + Users), case Dir of - [] -> ok; - _ -> - case Packet of - #presence{type = unavailable} -> - case xmpp:get_subtag(Packet, #muc_user{}) of - #muc_user{destroy = Destroy, - status_codes = Codes} -> - case Destroy /= undefined orelse - (lists:member(110,Codes) andalso - not lists:member(303, Codes)) of - true -> - ejabberd_router_multicast:route_multicast( - From, State#state.server_host, Dir, - #presence{id = p1_rand:get_string(), - type = unavailable}, false); - false -> - ok - end; - _ -> - false - end; - _ -> - ok - end, - ejabberd_router_multicast:route_multicast(From, State#state.server_host, - Dir, Packet, false) + [] -> ok; + _ -> + case Packet of + #presence{type = unavailable} -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{ + destroy = Destroy, + status_codes = Codes + } -> + case Destroy /= undefined orelse + (lists:member(110, Codes) andalso + not lists:member(303, Codes)) of + true -> + ejabberd_router_multicast:route_multicast( + From, + State#state.server_host, + Dir, + #presence{ + id = p1_rand:get_string(), + type = unavailable + }, + false); + false -> + ok + end; + _ -> + false + end; + _ -> + ok + end, + ejabberd_router_multicast:route_multicast(From, + State#state.server_host, + Dir, + Packet, + false) end, case Wra of - [] -> ok; - _ -> - MamEnabled = (State#state.config)#config.mam, - Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of - #stanza_id{id = Id2} -> - Id2; - _ -> - p1_rand:get_string() - end, - NewPacket = wrap(From, undefined, Packet, Node, Id), - NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), - ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, - Wra, NewPacket2, true) + [] -> ok; + _ -> + MamEnabled = (State#state.config)#config.mam, + Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of + #stanza_id{id = Id2} -> + Id2; + _ -> + p1_rand:get_string() + end, + NewPacket = wrap(From, undefined, Packet, Node, Id), + NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), + ejabberd_router_multicast:route_multicast(State#state.jid, + State#state.server_host, + Wra, + NewPacket2, + true) end. + %%%---------------------------------------------------------------------- %%% #muc_subscribers API %%%---------------------------------------------------------------------- + -spec muc_subscribers_new() -> #muc_subscribers{}. muc_subscribers_new() -> #muc_subscribers{}. + -spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}. muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) -> maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers). + -spec muc_subscribers_find(ljid(), #muc_subscribers{}) -> - {ok, #subscriber{}} | error. + {ok, #subscriber{}} | error. muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) -> maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers). + -spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean(). muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) -> maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers). + -spec muc_subscribers_size(#muc_subscribers{}) -> integer(). muc_subscribers_size(MUCSubscribers) -> maps:size(MUCSubscribers#muc_subscribers.subscribers). --spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when - Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc). + +-spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc + when Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc). muc_subscribers_fold(Fun, Init, MUCSubscribers) -> maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers). + -spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}]. muc_subscribers_get_by_nick(Nick, MUCSubscribers) -> maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []). + -spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers(). muc_subscribers_get_by_node(Node, MUCSubscribers) -> maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}). + -spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) -> - {#muc_subscribers{}, #subscriber{}}. + {#muc_subscribers{}, #subscriber{}}. muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) -> - #muc_subscribers{subscribers = Subs, - subscriber_nicks = SubNicks, - subscriber_nodes = SubNodes} = MUCSubscribers, + #muc_subscribers{ + subscribers = Subs, + subscriber_nicks = SubNicks, + subscriber_nodes = SubNodes + } = MUCSubscribers, Subscriber = maps:get(LJID, Subs), #subscriber{nick = Nick, nodes = Nodes} = Subscriber, NewSubNicks = maps:remove(Nick, SubNicks), @@ -5703,20 +7096,30 @@ muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) -> NodeSubs = maps:get(Node, Acc, #{}), NodeSubs2 = maps:remove(LJID, NodeSubs), maps:put(Node, NodeSubs2, Acc) - end, SubNodes, Nodes), - {#muc_subscribers{subscribers = NewSubs, - subscriber_nicks = NewSubNicks, - subscriber_nodes = NewSubNodes}, Subscriber}. + end, + SubNodes, + Nodes), + {#muc_subscribers{ + subscribers = NewSubs, + subscriber_nicks = NewSubNicks, + subscriber_nodes = NewSubNodes + }, + Subscriber}. + -spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) -> - #muc_subscribers{}. + #muc_subscribers{}. muc_subscribers_put(Subscriber, MUCSubscribers) -> - #subscriber{jid = JID, - nick = Nick, - nodes = Nodes} = Subscriber, - #muc_subscribers{subscribers = Subs, - subscriber_nicks = SubNicks, - subscriber_nodes = SubNodes} = MUCSubscribers, + #subscriber{ + jid = JID, + nick = Nick, + nodes = Nodes + } = Subscriber, + #muc_subscribers{ + subscribers = Subs, + subscriber_nicks = SubNicks, + subscriber_nodes = SubNodes + } = MUCSubscribers, LJID = jid:tolower(JID), NewSubs = maps:put(LJID, Subscriber, Subs), NewSubNicks = maps:put(Nick, [LJID], SubNicks), @@ -5726,10 +7129,14 @@ muc_subscribers_put(Subscriber, MUCSubscribers) -> NodeSubs = maps:get(Node, Acc, #{}), NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs), maps:put(Node, NodeSubs2, Acc) - end, SubNodes, Nodes), - #muc_subscribers{subscribers = NewSubs, - subscriber_nicks = NewSubNicks, - subscriber_nodes = NewSubNodes}. + end, + SubNodes, + Nodes), + #muc_subscribers{ + subscribers = NewSubs, + subscriber_nicks = NewSubNicks, + subscriber_nodes = NewSubNodes + }. cleanup_affiliations(State) -> @@ -5744,44 +7151,47 @@ cleanup_affiliations(State) -> false -> true end - end, State#state.affiliations), + end, + State#state.affiliations), State#state{affiliations = Affiliations}; false -> State end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaningful content -spec has_body_or_subject(message()) -> boolean(). has_body_or_subject(#message{body = Body, subject = Subj}) -> Body /= [] orelse Subj /= []. + -spec reset_hibernate_timer(state()) -> state(). reset_hibernate_timer(State) -> case State#state.hibernate_timer of - hibernating -> - ok; - _ -> - disable_hibernate_timer(State), - NewTimer = case {mod_muc_opt:hibernation_timeout(State#state.server_host), - maps:size(State#state.users)} of - {infinity, _} -> - none; - {Timeout, 0} -> - p1_fsm:send_event_after(Timeout, hibernate); - _ -> - none - end, - State#state{hibernate_timer = NewTimer} + hibernating -> + ok; + _ -> + disable_hibernate_timer(State), + NewTimer = case {mod_muc_opt:hibernation_timeout(State#state.server_host), + maps:size(State#state.users)} of + {infinity, _} -> + none; + {Timeout, 0} -> + p1_fsm:send_event_after(Timeout, hibernate); + _ -> + none + end, + State#state{hibernate_timer = NewTimer} end. -spec disable_hibernate_timer(state()) -> ok. disable_hibernate_timer(State) -> case State#state.hibernate_timer of - Ref when is_reference(Ref) -> - p1_fsm:cancel_timer(Ref), - ok; - _ -> - ok + Ref when is_reference(Ref) -> + p1_fsm:cancel_timer(Ref), + ok; + _ -> + ok end. diff --git a/src/mod_muc_rtbl.erl b/src/mod_muc_rtbl.erl index 94186e890..7edf875b2 100644 --- a/src/mod_muc_rtbl.erl +++ b/src/mod_muc_rtbl.erl @@ -29,46 +29,72 @@ -behaviour(gen_server). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("translate.hrl"). -include("mod_muc_room.hrl"). %% API --export([start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3, - mod_options/1, mod_opt_type/1, mod_doc/0, depends/2]). +-export([start/2, + stop/1, + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + mod_options/1, + mod_opt_type/1, + mod_doc/0, + depends/2]). -export([pubsub_event_handler/1, muc_presence_filter/3, muc_process_iq/2]). -record(muc_rtbl, {host_id, blank = blank}). -record(rtbl_state, {host, subscribed = false, retry_timer}). + start(Host, _Opts) -> gen_server:start({local, gen_mod:get_module_proc(Host, ?MODULE)}, ?MODULE, [Host], []). + stop(Host) -> gen_server:stop(gen_mod:get_module_proc(Host, ?MODULE)). + init([Host]) -> - ejabberd_mnesia:create(?MODULE, muc_rtbl, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, muc_rtbl)}, - {type, set}]), - ejabberd_hooks:add(local_send_to_resource_hook, Host, - ?MODULE, pubsub_event_handler, 50), - ejabberd_hooks:add(muc_filter_presence, Host, - ?MODULE, muc_presence_filter, 50), - ejabberd_hooks:add(muc_process_iq, Host, - ?MODULE, muc_process_iq, 50), + ejabberd_mnesia:create(?MODULE, + muc_rtbl, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, muc_rtbl)}, + {type, set}]), + ejabberd_hooks:add(local_send_to_resource_hook, + Host, + ?MODULE, + pubsub_event_handler, + 50), + ejabberd_hooks:add(muc_filter_presence, + Host, + ?MODULE, + muc_presence_filter, + 50), + ejabberd_hooks:add(muc_process_iq, + Host, + ?MODULE, + muc_process_iq, + 50), request_initial_items(Host), {ok, #rtbl_state{host = Host}}. + handle_call(_Request, _From, State) -> {noreply, State}. + handle_cast(_Request, State) -> {noreply, State}. + handle_info({iq_reply, IQReply, initial_items}, State) -> State2 = parse_initial_items(State, IQReply), {noreply, State2}; @@ -78,62 +104,88 @@ handle_info({iq_reply, IQReply, subscription}, State) -> handle_info(_Request, State) -> {noreply, State}. + terminate(_Reason, #rtbl_state{host = Host, subscribed = Sub, retry_timer = Timer}) -> - ejabberd_hooks:delete(local_send_to_resource_hook, Host, - ?MODULE, pubsub_event_handler, 50), - ejabberd_hooks:delete(muc_filter_presence, Host, - ?MODULE, muc_presence_filter, 50), - ejabberd_hooks:delete(muc_process_iq, Host, - ?MODULE, muc_process_iq, 50), + ejabberd_hooks:delete(local_send_to_resource_hook, + Host, + ?MODULE, + pubsub_event_handler, + 50), + ejabberd_hooks:delete(muc_filter_presence, + Host, + ?MODULE, + muc_presence_filter, + 50), + ejabberd_hooks:delete(muc_process_iq, + Host, + ?MODULE, + muc_process_iq, + 50), case Sub of - true -> - Jid = service_jid(Host), - IQ = #iq{type = set, from = Jid, to = jid:make(mod_muc_rtbl_opt:rtbl_server(Host)), - sub_els = [ - #pubsub{unsubscribe = - #ps_unsubscribe{jid = Jid, node = mod_muc_rtbl_opt:rtbl_node(Host)}}]}, - ejabberd_router:route_iq(IQ, fun(_) -> ok end); - _ -> - ok + true -> + Jid = service_jid(Host), + IQ = #iq{ + type = set, + from = Jid, + to = jid:make(mod_muc_rtbl_opt:rtbl_server(Host)), + sub_els = [#pubsub{ + unsubscribe = + #ps_unsubscribe{jid = Jid, node = mod_muc_rtbl_opt:rtbl_node(Host)} + }] + }, + ejabberd_router:route_iq(IQ, fun(_) -> ok end); + _ -> + ok end, misc:cancel_timer(Timer). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + request_initial_items(Host) -> - IQ = #iq{type = get, from = service_jid(Host), - to = jid:make(mod_muc_rtbl_opt:rtbl_server(Host)), - sub_els = [ - #pubsub{items = #ps_items{node = mod_muc_rtbl_opt:rtbl_node(Host)}}]}, + IQ = #iq{ + type = get, + from = service_jid(Host), + to = jid:make(mod_muc_rtbl_opt:rtbl_server(Host)), + sub_els = [#pubsub{items = #ps_items{node = mod_muc_rtbl_opt:rtbl_node(Host)}}] + }, ejabberd_router:route_iq(IQ, initial_items, self()). + parse_initial_items(State, timeout) -> ?WARNING_MSG("Fetching initial list failed: fetch timeout. Retrying in 60 seconds", []), State#rtbl_state{retry_timer = erlang:send_after(60000, self(), fetch_list)}; parse_initial_items(State, #iq{type = error} = IQ) -> ?WARNING_MSG("Fetching initial list failed: ~p. Retrying in 60 seconds", - [xmpp:format_stanza_error(xmpp:get_error(IQ))]), + [xmpp:format_stanza_error(xmpp:get_error(IQ))]), State#rtbl_state{retry_timer = erlang:send_after(60000, self(), fetch_list)}; parse_initial_items(State, #iq{from = From, to = #jid{lserver = Host} = To, type = result} = IQ) -> case xmpp:get_subtag(IQ, #pubsub{}) of - #pubsub{items = #ps_items{node = Node, items = Items}} -> - Added = lists:foldl( - fun(#ps_item{id = ID}, Acc) -> - mnesia:dirty_write(#muc_rtbl{host_id = {Host, ID}}), - maps:put(ID, true, Acc) - end, #{}, Items), - SubIQ = #iq{type = set, from = To, to = From, - sub_els = [ - #pubsub{subscribe = #ps_subscribe{jid = To, node = Node}}]}, - ejabberd_router:route_iq(SubIQ, subscription, self()), - notify_rooms(Host, Added), - State#rtbl_state{retry_timer = undefined, subscribed = true}; - _ -> - ?WARNING_MSG("Fetching initial list failed: invalid result payload", []), - State#rtbl_state{retry_timer = undefined} + #pubsub{items = #ps_items{node = Node, items = Items}} -> + Added = lists:foldl( + fun(#ps_item{id = ID}, Acc) -> + mnesia:dirty_write(#muc_rtbl{host_id = {Host, ID}}), + maps:put(ID, true, Acc) + end, + #{}, + Items), + SubIQ = #iq{ + type = set, + from = To, + to = From, + sub_els = [#pubsub{subscribe = #ps_subscribe{jid = To, node = Node}}] + }, + ejabberd_router:route_iq(SubIQ, subscription, self()), + notify_rooms(Host, Added), + State#rtbl_state{retry_timer = undefined, subscribed = true}; + _ -> + ?WARNING_MSG("Fetching initial list failed: invalid result payload", []), + State#rtbl_state{retry_timer = undefined} end. + parse_subscription(State, timeout) -> ?WARNING_MSG("Subscription error: request timeout", []), State#rtbl_state{subscribed = false}; @@ -143,140 +195,169 @@ parse_subscription(State, #iq{type = error} = IQ) -> parse_subscription(State, _) -> State. -pubsub_event_handler(#message{from = #jid{luser = <<>>, lserver = SServer}, - to = #jid{luser = <<>>, lserver = Server, - lresource = <<"rtbl-", _/binary>>}} = Msg) -> + +pubsub_event_handler(#message{ + from = #jid{luser = <<>>, lserver = SServer}, + to = #jid{ + luser = <<>>, + lserver = Server, + lresource = <<"rtbl-", _/binary>> + } + } = Msg) -> SServer2 = mod_muc_rtbl_opt:rtbl_server(Server), SNode = mod_muc_rtbl_opt:rtbl_node(Server), - if SServer == SServer2 -> - case xmpp:get_subtag(Msg, #ps_event{}) of - #ps_event{items = #ps_items{node = Node, retract = [Retract | _] = RetractList}} when Node == SNode, - is_binary(Retract) -> - [mnesia:dirty_delete(muc_rtbl, {Server, R1}) || R1 <- RetractList]; - #ps_event{items = #ps_items{node = Node, items = Items}} when Node == SNode -> - Added = lists:foldl( - fun(#ps_item{id = ID}, Acc) -> - mnesia:dirty_write(#muc_rtbl{host_id = {Server, ID}}), - maps:put(ID, true, Acc) - end, #{}, Items), - case maps:size(Added) of - 0 -> ok; - _ -> notify_rooms(Server, Added) - end; - _ -> - ok - end; - true -> - ok + if + SServer == SServer2 -> + case xmpp:get_subtag(Msg, #ps_event{}) of + #ps_event{items = #ps_items{node = Node, retract = [Retract | _] = RetractList}} when Node == SNode, + is_binary(Retract) -> + [ mnesia:dirty_delete(muc_rtbl, {Server, R1}) || R1 <- RetractList ]; + #ps_event{items = #ps_items{node = Node, items = Items}} when Node == SNode -> + Added = lists:foldl( + fun(#ps_item{id = ID}, Acc) -> + mnesia:dirty_write(#muc_rtbl{host_id = {Server, ID}}), + maps:put(ID, true, Acc) + end, + #{}, + Items), + case maps:size(Added) of + 0 -> ok; + _ -> notify_rooms(Server, Added) + end; + _ -> + ok + end; + true -> + ok end, stop; pubsub_event_handler(_) -> ok. + muc_presence_filter(#presence{from = #jid{lserver = Server} = From, lang = Lang} = Packet, _State, _Nick) -> Blocked = - case mnesia:dirty_read(muc_rtbl, {Server, sha256(Server)}) of - [] -> - JIDs = sha256(jid:encode(jid:tolower(jid:remove_resource(From)))), - case mnesia:dirty_read(muc_rtbl, {Server, JIDs}) of - [] -> false; - _ -> true - end; - _ -> true - end, + case mnesia:dirty_read(muc_rtbl, {Server, sha256(Server)}) of + [] -> + JIDs = sha256(jid:encode(jid:tolower(jid:remove_resource(From)))), + case mnesia:dirty_read(muc_rtbl, {Server, JIDs}) of + [] -> false; + _ -> true + end; + _ -> true + end, case Blocked of - false -> Packet; - _ -> - ErrText = ?T("You have been banned from this room"), - Err = xmpp:err_forbidden(ErrText, Lang), - ejabberd_router:route_error(Packet, Err), - drop + false -> Packet; + _ -> + ErrText = ?T("You have been banned from this room"), + Err = xmpp:err_forbidden(ErrText, Lang), + ejabberd_router:route_error(Packet, Err), + drop end. + muc_process_iq(#iq{type = set, sub_els = [{rtbl_update, Items}]}, #state{users = Users} = State0) -> {NewState, _} = - maps:fold( - fun(_, #user{role = moderator}, {State, HostHashes}) -> - {State, HostHashes}; - ({_, S, _} = LJid, #user{jid = JID}, {State, HostHashes}) -> - {Ban, HH2} = - case maps:find(S, HostHashes) of - {ok, Sha} -> - {maps:is_key(Sha, Items), HostHashes}; - _ -> - Sha = sha256(S), - {maps:is_key(Sha, Items), maps:put(S, Sha, HostHashes)} - end, - Ban2 = - case Ban of - false -> - Sha2 = sha256(jid:encode(jid:remove_resource(LJid))), - maps:is_key(Sha2, Items); - _ -> - true - end, - case Ban2 of - true -> - {_, _, State2} = mod_muc_room:handle_event({process_item_change, - {JID, role, none, <<"Banned by RTBL">>}, - undefined}, - normal_state, State), - {State2, HH2}; - _ -> - {State, HH2} - end - end, {State0, #{}}, Users), + maps:fold( + fun(_, #user{role = moderator}, {State, HostHashes}) -> + {State, HostHashes}; + ({_, S, _} = LJid, #user{jid = JID}, {State, HostHashes}) -> + {Ban, HH2} = + case maps:find(S, HostHashes) of + {ok, Sha} -> + {maps:is_key(Sha, Items), HostHashes}; + _ -> + Sha = sha256(S), + {maps:is_key(Sha, Items), maps:put(S, Sha, HostHashes)} + end, + Ban2 = + case Ban of + false -> + Sha2 = sha256(jid:encode(jid:remove_resource(LJid))), + maps:is_key(Sha2, Items); + _ -> + true + end, + case Ban2 of + true -> + {_, _, State2} = mod_muc_room:handle_event({process_item_change, + {JID, role, none, <<"Banned by RTBL">>}, + undefined}, + normal_state, + State), + {State2, HH2}; + _ -> + {State, HH2} + end + end, + {State0, #{}}, + Users), {stop, {ignore, NewState}}; muc_process_iq(IQ, _State) -> IQ. + sha256(Data) -> Bin = crypto:hash(sha256, Data), str:to_hexlist(Bin). + notify_rooms(Host, Items) -> IQ = #iq{type = set, to = jid:make(Host), sub_els = [{rtbl_update, Items}]}, lists:foreach( - fun(CHost) -> - lists:foreach( - fun({_, _, Pid}) when node(Pid) == node() -> - mod_muc_room:route(Pid, IQ); - (_) -> - ok - end, mod_muc:get_online_rooms(CHost)) - end, mod_muc_admin:find_hosts(Host)). + fun(CHost) -> + lists:foreach( + fun({_, _, Pid}) when node(Pid) == node() -> + mod_muc_room:route(Pid, IQ); + (_) -> + ok + end, + mod_muc:get_online_rooms(CHost)) + end, + mod_muc_admin:find_hosts(Host)). service_jid(Host) -> jid:make(<<>>, Host, <<"rtbl-", (ejabberd_cluster:node_id())/binary>>). + mod_opt_type(rtbl_server) -> econf:domain(); mod_opt_type(rtbl_node) -> econf:non_empty(econf:binary()). + mod_options(_Host) -> [{rtbl_server, <<"xmppbl.org">>}, {rtbl_node, <<"muc_bans_sha256">>}]. + mod_doc() -> - #{desc => - [?T("This module implement Real-time blocklists for MUC rooms."), "", - ?T("It works by observing remote pubsub node conforming with " - "specification described in https://xmppbl.org/.")], + #{ + desc => + [?T("This module implement Real-time blocklists for MUC rooms."), + "", + ?T("It works by observing remote pubsub node conforming with " + "specification described in https://xmppbl.org/.")], note => "added in 23.04", opts => - [{rtbl_server, - #{value => ?T("Domain"), - desc => - ?T("Domain of xmpp server that serves block list. " - "The default value is 'xmppbl.org'")}}, - {rtbl_node, - #{value => "PubsubNodeName", - desc => - ?T("Name of pubsub node that should be used to track blocked users. " - "The default value is 'muc_bans_sha256'.")}}]}. + [{rtbl_server, + #{ + value => ?T("Domain"), + desc => + ?T("Domain of xmpp server that serves block list. " + "The default value is 'xmppbl.org'") + }}, + {rtbl_node, + #{ + value => "PubsubNodeName", + desc => + ?T("Name of pubsub node that should be used to track blocked users. " + "The default value is 'muc_bans_sha256'.") + }}] + }. + depends(_, _) -> [{mod_muc, hard}, {mod_pubsub, soft}]. diff --git a/src/mod_muc_rtbl_opt.erl b/src/mod_muc_rtbl_opt.erl index b9394bd39..d3e49ee9d 100644 --- a/src/mod_muc_rtbl_opt.erl +++ b/src/mod_muc_rtbl_opt.erl @@ -6,15 +6,16 @@ -export([rtbl_node/1]). -export([rtbl_server/1]). + -spec rtbl_node(gen_mod:opts() | global | binary()) -> binary(). rtbl_node(Opts) when is_map(Opts) -> gen_mod:get_opt(rtbl_node, Opts); rtbl_node(Host) -> gen_mod:get_module_opt(Host, mod_muc_rtbl, rtbl_node). + -spec rtbl_server(gen_mod:opts() | global | binary()) -> binary(). rtbl_server(Opts) when is_map(Opts) -> gen_mod:get_opt(rtbl_server, Opts); rtbl_server(Host) -> gen_mod:get_module_opt(Host, mod_muc_rtbl, rtbl_server). - diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl index 31c8703c1..fde13f371 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -24,221 +24,274 @@ -module(mod_muc_sql). - -behaviour(mod_muc). -behaviour(mod_muc_room). %% API --export([init/2, store_room/5, store_changes/4, - restore_room/3, forget_room/3, - can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4, - import/3, export/1]). --export([register_online_room/4, unregister_online_room/4, find_online_room/3, - get_online_rooms/3, count_online_rooms/2, rsm_supported/0, - register_online_user/4, unregister_online_user/4, - count_online_rooms_by_user/3, get_online_rooms_by_user/3, - get_subscribed_rooms/3, get_rooms_without_subscribers/2, - get_hibernated_rooms_older_than/3, - 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]). +-export([init/2, + store_room/5, + store_changes/4, + restore_room/3, + forget_room/3, + can_use_nick/4, + get_rooms/2, + get_nick/3, + set_nick/4, + import/3, + export/1]). +-export([register_online_room/4, + unregister_online_room/4, + find_online_room/3, + get_online_rooms/3, + count_online_rooms/2, + rsm_supported/0, + register_online_user/4, + unregister_online_user/4, + count_online_rooms_by_user/3, + get_online_rooms_by_user/3, + get_subscribed_rooms/3, + get_rooms_without_subscribers/2, + get_hibernated_rooms_older_than/3, + 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]). -export([sql_schemas/0]). -include_lib("xmpp/include/jid.hrl"). + -include("mod_muc.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(Host, Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), case gen_mod:ram_db_mod(Opts, mod_muc) of - ?MODULE -> - clean_tables(Host); - _ -> - ok + ?MODULE -> + clean_tables(Host); + _ -> + ok end. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"muc_room">>, - columns = - [#sql_column{name = <<"name">>, type = text}, - #sql_column{name = <<"host">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"opts">>, type = {text, big}}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"name">>, <<"host">>], - unique = true}, - #sql_index{ - columns = [<<"host">>, <<"created_at">>]}]}, - #sql_table{ - name = <<"muc_registered">>, - columns = - [#sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"host">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"nick">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"jid">>, <<"host">>], - unique = true}, - #sql_index{ - columns = [<<"nick">>]}]}, - #sql_table{ - name = <<"muc_online_room">>, - columns = - [#sql_column{name = <<"name">>, type = text}, - #sql_column{name = <<"host">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"node">>, type = text}, - #sql_column{name = <<"pid">>, type = text}], - indices = [#sql_index{ - columns = [<<"name">>, <<"host">>], - unique = true}]}, - #sql_table{ - name = <<"muc_online_users">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server">>, type = {text, 75}}, - #sql_column{name = <<"resource">>, type = text}, - #sql_column{name = <<"name">>, type = text}, - #sql_column{name = <<"host">>, type = {text, 75}}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"node">>, type = text}], - indices = [#sql_index{ - columns = [<<"username">>, <<"server">>, - <<"resource">>, <<"name">>, - <<"host">>], - unique = true}]}, - #sql_table{ - name = <<"muc_room_subscribers">>, - columns = - [#sql_column{name = <<"room">>, type = text}, - #sql_column{name = <<"host">>, type = text}, - #sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"nick">>, type = text}, - #sql_column{name = <<"nodes">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"host">>, <<"room">>, <<"jid">>], - unique = true}, - #sql_index{ - columns = [<<"host">>, <<"jid">>]}, - #sql_index{ - columns = [<<"jid">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"muc_room">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"opts">>, type = {text, big}}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"name">>, <<"host">>], + unique = true + }, + #sql_index{ + columns = [<<"host">>, <<"created_at">>] + }] + }, + #sql_table{ + name = <<"muc_registered">>, + columns = + [#sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"jid">>, <<"host">>], + unique = true + }, + #sql_index{ + columns = [<<"nick">>] + }] + }, + #sql_table{ + name = <<"muc_online_room">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"pid">>, type = text}], + indices = [#sql_index{ + columns = [<<"name">>, <<"host">>], + unique = true + }] + }, + #sql_table{ + name = <<"muc_online_users">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server">>, type = {text, 75}}, + #sql_column{name = <<"resource">>, type = text}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"host">>, type = {text, 75}}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"node">>, type = text}], + indices = [#sql_index{ + columns = [<<"username">>, + <<"server">>, + <<"resource">>, + <<"name">>, + <<"host">>], + unique = true + }] + }, + #sql_table{ + name = <<"muc_room_subscribers">>, + columns = + [#sql_column{name = <<"room">>, type = text}, + #sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"nodes">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"host">>, <<"room">>, <<"jid">>], + unique = true + }, + #sql_index{ + columns = [<<"host">>, <<"jid">>] + }, + #sql_index{ + columns = [<<"jid">>] + }] + }] + }]. + store_room(LServer, Host, Name, Opts, ChangesHints) -> {Subs, Opts2} = case lists:keytake(subscribers, 1, Opts) of - {value, {subscribers, S}, OptN} -> {S, OptN}; - _ -> {[], Opts} - end, + {value, {subscribers, S}, OptN} -> {S, OptN}; + _ -> {[], Opts} + end, SOpts = misc:term_to_expr(Opts2), Timestamp = case lists:keyfind(hibernation_time, 1, Opts) of - false -> <<"1970-01-02 00:00:00">>; - {_, undefined} -> <<"1970-01-02 00:00:00">>; - {_, Time} -> usec_to_sql_timestamp(Time) - end, - F = fun () -> - ?SQL_UPSERT_T( - "muc_room", - ["!name=%(Name)s", - "!host=%(Host)s", - "server_host=%(LServer)s", - "opts=%(SOpts)s", - "created_at=%(Timestamp)t"]), + false -> <<"1970-01-02 00:00:00">>; + {_, undefined} -> <<"1970-01-02 00:00:00">>; + {_, Time} -> usec_to_sql_timestamp(Time) + end, + F = fun() -> + ?SQL_UPSERT_T( + "muc_room", + ["!name=%(Name)s", + "!host=%(Host)s", + "server_host=%(LServer)s", + "opts=%(SOpts)s", + "created_at=%(Timestamp)t"]), case ChangesHints of Changes when is_list(Changes) -> - [change_room(Host, Name, Change) || Change <- Changes]; + [ change_room(Host, Name, Change) || Change <- Changes ]; _ -> ejabberd_sql:sql_query_t( ?SQL("delete from muc_room_subscribers where " "room=%(Name)s and host=%(Host)s")), - [change_room(Host, Name, {add_subscription, JID, Nick, Nodes}) - || {JID, Nick, Nodes} <- Subs] + [ change_room(Host, Name, {add_subscription, JID, Nick, Nodes}) + || {JID, Nick, Nodes} <- Subs ] end - end, + end, ejabberd_sql:sql_transaction(LServer, F). + store_changes(LServer, Host, Name, Changes) -> - F = fun () -> - [change_room(Host, Name, Change) || Change <- Changes] - end, + F = fun() -> + [ change_room(Host, Name, Change) || Change <- Changes ] + end, ejabberd_sql:sql_transaction(LServer, F). + change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) -> SJID = jid:encode(JID), SNodes = misc:term_to_expr(Nodes), ?SQL_UPSERT_T( - "muc_room_subscribers", - ["!jid=%(SJID)s", - "!host=%(Host)s", - "!room=%(Room)s", - "nick=%(Nick)s", - "nodes=%(SNodes)s"]); + "muc_room_subscribers", + ["!jid=%(SJID)s", + "!host=%(Host)s", + "!room=%(Room)s", + "nick=%(Nick)s", + "nodes=%(SNodes)s"]); change_room(Host, Room, {del_subscription, JID}) -> SJID = jid:encode(JID), ejabberd_sql:sql_query_t(?SQL("delete from muc_room_subscribers where " - "room=%(Room)s and host=%(Host)s and jid=%(SJID)s")); + "room=%(Room)s and host=%(Host)s and jid=%(SJID)s")); change_room(Host, Room, Change) -> ?ERROR_MSG("Unsupported change on room ~ts@~ts: ~p", [Room, Host, Change]). + restore_room(LServer, Host, Name) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(opts)s from muc_room where name=%(Name)s" " and host=%(Host)s")) of - {selected, [{Opts}]} -> - OptsD = ejabberd_sql:decode_term(Opts), - case catch ejabberd_sql:sql_query( - LServer, - ?SQL("select @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers where room=%(Name)s" - " and host=%(Host)s")) of - {selected, []} -> - OptsR = mod_muc:opts_to_binary(OptsD), - case lists:keymember(subscribers, 1, OptsD) of - true -> - store_room(LServer, Host, Name, OptsR, undefined); - _ -> - ok - end, - OptsR; - {selected, Subs} -> - SubData = lists:map( - fun({Jid, Nick, Nodes}) -> - {jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)} - end, Subs), - Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}), - mod_muc:opts_to_binary(Opts2); - _ -> - {error, db_failure} - end; - {selected, _} -> + {selected, [{Opts}]} -> + OptsD = ejabberd_sql:decode_term(Opts), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers where room=%(Name)s" + " and host=%(Host)s")) of + {selected, []} -> + OptsR = mod_muc:opts_to_binary(OptsD), + case lists:keymember(subscribers, 1, OptsD) of + true -> + store_room(LServer, Host, Name, OptsR, undefined); + _ -> + ok + end, + OptsR; + {selected, Subs} -> + SubData = lists:map( + fun({Jid, Nick, Nodes}) -> + {jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)} + end, + Subs), + Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}), + mod_muc:opts_to_binary(Opts2); + _ -> + {error, db_failure} + end; + {selected, _} -> error; - _ -> - {error, db_failure} + _ -> + {error, db_failure} end. + forget_room(LServer, Host, Name) -> - F = fun () -> - ejabberd_sql:sql_query_t( + F = fun() -> + ejabberd_sql:sql_query_t( ?SQL("delete from muc_room where name=%(Name)s" " and host=%(Host)s")), - ejabberd_sql:sql_query_t( - ?SQL("delete from muc_room_subscribers where room=%(Name)s" + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_room_subscribers where room=%(Name)s" " and host=%(Host)s")) - end, + end, ejabberd_sql:sql_transaction(LServer, F). + can_use_nick(LServer, ServiceOrRoom, JID, Nick) -> SJID = jid:encode(jid:tolower(jid:remove_resource(JID))), SqlQuery = case (jid:decode(ServiceOrRoom))#jid.lserver of @@ -252,109 +305,126 @@ can_use_nick(LServer, ServiceOrRoom, JID, Nick) -> " and (host=%(ServiceOrRoom)s or host=%(Service)s)") end, case catch ejabberd_sql:sql_query(LServer, SqlQuery) of - {selected, [{SJID1}]} -> SJID == SJID1; - _ -> true + {selected, [{SJID1}]} -> SJID == SJID1; + _ -> true end. + get_rooms_without_subscribers(LServer, Host) -> case catch ejabberd_sql:sql_query( - LServer, - ?SQL("select @(name)s, @(opts)s from muc_room" - " where host=%(Host)s")) of - {selected, RoomOpts} -> - lists:map( - fun({Room, Opts}) -> - OptsD = ejabberd_sql:decode_term(Opts), - #muc_room{name_host = {Room, Host}, - opts = mod_muc:opts_to_binary(OptsD)} - end, RoomOpts); - _Err -> - [] + LServer, + ?SQL("select @(name)s, @(opts)s from muc_room" + " where host=%(Host)s")) of + {selected, RoomOpts} -> + lists:map( + fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + #muc_room{ + name_host = {Room, Host}, + opts = mod_muc:opts_to_binary(OptsD) + } + end, + RoomOpts); + _Err -> + [] end. + get_hibernated_rooms_older_than(LServer, Host, Timestamp) -> TimestampS = usec_to_sql_timestamp(Timestamp), case catch ejabberd_sql:sql_query( - LServer, - ?SQL("select @(name)s, @(opts)s from muc_room" - " where host=%(Host)s and created_at < %(TimestampS)t and created_at > '1970-01-02 00:00:00'")) of - {selected, RoomOpts} -> - lists:map( - fun({Room, Opts}) -> - OptsD = ejabberd_sql:decode_term(Opts), - #muc_room{name_host = {Room, Host}, - opts = mod_muc:opts_to_binary(OptsD)} - end, RoomOpts); - _Err -> - [] + LServer, + ?SQL("select @(name)s, @(opts)s from muc_room" + " where host=%(Host)s and created_at < %(TimestampS)t and created_at > '1970-01-02 00:00:00'")) of + {selected, RoomOpts} -> + lists:map( + fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + #muc_room{ + name_host = {Room, Host}, + opts = mod_muc:opts_to_binary(OptsD) + } + end, + RoomOpts); + _Err -> + [] end. + get_rooms(LServer, Host) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s, @(opts)s from muc_room" " where host=%(Host)s")) of - {selected, RoomOpts} -> - case catch ejabberd_sql:sql_query( - LServer, - ?SQL("select @(room)s, @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers" - " where host=%(Host)s")) of - {selected, Subs} -> - SubsD = lists:foldl( - fun({Room, Jid, Nick, Nodes}, Dict) -> - Sub = {jid:decode(Jid), - Nick, ejabberd_sql:decode_term(Nodes)}, - maps:update_with( - Room, - fun(SubAcc) -> - [Sub | SubAcc] - end, - [Sub], - Dict) - end, maps:new(), Subs), - lists:map( - fun({Room, Opts}) -> - OptsD = ejabberd_sql:decode_term(Opts), - OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of - {_, true} -> - store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined), - OptsD; - {{ok, SubsI}, false} -> - lists:keystore(subscribers, 1, OptsD, {subscribers, SubsI}); - _ -> - OptsD - end, - #muc_room{name_host = {Room, Host}, - opts = mod_muc:opts_to_binary(OptsD2)} - end, RoomOpts); - _Err -> - [] - end; - _Err -> - [] + {selected, RoomOpts} -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(room)s, @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers" + " where host=%(Host)s")) of + {selected, Subs} -> + SubsD = lists:foldl( + fun({Room, Jid, Nick, Nodes}, Dict) -> + Sub = {jid:decode(Jid), + Nick, + ejabberd_sql:decode_term(Nodes)}, + maps:update_with( + Room, + fun(SubAcc) -> + [Sub | SubAcc] + end, + [Sub], + Dict) + end, + maps:new(), + Subs), + lists:map( + fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of + {_, true} -> + store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined), + OptsD; + {{ok, SubsI}, false} -> + lists:keystore(subscribers, 1, OptsD, {subscribers, SubsI}); + _ -> + OptsD + end, + #muc_room{ + name_host = {Room, Host}, + opts = mod_muc:opts_to_binary(OptsD2) + } + end, + RoomOpts); + _Err -> + [] + end; + _Err -> + [] end. + get_nick(LServer, Host, From) -> SJID = jid:encode(jid:tolower(jid:remove_resource(From))), case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(nick)s from muc_registered where" " jid=%(SJID)s and host=%(Host)s")) of - {selected, [{Nick}]} -> Nick; - _ -> error + {selected, [{Nick}]} -> Nick; + _ -> error end. + set_nick(LServer, ServiceOrRoom, From, Nick) -> JID = jid:encode(jid:tolower(jid:remove_resource(From))), - F = fun () -> - case Nick of - <<"">> -> - ejabberd_sql:sql_query_t( - ?SQL("delete from muc_registered where" + F = fun() -> + case Nick of + <<"">> -> + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_registered where" " jid=%(JID)s and host=%(ServiceOrRoom)s")), - ok; - _ -> - Service = (jid:decode(ServiceOrRoom))#jid.lserver, + ok; + _ -> + Service = (jid:decode(ServiceOrRoom))#jid.lserver, SqlQuery = case (ServiceOrRoom == Service) of true -> ?SQL("select @(jid)s, @(host)s from muc_registered " @@ -365,8 +435,8 @@ set_nick(LServer, ServiceOrRoom, From, Nick) -> "where nick=%(Nick)s" " and (host=%(ServiceOrRoom)s or host=%(Service)s)") end, - Allow = case ejabberd_sql:sql_query_t(SqlQuery) of - {selected, []} + Allow = case ejabberd_sql:sql_query_t(SqlQuery) of + {selected, []} when (ServiceOrRoom == Service) -> %% Registering in the service... %% check if nick is registered for some room in this service @@ -375,67 +445,76 @@ set_nick(LServer, ServiceOrRoom, From, Nick) -> ?SQL("select @(jid)s, @(host)s from muc_registered " "where nick=%(Nick)s")), not lists:any(fun({_NRJid, NRServiceOrRoom}) -> - Service == (jid:decode(NRServiceOrRoom))#jid.lserver end, + Service == (jid:decode(NRServiceOrRoom))#jid.lserver + end, NickRegistrations); - {selected, []} -> + {selected, []} -> %% Nick not registered in any service or room true; - {selected, [{_J, Host}]} + {selected, [{_J, Host}]} when (Host == Service) and (ServiceOrRoom /= Service) -> %% Registering in a room, but the nick is already registered in the service false; - {selected, [{J, _Host}]} -> + {selected, [{J, _Host}]} -> %% Registering in room (or service) a nick that is %% already registered in this room (or service) %% Only the owner of this registration can use the nick J == JID - end, - if Allow -> - ?SQL_UPSERT_T( + end, + if + Allow -> + ?SQL_UPSERT_T( "muc_registered", ["!jid=%(JID)s", "!host=%(ServiceOrRoom)s", "server_host=%(LServer)s", "nick=%(Nick)s"]), - ok; - true -> - false - end - end - end, + ok; + true -> + false + end + end + end, ejabberd_sql:sql_transaction(LServer, F). + set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) -> {error, not_implemented}. + set_affiliations(_ServerHost, _Room, _Host, _Affiliations) -> {error, not_implemented}. + get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) -> {error, not_implemented}. + get_affiliations(_ServerHost, _Room, _Host) -> {error, not_implemented}. + search_affiliation(_ServerHost, _Room, _Host, _Affiliation) -> {error, not_implemented}. + register_online_room(ServerHost, Room, Host, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), case ?SQL_UPSERT(ServerHost, - "muc_online_room", - ["!name=%(Room)s", - "!host=%(Host)s", + "muc_online_room", + ["!name=%(Room)s", + "!host=%(Host)s", "server_host=%(ServerHost)s", - "node=%(NodeS)s", - "pid=%(PidS)s"]) of - ok -> - ok; - Err -> - Err + "node=%(NodeS)s", + "pid=%(PidS)s"]) of + ok -> + ok; + Err -> + Err end. + unregister_online_room(ServerHost, Room, Host, Pid) -> %% TODO: report errors PidS = misc:encode_pid(Pid), @@ -443,114 +522,130 @@ unregister_online_room(ServerHost, Room, Host, Pid) -> ejabberd_sql:sql_query( ServerHost, ?SQL("delete from muc_online_room where name=%(Room)s and " - "host=%(Host)s and node=%(NodeS)s and pid=%(PidS)s")). + "host=%(Host)s and node=%(NodeS)s and pid=%(PidS)s")). + find_online_room(ServerHost, Room, Host) -> case ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(pid)s, @(node)s from muc_online_room where " - "name=%(Room)s and host=%(Host)s")) of - {selected, [{PidS, NodeS}]} -> - try {ok, misc:decode_pid(PidS, NodeS)} - catch _:{bad_node, _} -> error - end; - {selected, []} -> - error; - _Err -> - error + ServerHost, + ?SQL("select @(pid)s, @(node)s from muc_online_room where " + "name=%(Room)s and host=%(Host)s")) of + {selected, [{PidS, NodeS}]} -> + try + {ok, misc:decode_pid(PidS, NodeS)} + catch + _:{bad_node, _} -> error + end; + {selected, []} -> + error; + _Err -> + error end. + find_online_room_by_pid(ServerHost, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), case ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(name)s, @(host)s from muc_online_room where " - "node=%(NodeS)s and pid=%(PidS)s")) of - {selected, [{Room, Host}]} -> - {ok, Room, Host}; - {selected, []} -> - error; - _Err -> - error + ServerHost, + ?SQL("select @(name)s, @(host)s from muc_online_room where " + "node=%(NodeS)s and pid=%(PidS)s")) of + {selected, [{Room, Host}]} -> + {ok, Room, Host}; + {selected, []} -> + error; + _Err -> + error end. + count_online_rooms(ServerHost, Host) -> case ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(count(*))d from muc_online_room " - "where host=%(Host)s")) of - {selected, [{Num}]} -> - Num; - _Err -> - 0 + ServerHost, + ?SQL("select @(count(*))d from muc_online_room " + "where host=%(Host)s")) of + {selected, [{Num}]} -> + Num; + _Err -> + 0 end. + get_online_rooms(ServerHost, Host, _RSM) -> case ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(name)s, @(pid)s, @(node)s from muc_online_room " - "where host=%(Host)s")) of - {selected, Rows} -> - lists:flatmap( - fun({Room, PidS, NodeS}) -> - try [{Room, Host, misc:decode_pid(PidS, NodeS)}] - catch _:{bad_node, _} -> [] - end - end, Rows); - _Err -> - [] + ServerHost, + ?SQL("select @(name)s, @(pid)s, @(node)s from muc_online_room " + "where host=%(Host)s")) of + {selected, Rows} -> + lists:flatmap( + fun({Room, PidS, NodeS}) -> + try + [{Room, Host, misc:decode_pid(PidS, NodeS)}] + catch + _:{bad_node, _} -> [] + end + end, + Rows); + _Err -> + [] end. + rsm_supported() -> false. + register_online_user(ServerHost, {U, S, R}, Room, Host) -> NodeS = erlang:atom_to_binary(node(), latin1), - case ?SQL_UPSERT(ServerHost, "muc_online_users", - ["!username=%(U)s", - "!server=%(S)s", - "!resource=%(R)s", - "!name=%(Room)s", - "!host=%(Host)s", + case ?SQL_UPSERT(ServerHost, + "muc_online_users", + ["!username=%(U)s", + "!server=%(S)s", + "!resource=%(R)s", + "!name=%(Room)s", + "!host=%(Host)s", "server_host=%(ServerHost)s", - "node=%(NodeS)s"]) of - ok -> - ok; - Err -> - Err + "node=%(NodeS)s"]) of + ok -> + ok; + Err -> + Err end. + unregister_online_user(ServerHost, {U, S, R}, Room, Host) -> %% TODO: report errors ejabberd_sql:sql_query( ServerHost, ?SQL("delete from muc_online_users where username=%(U)s and " - "server=%(S)s and resource=%(R)s and name=%(Room)s and " - "host=%(Host)s")). + "server=%(S)s and resource=%(R)s and name=%(Room)s and " + "host=%(Host)s")). + count_online_rooms_by_user(ServerHost, U, S) -> case ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(count(*))d from muc_online_users where " - "username=%(U)s and server=%(S)s")) of - {selected, [{Num}]} -> - Num; - _Err -> - 0 + ServerHost, + ?SQL("select @(count(*))d from muc_online_users where " + "username=%(U)s and server=%(S)s")) of + {selected, [{Num}]} -> + Num; + _Err -> + 0 end. + get_online_rooms_by_user(ServerHost, U, S) -> case ejabberd_sql:sql_query( - ServerHost, - ?SQL("select @(name)s, @(host)s from muc_online_users where " - "username=%(U)s and server=%(S)s")) of - {selected, Rows} -> - Rows; - _Err -> - [] + ServerHost, + ?SQL("select @(name)s, @(host)s from muc_online_users where " + "username=%(U)s and server=%(S)s")) of + {selected, Rows} -> + Rows; + _Err -> + [] end. + export(_Server) -> [{muc_room, fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> @@ -560,50 +655,56 @@ export(_Server) -> [?SQL("delete from muc_room where name=%(Name)s" " and host=%(RoomHost)s;"), ?SQL_INSERT( - "muc_room", - ["name=%(Name)s", - "host=%(RoomHost)s", - "server_host=%(Host)s", - "opts=%(SOpts)s"])]; + "muc_room", + ["name=%(Name)s", + "host=%(RoomHost)s", + "server_host=%(Host)s", + "opts=%(SOpts)s"])]; false -> [] end end}, {muc_registered, - fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, - nick = Nick}) -> + fun(Host, + #muc_registered{ + us_host = {{U, S}, RoomHost}, + nick = Nick + }) -> case str:suffix(Host, RoomHost) of true -> SJID = jid:encode(jid:make(U, S)), [?SQL("delete from muc_registered where" " jid=%(SJID)s and host=%(RoomHost)s;"), ?SQL_INSERT( - "muc_registered", - ["jid=%(SJID)s", - "host=%(RoomHost)s", - "server_host=%(Host)s", - "nick=%(Nick)s"])]; + "muc_registered", + ["jid=%(SJID)s", + "host=%(RoomHost)s", + "server_host=%(Host)s", + "nick=%(Nick)s"])]; false -> [] end end}]. + import(_, _, _) -> ok. + get_subscribed_rooms(LServer, Host, Jid) -> JidS = jid:encode(Jid), case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(room)s, @(nick)s, @(nodes)s from muc_room_subscribers " - "where jid=%(JidS)s and host=%(Host)s")) of - {selected, Subs} -> - {ok, [{jid:make(Room, Host), Nick, ejabberd_sql:decode_term(Nodes)} - || {Room, Nick, Nodes} <- Subs]}; - _Error -> - {error, db_failure} + LServer, + ?SQL("select @(room)s, @(nick)s, @(nodes)s from muc_room_subscribers " + "where jid=%(JidS)s and host=%(Host)s")) of + {selected, Subs} -> + {ok, [ {jid:make(Room, Host), Nick, ejabberd_sql:decode_term(Nodes)} + || {Room, Nick, Nodes} <- Subs ]}; + _Error -> + {error, db_failure} end. + remove_user(LUser, LServer) -> SJID = jid:encode(jid:make(LUser, LServer)), ejabberd_sql:sql_query( @@ -611,6 +712,7 @@ remove_user(LUser, LServer) -> ?SQL("delete from muc_room_subscribers where jid=%(SJID)s")), ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -618,30 +720,31 @@ clean_tables(ServerHost) -> NodeS = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL muc_online_room table...", []), case ejabberd_sql:sql_query( - ServerHost, - ?SQL("delete from muc_online_room where node=%(NodeS)s")) of - {updated, _} -> - ok; - Err1 -> - ?ERROR_MSG("Failed to clean 'muc_online_room' table: ~p", [Err1]), - Err1 + ServerHost, + ?SQL("delete from muc_online_room where node=%(NodeS)s")) of + {updated, _} -> + ok; + Err1 -> + ?ERROR_MSG("Failed to clean 'muc_online_room' table: ~p", [Err1]), + Err1 end, ?DEBUG("Cleaning SQL muc_online_users table...", []), case ejabberd_sql:sql_query( - ServerHost, - ?SQL("delete from muc_online_users where node=%(NodeS)s")) of - {updated, _} -> - ok; - Err2 -> - ?ERROR_MSG("Failed to clean 'muc_online_users' table: ~p", [Err2]), - Err2 + ServerHost, + ?SQL("delete from muc_online_users where node=%(NodeS)s")) of + {updated, _} -> + ok; + Err2 -> + ?ERROR_MSG("Failed to clean 'muc_online_users' table: ~p", [Err2]), + Err2 end. + usec_to_sql_timestamp(Timestamp) -> TS = misc:usec_to_now(Timestamp), case calendar:now_to_universal_time(TS) of - {{Year, Month, Day}, {Hour, Minute, Second}} -> - list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B " - "~2..0B:~2..0B:~2..0B", - [Year, Month, Day, Hour, Minute, Second])) + {{Year, Month, Day}, {Hour, Minute, Second}} -> + list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B " + "~2..0B:~2..0B:~2..0B", + [Year, Month, Day, Hour, Minute, Second])) end. diff --git a/src/mod_muc_sup.erl b/src/mod_muc_sup.erl index 744e20c45..e41e13070 100644 --- a/src/mod_muc_sup.erl +++ b/src/mod_muc_sup.erl @@ -27,41 +27,52 @@ %% Supervisor callbacks -export([init/1]). + %%%=================================================================== %%% API functions %%%=================================================================== start(Host) -> - Spec = #{id => procname(Host), - start => {?MODULE, start_link, [Host]}, - restart => permanent, - shutdown => infinity, - type => supervisor, - modules => [?MODULE]}, + Spec = #{ + id => procname(Host), + start => {?MODULE, start_link, [Host]}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [?MODULE] + }, supervisor:start_child(ejabberd_gen_mod_sup, Spec). + start_link(Host) -> Proc = procname(Host), supervisor:start_link({local, Proc}, ?MODULE, [Host]). + -spec procname(binary()) -> atom(). procname(Host) -> gen_mod:get_module_proc(Host, ?MODULE). + %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([Host]) -> Cores = misc:logical_processors(), Specs = lists:foldl( - fun(I, Acc) -> - [#{id => mod_muc:procname(Host, I), - start => {mod_muc, start_link, [Host, I]}, - restart => permanent, - shutdown => timer:minutes(1), - type => worker, - modules => [mod_muc]}|Acc] - end, [room_sup_spec(Host)], lists:seq(1, Cores)), - {ok, {{one_for_one, 10*Cores, 1}, Specs}}. + fun(I, Acc) -> + [#{ + id => mod_muc:procname(Host, I), + start => {mod_muc, start_link, [Host, I]}, + restart => permanent, + shutdown => timer:minutes(1), + type => worker, + modules => [mod_muc] + } | Acc] + end, + [room_sup_spec(Host)], + lists:seq(1, Cores)), + {ok, {{one_for_one, 10 * Cores, 1}, Specs}}. + %%%=================================================================== %%% Internal functions @@ -69,9 +80,11 @@ init([Host]) -> -spec room_sup_spec(binary()) -> supervisor:child_spec(). room_sup_spec(Host) -> Name = mod_muc_room:supervisor(Host), - #{id => Name, + #{ + id => Name, start => {ejabberd_tmp_sup, start_link, [Name, mod_muc_room]}, restart => permanent, shutdown => infinity, type => supervisor, - modules => [ejabberd_tmp_sup]}. + modules => [ejabberd_tmp_sup] + }. diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index 369d5f92d..9c17a0f89 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -34,40 +34,55 @@ -behaviour(gen_mod). %% API --export([start/2, stop/1, reload/3, - user_send_packet/1]). +-export([start/2, + stop/1, + reload/3, + user_send_packet/1]). %% gen_server callbacks --export([init/1, handle_info/2, handle_call/3, - handle_cast/2, terminate/2, code_change/3]). +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3]). -export([purge_loop/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). --record(multicastc, {rserver :: binary(), - response, - ts :: integer()}). +-record(multicastc, { + rserver :: binary(), + response, + ts :: integer() + }). -type limit_value() :: {default | custom, integer()}. --record(limits, {message :: limit_value(), - presence :: limit_value()}). +-record(limits, { + message :: limit_value(), + presence :: limit_value() + }). --record(service_limits, {local :: #limits{}, - remote :: #limits{}}). +-record(service_limits, { + local :: #limits{}, + remote :: #limits{} + }). --record(state, {lserver :: binary(), - lservice :: binary(), - access :: atom(), - service_limits :: #service_limits{}}). +-record(state, { + lserver :: binary(), + lservice :: binary(), + access :: atom(), + service_limits :: #service_limits{} + }). -type state() :: #state{}. %% All the elements are of type value() -define(PURGE_PROCNAME, - ejabberd_mod_multicast_purgeloop). + ejabberd_mod_multicast_purgeloop). -define(MAXTIME_CACHE_POSITIVE, 86400). @@ -85,18 +100,23 @@ -define(DEFAULT_LIMIT_REMOTE_PRESENCE, 20). + start(LServerS, Opts) -> gen_mod:start_child(?MODULE, LServerS, Opts). + stop(LServerS) -> gen_mod:stop_child(?MODULE, LServerS). + reload(LServerS, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(LServerS, ?MODULE), gen_server:cast(Proc, {reload, NewOpts, OldOpts}). + -define(SETS, gb_sets). + user_send_packet({#presence{} = Packet, C2SState} = Acc) -> case xmpp:get_subtag(Packet, #addresses{}) of #addresses{list = Addresses} -> @@ -121,55 +141,73 @@ user_send_packet({#presence{} = Packet, C2SState} = Acc) -> undefined -> St end - end, C2SState, CC ++ BCC), + end, + C2SState, + CC ++ BCC), {Packet, NewState}; - false -> - Acc + false -> + Acc end; user_send_packet(Acc) -> Acc. + %%==================================================================== %% gen_server callbacks %%==================================================================== + -spec init(list()) -> {ok, state()}. -init([LServerS|_]) -> +init([LServerS | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(LServerS, ?MODULE), - [LServiceS|_] = gen_mod:get_opt_hosts(Opts), + [LServiceS | _] = gen_mod:get_opt_hosts(Opts), Access = mod_multicast_opt:access(Opts), SLimits = build_service_limit_record(mod_multicast_opt:limits(Opts)), create_cache(), try_start_loop(), ejabberd_router_multicast:register_route(LServerS), ejabberd_router:register_route(LServiceS, LServerS), - ejabberd_hooks:add(user_send_packet, LServerS, ?MODULE, - user_send_packet, 50), + ejabberd_hooks:add(user_send_packet, + LServerS, + ?MODULE, + user_send_packet, + 50), {ok, - #state{lservice = LServiceS, lserver = LServerS, - access = Access, service_limits = SLimits}}. + #state{ + lservice = LServiceS, + lserver = LServerS, + access = Access, + service_limits = SLimits + }}. + handle_call(stop, _From, State) -> try_stop_loop(), {stop, normal, ok, State}. + handle_cast({reload, NewOpts, NewOpts}, - #state{lserver = LServerS, lservice = OldLServiceS} = State) -> + #state{lserver = LServerS, lservice = OldLServiceS} = State) -> Access = mod_multicast_opt:access(NewOpts), SLimits = build_service_limit_record(mod_multicast_opt:limits(NewOpts)), - [NewLServiceS|_] = gen_mod:get_opt_hosts(NewOpts), - if NewLServiceS /= OldLServiceS -> - ejabberd_router:register_route(NewLServiceS, LServerS), - ejabberd_router:unregister_route(OldLServiceS); - true -> - ok + [NewLServiceS | _] = gen_mod:get_opt_hosts(NewOpts), + if + NewLServiceS /= OldLServiceS -> + ejabberd_router:register_route(NewLServiceS, LServerS), + ejabberd_router:unregister_route(OldLServiceS); + true -> + ok end, - {noreply, State#state{lservice = NewLServiceS, - access = Access, service_limits = SLimits}}; + {noreply, State#state{ + lservice = NewLServiceS, + access = Access, + service_limits = SLimits + }}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | @@ -177,6 +215,7 @@ handle_cast(Msg, State) -> %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- + handle_info({route, #iq{} = Packet}, State) -> case catch handle_iq(Packet, State) of {'EXIT', Reason} -> @@ -187,17 +226,24 @@ handle_info({route, #iq{} = Packet}, State) -> {noreply, State}; %% XEP33 allows only 'message' and 'presence' stanza type handle_info({route, Packet}, - #state{lservice = LServiceS, lserver = LServerS, - access = Access, service_limits = SLimits} = - State) when ?is_stanza(Packet) -> + #state{ + lservice = LServiceS, + lserver = LServerS, + access = Access, + service_limits = SLimits + } = + State) when ?is_stanza(Packet) -> route_untrusted(LServiceS, LServerS, Access, SLimits, Packet), {noreply, State}; %% Handle multicast packets sent by trusted local services handle_info({route_trusted, Destinations, Packet}, - #state{lservice = LServiceS, lserver = LServerS} = - State) -> + #state{lservice = LServiceS, lserver = LServerS} = + State) -> From = xmpp:get_from(Packet), - case catch route_trusted(LServiceS, LServerS, From, Destinations, + case catch route_trusted(LServiceS, + LServerS, + From, + Destinations, Packet) of {'EXIT', Reason} -> ?ERROR_MSG("Error in route_trusted: ~p", [Reason]); @@ -209,15 +255,21 @@ handle_info({get_host, Pid}, State) -> {noreply, State}; handle_info(_Info, State) -> {noreply, State}. + terminate(_Reason, State) -> - ejabberd_hooks:delete(user_send_packet, State#state.lserver, ?MODULE, - user_send_packet, 50), + ejabberd_hooks:delete(user_send_packet, + State#state.lserver, + ?MODULE, + user_send_packet, + 50), ejabberd_router_multicast:unregister_route(State#state.lserver), ejabberd_router:unregister_route(State#state.lservice), ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%==================================================================== %%% Internal functions %%==================================================================== @@ -226,34 +278,43 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%% IQ Request Processing %%%------------------------ + handle_iq(Packet, State) -> try - IQ = xmpp:decode_els(Packet), - case process_iq(IQ, State) of - {result, SubEl} -> - ejabberd_router:route(xmpp:make_iq_result(Packet, SubEl)); - {error, Error} -> - ejabberd_router:route_error(Packet, Error); - reply -> - To = xmpp:get_to(IQ), - LServiceS = jid:encode(To), - case Packet#iq.type of - result -> - process_iqreply_result(LServiceS, IQ); - error -> - process_iqreply_error(LServiceS, IQ) - end - end - catch _:{xmpp_codec, Why} -> - Lang = xmpp:get_lang(Packet), - Err = xmpp:err_bad_request(xmpp:io_format_error(Why), Lang), - ejabberd_router:route_error(Packet, Err) + IQ = xmpp:decode_els(Packet), + case process_iq(IQ, State) of + {result, SubEl} -> + ejabberd_router:route(xmpp:make_iq_result(Packet, SubEl)); + {error, Error} -> + ejabberd_router:route_error(Packet, Error); + reply -> + To = xmpp:get_to(IQ), + LServiceS = jid:encode(To), + case Packet#iq.type of + result -> + process_iqreply_result(LServiceS, IQ); + error -> + process_iqreply_error(LServiceS, IQ) + end + end + catch + _:{xmpp_codec, Why} -> + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_bad_request(xmpp:io_format_error(Why), Lang), + ejabberd_router:route_error(Packet, Err) end. + -spec process_iq(iq(), state()) -> {result, xmpp_element()} | - {error, stanza_error()} | reply. -process_iq(#iq{type = get, lang = Lang, from = From, - sub_els = [#disco_info{}]}, State) -> + {error, stanza_error()} | + reply. +process_iq(#iq{ + type = get, + lang = Lang, + from = From, + sub_els = [#disco_info{}] + }, + State) -> {result, iq_disco_info(From, Lang, State)}; process_iq(#iq{type = get, sub_els = [#disco_items{}]}, _) -> {result, #disco_items{}}; @@ -264,66 +325,88 @@ process_iq(#iq{type = T}, _) when T == set; T == get -> process_iq(_, _) -> reply. + iq_disco_info(From, Lang, State) -> Name = mod_multicast_opt:name(State#state.lserver), #disco_info{ - identities = [#identity{category = <<"service">>, - type = <<"multicast">>, - name = translate:translate(Lang, Name)}], - features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_VCARD, ?NS_ADDRESS], - xdata = iq_disco_info_extras(From, State)}. + identities = [#identity{ + category = <<"service">>, + type = <<"multicast">>, + name = translate:translate(Lang, Name) + }], + features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_VCARD, ?NS_ADDRESS], + xdata = iq_disco_info_extras(From, State) + }. + -spec iq_vcard(binary(), state()) -> #vcard_temp{}. iq_vcard(Lang, State) -> case mod_multicast_opt:vcard(State#state.lserver) of - undefined -> - #vcard_temp{fn = <<"ejabberd/mod_multicast">>, - url = ejabberd_config:get_uri(), - desc = misc:get_descr(Lang, ?T("ejabberd Multicast service"))}; - VCard -> - VCard + undefined -> + #vcard_temp{ + fn = <<"ejabberd/mod_multicast">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("ejabberd Multicast service")) + }; + VCard -> + VCard end. + %%%------------------------- %%% Route %%%------------------------- + -spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'. route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) -> - Addresses = [#address{type = bcc, jid = D} || D <- Destinations], + 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) -> - try route_untrusted2(LServiceS, LServerS, Access, - SLimits, Packet) + try + route_untrusted2(LServiceS, + LServerS, + Access, + SLimits, + Packet) catch - adenied -> - route_error(Packet, forbidden, - ?T("Access denied by service policy")); - eadsele -> - route_error(Packet, bad_request, - ?T("No addresses element found")); - eadeles -> - route_error(Packet, bad_request, - ?T("No address elements found")); - ewxmlns -> - route_error(Packet, bad_request, - ?T("Wrong xmlns")); - etoorec -> - route_error(Packet, not_acceptable, - ?T("Too many receiver fields were specified")); - edrelay -> - route_error(Packet, forbidden, - ?T("Packet relay is denied by service policy")); - EType:EReason -> - ?ERROR_MSG("Multicast unknown error: Type: ~p~nReason: ~p", - [EType, EReason]), - route_error(Packet, internal_server_error, - ?T("Internal server error")) + adenied -> + route_error(Packet, + forbidden, + ?T("Access denied by service policy")); + eadsele -> + route_error(Packet, + bad_request, + ?T("No addresses element found")); + eadeles -> + route_error(Packet, + bad_request, + ?T("No address elements found")); + ewxmlns -> + route_error(Packet, + bad_request, + ?T("Wrong xmlns")); + etoorec -> + route_error(Packet, + not_acceptable, + ?T("Too many receiver fields were specified")); + edrelay -> + route_error(Packet, + forbidden, + ?T("Packet relay is denied by service policy")); + EType:EReason -> + ?ERROR_MSG("Multicast unknown error: Type: ~p~nReason: ~p", + [EType, EReason]), + route_error(Packet, + internal_server_error, + ?T("Internal server error")) end. + -spec route_untrusted2(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'. route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) -> FromJID = xmpp:get_from(Packet), @@ -337,9 +420,11 @@ route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) -> ok = check_relay(FromJID#jid.server, LServerS, Groups), route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped). + -spec mark_as_delivered([address()]) -> [address()]. mark_as_delivered(Addresses) -> - [A#address{delivered = true} || A <- Addresses]. + [ A#address{delivered = true} || A <- Addresses ]. + -spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok. route_individual(From, CC, BCC, Other, Packet) -> @@ -347,25 +432,29 @@ route_individual(From, CC, BCC, Other, Packet) -> Addresses = CCDelivered ++ Other, PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]), lists:foreach( - fun(#address{jid = To}) -> - ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)) - end, CC), + fun(#address{jid = To}) -> + ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)) + end, + CC), lists:foreach( - 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). + 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; @@ -384,122 +473,149 @@ route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(B 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), + 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, jid:make(Service), CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) - end - end, ok, Groups). + 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, jid:make(Service), CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) + end + end, + ok, + Groups). + %%%------------------------- %%% Check access permission %%%------------------------- + check_access(LServerS, Access, From) -> case acl:match_rule(LServerS, Access, From) of - allow -> ok; - _ -> throw(adenied) + allow -> ok; + _ -> throw(adenied) end. + %%%------------------------- %%% Strip 'addresses' XML element %%%------------------------- + -spec strip_addresses_element(stanza()) -> {ok, stanza(), [address()]}. strip_addresses_element(Packet) -> case xmpp:get_subtag(Packet, #addresses{}) of - #addresses{list = Addrs} -> - PacketStripped = xmpp:remove_subtag(Packet, #addresses{}), - {ok, PacketStripped, Addrs}; - false -> - throw(eadsele) + #addresses{list = Addrs} -> + PacketStripped = xmpp:remove_subtag(Packet, #addresses{}), + {ok, PacketStripped, Addrs}; + false -> + throw(eadsele) end. + %%%------------------------- %%% Split 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). + 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(), integer()) -> ok. check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) -> SenderT = sender_type(FromJID), Limits = get_slimit_group(SenderT, SLimits), StanzaType = type_of_stanza(Packet), {_Type, Limit} = get_limit_number(StanzaType, - Limits), + Limits), case NumOfAddresses > Limit of - false -> ok; - true -> throw(etoorec) + false -> ok; + true -> throw(etoorec) end. -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). + 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 %%%------------------------- + 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). + 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 @@ -509,42 +625,52 @@ group_by_destinations(Addrs, Map) -> %%% Check relay %%%------------------------- + -spec check_relay(binary(), binary(), #{}) -> ok. check_relay(RS, LS, Gs) -> 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) + str:tokens(RS, <<".">>)) orelse + (maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of + true -> ok; + _ -> throw(edrelay) end. + %%%------------------------- %%% Check protocol support: Send request %%%------------------------- + -spec send_query_info(binary(), binary(), binary()) -> ok. send_query_info(RServerS, LServiceS, ID) -> case str:str(RServerS, <<"echo.">>) of - 1 -> ok; - _ -> send_query(RServerS, LServiceS, ID, #disco_info{}) + 1 -> ok; + _ -> send_query(RServerS, LServiceS, ID, #disco_info{}) end. + -spec send_query_items(binary(), binary(), binary()) -> ok. send_query_items(RServerS, LServiceS, ID) -> send_query(RServerS, LServiceS, ID, #disco_items{}). --spec send_query(binary(), binary(), binary(), disco_info()|disco_items()) -> ok. + +-spec send_query(binary(), binary(), binary(), disco_info() | disco_items()) -> ok. send_query(RServerS, LServiceS, ID, SubEl) -> - Packet = #iq{from = stj(LServiceS), - to = stj(RServerS), - id = ID, - type = get, sub_els = [SubEl]}, + Packet = #iq{ + from = stj(LServiceS), + to = stj(RServerS), + id = ID, + type = get, + sub_els = [SubEl] + }, ejabberd_router:route(Packet). + %%%------------------------- %%% Check protocol support: Receive response: Error %%%------------------------- + process_iqreply_error(LServiceS, Packet) -> FromS = jts(xmpp:get_from(Packet)), ID = Packet#iq.id, @@ -552,17 +678,21 @@ process_iqreply_error(LServiceS, Packet) -> [RServer, _] -> case look_server(RServer) of {cached, {_Response, {wait_for_info, ID}}, _TS} - when RServer == FromS -> + when RServer == FromS -> add_response(RServer, not_supported, cached); {cached, {_Response, {wait_for_items, ID}}, _TS} - when RServer == FromS -> + when RServer == FromS -> add_response(RServer, not_supported, cached); {cached, {Response, {wait_for_items_info, ID, Items}}, - _TS} -> + _TS} -> case lists:member(FromS, Items) of true -> received_awaiter( - FromS, RServer, Response, ID, Items, + FromS, + RServer, + Response, + ID, + Items, LServiceS); false -> ok @@ -574,42 +704,56 @@ process_iqreply_error(LServiceS, Packet) -> ok end. + %%%------------------------- %%% Check protocol support: Receive response: Disco %%%------------------------- + -spec process_iqreply_result(binary(), iq()) -> any(). process_iqreply_result(LServiceS, #iq{from = From, id = ID, sub_els = [SubEl]}) -> case SubEl of - #disco_info{} -> - process_discoinfo_result(From, LServiceS, ID, SubEl); - #disco_items{} -> - process_discoitems_result(From, LServiceS, ID, SubEl); - _ -> - ok + #disco_info{} -> + process_discoinfo_result(From, LServiceS, ID, SubEl); + #disco_items{} -> + process_discoitems_result(From, LServiceS, ID, SubEl); + _ -> + ok end. + %%%------------------------- %%% Check protocol support: Receive response: Disco Info %%%------------------------- + process_discoinfo_result(From, LServiceS, ID, DiscoInfo) -> FromS = jts(From), case str:tokens(ID, <<"/">>) of [RServer, _] -> case look_server(RServer) of {cached, {Response, {wait_for_info, ID} = ST}, _TS} - when RServer == FromS -> + when RServer == FromS -> process_discoinfo_result2( - From, FromS, LServiceS, DiscoInfo, - RServer, Response, ST); + From, + FromS, + LServiceS, + DiscoInfo, + RServer, + Response, + ST); {cached, {Response, {wait_for_items_info, ID, Items} = ST}, - _TS} -> + _TS} -> case lists:member(FromS, Items) of true -> process_discoinfo_result2( - From, FromS, LServiceS, DiscoInfo, - RServer, Response, ST); + From, + FromS, + LServiceS, + DiscoInfo, + RServer, + Response, + ST); false -> ok end; @@ -620,69 +764,84 @@ process_discoinfo_result(From, LServiceS, ID, DiscoInfo) -> ok end. -process_discoinfo_result2(From, FromS, LServiceS, - #disco_info{features = Feats} = DiscoInfo, - RServer, Response, ST) -> + +process_discoinfo_result2(From, + FromS, + LServiceS, + #disco_info{features = Feats} = DiscoInfo, + RServer, + Response, + ST) -> Multicast_support = lists:member(?NS_ADDRESS, Feats), case Multicast_support of - true -> - SenderT = sender_type(From), - RLimits = get_limits_xml(DiscoInfo, SenderT), - add_response(RServer, {multicast_supported, FromS, RLimits}, cached); - false -> - case ST of - {wait_for_info, _ID} -> - Random = p1_rand:get_string(), - ID = <>, - send_query_items(FromS, LServiceS, ID), - add_response(RServer, Response, {wait_for_items, ID}); - %% We asked a component, and it does not support XEP33 - {wait_for_items_info, ID, Items} -> - received_awaiter(FromS, RServer, Response, ID, Items, LServiceS) - end + true -> + SenderT = sender_type(From), + RLimits = get_limits_xml(DiscoInfo, SenderT), + add_response(RServer, {multicast_supported, FromS, RLimits}, cached); + false -> + case ST of + {wait_for_info, _ID} -> + Random = p1_rand:get_string(), + ID = <>, + send_query_items(FromS, LServiceS, ID), + add_response(RServer, Response, {wait_for_items, ID}); + %% We asked a component, and it does not support XEP33 + {wait_for_items_info, ID, Items} -> + received_awaiter(FromS, RServer, Response, ID, Items, LServiceS) + end end. + get_limits_xml(DiscoInfo, SenderT) -> LimitOpts = get_limits_els(DiscoInfo), build_remote_limit_record(LimitOpts, SenderT). + -spec get_limits_els(disco_info()) -> [{atom(), integer()}]. get_limits_els(DiscoInfo) -> lists:flatmap( fun(#xdata{type = result} = X) -> - get_limits_fields(X); - (_) -> - [] - end, DiscoInfo#disco_info.xdata). + get_limits_fields(X); + (_) -> + [] + end, + DiscoInfo#disco_info.xdata). + -spec get_limits_fields(xdata()) -> [{atom(), integer()}]. get_limits_fields(X) -> {Head, Tail} = lists:partition( - fun(#xdata_field{var = Var, type = Type}) -> - Var == <<"FORM_TYPE">> andalso Type == hidden - end, X#xdata.fields), + fun(#xdata_field{var = Var, type = Type}) -> + Var == <<"FORM_TYPE">> andalso Type == hidden + end, + X#xdata.fields), case Head of - [] -> []; - _ -> get_limits_values(Tail) + [] -> []; + _ -> get_limits_values(Tail) end. + -spec get_limits_values([xdata_field()]) -> [{atom(), integer()}]. get_limits_values(Fields) -> lists:flatmap( fun(#xdata_field{var = Name, values = [Number]}) -> - try - [{binary_to_atom(Name, utf8), binary_to_integer(Number)}] - catch _:badarg -> - [] - end; - (_) -> - [] - end, Fields). + try + [{binary_to_atom(Name, utf8), binary_to_integer(Number)}] + catch + _:badarg -> + [] + end; + (_) -> + [] + end, + Fields). + %%%------------------------- %%% Check protocol support: Receive response: Disco Items %%%------------------------- + process_discoitems_result(From, LServiceS, ID, #disco_items{items = Items}) -> FromS = jts(From), case str:tokens(ID, <<"/">>) of @@ -690,21 +849,27 @@ process_discoitems_result(From, LServiceS, ID, #disco_items{items = Items}) -> case look_server(RServer) of {cached, {Response, {wait_for_items, ID}}, _TS} -> List = lists:flatmap( - fun(#disco_item{jid = #jid{luser = <<"">>, - lserver = LServer, - lresource = <<"">>}}) -> + fun(#disco_item{ + jid = #jid{ + luser = <<"">>, + lserver = LServer, + lresource = <<"">> + } + }) -> [LServer]; (_) -> [] - end, Items), + end, + Items), case List of [] -> add_response(RServer, not_supported, cached); _ -> Random = p1_rand:get_string(), ID2 = <>, - [send_query_info(Item, LServiceS, ID2) || Item <- List], - add_response(RServer, Response, + [ send_query_info(Item, LServiceS, ID2) || Item <- List ], + add_response(RServer, + Response, {wait_for_items_info, ID2, List}) end; _ -> @@ -714,10 +879,12 @@ process_discoitems_result(From, LServiceS, ID, #disco_items{items = Items}) -> ok end. + %%%------------------------- %%% Check protocol support: Receive response: Received awaiter %%%------------------------- + received_awaiter(JID, RServer, Response, ID, JIDs, _LServiceS) -> case lists:delete(JID, JIDs) of [] -> @@ -726,25 +893,33 @@ received_awaiter(JID, RServer, Response, ID, JIDs, _LServiceS) -> add_response(RServer, Response, {wait_for_items_info, ID, JIDs2}) end. + %%%------------------------- %%% Cache %%%------------------------- + create_cache() -> - ejabberd_mnesia:create(?MODULE, multicastc, - [{ram_copies, [node()]}, - {attributes, record_info(fields, multicastc)}]). + ejabberd_mnesia:create(?MODULE, + multicastc, + [{ram_copies, [node()]}, + {attributes, record_info(fields, multicastc)}]). + add_response(RServer, Response, State) -> Secs = calendar:datetime_to_gregorian_seconds(calendar:local_time()), - mnesia:dirty_write(#multicastc{rserver = RServer, - response = {Response, State}, ts = Secs}). + mnesia:dirty_write(#multicastc{ + rserver = RServer, + response = {Response, State}, + ts = Secs + }). + search_server_on_cache(RServer, LServerS, _LServiceS, _Maxmins) - when RServer == LServerS -> + when RServer == LServerS -> route_single; search_server_on_cache(RServer, _LServerS, LServiceS, _Maxmins) - when RServer == LServiceS -> + when RServer == LServiceS -> route_single; search_server_on_cache(RServer, _LServerS, LServiceS, Maxmins) -> case look_server(RServer) of @@ -778,29 +953,34 @@ search_server_on_cache(RServer, _LServerS, LServiceS, Maxmins) -> end end. + query_info(RServer, LServiceS, Response) -> Random = p1_rand:get_string(), ID = <>, send_query_info(RServer, LServiceS, ID), add_response(RServer, Response, {wait_for_info, ID}). + look_server(RServer) -> case mnesia:dirty_read(multicastc, RServer) of - [] -> not_cached; - [M] -> {cached, M#multicastc.response, M#multicastc.ts} + [] -> not_cached; + [M] -> {cached, M#multicastc.response, M#multicastc.ts} end. + is_obsolete(Response, Ts, Now, {Max_pos, Max_neg}) -> Max = case Response of - multicast_not_supported -> Max_neg; - _ -> Max_pos - end, + multicast_not_supported -> Max_neg; + _ -> Max_pos + end, Now - Ts > Max. + %%%------------------------- %%% Purge cache %%%------------------------- + purge() -> Maxmins_positive = (?MAXTIME_CACHE_POSITIVE), Maxmins_negative = (?MAXTIME_CACHE_NEGATIVE), @@ -808,52 +988,62 @@ purge() -> calendar:datetime_to_gregorian_seconds(calendar:local_time()), purge(Now, {Maxmins_positive, Maxmins_negative}). + purge(Now, Maxmins) -> - F = fun () -> - mnesia:foldl(fun (R, _) -> - #multicastc{response = Response, ts = Ts} = - R, - case is_obsolete(Response, Ts, Now, - Maxmins) - of - true -> mnesia:delete_object(R); - false -> ok - end - end, - none, multicastc) - end, + F = fun() -> + mnesia:foldl(fun(R, _) -> + #multicastc{response = Response, ts = Ts} = + R, + case is_obsolete(Response, + Ts, + Now, + Maxmins) of + true -> mnesia:delete_object(R); + false -> ok + end + end, + none, + multicastc) + end, mnesia:transaction(F). + %%%------------------------- %%% Purge cache loop %%%------------------------- + try_start_loop() -> case lists:member(?PURGE_PROCNAME, registered()) of - true -> ok; - false -> start_loop() + true -> ok; + false -> start_loop() end, (?PURGE_PROCNAME) ! new_module. + start_loop() -> register(?PURGE_PROCNAME, - spawn(?MODULE, purge_loop, [0])), + spawn(?MODULE, purge_loop, [0])), (?PURGE_PROCNAME) ! purge_now. + try_stop_loop() -> (?PURGE_PROCNAME) ! try_stop. + purge_loop(NM) -> receive - purge_now -> - purge(), - timer:send_after(?CACHE_PURGE_TIMER, ?PURGE_PROCNAME, - purge_now), - purge_loop(NM); - new_module -> purge_loop(NM + 1); - try_stop when NM > 1 -> purge_loop(NM - 1); - try_stop -> purge_loop_finished + purge_now -> + purge(), + timer:send_after(?CACHE_PURGE_TIMER, + ?PURGE_PROCNAME, + purge_now), + purge_loop(NM); + new_module -> purge_loop(NM + 1); + try_stop when NM > 1 -> purge_loop(NM - 1); + try_stop -> purge_loop_finished end. + %%%------------------------- %%% Limits: utils %%%------------------------- @@ -865,6 +1055,7 @@ purge_loop(NM) -> %% Type = default | custom %% Number = integer() | infinite + list_of_limits(local) -> [{message, ?DEFAULT_LIMIT_LOCAL_MESSAGE}, {presence, ?DEFAULT_LIMIT_LOCAL_PRESENCE}]; @@ -872,48 +1063,57 @@ list_of_limits(remote) -> [{message, ?DEFAULT_LIMIT_REMOTE_MESSAGE}, {presence, ?DEFAULT_LIMIT_REMOTE_PRESENCE}]. + build_service_limit_record(LimitOpts) -> LimitOptsL = get_from_limitopts(LimitOpts, local), LimitOptsR = get_from_limitopts(LimitOpts, remote), {service_limits, build_limit_record(LimitOptsL, local), - build_limit_record(LimitOptsR, remote)}. + build_limit_record(LimitOptsR, remote)}. + get_from_limitopts(LimitOpts, SenderT) -> case lists:keyfind(SenderT, 1, LimitOpts) of - false -> []; - {SenderT, Result} -> Result + false -> []; + {SenderT, Result} -> Result end. + build_remote_limit_record(LimitOpts, SenderT) -> build_limit_record(LimitOpts, SenderT). + -spec build_limit_record(any(), local | remote) -> #limits{}. build_limit_record(LimitOpts, SenderT) -> - Limits = [get_limit_value(Name, Default, LimitOpts) - || {Name, Default} <- list_of_limits(SenderT)], + Limits = [ get_limit_value(Name, Default, LimitOpts) + || {Name, Default} <- list_of_limits(SenderT) ], list_to_tuple([limits | Limits]). + -spec get_limit_value(atom(), integer(), any()) -> limit_value(). get_limit_value(Name, Default, LimitOpts) -> case lists:keysearch(Name, 1, LimitOpts) of - {value, {Name, Number}} -> {custom, Number}; - false -> {default, Default} + {value, {Name, Number}} -> {custom, Number}; + false -> {default, Default} end. + type_of_stanza(Stanza) -> element(1, Stanza). + -spec get_limit_number(message | presence, #limits{}) -> limit_value(). get_limit_number(message, Limits) -> Limits#limits.message; get_limit_number(presence, Limits) -> Limits#limits.presence. + -spec get_slimit_group(local | remote, #service_limits{}) -> #limits{}. get_slimit_group(local, SLimits) -> SLimits#service_limits.local; get_slimit_group(remote, SLimits) -> SLimits#service_limits.remote. + %%%------------------------- %%% Limits: XEP-0128 Service Discovery Extensions %%%------------------------- @@ -921,54 +1121,62 @@ get_slimit_group(remote, SLimits) -> %% Some parts of code are borrowed from mod_muc_room.erl -define(RFIELDT(Type, Var, Val), - #xdata_field{type = Type, var = Var, values = [Val]}). + #xdata_field{type = Type, var = Var, values = [Val]}). -define(RFIELDV(Var, Val), - #xdata_field{var = Var, values = [Val]}). + #xdata_field{var = Var, values = [Val]}). + iq_disco_info_extras(From, State) -> SenderT = sender_type(From), Service_limits = State#state.service_limits, case iq_disco_info_extras2(SenderT, Service_limits) of - [] -> []; - List_limits_xmpp -> - [#xdata{type = result, - fields = [?RFIELDT(hidden, <<"FORM_TYPE">>, ?NS_ADDRESS) - | List_limits_xmpp]}] + [] -> []; + List_limits_xmpp -> + [#xdata{ + type = result, + fields = [?RFIELDT(hidden, <<"FORM_TYPE">>, ?NS_ADDRESS) | List_limits_xmpp] + }] end. + sender_type(From) -> Local_hosts = ejabberd_option:hosts(), case lists:member(From#jid.lserver, Local_hosts) of - true -> local; - false -> remote + true -> local; + false -> remote end. + iq_disco_info_extras2(SenderT, SLimits) -> Limits = get_slimit_group(SenderT, SLimits), Stanza_types = [message, presence], - lists:foldl(fun (Type_of_stanza, R) -> - case get_limit_number(Type_of_stanza, Limits) of - {custom, Number} -> - [?RFIELDV((to_binary(Type_of_stanza)), - (to_binary(Number))) - | R]; - {default, _} -> R - end - end, - [], Stanza_types). + lists:foldl(fun(Type_of_stanza, R) -> + case get_limit_number(Type_of_stanza, Limits) of + {custom, Number} -> + [?RFIELDV((to_binary(Type_of_stanza)), + (to_binary(Number))) | R]; + {default, _} -> R + end + end, + [], + Stanza_types). + to_binary(A) -> list_to_binary(hd(io_lib:format("~p", [A]))). + %%%------------------------- %%% Error report %%%------------------------- + route_error(Packet, ErrType, ErrText) -> Lang = xmpp:get_lang(Packet), Err = make_reply(ErrType, Lang, ErrText), ejabberd_router:route_error(Packet, Err). + make_reply(bad_request, Lang, ErrText) -> xmpp:err_bad_request(ErrText, Lang); make_reply(jid_malformed, Lang, ErrText) -> @@ -980,27 +1188,37 @@ make_reply(internal_server_error, Lang, ErrText) -> make_reply(forbidden, Lang, ErrText) -> xmpp:err_forbidden(ErrText, Lang). + stj(String) -> jid:decode(String). + jts(String) -> jid:encode(String). + depends(_Host, _Opts) -> []. + mod_opt_type(access) -> econf:acl(); mod_opt_type(name) -> econf:binary(); mod_opt_type(limits) -> econf:options( - #{local => - econf:options( - #{message => econf:non_neg_int(infinite), - presence => econf:non_neg_int(infinite)}), - remote => - econf:options( - #{message => econf:non_neg_int(infinite), - presence => econf:non_neg_int(infinite)})}); + #{ + local => + econf:options( + #{ + message => econf:non_neg_int(infinite), + presence => econf:non_neg_int(infinite) + }), + remote => + econf:options( + #{ + message => econf:non_neg_int(infinite), + presence => econf:non_neg_int(infinite) + }) + }); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> @@ -1008,6 +1226,7 @@ mod_opt_type(hosts) -> mod_opt_type(vcard) -> econf:vcard_temp(). + mod_options(Host) -> [{access, all}, {host, <<"multicast.", Host/binary>>}, @@ -1016,81 +1235,94 @@ mod_options(Host) -> {vcard, undefined}, {name, ?T("Multicast")}]. + mod_doc() -> - #{desc => - [?T("This module implements a service for " - "https://xmpp.org/extensions/xep-0033.html" - "[XEP-0033: Extended Stanza Addressing].")], + #{ + desc => + [?T("This module implements a service for " + "https://xmpp.org/extensions/xep-0033.html" + "[XEP-0033: Extended Stanza Addressing].")], opts => [{access, - #{value => "Access", + #{ + value => "Access", desc => ?T("The access rule to restrict who can send packets to " - "the multicast service. Default value: 'all'.")}}, + "the multicast service. Default value: 'all'.") + }}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, - #{value => ?T("[Host, ...]"), + #{ + value => ?T("[Host, ...]"), desc => [?T("This option defines the Jabber IDs of the service. " - "If the 'hosts' option is not specified, the only " - "Jabber ID will be the hostname of the virtual host " - "with the prefix \"multicast.\". The keyword '@HOST@' " - "is replaced with the real virtual host name."), - ?T("The default value is 'multicast.@HOST@'.")]}}, - {limits, - #{value => "Sender: Stanza: Number", - desc => - [?T("Specify a list of custom limits which override the " - "default ones defined in XEP-0033. Limits are defined " - "per sender type and stanza type, where:"), "", - ?T("- 'sender' can be: 'local' or 'remote'."), - ?T("- 'stanza' can be: 'message' or 'presence'."), - ?T("- 'number' can be a positive integer or 'infinite'.")], + "If the 'hosts' option is not specified, the only " + "Jabber ID will be the hostname of the virtual host " + "with the prefix \"multicast.\". The keyword '@HOST@' " + "is replaced with the real virtual host name."), + ?T("The default value is 'multicast.@HOST@'.")] + }}, + {limits, + #{ + value => "Sender: Stanza: Number", + desc => + [?T("Specify a list of custom limits which override the " + "default ones defined in XEP-0033. Limits are defined " + "per sender type and stanza type, where:"), + "", + ?T("- 'sender' can be: 'local' or 'remote'."), + ?T("- 'stanza' can be: 'message' or 'presence'."), + ?T("- 'number' can be a positive integer or 'infinite'.")], example => - ["# Default values:", - "local:", - " message: 100", - " presence: 100", - "remote:", - " message: 20", - " presence: 20"] - }}, + ["# Default values:", + "local:", + " message: 100", + " presence: 100", + "remote:", + " message: 20", + " presence: 20"] + }}, {name, - #{desc => ?T("Service name to provide in the Info query to the " - "Service Discovery. Default is '\"Multicast\"'.")}}, + #{ + desc => ?T("Service name to provide in the Info query to the " + "Service Discovery. Default is '\"Multicast\"'.") + }}, {vcard, - #{desc => ?T("vCard element to return when queried. " - "Default value is 'undefined'.")}}], + #{ + desc => ?T("vCard element to return when queried. " + "Default value is 'undefined'.") + }}], example => ["# Only admins can send packets to multicast service", - "access_rules:", - " multicast:", - " - allow: admin", - "", - "# If you want to allow all your users:", - "access_rules:", - " multicast:", - " - allow", - "", - "# This allows both admins and remote users to send packets,", - "# but does not allow local users", - "acl:", - " allservers:", - " server_glob: \"*\"", - "access_rules:", - " multicast:", - " - allow: admin", - " - deny: local", - " - allow: allservers", - "", - "modules:", - " mod_multicast:", - " host: multicast.example.org", - " access: multicast", - " limits:", - " local:", - " message: 40", - " presence: infinite", - " remote:", - " message: 150"]}. + "access_rules:", + " multicast:", + " - allow: admin", + "", + "# If you want to allow all your users:", + "access_rules:", + " multicast:", + " - allow", + "", + "# This allows both admins and remote users to send packets,", + "# but does not allow local users", + "acl:", + " allservers:", + " server_glob: \"*\"", + "access_rules:", + " multicast:", + " - allow: admin", + " - deny: local", + " - allow: allservers", + "", + "modules:", + " mod_multicast:", + " host: multicast.example.org", + " access: multicast", + " limits:", + " local:", + " message: 40", + " presence: infinite", + " remote:", + " message: 150"] + }. diff --git a/src/mod_multicast_opt.erl b/src/mod_multicast_opt.erl index bdf709803..4fc157c16 100644 --- a/src/mod_multicast_opt.erl +++ b/src/mod_multicast_opt.erl @@ -10,39 +10,44 @@ -export([name/1]). -export([vcard/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_multicast, access). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_multicast, host). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_multicast, hosts). --spec limits(gen_mod:opts() | global | binary()) -> [{'local',[{'message','infinite' | non_neg_integer()} | {'presence','infinite' | non_neg_integer()}]} | {'remote',[{'message','infinite' | non_neg_integer()} | {'presence','infinite' | non_neg_integer()}]}]. + +-spec limits(gen_mod:opts() | global | binary()) -> [{'local', [{'message', 'infinite' | non_neg_integer()} | {'presence', 'infinite' | non_neg_integer()}]} | {'remote', [{'message', 'infinite' | non_neg_integer()} | {'presence', 'infinite' | non_neg_integer()}]}]. limits(Opts) when is_map(Opts) -> gen_mod:get_opt(limits, Opts); limits(Host) -> gen_mod:get_module_opt(Host, mod_multicast, limits). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_multicast, name). + -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_multicast, vcard). - diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 32277ba7d..6d6b260dd 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -37,41 +37,41 @@ -behaviour(gen_mod). -export([start/2, - stop/1, - reload/3, - store_packet/1, - store_offline_msg/1, - c2s_self_presence/1, - get_sm_features/5, - get_sm_identity/5, - get_sm_items/5, - get_info/5, - handle_offline_query/1, - remove_expired_messages/1, - remove_old_messages/2, - remove_user/2, - import_info/0, - import_start/2, - import/5, - export/1, - get_queue_length/2, - count_offline_messages/2, - get_offline_els/2, - find_x_expire/2, - c2s_handle_info/2, - c2s_copy_session/2, + stop/1, + reload/3, + store_packet/1, + store_offline_msg/1, + c2s_self_presence/1, + get_sm_features/5, + get_sm_identity/5, + get_sm_items/5, + get_info/5, + handle_offline_query/1, + remove_expired_messages/1, + remove_old_messages/2, + remove_user/2, + import_info/0, + import_start/2, + import/5, + export/1, + get_queue_length/2, + count_offline_messages/2, + get_offline_els/2, + find_x_expire/2, + c2s_handle_info/2, + c2s_copy_session/2, get_offline_messages/2, - webadmin_menu_hostuser/4, - webadmin_page_hostuser/4, - webadmin_user/4, - webadmin_user_parse_query/5, - c2s_handle_bind2_inline/1]). + webadmin_menu_hostuser/4, + webadmin_page_hostuser/4, + webadmin_user/4, + webadmin_user_parse_query/5, + c2s_handle_bind2_inline/1]). -export([mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]). -import(ejabberd_web_admin, [make_command/4, make_command/2]). --deprecated({get_queue_length,2}). +-deprecated({get_queue_length, 2}). -include("logger.hrl"). @@ -92,18 +92,19 @@ -type c2s_state() :: ejabberd_c2s:state(). + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(#offline_msg{}) -> ok. -callback store_message(#offline_msg{}) -> ok | {error, any()}. -callback pop_messages(binary(), binary()) -> - {ok, [#offline_msg{}]} | {error, any()}. + {ok, [#offline_msg{}]} | {error, any()}. -callback remove_expired_messages(binary()) -> {atomic, any()}. -callback remove_old_messages(non_neg_integer(), binary()) -> {atomic, any()}. -callback remove_user(binary(), binary()) -> any(). -callback read_message_headers(binary(), binary()) -> - [{non_neg_integer(), jid(), jid(), undefined | erlang:timestamp(), xmlel()}] | error. + [{non_neg_integer(), jid(), jid(), undefined | erlang:timestamp(), xmlel()}] | error. -callback read_message(binary(), binary(), non_neg_integer()) -> - {ok, #offline_msg{}} | error. + {ok, #offline_msg{}} | error. -callback remove_message(binary(), binary(), non_neg_integer()) -> ok | {error, any()}. -callback read_all_messages(binary(), binary()) -> [#offline_msg{}]. -callback remove_all_messages(binary(), binary()) -> {atomic, any()}. @@ -111,17 +112,22 @@ -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer()) -> - {ok, non_neg_integer()} | {error, term()}. + {ok, non_neg_integer()} | {error, term()}. -callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer(), any()) -> - {ok, any(), non_neg_integer()} | {error, term()}. + {ok, any(), non_neg_integer()} | {error, term()}. + +-optional_callbacks([remove_expired_messages/1, + remove_old_messages/2, + use_cache/1, + cache_nodes/1, + remove_old_messages_batch/3, + remove_old_messages_batch/4]). --optional_callbacks([remove_expired_messages/1, remove_old_messages/2, - use_cache/1, cache_nodes/1, remove_old_messages_batch/3, - remove_old_messages_batch/4]). depends(_Host, _Opts) -> []. + start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), @@ -136,37 +142,42 @@ start(Host, Opts) -> {hook, disco_info, get_info, 50}, {hook, c2s_handle_info, c2s_handle_info, 50}, {hook, c2s_copy_session, c2s_copy_session, 50}, - {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, + {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, {hook, webadmin_user, webadmin_user, 50}, - {hook, webadmin_user_parse_query, webadmin_user_parse_query, 50}, + {hook, webadmin_user_parse_query, webadmin_user_parse_query, 50}, {iq_handler, ejabberd_sm, ?NS_FLEX_OFFLINE, handle_offline_query}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), init_cache(NewMod, Host, NewOpts), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end. + init_cache(Mod, Host, Opts) -> CacheOpts = [{max_size, mod_offline_opt:cache_size(Opts)}, - {life_time, mod_offline_opt:cache_life_time(Opts)}, - {cache_missed, false}], + {life_time, mod_offline_opt:cache_life_time(Opts)}, + {cache_missed, false}], case use_cache(Mod, Host) of - true -> - ets_cache:new(?SPOOL_COUNTER_CACHE, CacheOpts); - false -> - ets_cache:delete(?SPOOL_COUNTER_CACHE) + true -> + ets_cache:new(?SPOOL_COUNTER_CACHE, CacheOpts); + false -> + ets_cache:delete(?SPOOL_COUNTER_CACHE) end. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of @@ -174,6 +185,7 @@ use_cache(Mod, Host) -> false -> mod_offline_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of @@ -181,130 +193,160 @@ cache_nodes(Mod, Host) -> false -> ejabberd_cluster:get_nodes() end. + -spec flush_cache(module(), binary(), binary()) -> ok. flush_cache(Mod, User, Server) -> case use_cache(Mod, Server) of - true -> - ets_cache:delete(?SPOOL_COUNTER_CACHE, - {User, Server}, - cache_nodes(Mod, Server)); - false -> - ok + true -> + ets_cache:delete(?SPOOL_COUNTER_CACHE, + {User, Server}, + cache_nodes(Mod, Server)); + false -> + ok end. + -spec store_offline_msg(#offline_msg{}) -> ok | {error, full | any()}. store_offline_msg(#offline_msg{us = {User, Server}, packet = Pkt} = Msg) -> UseMam = use_mam_for_user(User, Server), Mod = gen_mod:db_mod(Server, ?MODULE), case UseMam andalso xmpp:get_meta(Pkt, mam_archived, false) of - true -> - case count_offline_messages(User, Server) of - 0 -> - store_message_in_db(Mod, Msg); - _ -> - case use_cache(Mod, Server) of - true -> - ets_cache:incr( - ?SPOOL_COUNTER_CACHE, - {User, Server}, 1, - cache_nodes(Mod, Server)); - false -> - ok - end - end; - false -> - case get_max_user_messages(User, Server) of - infinity -> - store_message_in_db(Mod, Msg); - Limit -> - Num = count_offline_messages(User, Server), - if Num < Limit -> - store_message_in_db(Mod, Msg); - true -> - {error, full} - end - end + true -> + case count_offline_messages(User, Server) of + 0 -> + store_message_in_db(Mod, Msg); + _ -> + case use_cache(Mod, Server) of + true -> + ets_cache:incr( + ?SPOOL_COUNTER_CACHE, + {User, Server}, + 1, + cache_nodes(Mod, Server)); + false -> + ok + end + end; + false -> + case get_max_user_messages(User, Server) of + infinity -> + store_message_in_db(Mod, Msg); + Limit -> + Num = count_offline_messages(User, Server), + if + Num < Limit -> + store_message_in_db(Mod, Msg); + true -> + {error, full} + end + end end. + get_max_user_messages(User, Server) -> Access = mod_offline_opt:access_max_user_messages(Server), case ejabberd_shaper:match(Server, Access, jid:make(User, Server)) of - Max when is_integer(Max) -> Max; - infinity -> infinity; - _ -> ?MAX_USER_MESSAGES + Max when is_integer(Max) -> Max; + infinity -> infinity; + _ -> ?MAX_USER_MESSAGES end. + get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of - {result, I} -> I; - _ -> [] - end, + {result, I} -> I; + _ -> [] + end, {result, Feats ++ [?NS_FEATURE_MSGOFFLINE, ?NS_FLEX_OFFLINE]}; get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) -> %% override all lesser features... {result, []}; -get_sm_features(_Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, - ?NS_FLEX_OFFLINE, _Lang) -> +get_sm_features(_Acc, + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, + _Lang) -> {result, [?NS_FLEX_OFFLINE]}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -get_sm_identity(Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, - ?NS_FLEX_OFFLINE, _Lang) -> - [#identity{category = <<"automation">>, - type = <<"message-list">>}|Acc]; + +get_sm_identity(Acc, + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, + _Lang) -> + [#identity{ + category = <<"automation">>, + type = <<"message-list">> + } | Acc]; get_sm_identity(Acc, _From, _To, _Node, _Lang) -> Acc. -get_sm_items(_Acc, #jid{luser = U, lserver = S} = JID, - #jid{luser = U, lserver = S}, - ?NS_FLEX_OFFLINE, _Lang) -> + +get_sm_items(_Acc, + #jid{luser = U, lserver = S} = JID, + #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, + _Lang) -> ejabberd_sm:route(JID, {resend_offline, false}), - Mod = gen_mod:db_mod(S, ?MODULE), - Hdrs = case Mod:read_message_headers(U, S) of - L when is_list(L) -> - L; - _ -> - [] - end, - BareJID = jid:remove_resource(JID), - {result, lists:map( - fun({Seq, From, _To, _TS, _El}) -> - Node = integer_to_binary(Seq), - #disco_item{jid = BareJID, - node = Node, - name = jid:encode(From)} - end, Hdrs)}; + Mod = gen_mod:db_mod(S, ?MODULE), + Hdrs = case Mod:read_message_headers(U, S) of + L when is_list(L) -> + L; + _ -> + [] + end, + BareJID = jid:remove_resource(JID), + {result, lists:map( + fun({Seq, From, _To, _TS, _El}) -> + Node = integer_to_binary(Seq), + #disco_item{ + jid = BareJID, + node = Node, + name = jid:encode(From) + } + end, + Hdrs)}; get_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; - ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. -get_info(_Acc, #jid{luser = U, lserver = S} = JID, - #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, Lang) -> + ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. +get_info(_Acc, + #jid{luser = U, lserver = S} = JID, + #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, + Lang) -> ejabberd_sm:route(JID, {resend_offline, false}), - [#xdata{type = result, - fields = flex_offline:encode( - [{number_of_messages, count_offline_messages(U, S)}], - Lang)}]; + [#xdata{ + type = result, + fields = flex_offline:encode( + [{number_of_messages, count_offline_messages(U, S)}], + Lang) + }]; get_info(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec c2s_handle_info(c2s_state(), term()) -> c2s_state(). c2s_handle_info(State, {resend_offline, Flag}) -> {stop, State#{resend_offline => Flag}}; c2s_handle_info(State, _) -> State. + -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{resend_offline := Flag}) -> State#{resend_offline => Flag}; c2s_copy_session(State, _) -> State. + c2s_handle_bind2_inline({#{jid := #jid{luser = LUser, lserver = LServer}} = State, Els, Results}) -> case mod_mam:is_archiving_enabled(LUser, LServer) of true -> @@ -314,385 +356,440 @@ c2s_handle_bind2_inline({#{jid := #jid{luser = LUser, lserver = LServer}} = Stat end, {State, Els, Results}. + -spec handle_offline_query(iq()) -> iq(). -handle_offline_query(#iq{from = #jid{luser = U1, lserver = S1}, - to = #jid{luser = U2, lserver = S2}, - lang = Lang, - sub_els = [#offline{}]} = IQ) +handle_offline_query(#iq{ + from = #jid{luser = U1, lserver = S1}, + to = #jid{luser = U2, lserver = S2}, + lang = Lang, + sub_els = [#offline{}] + } = IQ) when {U1, S1} /= {U2, S2} -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); -handle_offline_query(#iq{from = #jid{luser = U, lserver = S} = From, - to = #jid{luser = U, lserver = S} = _To, - type = Type, lang = Lang, - sub_els = [#offline{} = Offline]} = IQ) -> +handle_offline_query(#iq{ + from = #jid{luser = U, lserver = S} = From, + to = #jid{luser = U, lserver = S} = _To, + type = Type, + lang = Lang, + sub_els = [#offline{} = Offline] + } = IQ) -> case {Type, Offline} of - {get, #offline{fetch = true, items = [], purge = false}} -> - %% TODO: report database errors - handle_offline_fetch(From), - xmpp:make_iq_result(IQ); - {get, #offline{fetch = false, items = [_|_] = Items, purge = false}} -> - case handle_offline_items_view(From, Items) of - true -> xmpp:make_iq_result(IQ); - false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) - end; - {set, #offline{fetch = false, items = [], purge = true}} -> - case delete_all_msgs(U, S) of - {atomic, ok} -> - xmpp:make_iq_result(IQ); - _Err -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) - end; - {set, #offline{fetch = false, items = [_|_] = Items, purge = false}} -> - case handle_offline_items_remove(From, Items) of - true -> xmpp:make_iq_result(IQ); - false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) - end; - _ -> - xmpp:make_error(IQ, xmpp:err_bad_request()) + {get, #offline{fetch = true, items = [], purge = false}} -> + %% TODO: report database errors + handle_offline_fetch(From), + xmpp:make_iq_result(IQ); + {get, #offline{fetch = false, items = [_ | _] = Items, purge = false}} -> + case handle_offline_items_view(From, Items) of + true -> xmpp:make_iq_result(IQ); + false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) + end; + {set, #offline{fetch = false, items = [], purge = true}} -> + case delete_all_msgs(U, S) of + {atomic, ok} -> + xmpp:make_iq_result(IQ); + _Err -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; + {set, #offline{fetch = false, items = [_ | _] = Items, purge = false}} -> + case handle_offline_items_remove(From, Items) of + true -> xmpp:make_iq_result(IQ); + false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) + end; + _ -> + xmpp:make_error(IQ, xmpp:err_bad_request()) end; handle_offline_query(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec handle_offline_items_view(jid(), [offline_item()]) -> boolean(). handle_offline_items_view(JID, Items) -> {U, S, R} = jid:tolower(JID), case use_mam_for_user(U, S) of - true -> - false; - _ -> - lists:foldl( - fun(#offline_item{node = Node, action = view}, Acc) -> - case fetch_msg_by_node(JID, Node) of - {ok, OfflineMsg} -> - case offline_msg_to_route(S, OfflineMsg) of - {route, El} -> - NewEl = set_offline_tag(El, Node), - case ejabberd_sm:get_session_pid(U, S, R) of - Pid when is_pid(Pid) -> - ejabberd_c2s:route(Pid, {route, NewEl}); - none -> - ok - end, - Acc or true; - error -> - Acc or false - end; - error -> - Acc or false - end - end, false, Items) end. + true -> + false; + _ -> + lists:foldl( + fun(#offline_item{node = Node, action = view}, Acc) -> + case fetch_msg_by_node(JID, Node) of + {ok, OfflineMsg} -> + case offline_msg_to_route(S, OfflineMsg) of + {route, El} -> + NewEl = set_offline_tag(El, Node), + case ejabberd_sm:get_session_pid(U, S, R) of + Pid when is_pid(Pid) -> + ejabberd_c2s:route(Pid, {route, NewEl}); + none -> + ok + end, + Acc or true; + error -> + Acc or false + end; + error -> + Acc or false + end + end, + false, + Items) + end. + -spec handle_offline_items_remove(jid(), [offline_item()]) -> boolean(). handle_offline_items_remove(JID, Items) -> {U, S, _R} = jid:tolower(JID), case use_mam_for_user(U, S) of - true -> - false; - _ -> - lists:foldl( - fun(#offline_item{node = Node, action = remove}, Acc) -> - Acc or remove_msg_by_node(JID, Node) - end, false, Items) + true -> + false; + _ -> + lists:foldl( + fun(#offline_item{node = Node, action = remove}, Acc) -> + Acc or remove_msg_by_node(JID, Node) + end, + false, + Items) end. + -spec set_offline_tag(message(), binary()) -> message(). set_offline_tag(Msg, Node) -> xmpp:set_subtag(Msg, #offline{items = [#offline_item{node = Node}]}). + -spec handle_offline_fetch(jid()) -> ok. handle_offline_fetch(#jid{luser = U, lserver = S} = JID) -> ejabberd_sm:route(JID, {resend_offline, false}), lists:foreach( - fun({Node, El}) -> - El1 = set_offline_tag(El, Node), - ejabberd_router:route(El1) - end, read_messages(U, S)). + fun({Node, El}) -> + El1 = set_offline_tag(El, Node), + ejabberd_router:route(El1) + end, + read_messages(U, S)). + -spec fetch_msg_by_node(jid(), binary()) -> error | {ok, #offline_msg{}}. fetch_msg_by_node(To, Seq) -> case catch binary_to_integer(Seq) of - I when is_integer(I), I >= 0 -> - LUser = To#jid.luser, - LServer = To#jid.lserver, - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:read_message(LUser, LServer, I); - _ -> - error + I when is_integer(I), I >= 0 -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_message(LUser, LServer, I); + _ -> + error end. + -spec remove_msg_by_node(jid(), binary()) -> boolean(). remove_msg_by_node(To, Seq) -> case catch binary_to_integer(Seq) of - I when is_integer(I), I>= 0 -> - LUser = To#jid.luser, - LServer = To#jid.lserver, - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:remove_message(LUser, LServer, I), - flush_cache(Mod, LUser, LServer), - true; - _ -> - false + I when is_integer(I), I >= 0 -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_message(LUser, LServer, I), + flush_cache(Mod, LUser, LServer), + true; + _ -> + false end. + -spec need_to_store(binary(), message()) -> boolean(). need_to_store(_LServer, #message{type = error}) -> false; need_to_store(LServer, #message{type = Type} = Packet) -> case xmpp:has_subtag(Packet, #offline{}) of - false -> - case misc:unwrap_mucsub_message(Packet) of - #message{type = groupchat} = Msg -> - need_to_store(LServer, Msg#message{type = chat}); - #message{} = Msg -> - need_to_store(LServer, Msg); - _ -> - case check_store_hint(Packet) of - store -> - true; - no_store -> - false; - none -> - Store = case Type of - groupchat -> - mod_offline_opt:store_groupchat(LServer); - headline -> - false; - _ -> - true - end, - case {misc:get_mucsub_event_type(Packet), Store, - mod_offline_opt:store_empty_body(LServer)} of - {?NS_MUCSUB_NODES_PRESENCE, _, _} -> - false; - {_, false, _} -> - false; - {_, _, true} -> - true; - {_, _, false} -> - Packet#message.body /= []; - {_, _, unless_chat_state} -> - not misc:is_standalone_chat_state(Packet) - end - end - end; - true -> - false + false -> + case misc:unwrap_mucsub_message(Packet) of + #message{type = groupchat} = Msg -> + need_to_store(LServer, Msg#message{type = chat}); + #message{} = Msg -> + need_to_store(LServer, Msg); + _ -> + case check_store_hint(Packet) of + store -> + true; + no_store -> + false; + none -> + Store = case Type of + groupchat -> + mod_offline_opt:store_groupchat(LServer); + headline -> + false; + _ -> + true + end, + case {misc:get_mucsub_event_type(Packet), + Store, + mod_offline_opt:store_empty_body(LServer)} of + {?NS_MUCSUB_NODES_PRESENCE, _, _} -> + false; + {_, false, _} -> + false; + {_, _, true} -> + true; + {_, _, false} -> + Packet#message.body /= []; + {_, _, unless_chat_state} -> + not misc:is_standalone_chat_state(Packet) + end + end + end; + true -> + false end. + -spec store_packet({any(), message()}) -> {any(), message()}. store_packet({_Action, #message{from = From, to = To} = Packet} = Acc) -> case need_to_store(To#jid.lserver, Packet) of - true -> - case check_event(Packet) of - true -> - #jid{luser = LUser, lserver = LServer} = To, - TimeStamp = erlang:timestamp(), - Expire = find_x_expire(TimeStamp, Packet), - OffMsg = #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, - expire = Expire, - from = From, - to = To, - packet = Packet}, - case store_offline_msg(OffMsg) of - ok -> - {offlined, Packet}; - {error, Reason} -> - discard_warn_sender(Packet, Reason), - stop - end; - _ -> - maybe_update_cache(To, Packet), - Acc - end; - false -> - maybe_update_cache(To, Packet), - Acc + true -> + case check_event(Packet) of + true -> + #jid{luser = LUser, lserver = LServer} = To, + TimeStamp = erlang:timestamp(), + Expire = find_x_expire(TimeStamp, Packet), + OffMsg = #offline_msg{ + us = {LUser, LServer}, + timestamp = TimeStamp, + expire = Expire, + from = From, + to = To, + packet = Packet + }, + case store_offline_msg(OffMsg) of + ok -> + {offlined, Packet}; + {error, Reason} -> + discard_warn_sender(Packet, Reason), + stop + end; + _ -> + maybe_update_cache(To, Packet), + Acc + end; + false -> + maybe_update_cache(To, Packet), + Acc end. + -spec maybe_update_cache(jid(), message()) -> ok. maybe_update_cache(#jid{lserver = Server, luser = User}, Packet) -> case xmpp:get_meta(Packet, mam_archived, false) of - true -> - Mod = gen_mod:db_mod(Server, ?MODULE), - case use_mam_for_user(User, Server) andalso use_cache(Mod, Server) of - true -> - ets_cache:incr( - ?SPOOL_COUNTER_CACHE, - {User, Server}, 1, - cache_nodes(Mod, Server)); - _ -> - ok - end; - _ -> - ok + true -> + Mod = gen_mod:db_mod(Server, ?MODULE), + case use_mam_for_user(User, Server) andalso use_cache(Mod, Server) of + true -> + ets_cache:incr( + ?SPOOL_COUNTER_CACHE, + {User, Server}, + 1, + cache_nodes(Mod, Server)); + _ -> + ok + end; + _ -> + ok end. + -spec check_store_hint(message()) -> store | no_store | none. check_store_hint(Packet) -> case has_store_hint(Packet) of - true -> - store; - false -> - case has_no_store_hint(Packet) of - true -> - no_store; - false -> - none - end + true -> + store; + false -> + case has_no_store_hint(Packet) of + true -> + no_store; + false -> + none + end end. + -spec has_store_hint(message()) -> boolean(). has_store_hint(Packet) -> xmpp:has_subtag(Packet, #hint{type = 'store'}). + -spec has_no_store_hint(message()) -> boolean(). has_no_store_hint(Packet) -> - xmpp:has_subtag(Packet, #hint{type = 'no-store'}) - orelse - xmpp:has_subtag(Packet, #hint{type = 'no-storage'}). + xmpp:has_subtag(Packet, #hint{type = 'no-store'}) orelse + xmpp:has_subtag(Packet, #hint{type = 'no-storage'}). + %% Check if the packet has any content about XEP-0022 -spec check_event(message()) -> boolean(). check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) -> case xmpp:get_subtag(Msg, #xevent{}) of - false -> - true; - #xevent{id = undefined, offline = false} -> - true; - #xevent{id = undefined, offline = true} -> - NewMsg = #message{from = To, to = From, id = ID, type = Type, - sub_els = [#xevent{id = ID, offline = true}]}, - ejabberd_router:route(NewMsg), - true; - % Don't store composing events - #xevent{id = V, composing = true} when V /= undefined -> - false; - % Nor composing stopped events - #xevent{id = V, composing = false, delivered = false, - displayed = false, offline = false} when V /= undefined -> - false; - % But store other received notifications - #xevent{id = V} when V /= undefined -> - true; - _ -> - false + false -> + true; + #xevent{id = undefined, offline = false} -> + true; + #xevent{id = undefined, offline = true} -> + NewMsg = #message{ + from = To, + to = From, + id = ID, + type = Type, + sub_els = [#xevent{id = ID, offline = true}] + }, + ejabberd_router:route(NewMsg), + true; + % Don't store composing events + #xevent{id = V, composing = true} when V /= undefined -> + false; + % Nor composing stopped events + #xevent{ + id = V, + composing = false, + delivered = false, + displayed = false, + offline = false + } when V /= undefined -> + false; + % But store other received notifications + #xevent{id = V} when V /= undefined -> + true; + _ -> + false end. + -spec find_x_expire(erlang:timestamp(), message()) -> erlang:timestamp() | never. find_x_expire(TimeStamp, Msg) -> case xmpp:get_subtag(Msg, #expire{seconds = 0}) of - #expire{seconds = Int} -> - {MegaSecs, Secs, MicroSecs} = TimeStamp, - S = MegaSecs * 1000000 + Secs + Int, - MegaSecs1 = S div 1000000, - Secs1 = S rem 1000000, - {MegaSecs1, Secs1, MicroSecs}; - false -> - never + #expire{seconds = Int} -> + {MegaSecs, Secs, MicroSecs} = TimeStamp, + S = MegaSecs * 1000000 + Secs + Int, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + {MegaSecs1, Secs1, MicroSecs}; + false -> + never end. + c2s_self_presence({_Pres, #{resend_offline := false}} = Acc) -> Acc; c2s_self_presence({#presence{type = available} = NewPres, State} = Acc) -> NewPrio = get_priority_from_presence(NewPres), LastPrio = case maps:get(pres_last, State, undefined) of - undefined -> -1; - LastPres -> get_priority_from_presence(LastPres) - end, - if LastPrio < 0 andalso NewPrio >= 0 -> - route_offline_messages(State); - true -> - ok + undefined -> -1; + LastPres -> get_priority_from_presence(LastPres) + end, + if + LastPrio < 0 andalso NewPrio >= 0 -> + route_offline_messages(State); + true -> + ok end, Acc; c2s_self_presence(Acc) -> Acc. + -spec route_offline_messages(c2s_state()) -> ok. route_offline_messages(#{jid := #jid{luser = LUser, lserver = LServer}} = State) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Msgs = case Mod:pop_messages(LUser, LServer) of - {ok, OffMsgs} -> - case use_mam_for_user(LUser, LServer) of - true -> - flush_cache(Mod, LUser, LServer), - lists:map( - fun({_, #message{from = From, to = To} = Msg}) -> - #offline_msg{from = From, to = To, - us = {LUser, LServer}, - packet = Msg} - end, read_mam_messages(LUser, LServer, OffMsgs)); - _ -> - flush_cache(Mod, LUser, LServer), - OffMsgs - end; - _ -> - [] - end, + {ok, OffMsgs} -> + case use_mam_for_user(LUser, LServer) of + true -> + flush_cache(Mod, LUser, LServer), + lists:map( + fun({_, #message{from = From, to = To} = Msg}) -> + #offline_msg{ + from = From, + to = To, + us = {LUser, LServer}, + packet = Msg + } + end, + read_mam_messages(LUser, LServer, OffMsgs)); + _ -> + flush_cache(Mod, LUser, LServer), + OffMsgs + end; + _ -> + [] + end, lists:foreach( - fun(OffMsg) -> - route_offline_message(State, OffMsg) - end, Msgs). + fun(OffMsg) -> + route_offline_message(State, OffMsg) + end, + Msgs). + -spec route_offline_message(c2s_state(), #offline_msg{}) -> ok. route_offline_message(#{lserver := LServer} = State, - #offline_msg{expire = Expire} = OffMsg) -> + #offline_msg{expire = Expire} = OffMsg) -> case offline_msg_to_route(LServer, OffMsg) of - error -> - ok; - {route, Msg} -> - case is_message_expired(Expire, Msg) of - true -> - ok; - false -> - case privacy_check_packet(State, Msg, in) of - allow -> ejabberd_router:route(Msg); - deny -> ok - end - end + error -> + ok; + {route, Msg} -> + case is_message_expired(Expire, Msg) of + true -> + ok; + false -> + case privacy_check_packet(State, Msg, in) of + allow -> ejabberd_router:route(Msg); + deny -> ok + end + end end. + -spec is_message_expired(erlang:timestamp() | never, message()) -> boolean(). is_message_expired(Expire, Msg) -> TS = erlang:timestamp(), Expire1 = case Expire of - undefined -> find_x_expire(TS, Msg); - _ -> Expire - end, + undefined -> find_x_expire(TS, Msg); + _ -> Expire + end, Expire1 /= never andalso Expire1 =< TS. + -spec privacy_check_packet(c2s_state(), stanza(), in | out) -> allow | deny. privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) -> ejabberd_hooks:run_fold(privacy_check_packet, - LServer, allow, [State, Pkt, Dir]). + LServer, + allow, + [State, Pkt, Dir]). + remove_expired_messages(Server) -> LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case erlang:function_exported(Mod, remove_expired_messages, 1) of - true -> - Ret = Mod:remove_expired_messages(LServer), - ets_cache:clear(?SPOOL_COUNTER_CACHE), - Ret; - false -> - erlang:error(not_implemented) + true -> + Ret = Mod:remove_expired_messages(LServer), + ets_cache:clear(?SPOOL_COUNTER_CACHE), + Ret; + false -> + erlang:error(not_implemented) end. + remove_old_messages(Days, Server) -> LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case erlang:function_exported(Mod, remove_old_messages, 2) of - true -> - Ret = Mod:remove_old_messages(Days, LServer), - ets_cache:clear(?SPOOL_COUNTER_CACHE), - Ret; - false -> - erlang:error(not_implemented) + true -> + Ret = Mod:remove_old_messages(Days, LServer), + ets_cache:clear(?SPOOL_COUNTER_CACHE), + Ret; + false -> + erlang:error(not_implemented) end. + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), @@ -701,320 +798,388 @@ remove_user(User, Server) -> Mod:remove_user(LUser, LServer), flush_cache(Mod, LUser, LServer). + %% Helper functions: + -spec check_if_message_should_be_bounced(message()) -> boolean(). check_if_message_should_be_bounced(Packet) -> case Packet of - #message{type = groupchat, to = #jid{lserver = LServer}} -> - mod_offline_opt:bounce_groupchat(LServer); - #message{to = #jid{lserver = LServer}} -> - case misc:is_mucsub_message(Packet) of - true -> - mod_offline_opt:bounce_groupchat(LServer); - _ -> - true - end; - _ -> - true + #message{type = groupchat, to = #jid{lserver = LServer}} -> + mod_offline_opt:bounce_groupchat(LServer); + #message{to = #jid{lserver = LServer}} -> + case misc:is_mucsub_message(Packet) of + true -> + mod_offline_opt:bounce_groupchat(LServer); + _ -> + true + end; + _ -> + true end. + %% Warn senders that their messages have been discarded: + -spec discard_warn_sender(message(), full | any()) -> ok. discard_warn_sender(Packet, Reason) -> case check_if_message_should_be_bounced(Packet) of - true -> - Lang = xmpp:get_lang(Packet), - Err = case Reason of - full -> - ErrText = ?T("Your contact offline message queue is " - "full. The message has been discarded."), - xmpp:err_resource_constraint(ErrText, Lang); - _ -> - ErrText = ?T("Database failure"), - xmpp:err_internal_server_error(ErrText, Lang) - end, - ejabberd_router:route_error(Packet, Err); - _ -> - ok + true -> + Lang = xmpp:get_lang(Packet), + Err = case Reason of + full -> + ErrText = ?T("Your contact offline message queue is " + "full. The message has been discarded."), + xmpp:err_resource_constraint(ErrText, Lang); + _ -> + ErrText = ?T("Database failure"), + xmpp:err_internal_server_error(ErrText, Lang) + end, + ejabberd_router:route_error(Packet, Err); + _ -> + ok end. + %%% %%% Commands %%% + get_offline_messages(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), HdrsAll = case Mod:read_message_headers(LUser, LServer) of - error -> []; - L -> L - end, + error -> []; + L -> L + end, format_user_queue(HdrsAll). + %%% %%% WebAdmin %%% + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"queue">>, <<"Offline Queue">>}]. -webadmin_page_hostuser(_, Host, U, - #request{us = _US, path = [<<"queue">> | RPath], - lang = Lang} = R) -> + +webadmin_page_hostuser(_, + Host, + U, + #request{ + us = _US, + path = [<<"queue">> | RPath], + lang = Lang + } = R) -> US = {U, Host}, PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]), Head = ?H1GL(PageTitle, <<"modules/#mod_offline">>, <<"mod_offline">>), - Res = make_command(get_offline_messages, R, [{<<"user">>, U}, - {<<"host">>, Host}], - [{table_options, {10, RPath}}, - {result_links, [{packet, paragraph, 1, <<"">>}]}]), + Res = make_command(get_offline_messages, + R, + [{<<"user">>, U}, + {<<"host">>, Host}], + [{table_options, {10, RPath}}, + {result_links, [{packet, paragraph, 1, <<"">>}]}]), {stop, Head ++ [Res]}; webadmin_page_hostuser(Acc, _, _, _) -> Acc. + get_offline_els(LUser, LServer) -> - [Packet || {_Seq, Packet} <- read_messages(LUser, LServer)]. + [ Packet || {_Seq, Packet} <- read_messages(LUser, LServer) ]. + -spec offline_msg_to_route(binary(), #offline_msg{}) -> - {route, message()} | error. + {route, message()} | error. offline_msg_to_route(LServer, #offline_msg{from = From, to = To} = R) -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode(R#offline_msg.packet, ?NS_CLIENT, CodecOpts) of - Pkt -> - Pkt1 = xmpp:set_from_to(Pkt, From, To), - Pkt2 = add_delay_info(Pkt1, LServer, R#offline_msg.timestamp), - {route, Pkt2} - catch _:{xmpp_codec, Why} -> - ?ERROR_MSG("Failed to decode packet ~p of user ~ts: ~ts", - [R#offline_msg.packet, jid:encode(To), - xmpp:format_error(Why)]), - error + Pkt -> + Pkt1 = xmpp:set_from_to(Pkt, From, To), + Pkt2 = add_delay_info(Pkt1, LServer, R#offline_msg.timestamp), + {route, Pkt2} + catch + _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode packet ~p of user ~ts: ~ts", + [R#offline_msg.packet, + jid:encode(To), + xmpp:format_error(Why)]), + error end. + -spec read_messages(binary(), binary()) -> [{binary(), message()}]. read_messages(LUser, LServer) -> Res = case read_db_messages(LUser, LServer) of - error -> - []; - L when is_list(L) -> - L - end, + error -> + []; + L when is_list(L) -> + L + end, case use_mam_for_user(LUser, LServer) of - true -> - read_mam_messages(LUser, LServer, Res); - _ -> - Res + true -> + read_mam_messages(LUser, LServer, Res); + _ -> + Res end. + -spec read_db_messages(binary(), binary()) -> [{binary(), message()}] | error. read_db_messages(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), CodecOpts = ejabberd_config:codec_options(), case Mod:read_message_headers(LUser, LServer) of - error -> - error; - L -> - lists:flatmap( - fun({Seq, From, To, TS, El}) -> - Node = integer_to_binary(Seq), - try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of - Pkt -> - Node = integer_to_binary(Seq), - Pkt1 = add_delay_info(Pkt, LServer, TS), - Pkt2 = xmpp:set_from_to(Pkt1, From, To), - [{Node, Pkt2}] - catch _:{xmpp_codec, Why} -> - ?ERROR_MSG("Failed to decode packet ~p " - "of user ~ts: ~ts", - [El, jid:encode(To), - xmpp:format_error(Why)]), - [] - end - end, L) + error -> + error; + L -> + lists:flatmap( + fun({Seq, From, To, TS, El}) -> + Node = integer_to_binary(Seq), + try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of + Pkt -> + Node = integer_to_binary(Seq), + Pkt1 = add_delay_info(Pkt, LServer, TS), + Pkt2 = xmpp:set_from_to(Pkt1, From, To), + [{Node, Pkt2}] + catch + _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode packet ~p " + "of user ~ts: ~ts", + [El, + jid:encode(To), + xmpp:format_error(Why)]), + [] + end + end, + L) end. + -spec parse_marker_messages(binary(), [#offline_msg{} | {any(), message()}]) -> - {integer() | none, [message()]}. + {integer() | none, [message()]}. parse_marker_messages(LServer, ReadMsgs) -> {Timestamp, ExtraMsgs} = lists:foldl( - fun({_Node, #message{id = <<"ActivityMarker">>, - body = [], type = error} = Msg}, {T, E}) -> - case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of - #delay{stamp = Time} -> - if T == none orelse T > Time -> - {Time, E}; - true -> - {T, E} - end - end; - (#offline_msg{from = From, to = To, timestamp = TS, packet = Pkt}, - {T, E}) -> - try xmpp:decode(Pkt) of - #message{id = <<"ActivityMarker">>, - body = [], type = error} = Msg -> - TS2 = case TS of - undefined -> - case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of - #delay{stamp = TS0} -> - TS0; - _ -> - erlang:timestamp() - end; - _ -> - TS - end, - if T == none orelse T > TS2 -> - {TS2, E}; - true -> - {T, E} - end; - Decoded -> - Pkt1 = add_delay_info(Decoded, LServer, TS), - {T, [xmpp:set_from_to(Pkt1, From, To) | E]} - catch _:{xmpp_codec, _Why} -> - {T, E} - end; - ({_Node, Msg}, {T, E}) -> - {T, [Msg | E]} - end, {none, []}, ReadMsgs), + fun({_Node, + #message{ + id = <<"ActivityMarker">>, + body = [], + type = error + } = Msg}, + {T, E}) -> + case xmpp:get_subtag(Msg, #delay{stamp = {0, 0, 0}}) of + #delay{stamp = Time} -> + if + T == none orelse T > Time -> + {Time, E}; + true -> + {T, E} + end + end; + (#offline_msg{from = From, to = To, timestamp = TS, packet = Pkt}, + {T, E}) -> + try xmpp:decode(Pkt) of + #message{ + id = <<"ActivityMarker">>, + body = [], + type = error + } = Msg -> + TS2 = case TS of + undefined -> + case xmpp:get_subtag(Msg, #delay{stamp = {0, 0, 0}}) of + #delay{stamp = TS0} -> + TS0; + _ -> + erlang:timestamp() + end; + _ -> + TS + end, + if + T == none orelse T > TS2 -> + {TS2, E}; + true -> + {T, E} + end; + Decoded -> + Pkt1 = add_delay_info(Decoded, LServer, TS), + {T, [xmpp:set_from_to(Pkt1, From, To) | E]} + catch + _:{xmpp_codec, _Why} -> + {T, E} + end; + ({_Node, Msg}, {T, E}) -> + {T, [Msg | E]} + end, + {none, []}, + ReadMsgs), Start = case {Timestamp, ExtraMsgs} of - {none, [First|_]} -> - case xmpp:get_subtag(First, #delay{stamp = {0,0,0}}) of - #delay{stamp = {Mega, Sec, Micro}} -> - {Mega, Sec, Micro+1}; - _ -> - none - end; - {none, _} -> - none; - _ -> - Timestamp - end, + {none, [First | _]} -> + case xmpp:get_subtag(First, #delay{stamp = {0, 0, 0}}) of + #delay{stamp = {Mega, Sec, Micro}} -> + {Mega, Sec, Micro + 1}; + _ -> + none + end; + {none, _} -> + none; + _ -> + Timestamp + end, {Start, ExtraMsgs}. + -spec read_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}]) -> - [{integer(), message()}]. + [{integer(), message()}]. read_mam_messages(LUser, LServer, ReadMsgs) -> {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs), AllMsgs = case Start of - none -> - ExtraMsgs; - _ -> - MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of - Number when is_integer(Number) -> - max(0, Number - length(ExtraMsgs)); - infinity -> - undefined - end, - JID = jid:make(LUser, LServer, <<>>), - {MamMsgs, _, _} = mod_mam:select(LServer, JID, JID, - [{start, Start}], - #rsm_set{max = MaxOfflineMsgs, - before = <<"9999999999999999">>}, - chat, only_messages), - MamMsgs2 = lists:map( - fun({_, _, #forwarded{sub_els = [MM | _], delay = #delay{stamp = MMT}}}) -> - add_delay_info(MM, LServer, MMT) - end, MamMsgs), + none -> + ExtraMsgs; + _ -> + MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of + Number when is_integer(Number) -> + max(0, Number - length(ExtraMsgs)); + infinity -> + undefined + end, + JID = jid:make(LUser, LServer, <<>>), + {MamMsgs, _, _} = mod_mam:select(LServer, + JID, + JID, + [{start, Start}], + #rsm_set{ + max = MaxOfflineMsgs, + before = <<"9999999999999999">> + }, + chat, + only_messages), + MamMsgs2 = lists:map( + fun({_, _, #forwarded{sub_els = [MM | _], delay = #delay{stamp = MMT}}}) -> + add_delay_info(MM, LServer, MMT) + end, + MamMsgs), - ExtraMsgs ++ MamMsgs2 - end, + ExtraMsgs ++ MamMsgs2 + end, AllMsgs2 = lists:sort( - fun(A, B) -> - DA = case xmpp:get_subtag(A, #stanza_id{by = #jid{}}) of - #stanza_id{id = IDA} -> - IDA; - _ -> case xmpp:get_subtag(A, #delay{stamp = {0,0,0}}) of - #delay{stamp = STA} -> - integer_to_binary(misc:now_to_usec(STA)); - _ -> - <<"unknown">> - end - end, - DB = case xmpp:get_subtag(B, #stanza_id{by = #jid{}}) of - #stanza_id{id = IDB} -> - IDB; - _ -> case xmpp:get_subtag(B, #delay{stamp = {0,0,0}}) of - #delay{stamp = STB} -> - integer_to_binary(misc:now_to_usec(STB)); - _ -> - <<"unknown">> - end - end, - DA < DB - end, AllMsgs), + fun(A, B) -> + DA = case xmpp:get_subtag(A, #stanza_id{by = #jid{}}) of + #stanza_id{id = IDA} -> + IDA; + _ -> + case xmpp:get_subtag(A, #delay{stamp = {0, 0, 0}}) of + #delay{stamp = STA} -> + integer_to_binary(misc:now_to_usec(STA)); + _ -> + <<"unknown">> + end + end, + DB = case xmpp:get_subtag(B, #stanza_id{by = #jid{}}) of + #stanza_id{id = IDB} -> + IDB; + _ -> + case xmpp:get_subtag(B, #delay{stamp = {0, 0, 0}}) of + #delay{stamp = STB} -> + integer_to_binary(misc:now_to_usec(STB)); + _ -> + <<"unknown">> + end + end, + DA < DB + end, + AllMsgs), {AllMsgs3, _} = lists:mapfoldl( - fun(Msg, Counter) -> - {{Counter, Msg}, Counter + 1} - end, 1, AllMsgs2), + fun(Msg, Counter) -> + {{Counter, Msg}, Counter + 1} + end, + 1, + AllMsgs2), AllMsgs3. + -spec count_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}] | error) -> - {cache, integer()} | {nocache, integer()}. + {cache, integer()} | {nocache, integer()}. count_mam_messages(_LUser, _LServer, error) -> {nocache, 0}; count_mam_messages(LUser, LServer, ReadMsgs) -> {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs), case Start of - none -> - {cache, length(ExtraMsgs)}; - _ -> - MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of - Number when is_integer(Number) -> Number - length(ExtraMsgs); - infinity -> undefined - end, - JID = jid:make(LUser, LServer, <<>>), - {_, _, Count} = mod_mam:select(LServer, JID, JID, - [{start, Start}], - #rsm_set{max = MaxOfflineMsgs, - before = <<"9999999999999999">>}, - chat, only_count), - {cache, Count + length(ExtraMsgs)} + none -> + {cache, length(ExtraMsgs)}; + _ -> + MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of + Number when is_integer(Number) -> Number - length(ExtraMsgs); + infinity -> undefined + end, + JID = jid:make(LUser, LServer, <<>>), + {_, _, Count} = mod_mam:select(LServer, + JID, + JID, + [{start, Start}], + #rsm_set{ + max = MaxOfflineMsgs, + before = <<"9999999999999999">> + }, + chat, + only_count), + {cache, Count + length(ExtraMsgs)} end. + format_user_queue(Hdrs) -> lists:map( fun({_Seq, From, To, TS, El}) -> - FPacket = ejabberd_web_admin:pretty_print_xml(El), - SFrom = jid:encode(From), - STo = jid:encode(To), - Time = case TS of - undefined -> - Stamp = fxml:get_path_s(El, [{elem, <<"delay">>}, - {attr, <<"stamp">>}]), - try xmpp_util:decode_timestamp(Stamp) of - {_, _, _} = Now -> format_time(Now) - catch _:_ -> - <<"">> - end; - {_, _, _} = Now -> - format_time(Now) - end, + FPacket = ejabberd_web_admin:pretty_print_xml(El), + SFrom = jid:encode(From), + STo = jid:encode(To), + Time = case TS of + undefined -> + Stamp = fxml:get_path_s(El, + [{elem, <<"delay">>}, + {attr, <<"stamp">>}]), + try xmpp_util:decode_timestamp(Stamp) of + {_, _, _} = Now -> format_time(Now) + catch + _:_ -> + <<"">> + end; + {_, _, _} = Now -> + format_time(Now) + end, {Time, SFrom, STo, FPacket} - end, Hdrs). + end, + Hdrs). + format_time(Now) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now), str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Year, Month, Day, Hour, Minute, Second]). + [Year, Month, Day, Hour, Minute, Second]). + us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). + get_queue_length(LUser, LServer) -> count_offline_messages(LUser, LServer). + webadmin_user(Acc, User, Server, R) -> - Acc ++ [make_command(get_offline_count, R, [{<<"user">>, User}, {<<"host">>, Server}], - [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/queue/">>}]}] - )]. + Acc ++ [make_command(get_offline_count, + R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/queue/">>}]}])]. + %%% %%% %%% + -spec delete_all_msgs(binary(), binary()) -> {atomic, any()}. delete_all_msgs(User, Server) -> LUser = jid:nodeprep(User), @@ -1024,22 +1189,30 @@ delete_all_msgs(User, Server) -> flush_cache(Mod, LUser, LServer), Ret. -webadmin_user_parse_query(_, <<"removealloffline">>, - User, Server, _Query) -> + +webadmin_user_parse_query(_, + <<"removealloffline">>, + User, + Server, + _Query) -> case delete_all_msgs(User, Server) of - {atomic, ok} -> - ?INFO_MSG("Removed all offline messages for ~ts@~ts", - [User, Server]), - {stop, ok}; - Err -> - ?ERROR_MSG("Failed to remove offline messages: ~p", - [Err]), - {stop, error} + {atomic, ok} -> + ?INFO_MSG("Removed all offline messages for ~ts@~ts", + [User, Server]), + {stop, ok}; + Err -> + ?ERROR_MSG("Failed to remove offline messages: ~p", + [Err]), + {stop, error} end; -webadmin_user_parse_query(Acc, _Action, _User, _Server, - _Query) -> +webadmin_user_parse_query(Acc, + _Action, + _User, + _Server, + _Query) -> Acc. + %% Returns as integer the number of offline messages for a given user -spec count_offline_messages(binary(), binary()) -> non_neg_integer(). count_offline_messages(User, Server) -> @@ -1047,99 +1220,122 @@ count_offline_messages(User, Server) -> LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_mam_for_user(User, Server) of - true -> - case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?SPOOL_COUNTER_CACHE, {LUser, LServer}, - fun() -> - Res = read_db_messages(LUser, LServer), - count_mam_messages(LUser, LServer, Res) - end); - false -> - Res = read_db_messages(LUser, LServer), - ets_cache:untag(count_mam_messages(LUser, LServer, Res)) - end; - _ -> - case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?SPOOL_COUNTER_CACHE, {LUser, LServer}, - fun() -> - Mod:count_messages(LUser, LServer) - end); - false -> - ets_cache:untag(Mod:count_messages(LUser, LServer)) - end + true -> + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?SPOOL_COUNTER_CACHE, + {LUser, LServer}, + fun() -> + Res = read_db_messages(LUser, LServer), + count_mam_messages(LUser, LServer, Res) + end); + false -> + Res = read_db_messages(LUser, LServer), + ets_cache:untag(count_mam_messages(LUser, LServer, Res)) + end; + _ -> + case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?SPOOL_COUNTER_CACHE, + {LUser, LServer}, + fun() -> + Mod:count_messages(LUser, LServer) + end); + false -> + ets_cache:untag(Mod:count_messages(LUser, LServer)) + end end. + -spec store_message_in_db(module(), #offline_msg{}) -> ok | {error, any()}. store_message_in_db(Mod, #offline_msg{us = {User, Server}} = Msg) -> case Mod:store_message(Msg) of - ok -> - case use_cache(Mod, Server) of - true -> - ets_cache:incr( - ?SPOOL_COUNTER_CACHE, - {User, Server}, 1, - cache_nodes(Mod, Server)); - false -> - ok - end; - Err -> - Err + ok -> + case use_cache(Mod, Server) of + true -> + ets_cache:incr( + ?SPOOL_COUNTER_CACHE, + {User, Server}, + 1, + cache_nodes(Mod, Server)); + false -> + ok + end; + Err -> + Err end. --spec add_delay_info(message(), binary(), - undefined | erlang:timestamp()) -> message(). + +-spec add_delay_info(message(), + binary(), + undefined | erlang:timestamp()) -> message(). add_delay_info(Packet, LServer, TS) -> NewTS = case TS of - undefined -> erlang:timestamp(); - _ -> TS - end, + undefined -> erlang:timestamp(); + _ -> TS + end, Packet1 = xmpp:put_meta(Packet, from_offline, true), - misc:add_delay_info(Packet1, jid:make(LServer), NewTS, - <<"Offline storage">>). + misc:add_delay_info(Packet1, + jid:make(LServer), + NewTS, + <<"Offline storage">>). + -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of - undefined -> 0; - _ -> Prio + undefined -> 0; + _ -> Prio end. + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + import_info() -> [{<<"spool">>, 4}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, []). -import(LServer, {sql, _}, DBType, <<"spool">>, + +import(LServer, + {sql, _}, + DBType, + <<"spool">>, [LUser, XML, _Seq, _TimeStamp]) -> El = fxml_stream:parse_element(XML), #message{from = From, to = To} = Msg = xmpp:decode(El, ?NS_CLIENT, [ignore_els]), - TS = case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of - #delay{stamp = {MegaSecs, Secs, _}} -> - {MegaSecs, Secs, 0}; - false -> - erlang:timestamp() - end, + TS = case xmpp:get_subtag(Msg, #delay{stamp = {0, 0, 0}}) of + #delay{stamp = {MegaSecs, Secs, _}} -> + {MegaSecs, Secs, 0}; + false -> + erlang:timestamp() + end, US = {LUser, LServer}, Expire = find_x_expire(TS, Msg), - OffMsg = #offline_msg{us = US, packet = El, - from = From, to = To, - timestamp = TS, expire = Expire}, + OffMsg = #offline_msg{ + us = US, + packet = El, + from = From, + to = To, + timestamp = TS, + expire = Expire + }, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(OffMsg). + use_mam_for_user(_User, Server) -> mod_offline_opt:use_mam_for_storage(Server). + mod_opt_type(access_max_user_messages) -> econf:shaper(); mod_opt_type(store_groupchat) -> @@ -1161,6 +1357,7 @@ mod_opt_type(cache_size) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {access_max_user_messages, max_user_offline_messages}, @@ -1172,8 +1369,10 @@ mod_options(Host) -> {cache_size, ejabberd_option:cache_size(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0160.html" "[XEP-0160: Best Practices for Handling Offline Messages] " @@ -1183,12 +1382,14 @@ mod_doc() -> "will be stored on the server until that user comes online " "again. Thus it is very similar to how email works. A user " "is considered offline if no session presence priority > 0 " - "are currently open."), "", + "are currently open."), + "", ?T("The _`delete_expired_messages`_ API allows to delete expired messages, " "and _`delete_old_messages`_ API deletes older ones.")], opts => [{access_max_user_messages, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option defines which access rule will be " "enforced to limit the maximum number of offline " @@ -1196,9 +1397,11 @@ mod_doc() -> "has too many offline messages, any new messages that " "they receive are discarded, and a '' " "error is returned to the sender. The default value is " - "'max_user_offline_messages'.")}}, + "'max_user_offline_messages'.") + }}, {store_empty_body, - #{value => "true | false | unless_chat_state", + #{ + value => "true | false | unless_chat_state", desc => ?T("Whether or not to store messages that lack a '' " "element. The default value is 'unless_chat_state', " @@ -1206,14 +1409,18 @@ mod_doc() -> "lack the '' element, unless they only contain a " "chat state notification (as defined in " "https://xmpp.org/extensions/xep-0085.html" - "[XEP-0085: Chat State Notifications].")}}, - {store_groupchat, - #{value => "true | false", - desc => - ?T("Whether or not to store groupchat messages. " - "The default value is 'false'.")}}, + "[XEP-0085: Chat State Notifications].") + }}, + {store_groupchat, + #{ + value => "true | false", + desc => + ?T("Whether or not to store groupchat messages. " + "The default value is 'false'.") + }}, {use_mam_for_storage, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This is an experimental option. By enabling the option, " "this module uses the 'archive' table from _`mod_mam`_ instead " @@ -1226,9 +1433,11 @@ mod_doc() -> "flexible message retrieval queries don't work (those that " "allow retrieval/deletion of messages by id), but this " "specification is not widely used. The default value " - "is 'false' to keep former behaviour as default.")}}, + "is 'false' to keep former behaviour as default.") + }}, {bounce_groupchat, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This option is use the disable an optimization that " "avoids bouncing error messages when groupchat messages " @@ -1240,46 +1449,55 @@ mod_doc() -> "but the bounce is much more likely to happen in the context " "of MucSub, so it is even more important to have it on " "large MucSub services. The default value is 'false', meaning " - "the optimization is enabled.")}}, + "the optimization is enabled.") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}], example => - [{?T("This example allows power users to have as much as 5000 " - "offline messages, administrators up to 2000, and all the " - "other users up to 100:"), - ["acl:", - " admin:", - " user:", - " - admin1@localhost", - " - admin2@example.org", - " poweruser:", - " user:", - " - bob@example.org", - " - jane@example.org", - "", - "shaper_rules:", - " max_user_offline_messages:", - " - 5000: poweruser", - " - 2000: admin", - " - 100", - "", - "modules:", - " ...", - " mod_offline:", - " access_max_user_messages: max_user_offline_messages", - " ..." - ]}]}. + [{?T("This example allows power users to have as much as 5000 " + "offline messages, administrators up to 2000, and all the " + "other users up to 100:"), + ["acl:", + " admin:", + " user:", + " - admin1@localhost", + " - admin2@example.org", + " poweruser:", + " user:", + " - bob@example.org", + " - jane@example.org", + "", + "shaper_rules:", + " max_user_offline_messages:", + " - 5000: poweruser", + " - 2000: admin", + " - 100", + "", + "modules:", + " ...", + " mod_offline:", + " access_max_user_messages: max_user_offline_messages", + " ..."]}] + }. diff --git a/src/mod_offline_mnesia.erl b/src/mod_offline_mnesia.erl index 24406c5ac..a9657962c 100644 --- a/src/mod_offline_mnesia.erl +++ b/src/mod_offline_mnesia.erl @@ -26,78 +26,99 @@ -behaviour(mod_offline). --export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1, - remove_old_messages/2, remove_user/2, read_message_headers/2, - read_message/3, remove_message/3, read_all_messages/2, - remove_all_messages/2, count_messages/2, import/1, - remove_old_messages_batch/4]). +-export([init/2, + store_message/1, + pop_messages/2, + remove_expired_messages/1, + remove_old_messages/2, + remove_user/2, + read_message_headers/2, + read_message/3, + remove_message/3, + read_all_messages/2, + remove_all_messages/2, + count_messages/2, + import/1, + remove_old_messages_batch/4]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_offline.hrl"). -include("logger.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, offline_msg, - [{disc_only_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, offline_msg)}]). + ejabberd_mnesia:create(?MODULE, + offline_msg, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, offline_msg)}]). + store_message(#offline_msg{packet = Pkt} = OffMsg) -> El = xmpp:encode(Pkt), mnesia:dirty_write(OffMsg#offline_msg{packet = El}). + pop_messages(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> - Rs = mnesia:wread({offline_msg, US}), - mnesia:delete({offline_msg, US}), - Rs - end, + F = fun() -> + Rs = mnesia:wread({offline_msg, US}), + mnesia:delete({offline_msg, US}), + Rs + end, case mnesia:transaction(F) of - {atomic, L} -> - {ok, lists:keysort(#offline_msg.timestamp, L)}; - {aborted, Reason} -> - {error, Reason} + {atomic, L} -> + {ok, lists:keysort(#offline_msg.timestamp, L)}; + {aborted, Reason} -> + {error, Reason} end. + remove_expired_messages(_LServer) -> TimeStamp = erlang:timestamp(), - F = fun () -> - mnesia:write_lock_table(offline_msg), - mnesia:foldl(fun (Rec, _Acc) -> - case Rec#offline_msg.expire of - never -> ok; - TS -> - if TS < TimeStamp -> - mnesia:delete_object(Rec); - true -> ok - end - end - end, - ok, offline_msg) - end, + F = fun() -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl(fun(Rec, _Acc) -> + case Rec#offline_msg.expire of + never -> ok; + TS -> + if + TS < TimeStamp -> + mnesia:delete_object(Rec); + true -> ok + end + end + end, + ok, + offline_msg) + end, mnesia:transaction(F). + remove_old_messages(Days, _LServer) -> S = erlang:system_time(second) - 60 * 60 * 24 * Days, MegaSecs1 = S div 1000000, Secs1 = S rem 1000000, TimeStamp = {MegaSecs1, Secs1, 0}, - F = fun () -> - mnesia:write_lock_table(offline_msg), - mnesia:foldl(fun (#offline_msg{timestamp = TS} = Rec, - _Acc) - when TS < TimeStamp -> - mnesia:delete_object(Rec); - (_Rec, _Acc) -> ok - end, - ok, offline_msg) - end, + F = fun() -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl(fun(#offline_msg{timestamp = TS} = Rec, + _Acc) + when TS < TimeStamp -> + mnesia:delete_object(Rec); + (_Rec, _Acc) -> ok + end, + ok, + offline_msg) + end, mnesia:transaction(F). + delete_batch('$end_of_table', _LServer, _TS, Num) -> {Num, '$end_of_table'}; delete_batch(LastUS, _LServer, _TS, 0) -> @@ -108,104 +129,122 @@ delete_batch({_, LServer2} = LastUS, LServer, TS, Num) when LServer /= LServer2 delete_batch(mnesia:next(offline_msg, LastUS), LServer, TS, Num); delete_batch(LastUS, LServer, TS, Num) -> Left = - lists:foldl( - fun(_, 0) -> - 0; - (#offline_msg{timestamp = TS2} = O, Num2) when TS2 < TS -> - mnesia:delete_object(O), - Num2 - 1; - (_, Num2) -> - Num2 - end, Num, mnesia:wread({offline_msg, LastUS})), + lists:foldl( + fun(_, 0) -> + 0; + (#offline_msg{timestamp = TS2} = O, Num2) when TS2 < TS -> + mnesia:delete_object(O), + Num2 - 1; + (_, Num2) -> + Num2 + end, + Num, + mnesia:wread({offline_msg, LastUS})), case Left of - 0 -> {0, LastUS}; - _ -> delete_batch(mnesia:next(offline_msg, LastUS), LServer, TS, Left) + 0 -> {0, LastUS}; + _ -> delete_batch(mnesia:next(offline_msg, LastUS), LServer, TS, Left) end. + remove_old_messages_batch(LServer, Days, Batch, LastUS) -> S = erlang:system_time(second) - 60 * 60 * 24 * Days, MegaSecs1 = S div 1000000, Secs1 = S rem 1000000, TimeStamp = {MegaSecs1, Secs1, 0}, R = mnesia:transaction( - fun() -> - {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Batch), - {Batch - Num, NextUS} - end), + fun() -> + {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Batch), + {Batch - Num, NextUS} + end), case R of - {atomic, {Num, State}} -> - {ok, State, Num}; - {aborted, Err} -> - {error, Err} + {atomic, {Num, State}} -> + {ok, State, Num}; + {aborted, Err} -> + {error, Err} end. + remove_user(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> mnesia:delete({offline_msg, US}) end, + F = fun() -> mnesia:delete({offline_msg, US}) end, mnesia:transaction(F). + read_message_headers(LUser, LServer) -> Msgs = mnesia:dirty_read({offline_msg, {LUser, LServer}}), Hdrs = lists:map( - fun(#offline_msg{from = From, to = To, packet = Pkt, - timestamp = TS}) -> - Seq = now_to_integer(TS), - {Seq, From, To, TS, Pkt} - end, Msgs), + fun(#offline_msg{ + from = From, + to = To, + packet = Pkt, + timestamp = TS + }) -> + Seq = now_to_integer(TS), + {Seq, From, To, TS, Pkt} + end, + Msgs), lists:keysort(1, Hdrs). + read_message(LUser, LServer, I) -> US = {LUser, LServer}, TS = integer_to_now(I), case mnesia:dirty_match_object( - offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of - [Msg|_] -> - {ok, Msg}; - _ -> - error + offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of + [Msg | _] -> + {ok, Msg}; + _ -> + error end. + remove_message(LUser, LServer, I) -> US = {LUser, LServer}, TS = integer_to_now(I), case mnesia:dirty_match_object( - offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of - [] -> - {error, notfound}; - Msgs -> - lists:foreach( - fun(Msg) -> - mnesia:dirty_delete_object(Msg) - end, Msgs) + offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of + [] -> + {error, notfound}; + Msgs -> + lists:foreach( + fun(Msg) -> + mnesia:dirty_delete_object(Msg) + end, + Msgs) end. + read_all_messages(LUser, LServer) -> US = {LUser, LServer}, lists:keysort(#offline_msg.timestamp, - mnesia:dirty_read({offline_msg, US})). + mnesia:dirty_read({offline_msg, US})). + remove_all_messages(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> - mnesia:write_lock_table(offline_msg), - lists:foreach(fun (Msg) -> mnesia:delete_object(Msg) end, - mnesia:dirty_read({offline_msg, US})) - end, + F = fun() -> + mnesia:write_lock_table(offline_msg), + lists:foreach(fun(Msg) -> mnesia:delete_object(Msg) end, + mnesia:dirty_read({offline_msg, US})) + end, mnesia:transaction(F). + count_messages(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> - count_mnesia_records(US) - end, + F = fun() -> + count_mnesia_records(US) + end, {cache, case mnesia:async_dirty(F) of - I when is_integer(I) -> I; - _ -> 0 - end}. + I when is_integer(I) -> I; + _ -> 0 + end}. + import(#offline_msg{} = Msg) -> mnesia:dirty_write(Msg). + need_transform({offline_msg, {U, S}, _, _, _, _, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'offline_msg' will be converted to binary", []), @@ -215,15 +254,29 @@ need_transform({offline_msg, _, _, _, _, _, _, _}) -> need_transform(_) -> false. + transform({offline_msg, {U, S}, Timestamp, Expire, From, To, _, Packet}) -> - #offline_msg{us = {U, S}, timestamp = Timestamp, expire = Expire, - from = From, to = To, packet = Packet}; -transform(#offline_msg{us = {U, S}, from = From, to = To, - packet = El} = R) -> - R#offline_msg{us = {iolist_to_binary(U), iolist_to_binary(S)}, - from = jid_to_binary(From), - to = jid_to_binary(To), - packet = fxml:to_xmlel(El)}. + #offline_msg{ + us = {U, S}, + timestamp = Timestamp, + expire = Expire, + from = From, + to = To, + packet = Packet + }; +transform(#offline_msg{ + us = {U, S}, + from = From, + to = To, + packet = El + } = R) -> + R#offline_msg{ + us = {iolist_to_binary(U), iolist_to_binary(S)}, + from = jid_to_binary(From), + to = jid_to_binary(To), + packet = fxml:to_xmlel(El) + }. + %%%=================================================================== %%% Internal functions @@ -234,38 +287,53 @@ transform(#offline_msg{us = {U, S}, from = From, to = To, %% getting the record by small increment and by using continuation. -define(BATCHSIZE, 100). + count_mnesia_records(US) -> - MatchExpression = #offline_msg{us = US, _ = '_'}, - case mnesia:select(offline_msg, [{MatchExpression, [], [[]]}], - ?BATCHSIZE, read) of - {Result, Cont} -> - Count = length(Result), - count_records_cont(Cont, Count); - '$end_of_table' -> - 0 + MatchExpression = #offline_msg{us = US, _ = '_'}, + case mnesia:select(offline_msg, + [{MatchExpression, [], [[]]}], + ?BATCHSIZE, + read) of + {Result, Cont} -> + Count = length(Result), + count_records_cont(Cont, Count); + '$end_of_table' -> + 0 end. + count_records_cont(Cont, Count) -> case mnesia:select(Cont) of - {Result, Cont} -> - NewCount = Count + length(Result), - count_records_cont(Cont, NewCount); - '$end_of_table' -> - Count + {Result, Cont} -> + NewCount = Count + length(Result), + count_records_cont(Cont, NewCount); + '$end_of_table' -> + Count end. -jid_to_binary(#jid{user = U, server = S, resource = R, - luser = LU, lserver = LS, lresource = LR}) -> - #jid{user = iolist_to_binary(U), - server = iolist_to_binary(S), - resource = iolist_to_binary(R), - luser = iolist_to_binary(LU), - lserver = iolist_to_binary(LS), - lresource = iolist_to_binary(LR)}. + +jid_to_binary(#jid{ + user = U, + server = S, + resource = R, + luser = LU, + lserver = LS, + lresource = LR + }) -> + #jid{ + user = iolist_to_binary(U), + server = iolist_to_binary(S), + resource = iolist_to_binary(R), + luser = iolist_to_binary(LU), + lserver = iolist_to_binary(LS), + lresource = iolist_to_binary(LR) + }. + now_to_integer({MS, S, US}) -> (MS * 1000000 + S) * 1000000 + US. + integer_to_now(Int) -> Secs = Int div 1000000, USec = Int rem 1000000, diff --git a/src/mod_offline_opt.erl b/src/mod_offline_opt.erl index e9ab7c71b..eced0cd3a 100644 --- a/src/mod_offline_opt.erl +++ b/src/mod_offline_opt.erl @@ -13,57 +13,65 @@ -export([use_cache/1]). -export([use_mam_for_storage/1]). + -spec access_max_user_messages(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. access_max_user_messages(Opts) when is_map(Opts) -> gen_mod:get_opt(access_max_user_messages, Opts); access_max_user_messages(Host) -> gen_mod:get_module_opt(Host, mod_offline, access_max_user_messages). + -spec bounce_groupchat(gen_mod:opts() | global | binary()) -> boolean(). bounce_groupchat(Opts) when is_map(Opts) -> gen_mod:get_opt(bounce_groupchat, Opts); bounce_groupchat(Host) -> gen_mod:get_module_opt(Host, mod_offline, bounce_groupchat). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_offline, cache_life_time). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_offline, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_offline, db_type). + -spec store_empty_body(gen_mod:opts() | global | binary()) -> 'false' | 'true' | 'unless_chat_state'. store_empty_body(Opts) when is_map(Opts) -> gen_mod:get_opt(store_empty_body, Opts); store_empty_body(Host) -> gen_mod:get_module_opt(Host, mod_offline, store_empty_body). + -spec store_groupchat(gen_mod:opts() | global | binary()) -> boolean(). store_groupchat(Opts) when is_map(Opts) -> gen_mod:get_opt(store_groupchat, Opts); store_groupchat(Host) -> gen_mod:get_module_opt(Host, mod_offline, store_groupchat). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_offline, use_cache). + -spec use_mam_for_storage(gen_mod:opts() | global | binary()) -> boolean(). use_mam_for_storage(Opts) when is_map(Opts) -> gen_mod:get_opt(use_mam_for_storage, Opts); use_mam_for_storage(Host) -> gen_mod:get_module_opt(Host, mod_offline, use_mam_for_storage). - diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index 9078b082c..d58954c7e 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -24,20 +24,32 @@ -module(mod_offline_sql). - -behaviour(mod_offline). --export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1, - remove_old_messages/2, remove_user/2, read_message_headers/2, - read_message/3, remove_message/3, read_all_messages/2, - remove_all_messages/2, count_messages/2, import/1, export/1, remove_old_messages_batch/3]). +-export([init/2, + store_message/1, + pop_messages/2, + remove_expired_messages/1, + remove_old_messages/2, + remove_user/2, + read_message_headers/2, + read_message/3, + remove_message/3, + read_all_messages/2, + remove_all_messages/2, + count_messages/2, + import/1, + export/1, + remove_old_messages_batch/3]). -export([sql_schemas/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_offline.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -45,162 +57,182 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"spool">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"xml">>, type = {text, big}}, - #sql_column{name = <<"seq">>, type = bigserial}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>]}, - #sql_index{ - columns = [<<"created_at">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"spool">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"xml">>, type = {text, big}}, + #sql_column{name = <<"seq">>, type = bigserial}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>] + }, + #sql_index{ + columns = [<<"created_at">>] + }] + }] + }]. + store_message(#offline_msg{us = {LUser, LServer}} = M) -> From = M#offline_msg.from, To = M#offline_msg.to, Packet = xmpp:set_from_to(M#offline_msg.packet, From, To), NewPacket = misc:add_delay_info( - Packet, jid:make(LServer), - M#offline_msg.timestamp, - <<"Offline Storage">>), + Packet, + jid:make(LServer), + M#offline_msg.timestamp, + <<"Offline Storage">>), XML = fxml:element_to_binary( - xmpp:encode(NewPacket)), + xmpp:encode(NewPacket)), case ejabberd_sql:sql_query( - LServer, + LServer, ?SQL_INSERT( - "spool", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "xml=%(XML)s"])) of - {updated, _} -> - ok; - _ -> - {error, db_failure} + "spool", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=%(XML)s"])) of + {updated, _} -> + ok; + _ -> + {error, db_failure} end. + pop_messages(LUser, LServer) -> case get_and_del_spool_msg_t(LServer, LUser) of - {atomic, {selected, Rs}} -> - {ok, lists:flatmap( - fun({_, XML}) -> - case xml_to_offline_msg(XML) of - {ok, Msg} -> - [Msg]; - _Err -> - [] - end - end, Rs)}; - Err -> - {error, Err} + {atomic, {selected, Rs}} -> + {ok, lists:flatmap( + fun({_, XML}) -> + case xml_to_offline_msg(XML) of + {ok, Msg} -> + [Msg]; + _Err -> + [] + end + end, + Rs)}; + Err -> + {error, Err} end. + remove_expired_messages(_LServer) -> %% TODO {atomic, ok}. + remove_old_messages(Days, LServer) -> case ejabberd_sql:sql_query( - LServer, + LServer, fun(pgsql, _) -> ejabberd_sql:sql_query_t( ?SQL("DELETE FROM spool" " WHERE created_at <" " NOW() - %(Days)d * INTERVAL '1 DAY'")); - (sqlite, _) -> - ejabberd_sql:sql_query_t( - ?SQL("DELETE FROM spool" - " WHERE created_at <" - " DATETIME('now', '-%(Days)d days')")); + (sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " DATETIME('now', '-%(Days)d days')")); (_, _) -> ejabberd_sql:sql_query_t( ?SQL("DELETE FROM spool" " WHERE created_at < NOW() - INTERVAL %(Days)d DAY")) - end) - of - {updated, N} -> - ?INFO_MSG("~p message(s) deleted from offline spool", [N]); - Error -> - ?ERROR_MSG("Cannot delete message in offline spool: ~p", [Error]) + end) of + {updated, N} -> + ?INFO_MSG("~p message(s) deleted from offline spool", [N]); + Error -> + ?ERROR_MSG("Cannot delete message in offline spool: ~p", [Error]) end, {atomic, ok}. + remove_old_messages_batch(LServer, Days, Batch) -> case ejabberd_sql:sql_query( - LServer, - fun(pgsql, _) -> - ejabberd_sql:sql_query_t( - ?SQL("DELETE FROM spool" - " WHERE created_at <" - " NOW() - %(Days)d * INTERVAL '1 DAY' LIMIT %(Batch)d")); - (sqlite, _) -> - ejabberd_sql:sql_query_t( - ?SQL("DELETE FROM spool" - " WHERE created_at <" - " DATETIME('now', '-%(Days)d days') LIMIT %(Batch)d")); - (_, _) -> - ejabberd_sql:sql_query_t( - ?SQL("DELETE FROM spool" - " WHERE created_at < NOW() - INTERVAL %(Days)d DAY LIMIT %(Batch)d")) - end) - of - {updated, N} -> - {ok, N}; - Error -> - {error, Error} + LServer, + fun(pgsql, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " NOW() - %(Days)d * INTERVAL '1 DAY' LIMIT %(Batch)d")); + (sqlite, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at <" + " DATETIME('now', '-%(Days)d days') LIMIT %(Batch)d")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("DELETE FROM spool" + " WHERE created_at < NOW() - INTERVAL %(Days)d DAY LIMIT %(Batch)d")) + end) of + {updated, N} -> + {ok, N}; + Error -> + {error, Error} end. + remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from spool where username=%(LUser)s and %(LServer)H")). + read_message_headers(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(xml)s, @(seq)d from spool" - " where username=%(LUser)s and %(LServer)H order by seq")) of - {selected, Rows} -> - lists:flatmap( - fun({XML, Seq}) -> - case xml_to_offline_msg(XML) of - {ok, #offline_msg{from = From, - to = To, - timestamp = TS, - packet = El}} -> - [{Seq, From, To, TS, El}]; - _ -> - [] - end - end, Rows); - _Err -> - error + LServer, + ?SQL("select @(xml)s, @(seq)d from spool" + " where username=%(LUser)s and %(LServer)H order by seq")) of + {selected, Rows} -> + lists:flatmap( + fun({XML, Seq}) -> + case xml_to_offline_msg(XML) of + {ok, #offline_msg{ + from = From, + to = To, + timestamp = TS, + packet = El + }} -> + [{Seq, From, To, TS, El}]; + _ -> + [] + end + end, + Rows); + _Err -> + error end. + read_message(LUser, LServer, Seq) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(xml)s from spool where username=%(LUser)s" + LServer, + ?SQL("select @(xml)s from spool where username=%(LUser)s" " and %(LServer)H" " and seq=%(Seq)d")) of - {selected, [{RawXML}|_]} -> - case xml_to_offline_msg(RawXML) of - {ok, Msg} -> - {ok, Msg}; - _ -> - error - end; - _ -> - error + {selected, [{RawXML} | _]} -> + case xml_to_offline_msg(RawXML) of + {ok, Msg} -> + {ok, Msg}; + _ -> + error + end; + _ -> + error end. + remove_message(LUser, LServer, Seq) -> ejabberd_sql:sql_query( LServer, @@ -208,27 +240,31 @@ remove_message(LUser, LServer, Seq) -> " and seq=%(Seq)d")), ok. + read_all_messages(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(xml)s from spool where " - "username=%(LUser)s and %(LServer)H order by seq")) of + LServer, + ?SQL("select @(xml)s from spool where " + "username=%(LUser)s and %(LServer)H order by seq")) of {selected, Rs} -> lists:flatmap( fun({XML}) -> - case xml_to_offline_msg(XML) of - {ok, Msg} -> [Msg]; - _ -> [] - end - end, Rs); + case xml_to_offline_msg(XML) of + {ok, Msg} -> [Msg]; + _ -> [] + end + end, + Rs); _ -> - [] + [] end. + remove_all_messages(LUser, LServer) -> remove_user(LUser, LServer), {atomic, ok}. + count_messages(LUser, LServer) -> case catch ejabberd_sql:sql_query( LServer, @@ -236,96 +272,112 @@ count_messages(LUser, LServer) -> "where username=%(LUser)s and %(LServer)H")) of {selected, [{Res}]} -> {cache, Res}; - {selected, []} -> - {cache, 0}; + {selected, []} -> + {cache, 0}; _ -> - {nocache, 0} + {nocache, 0} end. + export(_Server) -> [{offline_msg, fun(Host, #offline_msg{us = {LUser, LServer}}) when LServer == Host -> - [?SQL("delete from spool where username=%(LUser)s" - " and %(LServer)H;")]; + [?SQL("delete from spool where username=%(LUser)s" + " and %(LServer)H;")]; (_Host, _R) -> [] end}, {offline_msg, - fun(Host, #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, from = From, to = To, - packet = El}) + fun(Host, + #offline_msg{ + us = {LUser, LServer}, + timestamp = TimeStamp, + from = From, + to = To, + packet = El + }) when LServer == Host -> - try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of - Packet -> - Packet1 = xmpp:set_from_to(Packet, From, To), - Packet2 = misc:add_delay_info( - Packet1, jid:make(LServer), - TimeStamp, <<"Offline Storage">>), - XML = fxml:element_to_binary(xmpp:encode(Packet2)), - [?SQL_INSERT( - "spool", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "xml=%(XML)s"])] - catch _:{xmpp_codec, Why} -> - ?ERROR_MSG("Failed to decode packet ~p of user ~ts@~ts: ~ts", - [El, LUser, LServer, xmpp:format_error(Why)]), - [] - end; + try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of + Packet -> + Packet1 = xmpp:set_from_to(Packet, From, To), + Packet2 = misc:add_delay_info( + Packet1, + jid:make(LServer), + TimeStamp, + <<"Offline Storage">>), + XML = fxml:element_to_binary(xmpp:encode(Packet2)), + [?SQL_INSERT( + "spool", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=%(XML)s"])] + catch + _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode packet ~p of user ~ts@~ts: ~ts", + [El, LUser, LServer, xmpp:format_error(Why)]), + [] + end; (_Host, _R) -> [] end}]. + import(_) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== xml_to_offline_msg(XML) -> case fxml_stream:parse_element(XML) of - #xmlel{} = El -> - el_to_offline_msg(El); - Err -> - ?ERROR_MSG("Got ~p when parsing XML packet ~ts", - [Err, XML]), - Err + #xmlel{} = El -> + el_to_offline_msg(El); + Err -> + ?ERROR_MSG("Got ~p when parsing XML packet ~ts", + [Err, XML]), + Err end. + el_to_offline_msg(El) -> To_s = fxml:get_tag_attr_s(<<"to">>, El), From_s = fxml:get_tag_attr_s(<<"from">>, El), try - To = jid:decode(To_s), - From = jid:decode(From_s), - {ok, #offline_msg{us = {To#jid.luser, To#jid.lserver}, - from = From, - to = To, - packet = El}} - catch _:{bad_jid, To_s} -> - ?ERROR_MSG("Failed to get 'to' JID from offline XML ~p", [El]), - {error, bad_jid_to}; - _:{bad_jid, From_s} -> - ?ERROR_MSG("Failed to get 'from' JID from offline XML ~p", [El]), - {error, bad_jid_from} + To = jid:decode(To_s), + From = jid:decode(From_s), + {ok, #offline_msg{ + us = {To#jid.luser, To#jid.lserver}, + from = From, + to = To, + packet = El + }} + catch + _:{bad_jid, To_s} -> + ?ERROR_MSG("Failed to get 'to' JID from offline XML ~p", [El]), + {error, bad_jid_to}; + _:{bad_jid, From_s} -> + ?ERROR_MSG("Failed to get 'from' JID from offline XML ~p", [El]), + {error, bad_jid_from} end. + get_and_del_spool_msg_t(LServer, LUser) -> - F = fun () -> - Result = - ejabberd_sql:sql_query_t( + F = fun() -> + Result = + ejabberd_sql:sql_query_t( ?SQL("select @(username)s, @(xml)s from spool where " "username=%(LUser)s and %(LServer)H order by seq;")), - DResult = - ejabberd_sql:sql_query_t( + DResult = + ejabberd_sql:sql_query_t( ?SQL("delete from spool where" " username=%(LUser)s and %(LServer)H;")), - case {Result, DResult} of - {{selected, Rs}, {updated, DC}} when length(Rs) /= DC -> - ejabberd_sql:restart(concurent_insert); - _ -> - Result - end - end, + case {Result, DResult} of + {{selected, Rs}, {updated, DC}} when length(Rs) /= DC -> + ejabberd_sql:restart(concurent_insert); + _ -> + Result + end + end, ejabberd_sql:sql_transaction(LServer, F). diff --git a/src/mod_ping.erl b/src/mod_ping.erl index b760db68c..65631598e 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -46,21 +46,34 @@ -export([start/2, stop/1, reload/3]). %% gen_server callbacks --export([init/1, terminate/2, handle_call/3, - handle_cast/2, handle_info/2, code_change/3]). +-export([init/1, + terminate/2, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3]). --export([iq_ping/1, user_online/3, user_offline/3, mod_doc/0, user_send/1, - c2s_handle_cast/2, mod_opt_type/1, mod_options/1, depends/2]). +-export([iq_ping/1, + user_online/3, + user_offline/3, + mod_doc/0, + user_send/1, + c2s_handle_cast/2, + mod_opt_type/1, + mod_options/1, + depends/2]). --record(state, - {host :: binary(), - send_pings :: boolean(), - ping_interval :: pos_integer(), - timeout_action :: none | kill, - timers :: timers()}). +-record(state, { + host :: binary(), + send_pings :: boolean(), + ping_interval :: pos_integer(), + timeout_action :: none | kill, + timers :: timers() + }). -type timers() :: #{ljid() => reference()}. + %%==================================================================== %% API %%==================================================================== @@ -69,60 +82,69 @@ start_ping(Host, JID) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {start_ping, JID}). + -spec stop_ping(binary(), jid()) -> ok. stop_ping(Host, JID) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {stop_ping, JID}). + %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), State = init_state(Host, Opts), register_iq_handlers(Host), case State#state.send_pings of - true -> register_hooks(Host); - false -> ok + true -> register_hooks(Host); + false -> ok end, {ok, State}. + terminate(_Reason, #state{host = Host}) -> unregister_hooks(Host), unregister_iq_handlers(Host). + handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast({reload, Host, NewOpts, _OldOpts}, - #state{timers = Timers} = OldState) -> + #state{timers = Timers} = OldState) -> NewState = init_state(Host, NewOpts), case {NewState#state.send_pings, OldState#state.send_pings} of - {true, false} -> register_hooks(Host); - {false, true} -> unregister_hooks(Host); - _ -> ok + {true, false} -> register_hooks(Host); + {false, true} -> unregister_hooks(Host); + _ -> ok end, {noreply, NewState#state{timers = Timers}}; handle_cast({start_ping, JID}, State) -> - Timers = add_timer(JID, State#state.ping_interval, - State#state.timers), + Timers = add_timer(JID, + State#state.ping_interval, + State#state.timers), {noreply, State#state{timers = Timers}}; handle_cast({stop_ping, JID}, State) -> Timers = del_timer(JID, State#state.timers), @@ -131,53 +153,61 @@ handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({iq_reply, #iq{type = error} = IQ, JID}, State) -> Timers = case xmpp:get_error(IQ) of - #stanza_error{type=cancel, reason='service-unavailable'} -> - del_timer(JID, State#state.timers); - _ -> - State#state.timers - end, + #stanza_error{type = cancel, reason = 'service-unavailable'} -> + del_timer(JID, State#state.timers); + _ -> + State#state.timers + end, {noreply, State#state{timers = Timers}}; handle_info({iq_reply, #iq{}, _JID}, State) -> {noreply, State}; handle_info({iq_reply, timeout, JID}, State) -> - ejabberd_hooks:run(user_ping_timeout, State#state.host, - [JID]), + ejabberd_hooks:run(user_ping_timeout, + State#state.host, + [JID]), Timers = case State#state.timeout_action of - kill -> - #jid{user = User, server = Server, - resource = Resource} = - JID, - case ejabberd_sm:get_session_pid(User, Server, Resource) of - Pid when is_pid(Pid) -> - ejabberd_c2s:close(Pid, ping_timeout); - _ -> - ok - end, - del_timer(JID, State#state.timers); - _ -> - State#state.timers - end, + kill -> + #jid{ + user = User, + server = Server, + resource = Resource + } = + JID, + case ejabberd_sm:get_session_pid(User, Server, Resource) of + Pid when is_pid(Pid) -> + ejabberd_c2s:close(Pid, ping_timeout); + _ -> + ok + end, + del_timer(JID, State#state.timers); + _ -> + State#state.timers + end, {noreply, State#state{timers = Timers}}; handle_info({timeout, _TRef, {ping, JID}}, State) -> Timers = case ejabberd_sm:get_session_pid(JID#jid.luser, - JID#jid.lserver, - JID#jid.lresource) of - none -> - del_timer(JID, State#state.timers); - Pid -> - ejabberd_c2s:cast(Pid, send_ping), - add_timer(JID, State#state.ping_interval, - State#state.timers) - end, + JID#jid.lserver, + JID#jid.lresource) of + none -> + del_timer(JID, State#state.timers); + Pid -> + ejabberd_c2s:cast(Pid, send_ping), + add_timer(JID, + State#state.ping_interval, + State#state.timers) + end, {noreply, State#state{timers = Timers}}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%==================================================================== %% Hook callbacks %%==================================================================== @@ -188,10 +218,12 @@ iq_ping(#iq{lang = Lang} = IQ) -> Txt = ?T("Ping query is incorrect"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). + -spec user_online(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. user_online(_SID, JID, _Info) -> start_ping(JID#jid.lserver, JID). + -spec user_offline(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. user_offline(_SID, JID, _Info) -> case ejabberd_sm:get_session_pid(JID#jid.luser, @@ -203,13 +235,15 @@ user_offline(_SID, JID, _Info) -> ok end. + -spec user_send({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_send({Packet, #{jid := JID} = C2SState}) -> start_ping(JID#jid.lserver, JID), {Packet, C2SState}. --spec c2s_handle_cast(ejabberd_c2s:state(), send_ping | term()) - -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. + +-spec c2s_handle_cast(ejabberd_c2s:state(), send_ping | term()) -> + ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. c2s_handle_cast(#{lserver := Host, jid := JID} = C2SState, send_ping) -> From = jid:make(Host), IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, @@ -220,6 +254,7 @@ c2s_handle_cast(#{lserver := Host, jid := JID} = C2SState, send_ping) -> c2s_handle_cast(C2SState, _Msg) -> C2SState. + %%==================================================================== %% Internal functions %%==================================================================== @@ -227,67 +262,107 @@ init_state(Host, Opts) -> SendPings = mod_ping_opt:send_pings(Opts), PingInterval = mod_ping_opt:ping_interval(Opts), TimeoutAction = mod_ping_opt:timeout_action(Opts), - #state{host = Host, - send_pings = SendPings, - ping_interval = PingInterval, - timeout_action = TimeoutAction, - timers = #{}}. + #state{ + host = Host, + send_pings = SendPings, + ping_interval = PingInterval, + timeout_action = TimeoutAction, + timers = #{} + }. + register_hooks(Host) -> - ejabberd_hooks:add(sm_register_connection_hook, Host, - ?MODULE, user_online, 100), - ejabberd_hooks:add(sm_remove_connection_hook, Host, - ?MODULE, user_offline, 100), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send, 100), - ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, - c2s_handle_cast, 99). + ejabberd_hooks:add(sm_register_connection_hook, + Host, + ?MODULE, + user_online, + 100), + ejabberd_hooks:add(sm_remove_connection_hook, + Host, + ?MODULE, + user_offline, + 100), + ejabberd_hooks:add(user_send_packet, + Host, + ?MODULE, + user_send, + 100), + ejabberd_hooks:add(c2s_handle_cast, + Host, + ?MODULE, + c2s_handle_cast, + 99). + unregister_hooks(Host) -> - ejabberd_hooks:delete(sm_remove_connection_hook, Host, - ?MODULE, user_offline, 100), - ejabberd_hooks:delete(sm_register_connection_hook, Host, - ?MODULE, user_online, 100), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send, 100), - ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, - c2s_handle_cast, 99). + ejabberd_hooks:delete(sm_remove_connection_hook, + Host, + ?MODULE, + user_offline, + 100), + ejabberd_hooks:delete(sm_register_connection_hook, + Host, + ?MODULE, + user_online, + 100), + ejabberd_hooks:delete(user_send_packet, + Host, + ?MODULE, + user_send, + 100), + ejabberd_hooks:delete(c2s_handle_cast, + Host, + ?MODULE, + c2s_handle_cast, + 99). + register_iq_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PING, - ?MODULE, iq_ping), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PING, - ?MODULE, iq_ping). + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_PING, + ?MODULE, + iq_ping), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_PING, + ?MODULE, + iq_ping). + unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PING), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PING). + -spec add_timer(jid(), pos_integer(), timers()) -> timers(). add_timer(JID, Interval, Timers) -> LJID = jid:tolower(JID), NewTimers = case maps:find(LJID, Timers) of - {ok, OldTRef} -> - misc:cancel_timer(OldTRef), - maps:remove(LJID, Timers); - _ -> Timers - end, + {ok, OldTRef} -> + misc:cancel_timer(OldTRef), + maps:remove(LJID, Timers); + _ -> Timers + end, TRef = erlang:start_timer(Interval, self(), {ping, JID}), maps:put(LJID, TRef, NewTimers). + -spec del_timer(jid(), timers()) -> timers(). del_timer(JID, Timers) -> LJID = jid:tolower(JID), case maps:find(LJID, Timers) of - {ok, TRef} -> - misc:cancel_timer(TRef), - maps:remove(LJID, Timers); - _ -> Timers + {ok, TRef} -> + misc:cancel_timer(TRef), + maps:remove(LJID, Timers); + _ -> Timers end. + depends(_Host, _Opts) -> []. + mod_opt_type(ping_interval) -> econf:timeout(second); mod_opt_type(ping_ack_timeout) -> @@ -297,14 +372,17 @@ mod_opt_type(send_pings) -> mod_opt_type(timeout_action) -> econf:enum([none, kill]). + mod_options(_Host) -> [{ping_interval, timer:minutes(1)}, {ping_ack_timeout, undefined}, {send_pings, false}, {timeout_action, none}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module implements support for " "https://xmpp.org/extensions/xep-0199.html" "[XEP-0199: XMPP Ping] and periodic keepalives. " @@ -312,33 +390,40 @@ mod_doc() -> "correctly to ping requests, as defined by the protocol."), opts => [{ping_interval, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("How often to send pings to connected clients, " "if option 'send_pings' is set to 'true'. If a client " "connection does not send or receive any stanza " "within this interval, a ping request is sent to " - "the client. The default value is '1' minute.")}}, + "the client. The default value is '1' minute.") + }}, {ping_ack_timeout, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("How long to wait before deeming that a client " "has not answered a given server ping request. NOTE: when " "_`mod_stream_mgmt`_ is loaded and stream management is " "enabled by a client, this value is ignored, and the " "`ack_timeout` applies instead. " - "The default value is 'undefined'.")}}, + "The default value is 'undefined'.") + }}, {send_pings, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If this option is set to 'true', the server " "sends pings to connected clients that are not " "active in a given interval defined in 'ping_interval' " "option. This is useful to keep client connections " "alive or checking availability. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {timeout_action, - #{value => "none | kill", + #{ + value => "none | kill", desc => ?T("What to do when a client does not answer to a " "server ping request in less than period defined " @@ -349,10 +434,12 @@ mod_doc() -> "a client, killing the client connection doesn't mean " "killing the client session - the session will be kept " "alive in order to give the client a chance to resume it. " - "The default value is 'none'.")}}], + "The default value is 'none'.") + }}], example => ["modules:", " mod_ping:", " send_pings: true", " ping_interval: 4 min", - " timeout_action: kill"]}. + " timeout_action: kill"] + }. diff --git a/src/mod_ping_opt.erl b/src/mod_ping_opt.erl index fd0052130..096564fa1 100644 --- a/src/mod_ping_opt.erl +++ b/src/mod_ping_opt.erl @@ -8,27 +8,30 @@ -export([send_pings/1]). -export([timeout_action/1]). + -spec ping_ack_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | pos_integer(). ping_ack_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(ping_ack_timeout, Opts); ping_ack_timeout(Host) -> gen_mod:get_module_opt(Host, mod_ping, ping_ack_timeout). + -spec ping_interval(gen_mod:opts() | global | binary()) -> pos_integer(). ping_interval(Opts) when is_map(Opts) -> gen_mod:get_opt(ping_interval, Opts); ping_interval(Host) -> gen_mod:get_module_opt(Host, mod_ping, ping_interval). + -spec send_pings(gen_mod:opts() | global | binary()) -> boolean(). send_pings(Opts) when is_map(Opts) -> gen_mod:get_opt(send_pings, Opts); send_pings(Host) -> gen_mod:get_module_opt(Host, mod_ping, send_pings). + -spec timeout_action(gen_mod:opts() | global | binary()) -> 'kill' | 'none'. timeout_action(Opts) when is_map(Opts) -> gen_mod:get_opt(timeout_action, Opts); timeout_action(Host) -> gen_mod:get_module_opt(Host, mod_ping, timeout_action). - diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index bb1b43af8..bef5e665b 100644 --- a/src/mod_pres_counter.erl +++ b/src/mod_pres_counter.erl @@ -27,107 +27,134 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, check_packet/4, - mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). +-export([start/2, + stop/1, + reload/3, + check_packet/4, + mod_opt_type/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). --record(pres_counter, - {dir, start, count, logged = false}). +-record(pres_counter, {dir, start, count, logged = false}). + start(_Host, _Opts) -> {ok, [{hook, privacy_check_packet, check_packet, 25}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. --spec check_packet(allow | deny, ejabberd_c2s:state() | jid(), - stanza(), in | out) -> allow | deny. + +-spec check_packet(allow | deny, + ejabberd_c2s:state() | jid(), + stanza(), + in | out) -> allow | deny. check_packet(Acc, #{jid := JID}, Packet, Dir) -> check_packet(Acc, JID, Packet, Dir); -check_packet(_, #jid{lserver = LServer}, - #presence{from = From, to = To, type = Type}, Dir) -> +check_packet(_, + #jid{lserver = LServer}, + #presence{from = From, to = To, type = Type}, + Dir) -> IsSubscription = case Type of - subscribe -> true; - subscribed -> true; - unsubscribe -> true; - unsubscribed -> true; - _ -> false - end, - if IsSubscription -> - JID = case Dir of - in -> To; - out -> From - end, - update(LServer, JID, Dir); - true -> allow + subscribe -> true; + subscribed -> true; + unsubscribe -> true; + unsubscribed -> true; + _ -> false + end, + if + IsSubscription -> + JID = case Dir of + in -> To; + out -> From + end, + update(LServer, JID, Dir); + true -> allow end; check_packet(Acc, _, _, _) -> Acc. + update(Server, JID, Dir) -> StormCount = mod_pres_counter_opt:count(Server), TimeInterval = mod_pres_counter_opt:interval(Server), TimeStamp = erlang:system_time(millisecond), case read(Dir) of - undefined -> - write(Dir, - #pres_counter{dir = Dir, start = TimeStamp, count = 1}), - allow; - #pres_counter{start = TimeStart, count = Count, - logged = Logged} = - R -> - if TimeStamp - TimeStart > TimeInterval -> - write(Dir, - R#pres_counter{start = TimeStamp, count = 1}), - allow; - (Count =:= StormCount) and Logged -> {stop, deny}; - Count =:= StormCount -> - write(Dir, R#pres_counter{logged = true}), - case Dir of - in -> - ?WARNING_MSG("User ~ts is being flooded, ignoring received " - "presence subscriptions", - [jid:encode(JID)]); - out -> - IP = ejabberd_sm:get_user_ip(JID#jid.luser, - JID#jid.lserver, - JID#jid.lresource), - ?WARNING_MSG("Flooder detected: ~ts, on IP: ~ts ignoring " - "sent presence subscriptions~n", - [jid:encode(JID), - misc:ip_to_list(IP)]) - end, - {stop, deny}; - true -> - write(Dir, - R#pres_counter{start = TimeStamp, count = Count + 1}), - allow - end + undefined -> + write(Dir, + #pres_counter{dir = Dir, start = TimeStamp, count = 1}), + allow; + #pres_counter{ + start = TimeStart, + count = Count, + logged = Logged + } = + R -> + if + TimeStamp - TimeStart > TimeInterval -> + write(Dir, + R#pres_counter{start = TimeStamp, count = 1}), + allow; + (Count =:= StormCount) and Logged -> {stop, deny}; + Count =:= StormCount -> + write(Dir, R#pres_counter{logged = true}), + case Dir of + in -> + ?WARNING_MSG("User ~ts is being flooded, ignoring received " + "presence subscriptions", + [jid:encode(JID)]); + out -> + IP = ejabberd_sm:get_user_ip(JID#jid.luser, + JID#jid.lserver, + JID#jid.lresource), + ?WARNING_MSG("Flooder detected: ~ts, on IP: ~ts ignoring " + "sent presence subscriptions~n", + [jid:encode(JID), + misc:ip_to_list(IP)]) + end, + {stop, deny}; + true -> + write(Dir, + R#pres_counter{start = TimeStamp, count = Count + 1}), + allow + end end. + read(K) -> get({pres_counter, K}). + write(K, V) -> put({pres_counter, K}, V). + mod_opt_type(count) -> econf:pos_int(); mod_opt_type(interval) -> econf:timeout(second). + mod_options(_) -> [{count, 5}, {interval, timer:seconds(60)}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module detects flood/spam in presence " "subscriptions traffic. If a user sends or receives " "more of those stanzas in a given time interval, " @@ -135,7 +162,8 @@ mod_doc() -> "warning is logged."), opts => [{count, - #{value => ?T("Number"), + #{ + value => ?T("Number"), desc => ?T("The number of subscription presence stanzas " "(subscribe, unsubscribe, subscribed, unsubscribed) " @@ -143,13 +171,17 @@ mod_doc() -> "defined in 'interval' option. Please note that two " "users subscribing to each other usually generate 4 " "stanzas, so the recommended value is '4' or more. " - "The default value is '5'.")}}, + "The default value is '5'.") + }}, {interval, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("The time interval. The default value is '1' minute.")}}], + ?T("The time interval. The default value is '1' minute.") + }}], example => ["modules:", " mod_pres_counter:", " count: 5", - " interval: 30 secs"]}. + " interval: 30 secs"] + }. diff --git a/src/mod_pres_counter_opt.erl b/src/mod_pres_counter_opt.erl index 7964fe368..7d48db522 100644 --- a/src/mod_pres_counter_opt.erl +++ b/src/mod_pres_counter_opt.erl @@ -6,15 +6,16 @@ -export([count/1]). -export([interval/1]). + -spec count(gen_mod:opts() | global | binary()) -> pos_integer(). count(Opts) when is_map(Opts) -> gen_mod:get_opt(count, Opts); count(Host) -> gen_mod:get_module_opt(Host, mod_pres_counter, count). + -spec interval(gen_mod:opts() | global | binary()) -> pos_integer(). interval(Opts) when is_map(Opts) -> gen_mod:get_opt(interval, Opts); interval(Host) -> gen_mod:get_module_opt(Host, mod_pres_counter, interval). - diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index c28e6fd89..716b19b3c 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -31,50 +31,71 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_iq/1, export/1, - c2s_copy_session/2, push_list_update/2, disco_features/5, - check_packet/4, remove_user/2, encode_list_item/1, - get_user_lists/2, get_user_list/3, - set_list/1, set_list/4, set_default_list/3, - user_send_packet/1, mod_doc/0, - import_start/2, import_stop/2, import/5, import_info/0, - mod_opt_type/1, mod_options/1, depends/2]). +-export([start/2, + stop/1, + reload/3, + process_iq/1, + export/1, + c2s_copy_session/2, + push_list_update/2, + disco_features/5, + check_packet/4, + remove_user/2, + encode_list_item/1, + get_user_lists/2, + get_user_list/3, + set_list/1, set_list/4, + set_default_list/3, + user_send_packet/1, + mod_doc/0, + import_start/2, + import_stop/2, + import/5, + import_info/0, + mod_opt_type/1, + mod_options/1, + depends/2]). -export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). -import(ejabberd_web_admin, [make_command/4, make_command/2]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("mod_privacy.hrl"). -include("translate.hrl"). --define(PRIVACY_CACHE, privacy_cache). +-define(PRIVACY_CACHE, privacy_cache). -define(PRIVACY_LIST_CACHE, privacy_list_cache). -type c2s_state() :: ejabberd_c2s:state(). + + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(#privacy{}) -> ok. -callback set_default(binary(), binary(), binary()) -> - ok | {error, notfound | any()}. + ok | {error, notfound | any()}. -callback unset_default(binary(), binary()) -> ok | {error, any()}. -callback remove_list(binary(), binary(), binary()) -> - ok | {error, notfound | conflict | any()}. + ok | {error, notfound | conflict | any()}. -callback remove_lists(binary(), binary()) -> ok | {error, any()}. -callback set_lists(#privacy{}) -> ok | {error, any()}. -callback set_list(binary(), binary(), binary(), [listitem()]) -> - ok | {error, any()}. + ok | {error, any()}. -callback get_list(binary(), binary(), binary() | default) -> - {ok, {binary(), [listitem()]}} | error | {error, any()}. + {ok, {binary(), [listitem()]}} | error | {error, any()}. -callback get_lists(binary(), binary()) -> - {ok, #privacy{}} | error | {error, any()}. + {ok, #privacy{}} | error | {error, any()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). + start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), @@ -88,471 +109,570 @@ start(Host, Opts) -> {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, {iq_handler, ejabberd_sm, ?NS_PRIVACY, process_iq}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts). + -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [binary()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [binary()]}. disco_features({error, Err}, _From, _To, _Node, _Lang) -> {error, Err}; disco_features(empty, _From, _To, <<"">>, _Lang) -> {result, [?NS_PRIVACY]}; disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> - {result, [?NS_PRIVACY|Feats]}; + {result, [?NS_PRIVACY | Feats]}; disco_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec process_iq(iq()) -> iq(). -process_iq(#iq{type = Type, - from = #jid{luser = U, lserver = S}, - to = #jid{luser = U, lserver = S}} = IQ) -> +process_iq(#iq{ + type = Type, + from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S} + } = IQ) -> case Type of - get -> process_iq_get(IQ); - set -> process_iq_set(IQ) + get -> process_iq_get(IQ); + set -> process_iq_set(IQ) end; process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). + -spec process_iq_get(iq()) -> iq(). -process_iq_get(#iq{lang = Lang, - sub_els = [#privacy_query{default = Default, - active = Active}]} = IQ) +process_iq_get(#iq{ + lang = Lang, + sub_els = [#privacy_query{ + default = Default, + active = Active + }] + } = IQ) when Default /= undefined; Active /= undefined -> Txt = ?T("Only element is allowed in this query"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); -process_iq_get(#iq{lang = Lang, - sub_els = [#privacy_query{lists = Lists}]} = IQ) -> +process_iq_get(#iq{ + lang = Lang, + sub_els = [#privacy_query{lists = Lists}] + } = IQ) -> case Lists of - [] -> - process_lists_get(IQ); - [#privacy_list{name = ListName}] -> - process_list_get(IQ, ListName); - _ -> - Txt = ?T("Too many elements"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + [] -> + process_lists_get(IQ); + [#privacy_list{name = ListName}] -> + process_list_get(IQ, ListName); + _ -> + Txt = ?T("Too many elements"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; process_iq_get(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_lists_get(iq()) -> iq(). -process_lists_get(#iq{from = #jid{luser = LUser, lserver = LServer}, - lang = Lang} = IQ) -> +process_lists_get(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + lang = Lang + } = IQ) -> case get_user_lists(LUser, LServer) of - {ok, #privacy{default = Default, lists = Lists}} -> - Active = xmpp:get_meta(IQ, privacy_active_list, none), - xmpp:make_iq_result( - IQ, #privacy_query{active = Active, - default = Default, - lists = [#privacy_list{name = Name} - || {Name, _} <- Lists]}); - error -> - xmpp:make_iq_result( - IQ, #privacy_query{active = none, default = none}); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + {ok, #privacy{default = Default, lists = Lists}} -> + Active = xmpp:get_meta(IQ, privacy_active_list, none), + xmpp:make_iq_result( + IQ, + #privacy_query{ + active = Active, + default = Default, + lists = [ #privacy_list{name = Name} + || {Name, _} <- Lists ] + }); + error -> + xmpp:make_iq_result( + IQ, #privacy_query{active = none, default = none}); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. + -spec process_list_get(iq(), binary()) -> iq(). -process_list_get(#iq{from = #jid{luser = LUser, lserver = LServer}, - lang = Lang} = IQ, Name) -> +process_list_get(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + lang = Lang + } = IQ, + Name) -> case get_user_list(LUser, LServer, Name) of - {ok, {_, List}} -> - Items = lists:map(fun encode_list_item/1, List), - xmpp:make_iq_result( - IQ, - #privacy_query{ - lists = [#privacy_list{name = Name, items = Items}]}); - error -> - Txt = ?T("No privacy list with this name found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + {ok, {_, List}} -> + Items = lists:map(fun encode_list_item/1, List), + xmpp:make_iq_result( + IQ, + #privacy_query{ + lists = [#privacy_list{name = Name, items = Items}] + }); + error -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. + -spec encode_list_item(listitem()) -> privacy_item(). -encode_list_item(#listitem{action = Action, - order = Order, - type = Type, - match_all = MatchAll, - match_iq = MatchIQ, - match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut, - value = Value}) -> - Item = #privacy_item{action = Action, - order = Order, - type = case Type of - none -> undefined; - Type -> Type - end, - value = encode_value(Type, Value)}, +encode_list_item(#listitem{ + action = Action, + order = Order, + type = Type, + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut, + value = Value + }) -> + Item = #privacy_item{ + action = Action, + order = Order, + type = case Type of + none -> undefined; + Type -> Type + end, + value = encode_value(Type, Value) + }, case MatchAll of - true -> - Item; - false -> - Item#privacy_item{message = MatchMessage, - iq = MatchIQ, - presence_in = MatchPresenceIn, - presence_out = MatchPresenceOut} + true -> + Item; + false -> + Item#privacy_item{ + message = MatchMessage, + iq = MatchIQ, + presence_in = MatchPresenceIn, + presence_out = MatchPresenceOut + } end. + -spec encode_value(listitem_type(), listitem_value()) -> binary(). encode_value(Type, Val) -> case Type of - jid -> jid:encode(Val); - group -> Val; - subscription -> - case Val of - both -> <<"both">>; - to -> <<"to">>; - from -> <<"from">>; - none -> <<"none">> - end; - none -> <<"">> + jid -> jid:encode(Val); + group -> Val; + subscription -> + case Val of + both -> <<"both">>; + to -> <<"to">>; + from -> <<"from">>; + none -> <<"none">> + end; + none -> <<"">> end. + -spec decode_value(jid | subscription | group | undefined, binary()) -> - listitem_value(). + listitem_value(). decode_value(Type, Value) -> case Type of - jid -> jid:tolower(jid:decode(Value)); - subscription -> - case Value of - <<"from">> -> from; - <<"to">> -> to; - <<"both">> -> both; - <<"none">> -> none - end; - group when Value /= <<"">> -> Value; - undefined -> none + jid -> jid:tolower(jid:decode(Value)); + subscription -> + case Value of + <<"from">> -> from; + <<"to">> -> to; + <<"both">> -> both; + <<"none">> -> none + end; + group when Value /= <<"">> -> Value; + undefined -> none end. + -spec process_iq_set(iq()) -> iq(). -process_iq_set(#iq{lang = Lang, - sub_els = [#privacy_query{default = Default, - active = Active, - lists = Lists}]} = IQ) -> +process_iq_set(#iq{ + lang = Lang, + sub_els = [#privacy_query{ + default = Default, + active = Active, + lists = Lists + }] + } = IQ) -> case Lists of - [#privacy_list{items = Items, name = ListName}] - when Default == undefined, Active == undefined -> - process_lists_set(IQ, ListName, Items); - [] when Default == undefined, Active /= undefined -> - process_active_set(IQ, Active); - [] when Active == undefined, Default /= undefined -> - process_default_set(IQ, Default); - _ -> - Txt = ?T("The stanza MUST contain only one element, " - "one element, or one element"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + [#privacy_list{items = Items, name = ListName}] + when Default == undefined, Active == undefined -> + process_lists_set(IQ, ListName, Items); + [] when Default == undefined, Active /= undefined -> + process_active_set(IQ, Active); + [] when Active == undefined, Default /= undefined -> + process_default_set(IQ, Default); + _ -> + Txt = ?T("The stanza MUST contain only one element, " + "one element, or one element"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; process_iq_set(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_default_set(iq(), none | binary()) -> iq(). -process_default_set(#iq{from = #jid{luser = LUser, lserver = LServer}, - lang = Lang} = IQ, Value) -> +process_default_set(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + lang = Lang + } = IQ, + Value) -> case set_default_list(LUser, LServer, Value) of - ok -> - xmpp:make_iq_result(IQ); - {error, notfound} -> - Txt = ?T("No privacy list with this name found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + ok -> + xmpp:make_iq_result(IQ); + {error, notfound} -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. + -spec process_active_set(IQ, none | binary()) -> IQ. process_active_set(IQ, none) -> xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, none)); -process_active_set(#iq{from = #jid{luser = LUser, lserver = LServer}, - lang = Lang} = IQ, Name) -> +process_active_set(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + lang = Lang + } = IQ, + Name) -> case get_user_list(LUser, LServer, Name) of - {ok, _} -> - xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, Name)); - error -> - Txt = ?T("No privacy list with this name found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + {ok, _} -> + xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, Name)); + error -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. + -spec set_list(privacy()) -> ok | {error, any()}. set_list(#privacy{us = {LUser, LServer}, lists = Lists} = Privacy) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_lists(Privacy) of - ok -> - Names = [Name || {Name, _} <- Lists], - delete_cache(Mod, LUser, LServer, Names); - {error, _} = Err -> - Err + ok -> + Names = [ Name || {Name, _} <- Lists ], + delete_cache(Mod, LUser, LServer, Names); + {error, _} = Err -> + Err end. + -spec process_lists_set(iq(), binary(), [privacy_item()]) -> iq(). -process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer}, - lang = Lang} = IQ, Name, []) -> +process_lists_set(#iq{ + from = #jid{luser = LUser, lserver = LServer}, + lang = Lang + } = IQ, + Name, + []) -> case xmpp:get_meta(IQ, privacy_active_list, none) of - Name -> - Txt = ?T("Cannot remove active list"), - xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); - _ -> - case remove_list(LUser, LServer, Name) of - ok -> - xmpp:make_iq_result(IQ); - {error, conflict} -> - Txt = ?T("Cannot remove default list"), - xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); - {error, notfound} -> - Txt = ?T("No privacy list with this name found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); - {error, _} -> - Txt = ?T("Database failure"), - Err = xmpp:err_internal_server_error(Txt, Lang), - xmpp:make_error(IQ, Err) - end + Name -> + Txt = ?T("Cannot remove active list"), + xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); + _ -> + case remove_list(LUser, LServer, Name) of + ok -> + xmpp:make_iq_result(IQ); + {error, conflict} -> + Txt = ?T("Cannot remove default list"), + xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); + {error, notfound} -> + Txt = ?T("No privacy list with this name found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end end; -process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer} = From, - lang = Lang} = IQ, Name, Items) -> +process_lists_set(#iq{ + from = #jid{luser = LUser, lserver = LServer} = From, + lang = Lang + } = IQ, + Name, + Items) -> case catch lists:map(fun decode_item/1, Items) of - {error, Why} -> - Txt = xmpp:io_format_error(Why), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); - List -> - case set_list(LUser, LServer, Name, List) of - ok -> - push_list_update(From, Name), - xmpp:make_iq_result(IQ); - {error, _} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) - end + {error, Why} -> + Txt = xmpp:io_format_error(Why), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + List -> + case set_list(LUser, LServer, Name, List) of + ok -> + push_list_update(From, Name), + xmpp:make_iq_result(IQ); + {error, _} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end end. + -spec push_list_update(jid(), binary()) -> ok. push_list_update(From, Name) -> BareFrom = jid:remove_resource(From), lists:foreach( fun(R) -> - To = jid:replace_resource(From, R), - IQ = #iq{type = set, from = BareFrom, to = To, - id = <<"push", (p1_rand:get_string())/binary>>, - sub_els = [#privacy_query{ - lists = [#privacy_list{name = Name}]}]}, - ejabberd_router:route(IQ) - end, ejabberd_sm:get_user_resources(From#jid.luser, From#jid.lserver)). + To = jid:replace_resource(From, R), + IQ = #iq{ + type = set, + from = BareFrom, + to = To, + id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [#privacy_query{ + lists = [#privacy_list{name = Name}] + }] + }, + ejabberd_router:route(IQ) + end, + ejabberd_sm:get_user_resources(From#jid.luser, From#jid.lserver)). + -spec decode_item(privacy_item()) -> listitem(). -decode_item(#privacy_item{order = Order, - action = Action, - type = T, - value = V, - message = MatchMessage, - iq = MatchIQ, - presence_in = MatchPresenceIn, - presence_out = MatchPresenceOut}) -> - Value = try decode_value(T, V) - catch _:_ -> - throw({error, {bad_attr_value, <<"value">>, - <<"item">>, ?NS_PRIVACY}}) - end, +decode_item(#privacy_item{ + order = Order, + action = Action, + type = T, + value = V, + message = MatchMessage, + iq = MatchIQ, + presence_in = MatchPresenceIn, + presence_out = MatchPresenceOut + }) -> + Value = try + decode_value(T, V) + catch + _:_ -> + throw({error, {bad_attr_value, <<"value">>, + <<"item">>, + ?NS_PRIVACY}}) + end, Type = case T of - undefined -> none; - _ -> T - end, - ListItem = #listitem{order = Order, - action = Action, - type = Type, - value = Value}, - if not (MatchMessage or MatchIQ or MatchPresenceIn or MatchPresenceOut) -> - ListItem#listitem{match_all = true}; - true -> - ListItem#listitem{match_iq = MatchIQ, - match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut} + undefined -> none; + _ -> T + end, + ListItem = #listitem{ + order = Order, + action = Action, + type = Type, + value = Value + }, + if + not (MatchMessage or MatchIQ or MatchPresenceIn or MatchPresenceOut) -> + ListItem#listitem{match_all = true}; + true -> + ListItem#listitem{ + match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut + } end. + -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{privacy_active_list := List}) -> State#{privacy_active_list => List}; c2s_copy_session(State, _) -> State. + %% Adjust the client's state, so next packets (which can be already queued) %% will take the active list into account. -spec update_c2s_state_with_privacy_list(stanza(), c2s_state()) -> c2s_state(). -update_c2s_state_with_privacy_list(#iq{type = set, - to = #jid{luser = U, lserver = S, - lresource = <<"">>} = To} = IQ, - State) -> +update_c2s_state_with_privacy_list(#iq{ + type = set, + to = #jid{ + luser = U, + lserver = S, + lresource = <<"">> + } = To + } = IQ, + State) -> %% Match a IQ set containing a new active privacy list case xmpp:get_subtag(IQ, #privacy_query{}) of - #privacy_query{default = undefined, active = Active} -> - case Active of - none -> - ?DEBUG("Removing active privacy list for user: ~ts", - [jid:encode(To)]), - State#{privacy_active_list => none}; - undefined -> - State; - _ -> - case get_user_list(U, S, Active) of - {ok, _} -> - ?DEBUG("Setting active privacy list '~ts' for user: ~ts", - [Active, jid:encode(To)]), - State#{privacy_active_list => Active}; - _ -> - %% unknown privacy list name - State - end - end; - _ -> - State + #privacy_query{default = undefined, active = Active} -> + case Active of + none -> + ?DEBUG("Removing active privacy list for user: ~ts", + [jid:encode(To)]), + State#{privacy_active_list => none}; + undefined -> + State; + _ -> + case get_user_list(U, S, Active) of + {ok, _} -> + ?DEBUG("Setting active privacy list '~ts' for user: ~ts", + [Active, jid:encode(To)]), + State#{privacy_active_list => Active}; + _ -> + %% unknown privacy list name + State + end + end; + _ -> + State end; update_c2s_state_with_privacy_list(_Packet, State) -> State. + %% Add the active privacy list to packet metadata -spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. -user_send_packet({#iq{type = Type, - to = #jid{luser = U, lserver = S, lresource = <<"">>}, - from = #jid{luser = U, lserver = S}, - sub_els = [_]} = IQ, - #{privacy_active_list := Name} = State}) +user_send_packet({#iq{ + type = Type, + to = #jid{luser = U, lserver = S, lresource = <<"">>}, + from = #jid{luser = U, lserver = S}, + sub_els = [_] + } = IQ, + #{privacy_active_list := Name} = State}) when Type == get; Type == set -> NewIQ = case xmpp:has_subtag(IQ, #privacy_query{}) of - true -> xmpp:put_meta(IQ, privacy_active_list, Name); - false -> IQ - end, + true -> xmpp:put_meta(IQ, privacy_active_list, Name); + false -> IQ + end, {NewIQ, update_c2s_state_with_privacy_list(IQ, State)}; %% For client with no active privacy list, see if there is %% one about to be activated in this packet and update client state user_send_packet({Packet, State}) -> {Packet, update_c2s_state_with_privacy_list(Packet, State)}. + -spec set_list(binary(), binary(), binary(), [listitem()]) -> ok | {error, any()}. set_list(LUser, LServer, Name, List) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_list(LUser, LServer, Name, List) of - ok -> - delete_cache(Mod, LUser, LServer, [Name]); - {error, _} = Err -> - Err + ok -> + delete_cache(Mod, LUser, LServer, [Name]); + {error, _} = Err -> + Err end. + -spec remove_list(binary(), binary(), binary()) -> - ok | {error, conflict | notfound | any()}. + ok | {error, conflict | notfound | any()}. remove_list(LUser, LServer, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:remove_list(LUser, LServer, Name) of - ok -> - delete_cache(Mod, LUser, LServer, [Name]); - Err -> - Err + ok -> + delete_cache(Mod, LUser, LServer, [Name]); + Err -> + Err end. + -spec get_user_lists(binary(), binary()) -> {ok, privacy()} | error | {error, any()}. get_user_lists(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?PRIVACY_CACHE, {LUser, LServer}, - fun() -> Mod:get_lists(LUser, LServer) end); - false -> - Mod:get_lists(LUser, LServer) + true -> + ets_cache:lookup( + ?PRIVACY_CACHE, + {LUser, LServer}, + fun() -> Mod:get_lists(LUser, LServer) end); + false -> + Mod:get_lists(LUser, LServer) end. + -spec get_user_list(binary(), binary(), binary() | default) -> - {ok, {binary(), [listitem()]}} | error | {error, any()}. + {ok, {binary(), [listitem()]}} | error | {error, any()}. get_user_list(LUser, LServer, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?PRIVACY_LIST_CACHE, {LUser, LServer, Name}, - fun() -> - case ets_cache:lookup( - ?PRIVACY_CACHE, {LUser, LServer}) of - {ok, Privacy} -> - get_list_by_name(Privacy, Name); - error -> - Mod:get_list(LUser, LServer, Name) - end - end); - false -> - Mod:get_list(LUser, LServer, Name) + true -> + ets_cache:lookup( + ?PRIVACY_LIST_CACHE, + {LUser, LServer, Name}, + fun() -> + case ets_cache:lookup( + ?PRIVACY_CACHE, {LUser, LServer}) of + {ok, Privacy} -> + get_list_by_name(Privacy, Name); + error -> + Mod:get_list(LUser, LServer, Name) + end + end); + false -> + Mod:get_list(LUser, LServer, Name) end. + -spec get_list_by_name(#privacy{}, binary() | default) -> - {ok, {binary(), [listitem()]}} | error. + {ok, {binary(), [listitem()]}} | error. get_list_by_name(#privacy{default = Default} = Privacy, default) -> get_list_by_name(Privacy, Default); get_list_by_name(#privacy{lists = Lists}, Name) -> case lists:keyfind(Name, 1, Lists) of - {_, List} -> {ok, {Name, List}}; - false -> error + {_, List} -> {ok, {Name, List}}; + false -> error end. + -spec set_default_list(binary(), binary(), binary() | none) -> - ok | {error, notfound | any()}. + ok | {error, notfound | any()}. set_default_list(LUser, LServer, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case Name of - none -> Mod:unset_default(LUser, LServer); - _ -> Mod:set_default(LUser, LServer, Name) - end, + none -> Mod:unset_default(LUser, LServer); + _ -> Mod:set_default(LUser, LServer, Name) + end, case Res of - ok -> - delete_cache(Mod, LUser, LServer, []); - Err -> - Err + ok -> + delete_cache(Mod, LUser, LServer, []); + Err -> + Err end. + -spec check_packet(allow | deny, c2s_state() | jid(), stanza(), in | out) -> allow | deny. check_packet(Acc, #{jid := JID} = State, Packet, Dir) -> case maps:get(privacy_active_list, State, none) of - none -> - check_packet(Acc, JID, Packet, Dir); - ListName -> - #jid{luser = LUser, lserver = LServer} = JID, - case get_user_list(LUser, LServer, ListName) of - {ok, {_, List}} -> - do_check_packet(JID, List, Packet, Dir); - _ -> - ?DEBUG("Non-existing active list '~ts' is set " - "for user '~ts'", [ListName, jid:encode(JID)]), - check_packet(Acc, JID, Packet, Dir) - end + none -> + check_packet(Acc, JID, Packet, Dir); + ListName -> + #jid{luser = LUser, lserver = LServer} = JID, + case get_user_list(LUser, LServer, ListName) of + {ok, {_, List}} -> + do_check_packet(JID, List, Packet, Dir); + _ -> + ?DEBUG("Non-existing active list '~ts' is set " + "for user '~ts'", + [ListName, jid:encode(JID)]), + check_packet(Acc, JID, Packet, Dir) + end end; check_packet(_, JID, Packet, Dir) -> #jid{luser = LUser, lserver = LServer} = JID, case get_user_list(LUser, LServer, default) of - {ok, {_, List}} -> - do_check_packet(JID, List, Packet, Dir); - _ -> - allow + {ok, {_, List}} -> + do_check_packet(JID, List, Packet, Dir); + _ -> + allow end. + %% From is the sender, To is the destination. %% If Dir = out, User@Server is the sender account (From). %% If Dir = in, User@Server is the destination account (To). @@ -563,104 +683,110 @@ do_check_packet(#jid{luser = LUser, lserver = LServer}, List, Packet, Dir) -> From = xmpp:get_from(Packet), To = xmpp:get_to(Packet), case {From, To} of - {#jid{luser = <<"">>, lserver = LServer}, - #jid{lserver = LServer}} when Dir == in -> - %% Allow any packets from local server - allow; - {#jid{lserver = LServer}, - #jid{luser = <<"">>, lserver = LServer}} when Dir == out -> - %% Allow any packets to local server - allow; - {#jid{luser = LUser, lserver = LServer, lresource = <<"">>}, - #jid{luser = LUser, lserver = LServer}} when Dir == in -> - %% Allow incoming packets from user's bare jid to his full jid - allow; - {#jid{luser = LUser, lserver = LServer}, - #jid{luser = LUser, lserver = LServer, lresource = <<"">>}} when Dir == out -> - %% Allow outgoing packets from user's full jid to his bare JID - allow; - _ -> - PType = case Packet of - #message{} -> message; - #iq{} -> iq; - #presence{type = available} -> presence; - #presence{type = unavailable} -> presence; - _ -> other - end, - PType2 = case {PType, Dir} of - {message, in} -> message; - {iq, in} -> iq; - {presence, in} -> presence_in; - {presence, out} -> presence_out; - {_, _} -> other - end, - LJID = case Dir of - in -> jid:tolower(From); - out -> jid:tolower(To) - end, - check_packet_aux(List, PType2, LJID, [LUser, LServer]) + {#jid{luser = <<"">>, lserver = LServer}, + #jid{lserver = LServer}} when Dir == in -> + %% Allow any packets from local server + allow; + {#jid{lserver = LServer}, + #jid{luser = <<"">>, lserver = LServer}} when Dir == out -> + %% Allow any packets to local server + allow; + {#jid{luser = LUser, lserver = LServer, lresource = <<"">>}, + #jid{luser = LUser, lserver = LServer}} when Dir == in -> + %% Allow incoming packets from user's bare jid to his full jid + allow; + {#jid{luser = LUser, lserver = LServer}, + #jid{luser = LUser, lserver = LServer, lresource = <<"">>}} when Dir == out -> + %% Allow outgoing packets from user's full jid to his bare JID + allow; + _ -> + PType = case Packet of + #message{} -> message; + #iq{} -> iq; + #presence{type = available} -> presence; + #presence{type = unavailable} -> presence; + _ -> other + end, + PType2 = case {PType, Dir} of + {message, in} -> message; + {iq, in} -> iq; + {presence, in} -> presence_in; + {presence, out} -> presence_out; + {_, _} -> other + end, + LJID = case Dir of + in -> jid:tolower(From); + out -> jid:tolower(To) + end, + check_packet_aux(List, PType2, LJID, [LUser, LServer]) end. + -spec check_packet_aux([listitem()], - message | iq | presence_in | presence_out | other, - ljid(), [binary()] | {none | both | from | to, [binary()]}) -> - allow | deny. + message | iq | presence_in | presence_out | other, + ljid(), + [binary()] | {none | both | from | to, [binary()]}) -> + allow | deny. %% Ptype = message | iq | presence_in | presence_out | other check_packet_aux([], _PType, _JID, _RosterInfo) -> allow; check_packet_aux([Item | List], PType, JID, RosterInfo) -> #listitem{type = Type, value = Value, action = Action} = - Item, + Item, case is_ptype_match(Item, PType) of - true -> - case is_type_match(Type, Value, JID, RosterInfo) of - {true, _} -> Action; - {false, RI} -> - check_packet_aux(List, PType, JID, RI) - end; - false -> - check_packet_aux(List, PType, JID, RosterInfo) + true -> + case is_type_match(Type, Value, JID, RosterInfo) of + {true, _} -> Action; + {false, RI} -> + check_packet_aux(List, PType, JID, RI) + end; + false -> + check_packet_aux(List, PType, JID, RosterInfo) end. + -spec is_ptype_match(listitem(), - message | iq | presence_in | presence_out | other) -> - boolean(). + message | iq | presence_in | presence_out | other) -> + boolean(). is_ptype_match(Item, PType) -> case Item#listitem.match_all of - true -> true; - false -> - case PType of - message -> Item#listitem.match_message; - iq -> Item#listitem.match_iq; - presence_in -> Item#listitem.match_presence_in; - presence_out -> Item#listitem.match_presence_out; - other -> false - end + true -> true; + false -> + case PType of + message -> Item#listitem.match_message; + iq -> Item#listitem.match_iq; + presence_in -> Item#listitem.match_presence_in; + presence_out -> Item#listitem.match_presence_out; + other -> false + end end. --spec is_type_match(none | jid | subscription | group, listitem_value(), - ljid(), [binary()] | {none | both | from | to, [binary()]}) -> - {boolean(), [binary()] | {none | both | from | to, [binary()]}}. + +-spec is_type_match(none | jid | subscription | group, + listitem_value(), + ljid(), + [binary()] | {none | both | from | to, [binary()]}) -> + {boolean(), [binary()] | {none | both | from | to, [binary()]}}. is_type_match(none, _Value, _JID, RosterInfo) -> {true, RosterInfo}; is_type_match(jid, Value, JID, RosterInfo) -> case Value of - {<<"">>, Server, <<"">>} -> - case JID of - {_, Server, _} -> {true, RosterInfo}; - _ -> {false, RosterInfo} - end; - {User, Server, <<"">>} -> - case JID of - {User, Server, _} -> {true, RosterInfo}; - _ -> {false, RosterInfo} - end; - {<<"">>, Server, Resource} -> - case JID of - {_, Server, Resource} -> {true, RosterInfo}; - _ -> {false, RosterInfo} - end; - _ -> {Value == JID, RosterInfo} + {<<"">>, Server, <<"">>} -> + case JID of + {_, Server, _} -> {true, RosterInfo}; + _ -> {false, RosterInfo} + end; + {User, Server, <<"">>} -> + case JID of + {User, Server, _} -> {true, RosterInfo}; + _ -> {false, RosterInfo} + end; + {<<"">>, Server, Resource} -> + case JID of + {_, Server, Resource} -> {true, RosterInfo}; + _ -> {false, RosterInfo} + end; + _ -> {Value == JID, RosterInfo} end; is_type_match(subscription, Value, JID, RosterInfo) -> {Subscription, _} = RI = resolve_roster_info(JID, RosterInfo), @@ -669,18 +795,21 @@ is_type_match(group, Group, JID, RosterInfo) -> {_, Groups} = RI = resolve_roster_info(JID, RosterInfo), {lists:member(Group, Groups), RI}. + -spec resolve_roster_info(ljid(), [binary()] | {none | both | from | to, [binary()]}) -> - {none | both | from | to, [binary()]}. + {none | both | from | to, [binary()]}. resolve_roster_info(JID, [LUser, LServer]) -> {Subscription, _Ask, Groups} = - ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, - {none, none, []}, - [LUser, LServer, JID]), + ejabberd_hooks:run_fold( + roster_get_jid_info, + LServer, + {none, none, []}, + [LUser, LServer, JID]), {Subscription, Groups}; resolve_roster_info(_, RosterInfo) -> RosterInfo. + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), @@ -689,25 +818,27 @@ remove_user(User, Server) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_lists(LUser, LServer), case Privacy of - {ok, #privacy{lists = Lists}} -> - Names = [Name || {Name, _} <- Lists], - delete_cache(Mod, LUser, LServer, Names); - _ -> - ok + {ok, #privacy{lists = Lists}} -> + Names = [ Name || {Name, _} <- Lists ], + delete_cache(Mod, LUser, LServer, Names); + _ -> + ok end. + -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?PRIVACY_CACHE, CacheOpts), - ets_cache:new(?PRIVACY_LIST_CACHE, CacheOpts); - false -> - ets_cache:delete(?PRIVACY_CACHE), - ets_cache:delete(?PRIVACY_LIST_CACHE) + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?PRIVACY_CACHE, CacheOpts), + ets_cache:new(?PRIVACY_LIST_CACHE, CacheOpts); + false -> + ets_cache:delete(?PRIVACY_CACHE), + ets_cache:delete(?PRIVACY_LIST_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_privacy_opt:cache_size(Opts), @@ -715,56 +846,75 @@ cache_opts(Opts) -> LifeTime = mod_privacy_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(Host); - false -> mod_privacy_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_privacy_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + -spec delete_cache(module(), binary(), binary(), [binary()]) -> ok. delete_cache(Mod, LUser, LServer, Names) -> case use_cache(Mod, LServer) of - true -> - Nodes = cache_nodes(Mod, LServer), - ets_cache:delete(?PRIVACY_CACHE, {LUser, LServer}, Nodes), - lists:foreach( - fun(Name) -> - ets_cache:delete( - ?PRIVACY_LIST_CACHE, - {LUser, LServer, Name}, - Nodes) - end, [default|Names]); - false -> - ok + true -> + Nodes = cache_nodes(Mod, LServer), + ets_cache:delete(?PRIVACY_CACHE, {LUser, LServer}, Nodes), + lists:foreach( + fun(Name) -> + ets_cache:delete( + ?PRIVACY_LIST_CACHE, + {LUser, LServer, Name}, + Nodes) + end, + [default | Names]); + false -> + ok end. + numeric_to_binary(<<0, 0, _/binary>>) -> <<"0">>; numeric_to_binary(<<0, _, _:6/binary, T/binary>>) -> Res = lists:foldl( fun(X, Sum) -> - Sum*10000 + X - end, 0, [X || <> <= T]), + Sum * 10000 + X + end, + 0, + [ X || <> <= T ]), integer_to_binary(Res). + bool_to_binary(<<0>>) -> <<"0">>; bool_to_binary(<<1>>) -> <<"1">>. -prepare_list_data(mysql, [ID|Row]) -> - [binary_to_integer(ID)|Row]; -prepare_list_data(pgsql, [<>, - SType, SValue, SAction, SOrder, SMatchAll, - SMatchIQ, SMatchMessage, SMatchPresenceIn, - SMatchPresenceOut]) -> - [ID, SType, SValue, SAction, + +prepare_list_data(mysql, [ID | Row]) -> + [binary_to_integer(ID) | Row]; +prepare_list_data(pgsql, + [<>, + SType, + SValue, + SAction, + SOrder, + SMatchAll, + SMatchIQ, + SMatchMessage, + SMatchPresenceIn, + SMatchPresenceOut]) -> + [ID, + SType, + SValue, + SAction, numeric_to_binary(SOrder), bool_to_binary(SMatchAll), bool_to_binary(SMatchIQ), @@ -772,30 +922,37 @@ prepare_list_data(pgsql, [<>, bool_to_binary(SMatchPresenceIn), bool_to_binary(SMatchPresenceOut)]. + prepare_id(mysql, ID) -> binary_to_integer(ID); prepare_id(pgsql, <>) -> ID. + import_info() -> [{<<"privacy_default_list">>, 2}, {<<"privacy_list_data">>, 10}, {<<"privacy_list">>, 4}]. + import_start(LServer, DBType) -> ets:new(privacy_default_list_tmp, [private, named_table]), ets:new(privacy_list_data_tmp, [private, named_table, bag]), - ets:new(privacy_list_tmp, [private, named_table, bag, - {keypos, #privacy.us}]), + ets:new(privacy_list_tmp, + [private, + named_table, + bag, + {keypos, #privacy.us}]), Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). + import(LServer, {sql, _}, _DBType, <<"privacy_default_list">>, [LUser, Name]) -> US = {LUser, LServer}, ets:insert(privacy_default_list_tmp, {US, Name}), ok; import(LServer, {sql, SQLType}, _DBType, <<"privacy_list_data">>, Row1) -> - [ID|Row] = prepare_list_data(SQLType, Row1), + [ID | Row] = prepare_list_data(SQLType, Row1), case mod_privacy_sql:raw_to_item(Row) of [Item] -> IS = {ID, LServer}, @@ -804,7 +961,10 @@ import(LServer, {sql, SQLType}, _DBType, <<"privacy_list_data">>, Row1) -> [] -> ok end; -import(LServer, {sql, SQLType}, _DBType, <<"privacy_list">>, +import(LServer, + {sql, SQLType}, + _DBType, + <<"privacy_list">>, [LUser, Name, ID, _TimeStamp]) -> US = {LUser, LServer}, IS = {prepare_id(SQLType, ID), LServer}, @@ -812,11 +972,13 @@ import(LServer, {sql, SQLType}, _DBType, <<"privacy_list">>, [{_, Name}] -> Name; _ -> none end, - case [Item || {_, Item} <- ets:lookup(privacy_list_data_tmp, IS)] of - [_|_] = Items -> - Privacy = #privacy{us = {LUser, LServer}, - default = Default, - lists = [{Name, Items}]}, + case [ Item || {_, Item} <- ets:lookup(privacy_list_data_tmp, IS) ] of + [_ | _] = Items -> + Privacy = #privacy{ + us = {LUser, LServer}, + default = Default, + lists = [{Name, Items}] + }, ets:insert(privacy_list_tmp, Privacy), ets:delete(privacy_list_data_tmp, IS), ok; @@ -824,6 +986,7 @@ import(LServer, {sql, SQLType}, _DBType, <<"privacy_list">>, ok end. + import_stop(_LServer, DBType) -> import_next(DBType, ets:first(privacy_list_tmp)), ets:delete(privacy_default_list_tmp), @@ -831,44 +994,55 @@ import_stop(_LServer, DBType) -> ets:delete(privacy_list_tmp), ok. + import_next(_DBType, '$end_of_table') -> ok; import_next(DBType, US) -> - [P|_] = Ps = ets:lookup(privacy_list_tmp, US), + [P | _] = Ps = ets:lookup(privacy_list_tmp, US), Lists = lists:flatmap( fun(#privacy{lists = Lists}) -> Lists - end, Ps), + end, + Ps), Privacy = P#privacy{lists = Lists}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(Privacy), import_next(DBType, ets:next(privacy_list_tmp, US)). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + %%% %%% WebAdmin %%% + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"privacy">>, <<"Privacy Lists">>}]. -webadmin_page_hostuser(_, Host, User, - #request{us = _US, path = [<<"privacy">>]} = R) -> - Res = ?H1GL(<<"Privacy Lists">>, <<"modules/#mod_privacy">>, <<"mod_privacy">>) - ++ [make_command(privacy_set, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + +webadmin_page_hostuser(_, + Host, + User, + #request{us = _US, path = [<<"privacy">>]} = R) -> + Res = ?H1GL(<<"Privacy Lists">>, <<"modules/#mod_privacy">>, <<"mod_privacy">>) ++ + [make_command(privacy_set, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], {stop, Res}; webadmin_page_hostuser(Acc, _, _, _) -> Acc. + %%% %%% Documentation %%% + depends(_Host, _Opts) -> []. + mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> @@ -880,6 +1054,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, @@ -887,11 +1062,14 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0016.html" - "[XEP-0016: Privacy Lists]."), "", + "[XEP-0016: Privacy Lists]."), + "", ?T("NOTE: Nowadays modern XMPP clients rely on " "https://xmpp.org/extensions/xep-0191.html" "[XEP-0191: Blocking Command] which is implemented by " @@ -899,22 +1077,33 @@ mod_doc() -> "'mod_privacy' loaded in order for 'mod_blocking' to work.")], opts => [{db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}] + }. diff --git a/src/mod_privacy_mnesia.erl b/src/mod_privacy_mnesia.erl index b8657e719..3e52846eb 100644 --- a/src/mod_privacy_mnesia.erl +++ b/src/mod_privacy_mnesia.erl @@ -27,22 +27,34 @@ -behaviour(mod_privacy). %% API --export([init/2, set_default/3, unset_default/2, set_lists/1, - set_list/4, get_lists/2, get_list/3, remove_lists/2, - remove_list/3, use_cache/1, import/1]). +-export([init/2, + set_default/3, + unset_default/2, + set_lists/1, + set_list/4, + get_lists/2, + get_list/3, + remove_lists/2, + remove_list/3, + use_cache/1, + import/1]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_privacy.hrl"). -include("logger.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, privacy, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, privacy)}]). + ejabberd_mnesia:create(?MODULE, + privacy, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, privacy)}]). + use_cache(Host) -> case mnesia:table_info(privacy, storage_type) of @@ -52,82 +64,94 @@ use_cache(Host) -> false end. + unset_default(LUser, LServer) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> ok; - [R] -> mnesia:write(R#privacy{default = none}) - end - end, + F = fun() -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> ok; + [R] -> mnesia:write(R#privacy{default = none}) + end + end, transaction(F). + set_default(LUser, LServer, Name) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> - {error, notfound}; - [#privacy{lists = Lists} = P] -> - case lists:keymember(Name, 1, Lists) of - true -> - mnesia:write(P#privacy{default = Name, - lists = Lists}); - false -> - {error, notfound} - end - end - end, + F = fun() -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> + {error, notfound}; + [#privacy{lists = Lists} = P] -> + case lists:keymember(Name, 1, Lists) of + true -> + mnesia:write(P#privacy{ + default = Name, + lists = Lists + }); + false -> + {error, notfound} + end + end + end, transaction(F). + remove_list(LUser, LServer, Name) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> - {error, notfound}; - [#privacy{default = Default, lists = Lists} = P] -> - if Name == Default -> - {error, conflict}; - true -> - NewLists = lists:keydelete(Name, 1, Lists), - mnesia:write(P#privacy{lists = NewLists}) - end - end - end, + F = fun() -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> + {error, notfound}; + [#privacy{default = Default, lists = Lists} = P] -> + if + Name == Default -> + {error, conflict}; + true -> + NewLists = lists:keydelete(Name, 1, Lists), + mnesia:write(P#privacy{lists = NewLists}) + end + end + end, transaction(F). + set_lists(Privacy) -> mnesia:dirty_write(Privacy). + set_list(LUser, LServer, Name, List) -> - F = fun () -> - case mnesia:wread({privacy, {LUser, LServer}}) of - [] -> - NewLists = [{Name, List}], - mnesia:write(#privacy{us = {LUser, LServer}, - lists = NewLists}); - [#privacy{lists = Lists} = P] -> - NewLists1 = lists:keydelete(Name, 1, Lists), - NewLists = [{Name, List} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}) - end - end, + F = fun() -> + case mnesia:wread({privacy, {LUser, LServer}}) of + [] -> + NewLists = [{Name, List}], + mnesia:write(#privacy{ + us = {LUser, LServer}, + lists = NewLists + }); + [#privacy{lists = Lists} = P] -> + NewLists1 = lists:keydelete(Name, 1, Lists), + NewLists = [{Name, List} | NewLists1], + mnesia:write(P#privacy{lists = NewLists}) + end + end, transaction(F). + get_list(LUser, LServer, Name) -> case mnesia:dirty_read(privacy, {LUser, LServer}) of - [#privacy{default = Default, lists = Lists}] when Name == default -> - case lists:keyfind(Default, 1, Lists) of - {_, List} -> {ok, {Default, List}}; - false -> error - end; - [#privacy{lists = Lists}] -> - case lists:keyfind(Name, 1, Lists) of - {_, List} -> {ok, {Name, List}}; - false -> error - end; - [] -> - error + [#privacy{default = Default, lists = Lists}] when Name == default -> + case lists:keyfind(Default, 1, Lists) of + {_, List} -> {ok, {Default, List}}; + false -> error + end; + [#privacy{lists = Lists}] -> + case lists:keyfind(Name, 1, Lists) of + {_, List} -> {ok, {Name, List}}; + false -> error + end; + [] -> + error end. + get_lists(LUser, LServer) -> case mnesia:dirty_read(privacy, {LUser, LServer}) of [#privacy{} = P] -> @@ -136,25 +160,29 @@ get_lists(LUser, LServer) -> error end. + remove_lists(LUser, LServer) -> - F = fun () -> mnesia:delete({privacy, {LUser, LServer}}) end, + F = fun() -> mnesia:delete({privacy, {LUser, LServer}}) end, transaction(F). + import(#privacy{} = P) -> mnesia:dirty_write(P). + need_transform({privacy, {U, S}, _, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'privacy' will be converted to binary", []), true; need_transform(_) -> false. + transform(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> NewLists = lists:map( - fun({Name, Ls}) -> - NewLs = lists:map( - fun(#listitem{value = Val} = L) -> - NewVal = case Val of + fun({Name, Ls}) -> + NewLs = lists:map( + fun(#listitem{value = Val} = L) -> + NewVal = case Val of {LU, LS, LR} -> {iolist_to_binary(LU), iolist_to_binary(LS), @@ -165,25 +193,28 @@ transform(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> to -> to; _ -> iolist_to_binary(Val) end, - L#listitem{value = NewVal} - end, Ls), - {iolist_to_binary(Name), NewLs} - end, Lists), + L#listitem{value = NewVal} + end, + Ls), + {iolist_to_binary(Name), NewLs} + end, + Lists), NewDef = case Def of - none -> none; - _ -> iolist_to_binary(Def) - end, + none -> none; + _ -> iolist_to_binary(Def) + end, NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, R#privacy{us = NewUS, default = NewDef, lists = NewLists}. + %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(F) -> case mnesia:transaction(F) of - {atomic, Result} -> - Result; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, db_failure} + {atomic, Result} -> + Result; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} end. diff --git a/src/mod_privacy_opt.erl b/src/mod_privacy_opt.erl index acc0f2ac9..0740b9eca 100644 --- a/src/mod_privacy_opt.erl +++ b/src/mod_privacy_opt.erl @@ -9,33 +9,37 @@ -export([db_type/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_privacy, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_privacy, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_privacy, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_privacy, db_type). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_privacy, use_cache). - diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl index e0dc4476b..7eb4b28b8 100644 --- a/src/mod_privacy_sql.erl +++ b/src/mod_privacy_sql.erl @@ -24,23 +24,32 @@ -module(mod_privacy_sql). - -behaviour(mod_privacy). %% API --export([init/2, set_default/3, unset_default/2, set_lists/1, - set_list/4, get_lists/2, get_list/3, remove_lists/2, - remove_list/3, import/1, export/1]). +-export([init/2, + set_default/3, + unset_default/2, + set_lists/1, + set_list/4, + get_lists/2, + get_list/3, + remove_lists/2, + remove_list/3, + import/1, + export/1]). -export([item_to_raw/1, raw_to_item/1]). -export([sql_schemas/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_privacy.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -48,297 +57,340 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"privacy_default_list">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"name">>, type = text}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}, - #sql_table{ - name = <<"privacy_list">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"name">>, type = text}, - #sql_column{name = <<"id">>, type = bigserial}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"id">>], - unique = true}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, - <<"name">>], - unique = true}]}, - #sql_table{ - name = <<"privacy_list_data">>, - columns = - [#sql_column{name = <<"id">>, type = bigint, - opts = [#sql_references{ - table = <<"privacy_list">>, - column = <<"id">>}]}, - #sql_column{name = <<"t">>, type = {char, 1}}, - #sql_column{name = <<"value">>, type = text}, - #sql_column{name = <<"action">>, type = {char, 1}}, - #sql_column{name = <<"ord">>, type = numeric}, - #sql_column{name = <<"match_all">>, type = boolean}, - #sql_column{name = <<"match_iq">>, type = boolean}, - #sql_column{name = <<"match_message">>, type = boolean}, - #sql_column{name = <<"match_presence_in">>, type = boolean}, - #sql_column{name = <<"match_presence_out">>, type = boolean}], - indices = [#sql_index{columns = [<<"id">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"privacy_default_list">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"name">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }, + #sql_table{ + name = <<"privacy_list">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"id">>, type = bigserial}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"id">>], + unique = true + }, + #sql_index{ + columns = [<<"server_host">>, + <<"username">>, + <<"name">>], + unique = true + }] + }, + #sql_table{ + name = <<"privacy_list_data">>, + columns = + [#sql_column{ + name = <<"id">>, + type = bigint, + opts = [#sql_references{ + table = <<"privacy_list">>, + column = <<"id">> + }] + }, + #sql_column{name = <<"t">>, type = {char, 1}}, + #sql_column{name = <<"value">>, type = text}, + #sql_column{name = <<"action">>, type = {char, 1}}, + #sql_column{name = <<"ord">>, type = numeric}, + #sql_column{name = <<"match_all">>, type = boolean}, + #sql_column{name = <<"match_iq">>, type = boolean}, + #sql_column{name = <<"match_message">>, type = boolean}, + #sql_column{name = <<"match_presence_in">>, type = boolean}, + #sql_column{name = <<"match_presence_out">>, type = boolean}], + indices = [#sql_index{columns = [<<"id">>]}] + }] + }]. + unset_default(LUser, LServer) -> case unset_default_privacy_list(LUser, LServer) of - ok -> - ok; - _Err -> - {error, db_failure} + ok -> + ok; + _Err -> + {error, db_failure} end. + set_default(LUser, LServer, Name) -> - F = fun () -> - case get_privacy_list_names_t(LUser, LServer) of - {selected, []} -> - {error, notfound}; - {selected, Names} -> - case lists:member({Name}, Names) of - true -> - set_default_privacy_list(LUser, LServer, Name); - false -> - {error, notfound} - end - end - end, + F = fun() -> + case get_privacy_list_names_t(LUser, LServer) of + {selected, []} -> + {error, notfound}; + {selected, Names} -> + case lists:member({Name}, Names) of + true -> + set_default_privacy_list(LUser, LServer, Name); + false -> + {error, notfound} + end + end + end, transaction(LServer, F). + remove_list(LUser, LServer, Name) -> - F = fun () -> - case get_default_privacy_list_t(LUser, LServer) of - {selected, []} -> - remove_privacy_list_t(LUser, LServer, Name); - {selected, [{Default}]} -> - if Name == Default -> - {error, conflict}; - true -> - remove_privacy_list_t(LUser, LServer, Name) - end - end - end, + F = fun() -> + case get_default_privacy_list_t(LUser, LServer) of + {selected, []} -> + remove_privacy_list_t(LUser, LServer, Name); + {selected, [{Default}]} -> + if + Name == Default -> + {error, conflict}; + true -> + remove_privacy_list_t(LUser, LServer, Name) + end + end + end, transaction(LServer, F). -set_lists(#privacy{us = {LUser, LServer}, - default = Default, - lists = Lists}) -> + +set_lists(#privacy{ + us = {LUser, LServer}, + default = Default, + lists = Lists + }) -> F = fun() -> - lists:foreach( - fun({Name, List}) -> - add_privacy_list(LUser, LServer, Name), - {selected, [{I}]} = - get_privacy_list_id_t(LUser, LServer, Name), - RItems = lists:map(fun item_to_raw/1, List), - set_privacy_list(I, RItems), - if is_binary(Default) -> - set_default_privacy_list( + lists:foreach( + fun({Name, List}) -> + add_privacy_list(LUser, LServer, Name), + {selected, [{I}]} = + get_privacy_list_id_t(LUser, LServer, Name), + RItems = lists:map(fun item_to_raw/1, List), + set_privacy_list(I, RItems), + if + is_binary(Default) -> + set_default_privacy_list( LUser, LServer, Default); - true -> - ok - end - end, Lists) - end, + true -> + ok + end + end, + Lists) + end, transaction(LServer, F). + set_list(LUser, LServer, Name, List) -> RItems = lists:map(fun item_to_raw/1, List), F = fun() -> - {ID, New} = case get_privacy_list_id_t(LUser, LServer, Name) of - {selected, []} -> - add_privacy_list(LUser, LServer, Name), - {selected, [{I}]} = - get_privacy_list_id_t(LUser, LServer, Name), - {I, true}; - {selected, [{I}]} -> {I, false} - end, - case New of - false -> - set_privacy_list(ID, RItems); - _ -> - set_privacy_list_new(ID, RItems) - end - end, + {ID, New} = case get_privacy_list_id_t(LUser, LServer, Name) of + {selected, []} -> + add_privacy_list(LUser, LServer, Name), + {selected, [{I}]} = + get_privacy_list_id_t(LUser, LServer, Name), + {I, true}; + {selected, [{I}]} -> {I, false} + end, + case New of + false -> + set_privacy_list(ID, RItems); + _ -> + set_privacy_list_new(ID, RItems) + end + end, transaction(LServer, F). + get_list(LUser, LServer, default) -> case get_default_privacy_list(LUser, LServer) of - {selected, []} -> - error; - {selected, [{Default}]} -> - get_list(LUser, LServer, Default); - _Err -> - {error, db_failure} + {selected, []} -> + error; + {selected, [{Default}]} -> + get_list(LUser, LServer, Default); + _Err -> + {error, db_failure} end; get_list(LUser, LServer, Name) -> case get_privacy_list_data(LUser, LServer, Name) of - {selected, []} -> - error; - {selected, RItems} -> - {ok, {Name, lists:flatmap(fun raw_to_item/1, RItems)}}; - _Err -> - {error, db_failure} + {selected, []} -> + error; + {selected, RItems} -> + {ok, {Name, lists:flatmap(fun raw_to_item/1, RItems)}}; + _Err -> + {error, db_failure} end. + get_lists(LUser, LServer) -> case get_default_privacy_list(LUser, LServer) of - {selected, Selected} -> - Default = case Selected of - [] -> none; - [{DefName}] -> DefName - end, - case get_privacy_list_names(LUser, LServer) of - {selected, Names} -> - case lists:foldl( - fun(_, {error, _} = Err) -> - Err; - ({Name}, Acc) -> - case get_privacy_list_data(LUser, LServer, Name) of - {selected, RItems} -> - Items = lists:flatmap( - fun raw_to_item/1, - RItems), - [{Name, Items}|Acc]; - _Err -> - {error, db_failure} - end - end, [], Names) of - {error, Reason} -> - {error, Reason}; - Lists -> - {ok, #privacy{default = Default, - us = {LUser, LServer}, - lists = Lists}} - end; - _Err -> - {error, db_failure} - end; - _Err -> - {error, db_failure} + {selected, Selected} -> + Default = case Selected of + [] -> none; + [{DefName}] -> DefName + end, + case get_privacy_list_names(LUser, LServer) of + {selected, Names} -> + case lists:foldl( + fun(_, {error, _} = Err) -> + Err; + ({Name}, Acc) -> + case get_privacy_list_data(LUser, LServer, Name) of + {selected, RItems} -> + Items = lists:flatmap( + fun raw_to_item/1, + RItems), + [{Name, Items} | Acc]; + _Err -> + {error, db_failure} + end + end, + [], + Names) of + {error, Reason} -> + {error, Reason}; + Lists -> + {ok, #privacy{ + default = Default, + us = {LUser, LServer}, + lists = Lists + }} + end; + _Err -> + {error, db_failure} + end; + _Err -> + {error, db_failure} end. + remove_lists(LUser, LServer) -> case del_privacy_lists(LUser, LServer) of - ok -> - ok; - _Err -> - {error, db_failure} + ok -> + ok; + _Err -> + {error, db_failure} end. + export(Server) -> SqlType = ejabberd_option:sql_type(Server), case catch ejabberd_sql:sql_query(jid:nameprep(Server), - [<<"select id from privacy_list order by " - "id desc limit 1;">>]) of + [<<"select id from privacy_list order by " + "id desc limit 1;">>]) of {selected, [<<"id">>], [[I]]} -> put(id, binary_to_integer(I)); _ -> put(id, 0) end, [{privacy, - fun(Host, #privacy{us = {LUser, LServer}, lists = Lists, - default = Default}) + fun(Host, + #privacy{ + us = {LUser, LServer}, + lists = Lists, + default = Default + }) when LServer == Host -> - if Default /= none -> + if + Default /= none -> [?SQL("delete from privacy_default_list where" " username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( - "privacy_default_list", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "name=%(Default)s"])]; - true -> + "privacy_default_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Default)s"])]; + true -> [] end ++ - lists:flatmap( - fun({Name, List}) -> - RItems = lists:map(fun item_to_raw/1, List), - ID = get_id(), - [?SQL("delete from privacy_list where" - " username=%(LUser)s and %(LServer)H and" - " name=%(Name)s;"), - ?SQL_INSERT( - "privacy_list", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "name=%(Name)s", - "id=%(ID)d"]), - ?SQL("delete from privacy_list_data where" - " id=%(ID)d;")] ++ - case SqlType of - pgsql -> - [?SQL("insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, " - "match_presence_out) " - "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," - " %(Order)d, CAST(%(MatchAll)b as boolean), CAST(%(MatchIQ)b as boolean)," - " CAST(%(MatchMessage)b as boolean), CAST(%(MatchPresenceIn)b as boolean)," - " CAST(%(MatchPresenceOut)b as boolean));") - || {SType, SValue, SAction, Order, - MatchAll, MatchIQ, - MatchMessage, MatchPresenceIn, - MatchPresenceOut} <- RItems]; - _ -> - [?SQL("insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, " - "match_presence_out) " - "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," - " %(Order)d, %(MatchAll)b, %(MatchIQ)b," - " %(MatchMessage)b, %(MatchPresenceIn)b," - " %(MatchPresenceOut)b);") - || {SType, SValue, SAction, Order, - MatchAll, MatchIQ, - MatchMessage, MatchPresenceIn, - MatchPresenceOut} <- RItems] - end - end, - Lists); + lists:flatmap( + fun({Name, List}) -> + RItems = lists:map(fun item_to_raw/1, List), + ID = get_id(), + [?SQL("delete from privacy_list where" + " username=%(LUser)s and %(LServer)H and" + " name=%(Name)s;"), + ?SQL_INSERT( + "privacy_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Name)s", + "id=%(ID)d"]), + ?SQL("delete from privacy_list_data where" + " id=%(ID)d;")] ++ + case SqlType of + pgsql -> + [ ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, " + "match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, CAST(%(MatchAll)b as boolean), CAST(%(MatchIQ)b as boolean)," + " CAST(%(MatchMessage)b as boolean), CAST(%(MatchPresenceIn)b as boolean)," + " CAST(%(MatchPresenceOut)b as boolean));") + || {SType, SValue, SAction, Order, + MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, + MatchPresenceOut} <- RItems ]; + _ -> + [ ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, " + "match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b);") + || {SType, SValue, SAction, Order, + MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, + MatchPresenceOut} <- RItems ] + end + end, + Lists); (_Host, _R) -> [] end}]. + get_id() -> ID = get(id), put(id, ID + 1), ID + 1. + import(_) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(LServer, F) -> case ejabberd_sql:sql_transaction(LServer, F) of - {atomic, Res} -> Res; - {aborted, _Reason} -> {error, db_failure} + {atomic, Res} -> Res; + {aborted, _Reason} -> {error, db_failure} end. + raw_to_item({SType, SValue, SAction, Order, MatchAll, - MatchIQ, MatchMessage, MatchPresenceIn, - MatchPresenceOut} = Row) -> + MatchIQ, MatchMessage, MatchPresenceIn, + MatchPresenceOut} = Row) -> try {Type, Value} = case SType of <<"n">> -> {none, none}; <<"j">> -> JID = jid:decode(SValue), - {jid, jid:tolower(JID)}; + {jid, jid:tolower(JID)}; <<"g">> -> {group, SValue}; <<"s">> -> case SValue of @@ -352,67 +404,87 @@ raw_to_item({SType, SValue, SAction, Order, MatchAll, <<"a">> -> allow; <<"d">> -> deny end, - [#listitem{type = Type, value = Value, action = Action, - order = Order, match_all = MatchAll, match_iq = MatchIQ, - match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}] - catch _:_ -> + [#listitem{ + type = Type, + value = Value, + action = Action, + order = Order, + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut + }] + catch + _:_ -> ?WARNING_MSG("Failed to parse row: ~p", [Row]), [] end. -item_to_raw(#listitem{type = Type, value = Value, - action = Action, order = Order, match_all = MatchAll, - match_iq = MatchIQ, match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}) -> + +item_to_raw(#listitem{ + type = Type, + value = Value, + action = Action, + order = Order, + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut + }) -> {SType, SValue} = case Type of - none -> {<<"n">>, <<"">>}; - jid -> {<<"j">>, jid:encode(Value)}; - group -> {<<"g">>, Value}; - subscription -> - case Value of - none -> {<<"s">>, <<"none">>}; - both -> {<<"s">>, <<"both">>}; - from -> {<<"s">>, <<"from">>}; - to -> {<<"s">>, <<"to">>} - end - end, + none -> {<<"n">>, <<"">>}; + jid -> {<<"j">>, jid:encode(Value)}; + group -> {<<"g">>, Value}; + subscription -> + case Value of + none -> {<<"s">>, <<"none">>}; + both -> {<<"s">>, <<"both">>}; + from -> {<<"s">>, <<"from">>}; + to -> {<<"s">>, <<"to">>} + end + end, SAction = case Action of - allow -> <<"a">>; - deny -> <<"d">> - end, + allow -> <<"a">>; + deny -> <<"d">> + end, {SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, MatchPresenceOut}. + get_default_privacy_list(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s from privacy_default_list " "where username=%(LUser)s and %(LServer)H")). + get_default_privacy_list_t(LUser, LServer) -> ejabberd_sql:sql_query_t( ?SQL("select @(name)s from privacy_default_list " "where username=%(LUser)s and %(LServer)H")). + get_privacy_list_names(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s from privacy_list" " where username=%(LUser)s and %(LServer)H")). + get_privacy_list_names_t(LUser, LServer) -> ejabberd_sql:sql_query_t( ?SQL("select @(name)s from privacy_list" " where username=%(LUser)s and %(LServer)H")). + get_privacy_list_id_t(LUser, LServer, Name) -> ejabberd_sql:sql_query_t( ?SQL("select @(id)d from privacy_list" " where username=%(LUser)s and %(LServer)H and name=%(Name)s")). + get_privacy_list_data(LUser, LServer, Name) -> ejabberd_sql:sql_query( LServer, @@ -424,53 +496,59 @@ get_privacy_list_data(LUser, LServer, Name) -> " where username=%(LUser)s and %(LServer)H and name=%(Name)s) " "order by ord")). + set_default_privacy_list(LUser, LServer, Name) -> ?SQL_UPSERT_T( - "privacy_default_list", - ["!username=%(LUser)s", - "!server_host=%(LServer)s", - "name=%(Name)s"]). + "privacy_default_list", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "name=%(Name)s"]). + unset_default_privacy_list(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from privacy_default_list" - " where username=%(LUser)s and %(LServer)H")) of - {updated, _} -> ok; - Err -> Err + LServer, + ?SQL("delete from privacy_default_list" + " where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + Err -> Err end. + remove_privacy_list_t(LUser, LServer, Name) -> case ejabberd_sql:sql_query_t( - ?SQL("delete from privacy_list where" - " username=%(LUser)s and %(LServer)H and name=%(Name)s")) of - {updated, 0} -> {error, notfound}; - {updated, _} -> ok; - Err -> Err + ?SQL("delete from privacy_list where" + " username=%(LUser)s and %(LServer)H and name=%(Name)s")) of + {updated, 0} -> {error, notfound}; + {updated, _} -> ok; + Err -> Err end. + add_privacy_list(LUser, LServer, Name) -> ejabberd_sql:sql_query_t( ?SQL_INSERT( - "privacy_list", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "name=%(Name)s"])). + "privacy_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Name)s"])). + set_privacy_list_new(ID, RItems) -> lists:foreach( - fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, - MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> - ejabberd_sql:sql_query_t( - ?SQL("insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, match_presence_out) " - "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," - " %(Order)d, %(MatchAll)b, %(MatchIQ)b," - " %(MatchMessage)b, %(MatchPresenceIn)b," - " %(MatchPresenceOut)b)")) - end, - RItems). + fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> + ejabberd_sql:sql_query_t( + ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b)")) + end, + RItems). + calculate_difference(List1, List2) -> Set1 = gb_sets:from_list(List1), @@ -478,51 +556,53 @@ calculate_difference(List1, List2) -> {gb_sets:to_list(gb_sets:subtract(Set1, Set2)), gb_sets:to_list(gb_sets:subtract(Set2, Set1))}. + set_privacy_list(ID, RItems) -> case ejabberd_sql:sql_query_t( - ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " - "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " - "@(match_presence_out)b from privacy_list_data " - "where id=%(ID)d")) of - {selected, ExistingItems} -> - {ToAdd2, ToDelete2} = calculate_difference(RItems, ExistingItems), - ToAdd3 = if - ToDelete2 /= [] -> - ejabberd_sql:sql_query_t( - ?SQL("delete from privacy_list_data where id=%(ID)d")), - RItems; - true -> - ToAdd2 - end, - lists:foreach( - fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, - MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> - ejabberd_sql:sql_query_t( - ?SQL("insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, match_presence_out) " - "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," - " %(Order)d, %(MatchAll)b, %(MatchIQ)b," - " %(MatchMessage)b, %(MatchPresenceIn)b," - " %(MatchPresenceOut)b)")) - end, - ToAdd3); - Err -> - Err + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id=%(ID)d")) of + {selected, ExistingItems} -> + {ToAdd2, ToDelete2} = calculate_difference(RItems, ExistingItems), + ToAdd3 = if + ToDelete2 /= [] -> + ejabberd_sql:sql_query_t( + ?SQL("delete from privacy_list_data where id=%(ID)d")), + RItems; + true -> + ToAdd2 + end, + lists:foreach( + fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> + ejabberd_sql:sql_query_t( + ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b)")) + end, + ToAdd3); + Err -> + Err end. + del_privacy_lists(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from privacy_list where username=%(LUser)s and %(LServer)H")) of - {updated, _} -> - case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from privacy_default_list " - "where username=%(LUser)s and %(LServer)H")) of - {updated, _} -> ok; - Err -> Err - end; - Err -> - Err + LServer, + ?SQL("delete from privacy_list where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from privacy_default_list " + "where username=%(LUser)s and %(LServer)H")) of + {updated, _} -> ok; + Err -> Err + end; + Err -> + Err end. diff --git a/src/mod_private.erl b/src/mod_private.erl index 6f70e1c9a..5159437a7 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -33,11 +33,25 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_sm_iq/1, import_info/0, - remove_user/2, get_data/2, get_data/3, export/1, mod_doc/0, - import/5, import_start/2, mod_opt_type/1, set_data/2, - mod_options/1, depends/2, get_sm_features/5, pubsub_publish_item/6, - pubsub_delete_item/5, pubsub_tree_call/4]). +-export([start/2, + stop/1, + reload/3, + process_sm_iq/1, + import_info/0, + remove_user/2, + get_data/2, get_data/3, + export/1, + mod_doc/0, + import/5, + import_start/2, + mod_opt_type/1, + set_data/2, + mod_options/1, + depends/2, + get_sm_features/5, + pubsub_publish_item/6, + pubsub_delete_item/5, + pubsub_tree_call/4]). -export([get_commands_spec/0, bookmarks_to_pep/2]). @@ -46,7 +60,9 @@ -import(ejabberd_web_admin, [make_command/4, make_command/2]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_private.hrl"). -include("ejabberd_commands.hrl"). -include("ejabberd_http.hrl"). @@ -56,6 +72,7 @@ -define(PRIVATE_CACHE, private_cache). + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback set_data(binary(), binary(), [{binary(), xmlel()}]) -> ok | {error, any()}. @@ -67,6 +84,7 @@ -optional_callbacks([use_cache/1, cache_nodes/1]). + start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), @@ -76,27 +94,32 @@ start(Host, Opts) -> {hook, disco_sm_features, get_sm_features, 50}, {hook, pubsub_publish_item, pubsub_publish_item, 50}, {hook, pubsub_delete_item, pubsub_delete_item, 50}, - {hook, pubsub_tree_call, pubsub_tree_call, 50}, + {hook, pubsub_tree_call, pubsub_tree_call, 50}, {hook, webadmin_menu_hostuser, webadmin_menu_hostuser, 50}, {hook, webadmin_page_hostuser, webadmin_page_hostuser, 50}, {iq_handler, ejabberd_sm, ?NS_PRIVATE, process_sm_iq}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts). + depends(_Host, _Opts) -> [{mod_pubsub, soft}]. + mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> @@ -108,6 +131,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, @@ -115,11 +139,14 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0049.html" - "[XEP-0049: Private XML Storage]."), "", + "[XEP-0049: Private XML Storage]."), + "", ?T("Using this method, XMPP entities can store " "private data on the server, retrieve it " "whenever necessary and share it between multiple " @@ -127,169 +154,199 @@ mod_doc() -> "might be anything, as long as it is a valid XML. " "One typical usage is storing a bookmark of all user's conferences " "(https://xmpp.org/extensions/xep-0048.html" - "[XEP-0048: Bookmarks])."), "", + "[XEP-0048: Bookmarks])."), + "", ?T("It also implements the bookmark conversion described in " "https://xmpp.org/extensions/xep-0402.html[XEP-0402: PEP Native Bookmarks]" ", see _`bookmarks_to_pep`_ API.")], opts => [{db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}] + }. + -spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | empty | {result, [binary()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_features(Acc, _From, To, <<"">>, _Lang) -> case gen_mod:is_loaded(To#jid.lserver, mod_pubsub) of - true -> - {result, [?NS_BOOKMARKS_CONVERSION_0, ?NS_PEP_BOOKMARKS_COMPAT, ?NS_PEP_BOOKMARKS_COMPAT_PEP | - case Acc of - {result, Features} -> Features; - empty -> [] - end]}; - false -> - Acc + true -> + {result, [?NS_BOOKMARKS_CONVERSION_0, ?NS_PEP_BOOKMARKS_COMPAT, ?NS_PEP_BOOKMARKS_COMPAT_PEP | case Acc of + {result, Features} -> Features; + empty -> [] + end]}; + false -> + Acc end; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec process_sm_iq(iq()) -> iq(). -process_sm_iq(#iq{type = Type, lang = Lang, - from = #jid{luser = LUser, lserver = LServer} = From, - to = #jid{luser = LUser, lserver = LServer}, - sub_els = [#private{sub_els = Els0}]} = IQ) -> +process_sm_iq(#iq{ + type = Type, + lang = Lang, + from = #jid{luser = LUser, lserver = LServer} = From, + to = #jid{luser = LUser, lserver = LServer}, + sub_els = [#private{sub_els = Els0}] + } = IQ) -> case filter_xmlels(Els0) of - [] -> - Txt = ?T("No private data found in this query"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); - Data when Type == set -> - case set_data(From, Data) of - ok -> - xmpp:make_iq_result(IQ); - {error, #stanza_error{} = Err} -> - xmpp:make_error(IQ, Err); - {error, _} -> - Txt = ?T("Database failure"), - Err = xmpp:err_internal_server_error(Txt, Lang), - xmpp:make_error(IQ, Err) - end; - Data when Type == get -> - case get_data(LUser, LServer, Data) of - {error, _} -> - Txt = ?T("Database failure"), - Err = xmpp:err_internal_server_error(Txt, Lang), - xmpp:make_error(IQ, Err); - Els -> - xmpp:make_iq_result(IQ, #private{sub_els = Els}) - end + [] -> + Txt = ?T("No private data found in this query"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + Data when Type == set -> + case set_data(From, Data) of + ok -> + xmpp:make_iq_result(IQ); + {error, #stanza_error{} = Err} -> + xmpp:make_error(IQ, Err); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end; + Data when Type == get -> + case get_data(LUser, LServer, Data) of + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err); + Els -> + xmpp:make_iq_result(IQ, #private{sub_els = Els}) + end end; process_sm_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). + -spec filter_xmlels([xmlel()]) -> [{binary(), xmlel()}]. filter_xmlels(Els) -> lists:flatmap( fun(#xmlel{} = El) -> - case fxml:get_tag_attr_s(<<"xmlns">>, El) of - <<"">> -> []; - NS -> [{NS, El}] - end - end, Els). + case fxml:get_tag_attr_s(<<"xmlns">>, El) of + <<"">> -> []; + NS -> [{NS, El}] + end + end, + Els). + -spec set_data(jid(), [{binary(), xmlel()}]) -> ok | {error, _}. set_data(JID, Data) -> set_data(JID, Data, true, true). + -spec set_data(jid(), [{binary(), xmlel()}], boolean(), boolean()) -> ok | {error, _}. set_data(JID, Data, PublishPepStorageBookmarks, PublishPepXmppBookmarks) -> {LUser, LServer, _} = jid:tolower(JID), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_data(LUser, LServer, Data) of - ok -> - delete_cache(Mod, LUser, LServer, Data), - case PublishPepStorageBookmarks of - true -> publish_pep_storage_bookmarks(JID, Data); - false -> ok - end, - case PublishPepXmppBookmarks of - true -> publish_pep_native_bookmarks(JID, Data); - false -> ok - end; - {error, _} = Err -> - Err + ok -> + delete_cache(Mod, LUser, LServer, Data), + case PublishPepStorageBookmarks of + true -> publish_pep_storage_bookmarks(JID, Data); + false -> ok + end, + case PublishPepXmppBookmarks of + true -> publish_pep_native_bookmarks(JID, Data); + false -> ok + end; + {error, _} = Err -> + Err end. + -spec get_data(binary(), binary(), [{binary(), xmlel()}]) -> [xmlel()] | {error, _}. get_data(LUser, LServer, Data) -> Mod = gen_mod:db_mod(LServer, ?MODULE), lists:foldr( fun(_, {error, _} = Err) -> - Err; - ({NS, El}, Els) -> - Res = case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?PRIVATE_CACHE, {LUser, LServer, NS}, - fun() -> Mod:get_data(LUser, LServer, NS) end); - false -> - Mod:get_data(LUser, LServer, NS) - end, - case Res of - {ok, StorageEl} -> - [StorageEl|Els]; - error -> - [El|Els]; - {error, _} = Err -> - Err - end - end, [], Data). + Err; + ({NS, El}, Els) -> + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?PRIVATE_CACHE, + {LUser, LServer, NS}, + fun() -> Mod:get_data(LUser, LServer, NS) end); + false -> + Mod:get_data(LUser, LServer, NS) + end, + case Res of + {ok, StorageEl} -> + [StorageEl | Els]; + error -> + [El | Els]; + {error, _} = Err -> + Err + end + end, + [], + Data). + -spec get_data(binary(), binary()) -> [xmlel()] | {error, _}. get_data(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:get_all_data(LUser, LServer) of - {ok, Els} -> Els; - error -> []; - {error, _} = Err -> Err + {ok, Els} -> Els; + error -> []; + {error, _} = Err -> Err end. + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(Server, ?MODULE), Data = case use_cache(Mod, LServer) of - true -> - case Mod:get_all_data(LUser, LServer) of - {ok, Els} -> filter_xmlels(Els); - _ -> [] - end; - false -> - [] - end, + true -> + case Mod:get_all_data(LUser, LServer) of + {ok, Els} -> filter_xmlels(Els); + _ -> [] + end; + false -> + [] + end, Mod:del_data(LUser, LServer), delete_cache(Mod, LUser, LServer, Data). + %%%=================================================================== %%% Pubsub %%%=================================================================== @@ -297,102 +354,147 @@ remove_user(User, Server) -> publish_pep_storage_bookmarks(JID, Data) -> {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case gen_mod:is_loaded(LServer, mod_pubsub) of - true -> - case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of - false -> ok; - {_, El} -> - case mod_pubsub:get_items(LBJID, ?NS_STORAGE_BOOKMARKS) of - {error, #stanza_error{reason = 'item-not-found'}} -> - PubOpts = [{persist_items, true}, - {access_model, whitelist}], - case mod_pubsub:publish_item( - LBJID, LServer, ?NS_STORAGE_BOOKMARKS, JID, - <<"current">>, [El], PubOpts, all) of - {result, _} -> ok; - {error, _} = Err -> Err - end; - _ -> - case mod_pubsub:publish_item( - LBJID, LServer, ?NS_STORAGE_BOOKMARKS, JID, - <<"current">>, [El], [], all) of - {result, _} -> ok; - {error, _} = Err -> Err - end - end - end; - false -> - ok + true -> + case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of + false -> ok; + {_, El} -> + case mod_pubsub:get_items(LBJID, ?NS_STORAGE_BOOKMARKS) of + {error, #stanza_error{reason = 'item-not-found'}} -> + PubOpts = [{persist_items, true}, + {access_model, whitelist}], + case mod_pubsub:publish_item( + LBJID, + LServer, + ?NS_STORAGE_BOOKMARKS, + JID, + <<"current">>, + [El], + PubOpts, + all) of + {result, _} -> ok; + {error, _} = Err -> Err + end; + _ -> + case mod_pubsub:publish_item( + LBJID, + LServer, + ?NS_STORAGE_BOOKMARKS, + JID, + <<"current">>, + [El], + [], + all) of + {result, _} -> ok; + {error, _} = Err -> Err + end + end + end; + false -> + ok end. + -spec publish_pep_native_bookmarks(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. publish_pep_native_bookmarks(JID, Data) -> {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case gen_mod:is_loaded(LServer, mod_pubsub) of - true -> - case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of - {_, Bookmarks0} -> - Bookmarks = try xmpp:decode(Bookmarks0) of - #bookmark_storage{conference = C} -> C; - _ -> [] - catch _:{xmpp_codec, Why} -> - ?DEBUG("Failed to decode bookmarks of ~ts: ~ts", - [jid:encode(JID), xmpp:format_error(Why)]), - [] - end, - PubOpts = [{persist_items, true}, {access_model, whitelist}, {max_items, max}, {notify_retract,true}, {notify_delete,true}, {send_last_published_item, never}], - case mod_pubsub:get_items(LBJID, ?NS_PEP_BOOKMARKS) of - PepBookmarks when is_list(PepBookmarks) -> - put(mod_private_pep_update, true), - PepBookmarksMap = lists:foldl(fun pubsub_item_to_map/2, #{}, PepBookmarks), - {ToDelete, Ret} = - lists:foldl( - fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, {Map2, Ret2}) -> - PB = storage_bookmark_to_xmpp_bookmark(Bookmark), - case maps:take(jid:tolower(BookmarkJID), Map2) of - {StoredBookmark, Map3} when StoredBookmark == PB -> - {Map3, Ret2}; - {_, Map4} -> - {Map4, - err_ret(Ret2, mod_pubsub:publish_item( - LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, - jid:encode(BookmarkJID), [xmpp:encode(PB)], [], all))}; - _ -> - {Map2, - err_ret(Ret2, mod_pubsub:publish_item( - LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, - jid:encode(BookmarkJID), [xmpp:encode(PB)], [], all))} - end - end, {PepBookmarksMap, ok}, Bookmarks), - Ret4 = - maps:fold( - fun(DeleteJid, _, Ret3) -> - err_ret(Ret3, mod_pubsub:delete_item(LBJID, ?NS_PEP_BOOKMARKS, - JID, jid:encode(DeleteJid))) - end, Ret, ToDelete), - erase(mod_private_pep_update), - Ret4; - {error, #stanza_error{reason = 'item-not-found'}} -> - put(mod_private_pep_update, true), - Ret7 = - lists:foldl( - fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, Ret5) -> - PB = storage_bookmark_to_xmpp_bookmark(Bookmark), - err_ret(Ret5, mod_pubsub:publish_item( - LBJID, LServer, ?NS_PEP_BOOKMARKS, JID, - jid:encode(BookmarkJID), [xmpp:encode(PB)], PubOpts, all)) - end, ok, Bookmarks), - erase(mod_private_pep_update), - Ret7; - _ -> - ok - end; - _ -> - ok - end; - false -> - ok + true -> + case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of + {_, Bookmarks0} -> + Bookmarks = try xmpp:decode(Bookmarks0) of + #bookmark_storage{conference = C} -> C; + _ -> [] + catch + _:{xmpp_codec, Why} -> + ?DEBUG("Failed to decode bookmarks of ~ts: ~ts", + [jid:encode(JID), xmpp:format_error(Why)]), + [] + end, + PubOpts = [{persist_items, true}, {access_model, whitelist}, {max_items, max}, {notify_retract, true}, {notify_delete, true}, {send_last_published_item, never}], + case mod_pubsub:get_items(LBJID, ?NS_PEP_BOOKMARKS) of + PepBookmarks when is_list(PepBookmarks) -> + put(mod_private_pep_update, true), + PepBookmarksMap = lists:foldl(fun pubsub_item_to_map/2, #{}, PepBookmarks), + {ToDelete, Ret} = + lists:foldl( + fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, {Map2, Ret2}) -> + PB = storage_bookmark_to_xmpp_bookmark(Bookmark), + case maps:take(jid:tolower(BookmarkJID), Map2) of + {StoredBookmark, Map3} when StoredBookmark == PB -> + {Map3, Ret2}; + {_, Map4} -> + {Map4, + err_ret(Ret2, + mod_pubsub:publish_item( + LBJID, + LServer, + ?NS_PEP_BOOKMARKS, + JID, + jid:encode(BookmarkJID), + [xmpp:encode(PB)], + [], + all))}; + _ -> + {Map2, + err_ret(Ret2, + mod_pubsub:publish_item( + LBJID, + LServer, + ?NS_PEP_BOOKMARKS, + JID, + jid:encode(BookmarkJID), + [xmpp:encode(PB)], + [], + all))} + end + end, + {PepBookmarksMap, ok}, + Bookmarks), + Ret4 = + maps:fold( + fun(DeleteJid, _, Ret3) -> + err_ret(Ret3, + mod_pubsub:delete_item(LBJID, + ?NS_PEP_BOOKMARKS, + JID, + jid:encode(DeleteJid))) + end, + Ret, + ToDelete), + erase(mod_private_pep_update), + Ret4; + {error, #stanza_error{reason = 'item-not-found'}} -> + put(mod_private_pep_update, true), + Ret7 = + lists:foldl( + fun(#bookmark_conference{jid = BookmarkJID} = Bookmark, Ret5) -> + PB = storage_bookmark_to_xmpp_bookmark(Bookmark), + err_ret(Ret5, + mod_pubsub:publish_item( + LBJID, + LServer, + ?NS_PEP_BOOKMARKS, + JID, + jid:encode(BookmarkJID), + [xmpp:encode(PB)], + PubOpts, + all)) + end, + ok, + Bookmarks), + erase(mod_private_pep_update), + Ret7; + _ -> + ok + end; + _ -> + ok + end; + false -> + ok end. + err_ret({error, _} = E, _) -> E; err_ret(ok, {error, _} = E) -> @@ -400,205 +502,254 @@ err_ret(ok, {error, _} = E) -> err_ret(_, _) -> ok. --spec pubsub_publish_item(binary(), binary(), jid(), jid(), - binary(), [xmlel()]) -> any(). -pubsub_publish_item(LServer, ?NS_STORAGE_BOOKMARKS, - #jid{luser = LUser, lserver = LServer} = From, - #jid{luser = LUser, lserver = LServer}, - _ItemId, [Payload|_]) -> + +-spec pubsub_publish_item(binary(), + binary(), + jid(), + jid(), + binary(), + [xmlel()]) -> any(). +pubsub_publish_item(LServer, + ?NS_STORAGE_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId, + [Payload | _]) -> set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], false, true); -pubsub_publish_item(LServer, ?NS_PEP_BOOKMARKS, - #jid{luser = LUser, lserver = LServer} = From, - #jid{luser = LUser, lserver = LServer}, - _ItemId, _Payload) -> +pubsub_publish_item(LServer, + ?NS_PEP_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId, + _Payload) -> NotRecursion = get(mod_private_pep_update) == undefined, case mod_pubsub:get_items({LUser, LServer, <<>>}, ?NS_PEP_BOOKMARKS) of - Bookmarks when is_list(Bookmarks), NotRecursion -> - Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), - Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), - set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); - _ -> - ok + Bookmarks when is_list(Bookmarks), NotRecursion -> + Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), + Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); + _ -> + ok end; pubsub_publish_item(_, _, _, _, _, _) -> ok. + -spec pubsub_delete_item(binary(), binary(), jid(), jid(), binary()) -> any(). -pubsub_delete_item(LServer, ?NS_PEP_BOOKMARKS, - #jid{luser = LUser, lserver = LServer} = From, - #jid{luser = LUser, lserver = LServer}, - _ItemId) -> +pubsub_delete_item(LServer, + ?NS_PEP_BOOKMARKS, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer}, + _ItemId) -> NotRecursion = get(mod_private_pep_update) == undefined, case mod_pubsub:get_items({LUser, LServer, <<>>}, ?NS_PEP_BOOKMARKS) of - Bookmarks when is_list(Bookmarks), NotRecursion -> - Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), - Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), - set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); - _ -> - ok + Bookmarks when is_list(Bookmarks), NotRecursion -> + Bookmarks2 = lists:filtermap(fun pubsub_item_to_storage_bookmark/1, Bookmarks), + Payload = xmpp:encode(#bookmark_storage{conference = Bookmarks2}), + set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], true, false); + _ -> + ok end; pubsub_delete_item(_, _, _, _, _) -> ok. + -spec pubsub_item_to_storage_bookmark(#pubsub_item{}) -> {true, bookmark_conference()} | false. pubsub_item_to_storage_bookmark(#pubsub_item{itemid = {Id, _}, payload = [#xmlel{} = B | _]}) -> try {xmpp:decode(B), jid:decode(Id)} of - {#pep_bookmarks_conference{name = Name, autojoin = AutoJoin, - nick = Nick, password = Password}, - #jid{} = Jid} -> - {true, #bookmark_conference{jid = Jid, name = Name, - autojoin = AutoJoin, nick = Nick, - password = Password}}; - {_, _} -> - false + {#pep_bookmarks_conference{ + name = Name, + autojoin = AutoJoin, + nick = Nick, + password = Password + }, + #jid{} = Jid} -> + {true, #bookmark_conference{ + jid = Jid, + name = Name, + autojoin = AutoJoin, + nick = Nick, + password = Password + }}; + {_, _} -> + false catch - _:{xmpp_codec, Why} -> - ?DEBUG("Failed to decode bookmark element (~ts): ~ts", - [Id, xmpp:format_error(Why)]), - false; - _:{bad_jid, _} -> - ?DEBUG("Failed to decode bookmark ID (~ts)", [Id]), - false + _:{xmpp_codec, Why} -> + ?DEBUG("Failed to decode bookmark element (~ts): ~ts", + [Id, xmpp:format_error(Why)]), + false; + _:{bad_jid, _} -> + ?DEBUG("Failed to decode bookmark ID (~ts)", [Id]), + false end; pubsub_item_to_storage_bookmark(_) -> false. --spec pubsub_tree_call(Res :: any(), _Tree::any(), atom(), any()) -> any(). -pubsub_tree_call({error, #stanza_error{reason = 'item-not-found'}} = Res, Tree, get_node, - [{User, Server, _}, ?NS_PEP_BOOKMARKS] = Args) -> + +-spec pubsub_tree_call(Res :: any(), _Tree :: any(), atom(), any()) -> any(). +pubsub_tree_call({error, #stanza_error{reason = 'item-not-found'}} = Res, + Tree, + get_node, + [{User, Server, _}, ?NS_PEP_BOOKMARKS] = Args) -> case get(mod_private_in_pubsub_tree_call) of - undefined -> - put(mod_private_in_pubsub_tree_call, true), - bookmarks_to_pep(User, Server), - Res2 = apply(Tree, get_node, Args), - erase(mod_private_in_pubsub_tree_call), - Res2; - _ -> - Res + undefined -> + put(mod_private_in_pubsub_tree_call, true), + bookmarks_to_pep(User, Server), + Res2 = apply(Tree, get_node, Args), + erase(mod_private_in_pubsub_tree_call), + Res2; + _ -> + Res end; pubsub_tree_call(Res, _Tree, _Function, _Args) -> Res. + -spec storage_bookmark_to_xmpp_bookmark(bookmark_conference()) -> pep_bookmarks_conference(). -storage_bookmark_to_xmpp_bookmark(#bookmark_conference{name = Name, autojoin = AutoJoin, nick = Nick, - password = Password}) -> - #pep_bookmarks_conference{name = Name, autojoin = AutoJoin, nick = Nick, - password = Password}. +storage_bookmark_to_xmpp_bookmark(#bookmark_conference{ + name = Name, + autojoin = AutoJoin, + nick = Nick, + password = Password + }) -> + #pep_bookmarks_conference{ + name = Name, + autojoin = AutoJoin, + nick = Nick, + password = Password + }. + -spec pubsub_item_to_map(#pubsub_item{}, map()) -> map(). pubsub_item_to_map(#pubsub_item{itemid = {Id, _}, payload = [#xmlel{} = B | _]}, Map) -> try {xmpp:decode(B), jid:decode(Id)} of - {#pep_bookmarks_conference{} = B1, #jid{} = Jid} -> - B2 = B1#pep_bookmarks_conference{extensions = undefined}, - maps:put(jid:tolower(Jid), B2, Map); - {_, _} -> - Map + {#pep_bookmarks_conference{} = B1, #jid{} = Jid} -> + B2 = B1#pep_bookmarks_conference{extensions = undefined}, + maps:put(jid:tolower(Jid), B2, Map); + {_, _} -> + Map catch - _:{xmpp_codec, Why} -> - ?DEBUG("Failed to decode bookmark element (~ts): ~ts", - [Id, xmpp:format_error(Why)]), - Map; - _:{bad_jid, _} -> - ?DEBUG("Failed to decode bookmark ID (~ts)", [Id]), - Map + _:{xmpp_codec, Why} -> + ?DEBUG("Failed to decode bookmark element (~ts): ~ts", + [Id, xmpp:format_error(Why)]), + Map; + _:{bad_jid, _} -> + ?DEBUG("Failed to decode bookmark ID (~ts)", [Id]), + Map end; pubsub_item_to_map(_, Map) -> Map. + %%%=================================================================== %%% Commands %%%=================================================================== -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> - [#ejabberd_commands{name = bookmarks_to_pep, tags = [private], - desc = "Export private XML storage bookmarks to PEP", - module = ?MODULE, function = bookmarks_to_pep, - args = [{user, binary}, {host, binary}], - args_rename = [{server, host}], - args_desc = ["Username", "Server"], - args_example = [<<"bob">>, <<"example.com">>], - result = {res, restuple}, - result_desc = "Result tuple", - result_example = {ok, <<"Bookmarks exported">>}}]. + [#ejabberd_commands{ + name = bookmarks_to_pep, + tags = [private], + desc = "Export private XML storage bookmarks to PEP", + module = ?MODULE, + function = bookmarks_to_pep, + args = [{user, binary}, {host, binary}], + args_rename = [{server, host}], + args_desc = ["Username", "Server"], + args_example = [<<"bob">>, <<"example.com">>], + result = {res, restuple}, + result_desc = "Result tuple", + result_example = {ok, <<"Bookmarks exported">>} + }]. --spec bookmarks_to_pep(binary(), binary()) - -> {ok, binary()} | {error, binary()}. + +-spec bookmarks_to_pep(binary(), binary()) -> + {ok, binary()} | {error, binary()}. bookmarks_to_pep(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?PRIVATE_CACHE, {LUser, LServer, ?NS_STORAGE_BOOKMARKS}, - fun() -> - Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) - end); - false -> - Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) - end, + true -> + ets_cache:lookup( + ?PRIVATE_CACHE, + {LUser, LServer, ?NS_STORAGE_BOOKMARKS}, + fun() -> + Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) + end); + false -> + Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) + end, case Res of - {ok, El} -> - Data = [{?NS_STORAGE_BOOKMARKS, El}], - case publish_pep_storage_bookmarks(jid:make(User, Server), Data) of - ok -> - case publish_pep_native_bookmarks(jid:make(User, Server), Data) of - ok -> - {ok, <<"Bookmarks exported to PEP node">>}; - {error, Err} -> - {error, xmpp:format_stanza_error(Err)} - end; - {error, Err} -> - {error, xmpp:format_stanza_error(Err)} + {ok, El} -> + Data = [{?NS_STORAGE_BOOKMARKS, El}], + case publish_pep_storage_bookmarks(jid:make(User, Server), Data) of + ok -> + case publish_pep_native_bookmarks(jid:make(User, Server), Data) of + ok -> + {ok, <<"Bookmarks exported to PEP node">>}; + {error, Err} -> + {error, xmpp:format_stanza_error(Err)} + end; + {error, Err} -> + {error, xmpp:format_stanza_error(Err)} - end; - _ -> - {error, <<"Cannot retrieve bookmarks from private XML storage">>} + end; + _ -> + {error, <<"Cannot retrieve bookmarks from private XML storage">>} end. + %%%=================================================================== %%% WebAdmin %%%=================================================================== + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"private">>, <<"Private XML Storage">>}]. -webadmin_page_hostuser(_, Host, User, - #request{path = [<<"private">>]} = R) -> - Res = ?H1GL(<<"Private XML Storage">>, <<"modules/#mod_private">>, <<"mod_private">>) - ++ [make_command(private_set, R, [{<<"user">>, User}, {<<"host">>, Host}], []), - make_command(private_get, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], + +webadmin_page_hostuser(_, + Host, + User, + #request{path = [<<"private">>]} = R) -> + Res = ?H1GL(<<"Private XML Storage">>, <<"modules/#mod_private">>, <<"mod_private">>) ++ + [make_command(private_set, R, [{<<"user">>, User}, {<<"host">>, Host}], []), + make_command(private_get, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], {stop, Res}; webadmin_page_hostuser(Acc, _, _, _) -> Acc. + %%%=================================================================== %%% Cache %%%=================================================================== -spec delete_cache(module(), binary(), binary(), [{binary(), xmlel()}]) -> ok. delete_cache(Mod, LUser, LServer, Data) -> case use_cache(Mod, LServer) of - true -> - Nodes = cache_nodes(Mod, LServer), - lists:foreach( - fun({NS, _}) -> - ets_cache:delete(?PRIVATE_CACHE, - {LUser, LServer, NS}, - Nodes) - end, Data); - false -> - ok + true -> + Nodes = cache_nodes(Mod, LServer), + lists:foreach( + fun({NS, _}) -> + ets_cache:delete(?PRIVATE_CACHE, + {LUser, LServer, NS}, + Nodes) + end, + Data); + false -> + ok end. + -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?PRIVATE_CACHE, CacheOpts); - false -> - ets_cache:delete(?PRIVATE_CACHE) + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?PRIVATE_CACHE, CacheOpts); + false -> + ets_cache:delete(?PRIVATE_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_private_opt:cache_size(Opts), @@ -606,34 +757,40 @@ cache_opts(Opts) -> LifeTime = mod_private_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(Host); - false -> mod_private_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_private_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + %%%=================================================================== %%% Import/Export %%%=================================================================== import_info() -> [{<<"private_storage">>, 4}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). diff --git a/src/mod_private_mnesia.erl b/src/mod_private_mnesia.erl index 42bab447f..3419605d4 100644 --- a/src/mod_private_mnesia.erl +++ b/src/mod_private_mnesia.erl @@ -27,86 +27,110 @@ -behaviour(mod_private). %% API --export([init/2, set_data/3, get_data/3, get_all_data/2, del_data/2, - use_cache/1, import/3]). +-export([init/2, + set_data/3, + get_data/3, + get_all_data/2, + del_data/2, + use_cache/1, + import/3]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_private.hrl"). -include("logger.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, private_storage, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, private_storage)}]). + ejabberd_mnesia:create(?MODULE, + private_storage, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, private_storage)}]). + use_cache(Host) -> case mnesia:table_info(private_storage, storage_type) of - disc_only_copies -> - mod_private_opt:use_cache(Host); - _ -> - false + disc_only_copies -> + mod_private_opt:use_cache(Host); + _ -> + false end. + set_data(LUser, LServer, Data) -> - F = fun () -> - lists:foreach( - fun({XmlNS, Xmlel}) -> - mnesia:write( - #private_storage{ - usns = {LUser, LServer, XmlNS}, - xml = Xmlel}) - end, Data) - end, + F = fun() -> + lists:foreach( + fun({XmlNS, Xmlel}) -> + mnesia:write( + #private_storage{ + usns = {LUser, LServer, XmlNS}, + xml = Xmlel + }) + end, + Data) + end, transaction(F). + get_data(LUser, LServer, XmlNS) -> case mnesia:dirty_read(private_storage, {LUser, LServer, XmlNS}) of - [#private_storage{xml = Storage_Xmlel}] -> - {ok, Storage_Xmlel}; - _ -> - error + [#private_storage{xml = Storage_Xmlel}] -> + {ok, Storage_Xmlel}; + _ -> + error end. + get_all_data(LUser, LServer) -> case lists:flatten( - mnesia:dirty_select(private_storage, - [{#private_storage{usns = {LUser, LServer, '_'}, - xml = '$1'}, - [], ['$1']}])) of - [] -> - error; - Res -> - {ok, Res} + mnesia:dirty_select(private_storage, + [{#private_storage{ + usns = {LUser, LServer, '_'}, + xml = '$1' + }, + [], + ['$1']}])) of + [] -> + error; + Res -> + {ok, Res} end. + del_data(LUser, LServer) -> - F = fun () -> - Namespaces = mnesia:select(private_storage, - [{#private_storage{usns = - {LUser, - LServer, - '$1'}, - _ = '_'}, - [], ['$$']}]), - lists:foreach(fun ([Namespace]) -> - mnesia:delete({private_storage, - {LUser, LServer, - Namespace}}) - end, - Namespaces) - end, + F = fun() -> + Namespaces = mnesia:select(private_storage, + [{#private_storage{ + usns = + {LUser, + LServer, + '$1'}, + _ = '_' + }, + [], + ['$$']}]), + lists:foreach(fun([Namespace]) -> + mnesia:delete({private_storage, + {LUser, LServer, + Namespace}}) + end, + Namespaces) + end, transaction(F). -import(LServer, <<"private_storage">>, + +import(LServer, + <<"private_storage">>, [LUser, XMLNS, XML, _TimeStamp]) -> El = #xmlel{} = fxml_stream:parse_element(XML), PS = #private_storage{usns = {LUser, LServer, XMLNS}, xml = El}, mnesia:dirty_write(PS). + need_transform({private_storage, {U, S, NS}, _}) when is_list(U) orelse is_list(S) orelse is_list(NS) -> ?INFO_MSG("Mnesia table 'private_storage' will be converted to binary", []), @@ -114,20 +138,24 @@ need_transform({private_storage, {U, S, NS}, _}) need_transform(_) -> false. + transform(#private_storage{usns = {U, S, NS}, xml = El} = R) -> - R#private_storage{usns = {iolist_to_binary(U), - iolist_to_binary(S), - iolist_to_binary(NS)}, - xml = fxml:to_xmlel(El)}. + R#private_storage{ + usns = {iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(NS)}, + xml = fxml:to_xmlel(El) + }. + %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(F) -> case mnesia:transaction(F) of - {atomic, Res} -> - Res; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, db_failure} + {atomic, Res} -> + Res; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, db_failure} end. diff --git a/src/mod_private_opt.erl b/src/mod_private_opt.erl index 71257217d..cd750a211 100644 --- a/src/mod_private_opt.erl +++ b/src/mod_private_opt.erl @@ -9,33 +9,37 @@ -export([db_type/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_private, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_private, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_private, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_private, db_type). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_private, use_cache). - diff --git a/src/mod_private_sql.erl b/src/mod_private_sql.erl index d493b0587..b5c3593da 100644 --- a/src/mod_private_sql.erl +++ b/src/mod_private_sql.erl @@ -26,15 +26,22 @@ -behaviour(mod_private). %% API --export([init/2, set_data/3, get_data/3, get_all_data/2, del_data/2, - import/3, export/1]). +-export([init/2, + set_data/3, + get_data/3, + get_all_data/2, + del_data/2, + import/3, + export/1]). -export([sql_schemas/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_private.hrl"). -include("ejabberd_sql_pt.hrl"). -include("logger.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -42,119 +49,139 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"private_storage">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"namespace">>, type = text}, - #sql_column{name = <<"data">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, - <<"namespace">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"private_storage">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"namespace">>, type = text}, + #sql_column{name = <<"data">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"username">>, + <<"namespace">>], + unique = true + }] + }] + }]. + set_data(LUser, LServer, Data) -> F = fun() -> - lists:foreach( - fun({XMLNS, El}) -> - SData = fxml:element_to_binary(El), - ?SQL_UPSERT_T( - "private_storage", - ["!username=%(LUser)s", - "!server_host=%(LServer)s", - "!namespace=%(XMLNS)s", - "data=%(SData)s"]) - end, Data) - end, + lists:foreach( + fun({XMLNS, El}) -> + SData = fxml:element_to_binary(El), + ?SQL_UPSERT_T( + "private_storage", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!namespace=%(XMLNS)s", + "data=%(SData)s"]) + end, + Data) + end, case ejabberd_sql:sql_transaction(LServer, F) of - {atomic, ok} -> - ok; - _ -> - {error, db_failure} + {atomic, ok} -> + ok; + _ -> + {error, db_failure} end. + get_data(LUser, LServer, XMLNS) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(data)s from private_storage" - " where username=%(LUser)s and %(LServer)H" + LServer, + ?SQL("select @(data)s from private_storage" + " where username=%(LUser)s and %(LServer)H" " and namespace=%(XMLNS)s")) of - {selected, [{SData}]} -> - parse_element(LUser, LServer, SData); - {selected, []} -> - error; - _ -> - {error, db_failure} + {selected, [{SData}]} -> + parse_element(LUser, LServer, SData); + {selected, []} -> + error; + _ -> + {error, db_failure} end. + get_all_data(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(namespace)s, @(data)s from private_storage" - " where username=%(LUser)s and %(LServer)H")) of - {selected, []} -> - error; + LServer, + ?SQL("select @(namespace)s, @(data)s from private_storage" + " where username=%(LUser)s and %(LServer)H")) of + {selected, []} -> + error; {selected, Res} -> {ok, lists:flatmap( - fun({_, SData}) -> - case parse_element(LUser, LServer, SData) of - {ok, El} -> [El]; - error -> [] - end - end, Res)}; + fun({_, SData}) -> + case parse_element(LUser, LServer, SData) of + {ok, El} -> [El]; + error -> [] + end + end, + Res)}; _ -> - {error, db_failure} + {error, db_failure} end. + del_data(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from private_storage" + LServer, + ?SQL("delete from private_storage" " where username=%(LUser)s and %(LServer)H")) of - {updated, _} -> - ok; - _ -> - {error, db_failure} + {updated, _} -> + ok; + _ -> + {error, db_failure} end. + export(_Server) -> [{private_storage, - fun(Host, #private_storage{usns = {LUser, LServer, XMLNS}, - xml = Data}) + fun(Host, + #private_storage{ + usns = {LUser, LServer, XMLNS}, + xml = Data + }) when LServer == Host -> SData = fxml:element_to_binary(Data), - [?SQL("delete from private_storage where" - " username=%(LUser)s and %(LServer)H and namespace=%(XMLNS)s;"), + [?SQL("delete from private_storage where" + " username=%(LUser)s and %(LServer)H and namespace=%(XMLNS)s;"), ?SQL_INSERT( - "private_storage", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "namespace=%(XMLNS)s", - "data=%(SData)s"])]; + "private_storage", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "namespace=%(XMLNS)s", + "data=%(SData)s"])]; (_Host, _R) -> [] end}]. + import(_, _, _) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== parse_element(LUser, LServer, XML) -> case fxml_stream:parse_element(XML) of - El when is_record(El, xmlel) -> - {ok, El}; - _ -> - ?ERROR_MSG("Malformed XML element in SQL table " - "'private_storage' for user ~ts@~ts: ~ts", - [LUser, LServer, XML]), - error + El when is_record(El, xmlel) -> + {ok, El}; + _ -> + ?ERROR_MSG("Malformed XML element in SQL table " + "'private_storage' for user ~ts@~ts: ~ts", + [LUser, LServer, XML]), + error end. diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl index d614c8a35..54f5a1a28 100644 --- a/src/mod_privilege.erl +++ b/src/mod_privilege.erl @@ -34,15 +34,24 @@ -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). --export([component_connected/1, component_disconnected/2, - component_send_packet/1, - roster_access/2, process_message/1, - process_presence_out/1, process_presence_in/1]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). +-export([component_connected/1, + component_disconnected/2, + component_send_packet/1, + roster_access/2, + process_message/1, + process_presence_out/1, + process_presence_in/1]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -type roster_permission() :: both | get | set. @@ -54,24 +63,28 @@ -type presence_permissions() :: [{presence_permission(), acl:acl()}]. -type message_permissions() :: [{message_permission(), acl:acl()}]. -type access() :: [{roster, roster_permission()} | - {iq, [privilege_namespace()]} | - {presence, presence_permission()} | - {message, message_permission()}]. + {iq, [privilege_namespace()]} | + {presence, presence_permission()} | + {message, message_permission()}]. -type permissions() :: #{binary() => access()}. -record(state, {server_host = <<"">> :: binary()}). + %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(_Host, _NewOpts, _OldOpts) -> ok. + mod_opt_type(roster) -> econf:options( #{both => econf:acl(), get => econf:acl(), set => econf:acl()}); @@ -86,14 +99,17 @@ mod_opt_type(presence) -> econf:options( #{managed_entity => econf:acl(), roster => econf:acl()}). + mod_options(_) -> [{roster, [{both, none}, {get, none}, {set, none}]}, {iq, []}, {presence, [{managed_entity, none}, {roster, none}]}, - {message, [{outgoing,none}]}]. + {message, [{outgoing, none}]}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module is an implementation of " "https://xmpp.org/extensions/xep-0356.html" "[XEP-0356: Privileged Entity]. This extension " @@ -104,94 +120,124 @@ mod_doc() -> "to write powerful external components, for example " "implementing an external " "https://xmpp.org/extensions/xep-0163.html[PEP] or " - "https://xmpp.org/extensions/xep-0313.html[MAM] service."), "", + "https://xmpp.org/extensions/xep-0313.html[MAM] service."), + "", ?T("By default a component does not have any privileged access. " "It is worth noting that the permissions grant access to " "the component to a specific data type for all users of " - "the virtual host on which 'mod_privilege' is loaded."), "", - ?T("Make sure you have a listener configured to connect your " - "component. Check the section about listening ports for more " - "information."), "", - ?T("WARNING: Security issue: Privileged access gives components " - "access to sensitive data, so permission should be granted " - "carefully, only if you trust a component."), "", + "the virtual host on which 'mod_privilege' is loaded."), + "", + ?T("Make sure you have a listener configured to connect your " + "component. Check the section about listening ports for more " + "information."), + "", + ?T("WARNING: Security issue: Privileged access gives components " + "access to sensitive data, so permission should be granted " + "carefully, only if you trust a component."), + "", ?T("NOTE: This module is complementary to _`mod_delegation`_, " "but can also be used separately.")], note => "improved in 24.10", opts => [{roster, - #{value => ?T("Options"), + #{ + value => ?T("Options"), desc => ?T("This option defines roster permissions. " "By default no permissions are given. " - "The 'Options' are:")}, + "The 'Options' are:") + }, [{both, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Sets read/write access to a user's roster. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {get, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Sets read access to a user's roster. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {set, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Sets write access to a user's roster. " - "The default value is 'none'.")}}]}, + "The default value is 'none'.") + }}]}, {iq, - #{value => "{Namespace: Options}", + #{ + value => "{Namespace: Options}", desc => ?T("This option defines namespaces and their IQ permissions. " "By default no permissions are given. " - "The 'Options' are:")}, + "The 'Options' are:") + }, [{both, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Allows sending IQ stanzas of type 'get' and 'set'. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {get, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Allows sending IQ stanzas of type 'get'. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {set, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Allows sending IQ stanzas of type 'set'. " - "The default value is 'none'.")}}]}, + "The default value is 'none'.") + }}]}, {message, - #{value => ?T("Options"), + #{ + value => ?T("Options"), desc => ?T("This option defines permissions for messages. " "By default no permissions are given. " - "The 'Options' are:")}, + "The 'Options' are:") + }, [{outgoing, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("The option defines an access rule for sending " "outgoing messages by the component. " - "The default value is 'none'.")}}]}, + "The default value is 'none'.") + }}]}, {presence, - #{value => ?T("Options"), + #{ + value => ?T("Options"), desc => ?T("This option defines permissions for presences. " "By default no permissions are given. " - "The 'Options' are:")}, + "The 'Options' are:") + }, [{managed_entity, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("An access rule that gives permissions to " "the component to receive server presences. " - "The default value is 'none'.")}}, + "The default value is 'none'.") + }}, {roster, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("An access rule that gives permissions to " "the component to receive the presence of both " "the users and the contacts in their roster. " - "The default value is 'none'.")}}]}], + "The default value is 'none'.") + }}]}], example => ["modules:", " mod_privilege:", @@ -203,77 +249,92 @@ mod_doc() -> " presence:", " managed_entity: all", " message:", - " outgoing: all"]}. + " outgoing: all"] + }. + depends(_, _) -> []. + -spec component_connected(binary()) -> ok. component_connected(Host) -> lists:foreach( fun(ServerHost) -> - Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), - gen_server:cast(Proc, {component_connected, Host}) - end, ejabberd_option:hosts()). + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_connected, Host}) + end, + ejabberd_option:hosts()). + -spec component_disconnected(binary(), binary()) -> ok. component_disconnected(Host, _Reason) -> lists:foreach( fun(ServerHost) -> - Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), - gen_server:cast(Proc, {component_disconnected, Host}) - end, ejabberd_option:hosts()). + Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), + gen_server:cast(Proc, {component_disconnected, Host}) + end, + ejabberd_option:hosts()). + %% %% Message processing %% + -spec process_message(stanza()) -> stop | ok. -process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From, - to = #jid{lresource = <<"">>} = To, - lang = Lang, type = T} = Msg) when T /= error -> +process_message(#message{ + from = #jid{luser = <<"">>, lresource = <<"">>} = From, + to = #jid{lresource = <<"">>} = To, + lang = Lang, + type = T + } = Msg) when T /= error -> Host = From#jid.lserver, ServerHost = To#jid.lserver, Permissions = get_permissions(ServerHost), case maps:find(Host, Permissions) of - {ok, Access} -> - case proplists:get_value(message, Access, none) of - outgoing -> - forward_message(Msg); - _ -> - Txt = ?T("Insufficient privilege"), - Err = xmpp:err_forbidden(Txt, Lang), - ejabberd_router:route_error(Msg, Err) - end, - stop; - error -> - %% Component is disconnected - ok + {ok, Access} -> + case proplists:get_value(message, Access, none) of + outgoing -> + forward_message(Msg); + _ -> + Txt = ?T("Insufficient privilege"), + Err = xmpp:err_forbidden(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end, + stop; + error -> + %% Component is disconnected + ok end; process_message(_Stanza) -> ok. + %% %% IQ processing %% %% @format-begin -component_send_packet({#iq{from = From, - to = #jid{lresource = <<"">>} = To, - id = Id, - type = Type} = + +component_send_packet({#iq{ + from = From, + to = #jid{lresource = <<"">>} = To, + id = Id, + type = Type + } = IQ, State}) - when Type /= error -> + when Type /= error -> Host = From#jid.lserver, ServerHost = To#jid.lserver, Permissions = get_permissions(ServerHost), Result = case {maps:find(Host, Permissions), get_iq_encapsulated_details(IQ)} of {{ok, Access}, {privileged_iq, EncapType, EncapNs, EncapFrom, EncIq}} - when (EncapType == Type) and ((EncapFrom == undefined) or (EncapFrom == To)) -> + when (EncapType == Type) and ((EncapFrom == undefined) or (EncapFrom == To)) -> NsPermissions = proplists:get_value(iq, Access, []), Permission = case lists:keyfind(EncapNs, 2, NsPermissions) of @@ -282,10 +343,9 @@ component_send_packet({#iq{from = From, _ -> none end, - case Permission == both - orelse Permission == get andalso Type == get - orelse Permission == set andalso Type == set - of + case Permission == both orelse + Permission == get andalso Type == get orelse + Permission == set andalso Type == set of true -> forward_iq(Host, To, Id, EncIq); false -> @@ -321,10 +381,12 @@ component_send_packet(Acc) -> Acc. %% @format-end + %% %% Roster processing %% + -spec roster_access({true, iq()} | false, iq()) -> {true, iq()} | false. roster_access({true, _IQ} = Acc, _) -> Acc; @@ -333,100 +395,132 @@ roster_access(false, #iq{from = From, to = To, type = Type} = IQ) -> ServerHost = To#jid.lserver, Permissions = get_permissions(ServerHost), case maps:find(Host, Permissions) of - {ok, Access} -> - Permission = proplists:get_value(roster, Access, none), - case (Permission == both) - orelse (Permission == get andalso Type == get) - orelse (Permission == set andalso Type == set) of - true -> - {true, xmpp:put_meta(IQ, privilege_from, To)}; - false -> - false - end; - error -> - %% Component is disconnected - false + {ok, Access} -> + Permission = proplists:get_value(roster, Access, none), + case (Permission == both) orelse + (Permission == get andalso Type == get) orelse + (Permission == set andalso Type == set) of + true -> + {true, xmpp:put_meta(IQ, privilege_from, To)}; + false -> + false + end; + error -> + %% Component is disconnected + false end. + -spec process_presence_out({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. process_presence_out({#presence{ - from = #jid{luser = LUser, lserver = LServer} = From, - to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>}, - type = Type} = Pres, C2SState}) + from = #jid{luser = LUser, lserver = LServer} = From, + to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>}, + type = Type + } = Pres, + C2SState}) when Type == available; Type == unavailable -> %% Self-presence processing Permissions = get_permissions(LServer), lists:foreach( fun({Host, Access}) -> - Permission = proplists:get_value(presence, Access, none), - if Permission == roster; Permission == managed_entity -> - To = jid:make(Host), - ejabberd_router:route( - xmpp:set_from_to(Pres, From, To)); - true -> - ok - end - end, maps:to_list(Permissions)), + Permission = proplists:get_value(presence, Access, none), + if + Permission == roster; Permission == managed_entity -> + To = jid:make(Host), + ejabberd_router:route( + xmpp:set_from_to(Pres, From, To)); + true -> + ok + end + end, + maps:to_list(Permissions)), {Pres, C2SState}; process_presence_out(Acc) -> Acc. + -spec process_presence_in({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. process_presence_in({#presence{ - from = #jid{luser = U, lserver = S} = From, - to = #jid{luser = LUser, lserver = LServer}, - type = Type} = Pres, C2SState}) + from = #jid{luser = U, lserver = S} = From, + to = #jid{luser = LUser, lserver = LServer}, + type = Type + } = Pres, + C2SState}) when {U, S} /= {LUser, LServer} andalso (Type == available orelse Type == unavailable) -> Permissions = get_permissions(LServer), lists:foreach( fun({Host, Access}) -> - case proplists:get_value(presence, Access, none) of - roster -> - Permission = proplists:get_value(roster, Access, none), - if Permission == both; Permission == get -> - To = jid:make(Host), - ejabberd_router:route( - xmpp:set_from_to(Pres, From, To)); - true -> - ok - end; - _ -> - ok - end - end, maps:to_list(Permissions)), + case proplists:get_value(presence, Access, none) of + roster -> + Permission = proplists:get_value(roster, Access, none), + if + Permission == both; Permission == get -> + To = jid:make(Host), + ejabberd_router:route( + xmpp:set_from_to(Pres, From, To)); + true -> + ok + end; + _ -> + ok + end + end, + maps:to_list(Permissions)), {Pres, C2SState}; process_presence_in(Acc) -> Acc. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), catch ets:new(?MODULE, - [named_table, public, + [named_table, + public, {heir, erlang:group_leader(), none}]), - ejabberd_hooks:add(component_connected, ?MODULE, - component_connected, 50), - ejabberd_hooks:add(component_disconnected, ?MODULE, - component_disconnected, 50), - ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, - process_message, 50), - ejabberd_hooks:add(roster_remote_access, Host, ?MODULE, - roster_access, 50), - ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - process_presence_out, 50), - ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, - process_presence_in, 50), - ejabberd_hooks:add(component_send_packet, ?MODULE, - component_send_packet, 50), + ejabberd_hooks:add(component_connected, + ?MODULE, + component_connected, + 50), + ejabberd_hooks:add(component_disconnected, + ?MODULE, + component_disconnected, + 50), + ejabberd_hooks:add(local_send_to_resource_hook, + Host, + ?MODULE, + process_message, + 50), + ejabberd_hooks:add(roster_remote_access, + Host, + ?MODULE, + roster_access, + 50), + ejabberd_hooks:add(user_send_packet, + Host, + ?MODULE, + process_presence_out, + 50), + ejabberd_hooks:add(user_receive_packet, + Host, + ?MODULE, + process_presence_in, + 50), + ejabberd_hooks:add(component_send_packet, + ?MODULE, + component_send_packet, + 50), {ok, #state{server_host = Host}}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast({component_connected, Host}, State) -> ServerHost = State#state.server_host, From = jid:make(ServerHost), @@ -435,75 +529,109 @@ handle_cast({component_connected, Host}, State) -> IqNamespaces = get_iq_namespaces(ServerHost, Host), PresencePerm = get_presence_permission(ServerHost, Host), MessagePerm = get_message_permission(ServerHost, Host), - if RosterPerm /= none; IqNamespaces /= []; PresencePerm /= none; MessagePerm /= none -> - Priv = #privilege{perms = [#privilege_perm{access = message, - type = MessagePerm}, - #privilege_perm{access = roster, - type = RosterPerm}, - #privilege_perm{access = iq, - namespaces = IqNamespaces}, - #privilege_perm{access = presence, - type = PresencePerm}]}, - ?INFO_MSG("Granting permissions to external " - "component '~ts': roster = ~ts, presence = ~ts, " - "message = ~ts,~n iq = ~p", - [Host, RosterPerm, PresencePerm, MessagePerm, IqNamespaces]), - Msg = #message{from = From, to = To, sub_els = [Priv]}, - ejabberd_router:route(Msg), - Permissions = maps:put(Host, [{roster, RosterPerm}, - {iq, IqNamespaces}, - {presence, PresencePerm}, - {message, MessagePerm}], - get_permissions(ServerHost)), - ets:insert(?MODULE, {ServerHost, Permissions}), - {noreply, State}; - true -> - ?INFO_MSG("Granting no permissions to external component '~ts'", - [Host]), - {noreply, State} + if + RosterPerm /= none; IqNamespaces /= []; PresencePerm /= none; MessagePerm /= none -> + Priv = #privilege{ + perms = [#privilege_perm{ + access = message, + type = MessagePerm + }, + #privilege_perm{ + access = roster, + type = RosterPerm + }, + #privilege_perm{ + access = iq, + namespaces = IqNamespaces + }, + #privilege_perm{ + access = presence, + type = PresencePerm + }] + }, + ?INFO_MSG("Granting permissions to external " + "component '~ts': roster = ~ts, presence = ~ts, " + "message = ~ts,~n iq = ~p", + [Host, RosterPerm, PresencePerm, MessagePerm, IqNamespaces]), + Msg = #message{from = From, to = To, sub_els = [Priv]}, + ejabberd_router:route(Msg), + Permissions = maps:put(Host, + [{roster, RosterPerm}, + {iq, IqNamespaces}, + {presence, PresencePerm}, + {message, MessagePerm}], + get_permissions(ServerHost)), + ets:insert(?MODULE, {ServerHost, Permissions}), + {noreply, State}; + true -> + ?INFO_MSG("Granting no permissions to external component '~ts'", + [Host]), + {noreply, State} end; handle_cast({component_disconnected, Host}, State) -> ServerHost = State#state.server_host, Permissions = maps:remove(Host, get_permissions(ServerHost)), case maps:size(Permissions) of - 0 -> ets:delete(?MODULE, ServerHost); - _ -> ets:insert(?MODULE, {ServerHost, Permissions}) + 0 -> ets:delete(?MODULE, ServerHost); + _ -> ets:insert(?MODULE, {ServerHost, Permissions}) end, {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, State) -> Host = State#state.server_host, case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_hooks:delete(component_send_packet, ?MODULE, - component_send_packet, 50), - ejabberd_hooks:delete(component_connected, ?MODULE, - component_connected, 50), - ejabberd_hooks:delete(component_disconnected, ?MODULE, - component_disconnected, 50); - true -> - ok + false -> + ejabberd_hooks:delete(component_send_packet, + ?MODULE, + component_send_packet, + 50), + ejabberd_hooks:delete(component_connected, + ?MODULE, + component_connected, + 50), + ejabberd_hooks:delete(component_disconnected, + ?MODULE, + component_disconnected, + 50); + true -> + ok end, - ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, - process_message, 50), - ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE, - roster_access, 50), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - process_presence_out, 50), - ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, - process_presence_in, 50), + ejabberd_hooks:delete(local_send_to_resource_hook, + Host, + ?MODULE, + process_message, + 50), + ejabberd_hooks:delete(roster_remote_access, + Host, + ?MODULE, + roster_access, + 50), + ejabberd_hooks:delete(user_send_packet, + Host, + ?MODULE, + process_presence_out, + 50), + ejabberd_hooks:delete(user_receive_packet, + Host, + ?MODULE, + process_presence_in, + 50), ets:delete(?MODULE, Host). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -514,60 +642,66 @@ get_permissions(ServerHost) -> [{_, Permissions}] -> Permissions end. + %% %% Message %% + -spec forward_message(message()) -> ok. forward_message(#message{to = To} = Msg) -> ServerHost = To#jid.lserver, Lang = xmpp:get_lang(Msg), CodecOpts = ejabberd_config:codec_options(), try xmpp:try_subtag(Msg, #privilege{}) of - #privilege{forwarded = #forwarded{sub_els = [SubEl]}} -> - try xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of - #message{} = NewMsg -> - case NewMsg#message.from of - #jid{lresource = <<"">>, lserver = ServerHost} -> + #privilege{forwarded = #forwarded{sub_els = [SubEl]}} -> + try xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of + #message{} = NewMsg -> + case NewMsg#message.from of + #jid{lresource = <<"">>, lserver = ServerHost} -> FromJID = NewMsg#message.from, State = #{jid => FromJID}, ejabberd_hooks:run_fold(user_send_packet, FromJID#jid.lserver, {NewMsg, State}, []), - ejabberd_router:route(NewMsg); - _ -> - Lang = xmpp:get_lang(Msg), - Txt = ?T("Invalid 'from' attribute in forwarded message"), - Err = xmpp:err_forbidden(Txt, Lang), - ejabberd_router:route_error(Msg, Err) - end; - _ -> - Txt = ?T("Message not found in forwarded payload"), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Msg, Err) - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Msg, Err) - end; - _ -> - Txt = ?T("No element found"), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Msg, Err) - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Err = xmpp:err_bad_request(Txt, Lang), - ejabberd_router:route_error(Msg, Err) + ejabberd_router:route(NewMsg); + _ -> + Lang = xmpp:get_lang(Msg), + Txt = ?T("Invalid 'from' attribute in forwarded message"), + Err = xmpp:err_forbidden(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end; + _ -> + Txt = ?T("Message not found in forwarded payload"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + end; + _ -> + Txt = ?T("No element found"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err) end. + %% %% IQ %% %% @format-begin + -spec get_iq_encapsulated_details(iq()) -> - {privileged_iq, iq_type(), binary(), jid(), iq()} | - {unprivileged_iq} | - {error, Why :: atom(), stanza_error()}. + {privileged_iq, iq_type(), binary(), jid(), iq()} | + {unprivileged_iq} | + {error, Why :: atom(), stanza_error()}. get_iq_encapsulated_details(#iq{sub_els = [IqSub]} = Msg) -> Lang = xmpp:get_lang(Msg), try xmpp:try_subtag(Msg, #privileged_iq{}) of @@ -585,6 +719,7 @@ get_iq_encapsulated_details(#iq{sub_els = [IqSub]} = Msg) -> {error, codec_error, Err} end. + -spec forward_iq(binary(), jid(), binary(), iq()) -> iq(). forward_iq(Host, ToplevelTo, Id, Iq) -> FromJID = ToplevelTo, @@ -592,75 +727,85 @@ forward_iq(Host, ToplevelTo, Id, Iq) -> xmpp:put_meta(NewIq0, privilege_iq, {Id, Host, FromJID}). %% @format-end + %% %% Permissions %% + -spec get_roster_permission(binary(), binary()) -> roster_permission() | none. get_roster_permission(ServerHost, Host) -> Perms = mod_privilege_opt:roster(ServerHost), case match_rule(ServerHost, Host, Perms, both) of - allow -> - both; - deny -> - Get = match_rule(ServerHost, Host, Perms, get), - Set = match_rule(ServerHost, Host, Perms, set), - if Get == allow, Set == allow -> both; - Get == allow -> get; - Set == allow -> set; - true -> none - end + allow -> + both; + deny -> + Get = match_rule(ServerHost, Host, Perms, get), + Set = match_rule(ServerHost, Host, Perms, set), + if + Get == allow, Set == allow -> both; + Get == allow -> get; + Set == allow -> set; + true -> none + end end. + -spec get_iq_namespaces(binary(), binary()) -> [privilege_namespace()]. get_iq_namespaces(ServerHost, Host) -> NsPerms = mod_privilege_opt:iq(ServerHost), - [#privilege_namespace{ns = Ns, type = get_iq_permission(ServerHost, Host, Perms)} || {Ns, Perms} <- NsPerms]. + [ #privilege_namespace{ns = Ns, type = get_iq_permission(ServerHost, Host, Perms)} || {Ns, Perms} <- NsPerms ]. + -spec get_iq_permission(binary(), binary(), [iq_permission()]) -> iq_permission() | none. get_iq_permission(ServerHost, Host, Perms) -> case match_rule(ServerHost, Host, Perms, both) of - allow -> - both; - deny -> - Get = match_rule(ServerHost, Host, Perms, get), - Set = match_rule(ServerHost, Host, Perms, set), - if Get == allow, Set == allow -> both; - Get == allow -> get; - Set == allow -> set; - true -> none - end + allow -> + both; + deny -> + Get = match_rule(ServerHost, Host, Perms, get), + Set = match_rule(ServerHost, Host, Perms, set), + if + Get == allow, Set == allow -> both; + Get == allow -> get; + Set == allow -> set; + true -> none + end end. + -spec get_message_permission(binary(), binary()) -> message_permission() | none. get_message_permission(ServerHost, Host) -> Perms = mod_privilege_opt:message(ServerHost), case match_rule(ServerHost, Host, Perms, outgoing) of - allow -> outgoing; - deny -> none + allow -> outgoing; + deny -> none end. + -spec get_presence_permission(binary(), binary()) -> presence_permission() | none. get_presence_permission(ServerHost, Host) -> Perms = mod_privilege_opt:presence(ServerHost), case match_rule(ServerHost, Host, Perms, roster) of - allow -> - roster; - deny -> - case match_rule(ServerHost, Host, Perms, managed_entity) of - allow -> managed_entity; - deny -> none - end + allow -> + roster; + deny -> + case match_rule(ServerHost, Host, Perms, managed_entity) of + allow -> managed_entity; + deny -> none + end end. + -ifdef(OTP_BELOW_26). -dialyzer({no_contracts, match_rule/4}). -endif. + -spec match_rule(binary(), binary(), roster_permissions(), roster_permission()) -> allow | deny; - (binary(), binary(), iq_permissions(), iq_permission()) -> allow | deny; - (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny; - (binary(), binary(), message_permissions(), message_permission()) -> allow | deny. + (binary(), binary(), iq_permissions(), iq_permission()) -> allow | deny; + (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny; + (binary(), binary(), message_permissions(), message_permission()) -> allow | deny. match_rule(ServerHost, Host, Perms, Type) -> Access = proplists:get_value(Type, Perms, none), acl:match_rule(ServerHost, Access, jid:make(Host)). diff --git a/src/mod_privilege_opt.erl b/src/mod_privilege_opt.erl index 36bf54efa..6310b7b09 100644 --- a/src/mod_privilege_opt.erl +++ b/src/mod_privilege_opt.erl @@ -8,27 +8,30 @@ -export([presence/1]). -export([roster/1]). --spec iq(gen_mod:opts() | global | binary()) -> [{binary(),[{'both',acl:acl()} | {'get',acl:acl()} | {'set',acl:acl()}]}]. + +-spec iq(gen_mod:opts() | global | binary()) -> [{binary(), [{'both', acl:acl()} | {'get', acl:acl()} | {'set', acl:acl()}]}]. iq(Opts) when is_map(Opts) -> gen_mod:get_opt(iq, Opts); iq(Host) -> gen_mod:get_module_opt(Host, mod_privilege, iq). --spec message(gen_mod:opts() | global | binary()) -> [{'outgoing','none' | acl:acl()}]. + +-spec message(gen_mod:opts() | global | binary()) -> [{'outgoing', 'none' | acl:acl()}]. message(Opts) when is_map(Opts) -> gen_mod:get_opt(message, Opts); message(Host) -> gen_mod:get_module_opt(Host, mod_privilege, message). --spec presence(gen_mod:opts() | global | binary()) -> [{'managed_entity','none' | acl:acl()} | {'roster','none' | acl:acl()}]. + +-spec presence(gen_mod:opts() | global | binary()) -> [{'managed_entity', 'none' | acl:acl()} | {'roster', 'none' | acl:acl()}]. presence(Opts) when is_map(Opts) -> gen_mod:get_opt(presence, Opts); presence(Host) -> gen_mod:get_module_opt(Host, mod_privilege, presence). --spec roster(gen_mod:opts() | global | binary()) -> [{'both','none' | acl:acl()} | {'get','none' | acl:acl()} | {'set','none' | acl:acl()}]. + +-spec roster(gen_mod:opts() | global | binary()) -> [{'both', 'none' | acl:acl()} | {'get', 'none' | acl:acl()} | {'set', 'none' | acl:acl()}]. roster(Opts) when is_map(Opts) -> gen_mod:get_opt(roster, Opts); roster(Host) -> gen_mod:get_module_opt(Host, mod_privilege, roster). - diff --git a/src/mod_providers.erl b/src/mod_providers.erl index 24796b65d..8e89150dc 100644 --- a/src/mod_providers.erl +++ b/src/mod_providers.erl @@ -50,27 +50,35 @@ %%-------------------------------------------------------------------- %%| gen_mod callbacks + start(_Host, _Opts) -> report_hostmeta_listener(), ok. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> report_hostmeta_listener(), ok. + depends(_Host, _Opts) -> []. + %%-------------------------------------------------------------------- %%| HTTP handlers + process([], - #request{method = 'GET', - host = Host, - path = Path}) -> + #request{ + method = 'GET', + host = Host, + path = Path + }) -> case lists:last(Path) of <<"xmpp-provider-v2.json">> -> file_json(Host) @@ -78,12 +86,15 @@ process([], process(_Path, _Request) -> {404, [], "Not Found"}. + %%-------------------------------------------------------------------- %%| JSON + file_json(Host) -> Content = - #{website => build_urls(Host, website), + #{ + website => build_urls(Host, website), alternativeJids => gen_mod:get_module_opt(Host, ?MODULE, alternativeJids), busFactor => gen_mod:get_module_opt(Host, ?MODULE, busFactor), organization => gen_mod:get_module_opt(Host, ?MODULE, organization), @@ -97,16 +108,19 @@ file_json(Host) -> freeOfCharge => gen_mod:get_module_opt(Host, ?MODULE, freeOfCharge), legalNotice => build_urls(Host, legalNotice), serverLocations => gen_mod:get_module_opt(Host, ?MODULE, serverLocations), - since => gen_mod:get_module_opt(Host, ?MODULE, since)}, + since => gen_mod:get_module_opt(Host, ?MODULE, since) + }, {200, [html, {<<"Content-Type">>, <<"application/json">>}, {<<"Access-Control-Allow-Origin">>, <<"*">>}], [misc:json_encode(Content)]}. + %%-------------------------------------------------------------------- %%| Upload Size + get_upload_size(Host) -> case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadTotalSize) of default_value -> @@ -115,6 +129,7 @@ get_upload_size(Host) -> I end. + get_upload_size_mhuq(Host) -> case gen_mod:is_loaded(Host, mod_http_upload_quota) of true -> @@ -125,6 +140,7 @@ get_upload_size_mhuq(Host) -> 0 end. + get_upload_size_rules(Rules) -> case lists:keysearch([{acl, all}], 2, Rules) of {value, {Size, _}} -> @@ -133,9 +149,11 @@ get_upload_size_rules(Rules) -> 0 end. + %%-------------------------------------------------------------------- %%| Upload Time + get_upload_time(Host) -> case gen_mod:get_module_opt(Host, ?MODULE, maximumHttpFileUploadStorageTime) of default_value -> @@ -144,6 +162,7 @@ get_upload_time(Host) -> I end. + get_upload_time_mhuq(Host) -> case gen_mod:is_loaded(Host, mod_http_upload_quota) of true -> @@ -157,12 +176,15 @@ get_upload_time_mhuq(Host) -> 0 end. + %%-------------------------------------------------------------------- %%| Password URL + get_password_url(Host) -> build_urls(Host, get_password_url2(Host)). + get_password_url2(Host) -> case gen_mod:get_module_opt(Host, ?MODULE, passwordReset) of default_value -> @@ -171,6 +193,7 @@ get_password_url2(Host) -> U end. + get_password_url3(Host) -> case find_handler_port_path2(any, mod_register_web) of [] -> @@ -193,51 +216,56 @@ get_password_url3(Host) -> "/">> end. + %% TODO Ya hay otra funciona como esta find_handler_port_path2(Tls, Module) -> - lists:filtermap(fun ({{Port, _, _}, - ejabberd_http, - #{tls := ThisTls, request_handlers := Handlers}}) - when (Tls == any) or (Tls == ThisTls) -> + lists:filtermap(fun({{Port, _, _}, + ejabberd_http, + #{tls := ThisTls, request_handlers := Handlers}}) + when (Tls == any) or (Tls == ThisTls) -> case lists:keyfind(Module, 2, Handlers) of false -> false; {Path, Module} -> {true, {ThisTls, Port, Path}} end; - (_) -> + (_) -> false end, ets:tab2list(ejabberd_listener)). + %%-------------------------------------------------------------------- %%| Build URLs + build_urls(Host, Option) when is_atom(Option) -> build_urls(Host, gen_mod:get_module_opt(Host, ?MODULE, Option)); build_urls(_Host, <<"">>) -> #{}; build_urls(Host, Url) when not is_atom(Url) -> Languages = gen_mod:get_module_opt(Host, ?MODULE, languages), - maps:from_list([{L, misc:expand_keyword(<<"@LANGUAGE_URL@">>, Url, L)} - || L <- Languages]). + maps:from_list([ {L, misc:expand_keyword(<<"@LANGUAGE_URL@">>, Url, L)} + || L <- Languages ]). + find_handler_port_path(Tls, Module) -> - lists:filtermap(fun ({{Port, _, _}, - ejabberd_http, - #{tls := ThisTls, request_handlers := Handlers}}) - when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) -> + lists:filtermap(fun({{Port, _, _}, + ejabberd_http, + #{tls := ThisTls, request_handlers := Handlers}}) + when is_integer(Port) and ((Tls == any) or (Tls == ThisTls)) -> case lists:keyfind(Module, 2, Handlers) of false -> false; {Path, Module} -> {true, {ThisTls, Port, Path}} end; - (_) -> + (_) -> false end, ets:tab2list(ejabberd_listener)). + report_hostmeta_listener() -> case {find_handler_port_path(false, ?MODULE), find_handler_port_path(true, ?MODULE)} of {[], []} -> @@ -255,17 +283,19 @@ report_hostmeta_listener() -> ok end. + %%-------------------------------------------------------------------- %%| Options + mod_opt_type(languages) -> econf:list( - econf:binary()); + econf:binary()); mod_opt_type(website) -> econf:binary(); mod_opt_type(alternativeJids) -> econf:list( - econf:domain(), [unique]); + econf:domain(), [unique]); mod_opt_type(busFactor) -> econf:int(); mod_opt_type(organization) -> @@ -292,10 +322,11 @@ mod_opt_type(legalNotice) -> econf:binary(); mod_opt_type(serverLocations) -> econf:list( - econf:binary()); + econf:binary()); mod_opt_type(since) -> econf:binary(). + mod_options(Host) -> [{languages, [ejabberd_option:language(Host)]}, {website, <<"">>}, @@ -313,11 +344,14 @@ mod_options(Host) -> {serverLocations, []}, {since, <<"">>}]. + %%-------------------------------------------------------------------- %%| Doc + mod_doc() -> - #{desc => + #{ + desc => [?T("This module serves JSON provider files API v2 as described by " "https://providers.xmpp.net/provider-file-generator/[XMPP Providers]."), "", @@ -331,40 +365,51 @@ mod_doc() -> note => "added in 25.08", opts => [{languages, - #{value => "[string()]", + #{ + value => "[string()]", desc => ?T("List of language codes that your pages are available. " "Some options define URL where the keyword '@LANGUAGE_URL@' " "will be replaced with each of those language codes. " "The default value is a list with the language set in the " - "option _`language`_, for example: '[en]'.")}}, + "option _`language`_, for example: '[en]'.") + }}, {website, - #{value => "string()", + #{ + value => "string()", desc => ?T("Provider website. " "The keyword '@LANGUAGE_URL@' is replaced with each language. " - "The default value is '\"\"'.")}}, + "The default value is '\"\"'.") + }}, {alternativeJids, - #{value => "[string()]", + #{ + value => "[string()]", desc => ?T("List of JIDs (XMPP server domains) a provider offers for " "registration other than its main JID. " - "The default value is '[]'.")}}, + "The default value is '[]'.") + }}, {busFactor, - #{value => "integer()", + #{ + value => "integer()", desc => ?T("Bus factor of the XMPP service (i.e., the minimum number of " "team members that the service could not survive losing) or '-1' for n/a. " - "The default value is '-1'.")}}, + "The default value is '-1'.") + }}, {organization, - #{value => "string()", + #{ + value => "string()", desc => ?T("Type of organization providing the XMPP service. " "Allowed values are: 'company', '\"commercial person\"', '\"private person\"', " "'governmental', '\"non-governmental\"' or '\"\"'. " - "The default value is '\"\"'.")}}, + "The default value is '\"\"'.") + }}, {passwordReset, - #{value => "string()", + #{ + value => "string()", desc => ?T("Password reset web page (per language) used for an automatic password reset " "(e.g., via email) or describing how to manually reset a password " @@ -372,15 +417,19 @@ mod_doc() -> "The keyword '@LANGUAGE_URL@' is replaced with each language. " "The default value is an URL built automatically " "if _`mod_register_web`_ is configured as a 'request_handler', " - "or '\"\"' otherwise.")}}, + "or '\"\"' otherwise.") + }}, {serverTesting, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Whether tests against the provider's server are allowed " "(e.g., certificate checks and uptime monitoring). " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {maximumHttpFileUploadTotalSize, - #{value => "integer()", + #{ + value => "integer()", desc => ?T("Maximum size of all shared files in total per user (number in megabytes (MB), " "'0' for no limit or '-1' for less than 1 MB). " @@ -389,47 +438,62 @@ mod_doc() -> "which is already retrieved via XMPP. " "The default value is the value of the shaper value " "of option 'access_hard_quota' " - "from module _`mod_http_upload_quota`_, or '0' otherwise.")}}, + "from module _`mod_http_upload_quota`_, or '0' otherwise.") + }}, {maximumHttpFileUploadStorageTime, - #{value => "integer()", + #{ + value => "integer()", desc => ?T("Maximum storage duration of each shared file " "(number in days, '0' for no limit or '-1' for less than 1 day). " "The default value is the same as option 'max_days' " - "from module _`mod_http_upload_quota`_, or '0' otherwise.")}}, + "from module _`mod_http_upload_quota`_, or '0' otherwise.") + }}, {maximumMessageArchiveManagementStorageTime, - #{value => "integer()", + #{ + value => "integer()", desc => ?T("Maximum storage duration of each exchanged message " "(number in days, '0' for no limit or '-1' for less than 1 day). " - "The default value is '0'.")}}, + "The default value is '0'.") + }}, {professionalHosting, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Whether the XMPP server is hosted with good internet connection speed, " "uninterruptible power supply, access protection and regular backups. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {freeOfCharge, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Whether the XMPP service can be used for free. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {legalNotice, - #{value => "string()", + #{ + value => "string()", desc => ?T("Legal notice web page (per language). " "The keyword '@LANGUAGE_URL@' is replaced with each language. " - "The default value is '\"\"'.")}}, + "The default value is '\"\"'.") + }}, {serverLocations, - #{value => "[string()]", + #{ + value => "[string()]", desc => ?T("List of language codes of Server/Backup locations. " - "The default value is an empty list: '[]'.")}}, + "The default value is an empty list: '[]'.") + }}, {since, - #{value => "string()", + #{ + value => "string()", desc => ?T("Date since the XMPP service is available. " - "The default value is an empty string: '\"\"'.")}}], + "The default value is an empty string: '\"\"'.") + }}], example => ["listen:", " -", @@ -455,7 +519,8 @@ mod_doc() -> " serverLocations: [ao, bg]", " serverTesting: true", " since: \"2025-12-31\"", - " website: \"http://@HOST@/website/@LANGUAGE_URL@/\""]}. + " website: \"http://@HOST@/website/@LANGUAGE_URL@/\""] + }. %%-------------------------------------------------------------------- diff --git a/src/mod_providers_opt.erl b/src/mod_providers_opt.erl index ad398295f..c949c1f1b 100644 --- a/src/mod_providers_opt.erl +++ b/src/mod_providers_opt.erl @@ -19,93 +19,107 @@ -export([since/1]). -export([website/1]). + -spec alternativeJids(gen_mod:opts() | global | binary()) -> [binary()]. alternativeJids(Opts) when is_map(Opts) -> gen_mod:get_opt(alternativeJids, Opts); alternativeJids(Host) -> gen_mod:get_module_opt(Host, mod_providers, alternativeJids). + -spec busFactor(gen_mod:opts() | global | binary()) -> integer(). busFactor(Opts) when is_map(Opts) -> gen_mod:get_opt(busFactor, Opts); busFactor(Host) -> gen_mod:get_module_opt(Host, mod_providers, busFactor). + -spec freeOfCharge(gen_mod:opts() | global | binary()) -> boolean(). freeOfCharge(Opts) when is_map(Opts) -> gen_mod:get_opt(freeOfCharge, Opts); freeOfCharge(Host) -> gen_mod:get_module_opt(Host, mod_providers, freeOfCharge). + -spec languages(gen_mod:opts() | global | binary()) -> [binary()]. languages(Opts) when is_map(Opts) -> gen_mod:get_opt(languages, Opts); languages(Host) -> gen_mod:get_module_opt(Host, mod_providers, languages). + -spec legalNotice(gen_mod:opts() | global | binary()) -> binary(). legalNotice(Opts) when is_map(Opts) -> gen_mod:get_opt(legalNotice, Opts); legalNotice(Host) -> gen_mod:get_module_opt(Host, mod_providers, legalNotice). + -spec maximumHttpFileUploadStorageTime(gen_mod:opts() | global | binary()) -> 'default_value' | integer(). maximumHttpFileUploadStorageTime(Opts) when is_map(Opts) -> gen_mod:get_opt(maximumHttpFileUploadStorageTime, Opts); maximumHttpFileUploadStorageTime(Host) -> gen_mod:get_module_opt(Host, mod_providers, maximumHttpFileUploadStorageTime). + -spec maximumHttpFileUploadTotalSize(gen_mod:opts() | global | binary()) -> 'default_value' | integer(). maximumHttpFileUploadTotalSize(Opts) when is_map(Opts) -> gen_mod:get_opt(maximumHttpFileUploadTotalSize, Opts); maximumHttpFileUploadTotalSize(Host) -> gen_mod:get_module_opt(Host, mod_providers, maximumHttpFileUploadTotalSize). + -spec maximumMessageArchiveManagementStorageTime(gen_mod:opts() | global | binary()) -> integer(). maximumMessageArchiveManagementStorageTime(Opts) when is_map(Opts) -> gen_mod:get_opt(maximumMessageArchiveManagementStorageTime, Opts); maximumMessageArchiveManagementStorageTime(Host) -> gen_mod:get_module_opt(Host, mod_providers, maximumMessageArchiveManagementStorageTime). + -spec organization(gen_mod:opts() | global | binary()) -> '' | 'commercial person' | 'company' | 'governmental' | 'non-governmental' | 'private person'. organization(Opts) when is_map(Opts) -> gen_mod:get_opt(organization, Opts); organization(Host) -> gen_mod:get_module_opt(Host, mod_providers, organization). + -spec passwordReset(gen_mod:opts() | global | binary()) -> 'default_value' | binary(). passwordReset(Opts) when is_map(Opts) -> gen_mod:get_opt(passwordReset, Opts); passwordReset(Host) -> gen_mod:get_module_opt(Host, mod_providers, passwordReset). + -spec professionalHosting(gen_mod:opts() | global | binary()) -> boolean(). professionalHosting(Opts) when is_map(Opts) -> gen_mod:get_opt(professionalHosting, Opts); professionalHosting(Host) -> gen_mod:get_module_opt(Host, mod_providers, professionalHosting). + -spec serverLocations(gen_mod:opts() | global | binary()) -> [binary()]. serverLocations(Opts) when is_map(Opts) -> gen_mod:get_opt(serverLocations, Opts); serverLocations(Host) -> gen_mod:get_module_opt(Host, mod_providers, serverLocations). + -spec serverTesting(gen_mod:opts() | global | binary()) -> boolean(). serverTesting(Opts) when is_map(Opts) -> gen_mod:get_opt(serverTesting, Opts); serverTesting(Host) -> gen_mod:get_module_opt(Host, mod_providers, serverTesting). + -spec since(gen_mod:opts() | global | binary()) -> binary(). since(Opts) when is_map(Opts) -> gen_mod:get_opt(since, Opts); since(Host) -> gen_mod:get_module_opt(Host, mod_providers, since). + -spec website(gen_mod:opts() | global | binary()) -> binary(). website(Opts) when is_map(Opts) -> gen_mod:get_opt(website, Opts); website(Host) -> gen_mod:get_module_opt(Host, mod_providers, website). - diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl index 4143defaa..5435b78d4 100644 --- a/src/mod_proxy65.erl +++ b/src/mod_proxy65.erl @@ -45,54 +45,68 @@ -include("translate.hrl"). + -callback init() -> any(). -callback register_stream(binary(), pid()) -> ok | {error, any()}. -callback unregister_stream(binary()) -> ok | {error, any()}. -callback activate_stream(binary(), binary(), pos_integer() | infinity, node()) -> - ok | {error, limit | conflict | notfound | term()}. + ok | {error, limit | conflict | notfound | term()}. start(Host, Opts) -> case mod_proxy65_service:add_listener(Host, Opts) of - {error, _} = Err -> - Err; - _ -> - Mod = gen_mod:ram_db_mod(global, ?MODULE), - Mod:init(), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host]}, - transient, infinity, supervisor, [?MODULE]}, - supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec) + {error, _} = Err -> + Err; + _ -> + Mod = gen_mod:ram_db_mod(global, ?MODULE), + Mod:init(), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = {Proc, + {?MODULE, start_link, [Host]}, + transient, + infinity, + supervisor, + [?MODULE]}, + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec) end. + stop(Host) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - mod_proxy65_service:delete_listener(Host); - true -> - ok + false -> + mod_proxy65_service:delete_listener(Host); + true -> + ok end, Proc = gen_mod:get_module_proc(Host, ?PROCNAME), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). + reload(Host, NewOpts, OldOpts) -> Mod = gen_mod:ram_db_mod(global, ?MODULE), Mod:init(), mod_proxy65_service:reload(Host, NewOpts, OldOpts). + start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), supervisor:start_link({local, Proc}, ?MODULE, [Host]). + init([Host]) -> Service = {mod_proxy65_service, - {mod_proxy65_service, start_link, [Host]}, - transient, 5000, worker, [mod_proxy65_service]}, + {mod_proxy65_service, start_link, [Host]}, + transient, + 5000, + worker, + [mod_proxy65_service]}, {ok, {{one_for_one, 10, 1}, [Service]}}. + depends(_Host, _Opts) -> []. + mod_opt_type(access) -> econf:acl(); mod_opt_type(hostname) -> @@ -124,6 +138,7 @@ mod_opt_type(sndbuf) -> mod_opt_type(vcard) -> econf:vcard_temp(). + mod_options(Host) -> [{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, {access, all}, @@ -140,8 +155,10 @@ mod_options(Host) -> {sndbuf, 65536}, {shaper, none}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module implements " "https://xmpp.org/extensions/xep-0065.html" "[XEP-0065: SOCKS5 Bytestreams]. It allows ejabberd " @@ -150,94 +167,120 @@ mod_doc() -> [{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, - #{value => ?T("[Host, ...]"), + #{ + value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"proxy.\". " - "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + "The keyword '@HOST@' is replaced with the real virtual host name.") + }}, {name, - #{value => ?T("Name"), + #{ + value => ?T("Name"), desc => ?T("The value of the service name. This name is only visible in some " "clients that support https://xmpp.org/extensions/xep-0030.html" - "[XEP-0030: Service Discovery]. The default is \"SOCKS5 Bytestreams\".")}}, + "[XEP-0030: Service Discovery]. The default is \"SOCKS5 Bytestreams\".") + }}, {access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Defines an access rule for file transfer initiators. " "The default value is 'all'. You may want to restrict " "access to the users of your server only, in order to " "avoid abusing your proxy by the users of remote " - "servers.")}}, + "servers.") + }}, {ram_db_type, - #{value => "mnesia | redis | sql", + #{ + value => "mnesia | redis | sql", desc => ?T("Same as top-level _`default_ram_db`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {ip, - #{value => ?T("IPAddress"), + #{ + value => ?T("IPAddress"), desc => ?T("This option specifies which network interface to listen " "for. The default value is an IP address of the service's " - "DNS name, or, if fails, '127.0.0.1'.")}}, + "DNS name, or, if fails, '127.0.0.1'.") + }}, {hostname, - #{value => ?T("Host"), + #{ + value => ?T("Host"), desc => ?T("Defines a hostname offered by the proxy when " "establishing a session with clients. This is useful " "when you run the proxy behind a NAT. The keyword " "'@HOST@' is replaced with the virtual host name. " "The default is to use the value of 'ip' option. " - "Examples: 'proxy.mydomain.org', '200.150.100.50'.")}}, + "Examples: 'proxy.mydomain.org', '200.150.100.50'.") + }}, {port, - #{value => "1..65535", + #{ + value => "1..65535", desc => ?T("A port number to listen for incoming connections. " - "The default value is '7777'.")}}, + "The default value is '7777'.") + }}, {auth_type, - #{value => "anonymous | plain", + #{ + value => "anonymous | plain", desc => ?T("SOCKS5 authentication type. " "The default value is 'anonymous'. " "If set to 'plain', ejabberd will use " "authentication backend as it would " - "for SASL PLAIN.")}}, + "for SASL PLAIN.") + }}, {max_connections, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => ?T("Maximum number of active connections per file transfer " - "initiator. The default value is 'infinity'.")}}, + "initiator. The default value is 'infinity'.") + }}, {shaper, - #{value => ?T("Shaper"), + #{ + value => ?T("Shaper"), desc => ?T("This option defines a shaper for the file transfer peers. " "A shaper with the maximum bandwidth will be selected. " - "The default is 'none', i.e. no shaper.")}}, + "The default is 'none', i.e. no shaper.") + }}, {recbuf, - #{value => ?T("Size"), + #{ + value => ?T("Size"), desc => ?T("A size of the buffer for incoming packets. " "If you define a shaper, set the value of this " "option to the size of the shaper in order " "to avoid traffic spikes in file transfers. " - "The default value is '65536' bytes.")}}, + "The default value is '65536' bytes.") + }}, {sndbuf, - #{value => ?T("Size"), + #{ + value => ?T("Size"), desc => ?T("A size of the buffer for outgoing packets. " "If you define a shaper, set the value of this " "option to the size of the shaper in order " "to avoid traffic spikes in file transfers. " - "The default value is '65536' bytes.")}}, + "The default value is '65536' bytes.") + }}, {vcard, - #{value => ?T("vCard"), + #{ + value => ?T("vCard"), desc => ?T("A custom vCard of the service that will be displayed " "by some XMPP clients in Service Discovery. The value of " "'vCard' is a YAML map constructed from an XML representation " "of vCard. Since the representation has no attributes, " - "the mapping is straightforward.")}}], + "the mapping is straightforward.") + }}], example => ["acl:", " admin:", @@ -267,4 +310,5 @@ mod_doc() -> " access: proxy65_access", " shaper: proxy65_shaper", " recbuf: 10240", - " sndbuf: 10240"]}. + " sndbuf: 10240"] + }. diff --git a/src/mod_proxy65_lib.erl b/src/mod_proxy65_lib.erl index 1f5b25ed0..b227398b0 100644 --- a/src/mod_proxy65_lib.erl +++ b/src/mod_proxy65_lib.erl @@ -29,44 +29,76 @@ -include("mod_proxy65.hrl"). --export([unpack_init_message/1, unpack_auth_request/1, - unpack_request/1, make_init_reply/1, make_auth_reply/1, - make_reply/1, make_error_reply/1, make_error_reply/2]). +-export([unpack_init_message/1, + unpack_auth_request/1, + unpack_request/1, + make_init_reply/1, + make_auth_reply/1, + make_reply/1, + make_error_reply/1, make_error_reply/2]). -unpack_init_message(<<(?VERSION_5), N, - AuthMethodList:N/binary>>) - when N > 0, N < 256 -> + +unpack_init_message(<<(?VERSION_5), + N, + AuthMethodList:N/binary>>) + when N > 0, N < 256 -> {ok, binary_to_list(AuthMethodList)}; unpack_init_message(_) -> error. + unpack_auth_request(<<1, ULen, User:ULen/binary, PLen, - Pass:PLen/binary>>) - when ULen < 256, PLen < 256 -> + Pass:PLen/binary>>) + when ULen < 256, PLen < 256 -> {(User), (Pass)}; unpack_auth_request(_) -> error. -unpack_request(<<(?VERSION_5), CMD, RSV, - (?ATYP_DOMAINNAME), 40, SHA1:40/binary, 0, 0>>) - when CMD == (?CMD_CONNECT); CMD == (?CMD_UDP) -> - Command = if CMD == (?CMD_CONNECT) -> connect; - CMD == (?CMD_UDP) -> udp - end, + +unpack_request(<<(?VERSION_5), + CMD, + RSV, + (?ATYP_DOMAINNAME), + 40, + SHA1:40/binary, + 0, + 0>>) + when CMD == (?CMD_CONNECT); CMD == (?CMD_UDP) -> + Command = if + CMD == (?CMD_CONNECT) -> connect; + CMD == (?CMD_UDP) -> udp + end, #s5_request{cmd = Command, rsv = RSV, sha1 = (SHA1)}; unpack_request(_) -> error. + make_init_reply(Method) -> [?VERSION_5, Method]. + make_auth_reply(true) -> [1, ?SUCCESS]; make_auth_reply(false) -> [1, ?ERR_NOT_ALLOWED]. + make_reply(#s5_request{rsv = RSV, sha1 = SHA1}) -> - [?VERSION_5, ?SUCCESS, RSV, ?ATYP_DOMAINNAME, - byte_size(SHA1), SHA1, 0, 0]. + [?VERSION_5, + ?SUCCESS, + RSV, + ?ATYP_DOMAINNAME, + byte_size(SHA1), + SHA1, + 0, + 0]. + make_error_reply(Request) -> make_error_reply(Request, ?ERR_NOT_ALLOWED). + make_error_reply(#s5_request{rsv = RSV, sha1 = SHA1}, - Reason) -> - [?VERSION_5, Reason, RSV, ?ATYP_DOMAINNAME, - byte_size(SHA1), SHA1, 0, 0]. + Reason) -> + [?VERSION_5, + Reason, + RSV, + ?ATYP_DOMAINNAME, + byte_size(SHA1), + SHA1, + 0, + 0]. diff --git a/src/mod_proxy65_mnesia.erl b/src/mod_proxy65_mnesia.erl index 3661b62c0..ff815bc26 100644 --- a/src/mod_proxy65_mnesia.erl +++ b/src/mod_proxy65_mnesia.erl @@ -27,130 +27,160 @@ -export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). -export([start_link/0]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). --record(bytestream, - {sha1 = <<"">> :: binary() | '$1', - target :: pid() | '_', - initiator :: pid() | '_' | undefined, - active = false :: boolean() | '_', - jid_i :: undefined | binary() | '_'}). +-record(bytestream, { + sha1 = <<"">> :: binary() | '$1', + target :: pid() | '_', + initiator :: pid() | '_' | undefined, + active = false :: boolean() | '_', + jid_i :: undefined | binary() | '_' + }). -record(state, {}). + %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + init() -> - Spec = {?MODULE, {?MODULE, start_link, []}, transient, - 5000, worker, [?MODULE]}, + Spec = {?MODULE, {?MODULE, start_link, []}, + transient, + 5000, + worker, + [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, Spec). + register_stream(SHA1, StreamPid) -> - F = fun () -> - case mnesia:read(bytestream, SHA1, write) of - [] -> - mnesia:write(#bytestream{sha1 = SHA1, - target = StreamPid}); - [#bytestream{target = Pid, initiator = undefined} = - ByteStream] when is_pid(Pid), Pid /= StreamPid -> - mnesia:write(ByteStream#bytestream{ - initiator = StreamPid}) - end - end, + F = fun() -> + case mnesia:read(bytestream, SHA1, write) of + [] -> + mnesia:write(#bytestream{ + sha1 = SHA1, + target = StreamPid + }); + [#bytestream{target = Pid, initiator = undefined} = + ByteStream] when is_pid(Pid), Pid /= StreamPid -> + mnesia:write(ByteStream#bytestream{ + initiator = StreamPid + }) + end + end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, Reason} + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, Reason} end. + unregister_stream(SHA1) -> - F = fun () -> mnesia:delete({bytestream, SHA1}) end, + F = fun() -> mnesia:delete({bytestream, SHA1}) end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), - {error, Reason} + {atomic, ok} -> + ok; + {aborted, Reason} -> + ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), + {error, Reason} end. + activate_stream(SHA1, Initiator, MaxConnections, _Node) -> case gen_server:call(?MODULE, - {activate_stream, SHA1, Initiator, MaxConnections}) of - {atomic, {ok, IPid, TPid}} -> - {ok, IPid, TPid}; - {atomic, {limit, IPid, TPid}} -> - {error, {limit, IPid, TPid}}; - {atomic, conflict} -> - {error, conflict}; - {atomic, notfound} -> - {error, notfound}; - Err -> - {error, Err} + {activate_stream, SHA1, Initiator, MaxConnections}) of + {atomic, {ok, IPid, TPid}} -> + {ok, IPid, TPid}; + {atomic, {limit, IPid, TPid}} -> + {error, {limit, IPid, TPid}}; + {atomic, conflict} -> + {error, conflict}; + {atomic, notfound} -> + {error, notfound}; + Err -> + {error, Err} end. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> - ejabberd_mnesia:create(?MODULE, bytestream, - [{ram_copies, [node()]}, - {attributes, record_info(fields, bytestream)}]), + ejabberd_mnesia:create(?MODULE, + bytestream, + [{ram_copies, [node()]}, + {attributes, record_info(fields, bytestream)}]), {ok, #state{}}. + handle_call({activate_stream, SHA1, Initiator, MaxConnections}, _From, State) -> - F = fun () -> - case mnesia:read(bytestream, SHA1, write) of - [#bytestream{target = TPid, initiator = IPid} = - ByteStream] when is_pid(TPid), is_pid(IPid) -> - ActiveFlag = ByteStream#bytestream.active, - if ActiveFlag == false -> - ConnsPerJID = mnesia:select( - bytestream, - [{#bytestream{sha1 = '$1', - jid_i = Initiator, - _ = '_'}, - [], ['$1']}]), - if length(ConnsPerJID) < MaxConnections -> - mnesia:write( - ByteStream#bytestream{active = true, - jid_i = Initiator}), - {ok, IPid, TPid}; - true -> - {limit, IPid, TPid} - end; - true -> - conflict - end; - _ -> - notfound - end - end, + F = fun() -> + case mnesia:read(bytestream, SHA1, write) of + [#bytestream{target = TPid, initiator = IPid} = + ByteStream] when is_pid(TPid), is_pid(IPid) -> + ActiveFlag = ByteStream#bytestream.active, + if + ActiveFlag == false -> + ConnsPerJID = mnesia:select( + bytestream, + [{#bytestream{ + sha1 = '$1', + jid_i = Initiator, + _ = '_' + }, + [], + ['$1']}]), + if + length(ConnsPerJID) < MaxConnections -> + mnesia:write( + ByteStream#bytestream{ + active = true, + jid_i = Initiator + }), + {ok, IPid, TPid}; + true -> + {limit, IPid, TPid} + end; + true -> + conflict + end; + _ -> + notfound + end + end, Reply = mnesia:transaction(F), {reply, Reply, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. diff --git a/src/mod_proxy65_opt.erl b/src/mod_proxy65_opt.erl index 95f039b16..3b0ece077 100644 --- a/src/mod_proxy65_opt.erl +++ b/src/mod_proxy65_opt.erl @@ -19,93 +19,107 @@ -export([sndbuf/1]). -export([vcard/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, access). + -spec auth_type(gen_mod:opts() | global | binary()) -> 'anonymous' | 'plain'. auth_type(Opts) when is_map(Opts) -> gen_mod:get_opt(auth_type, Opts); auth_type(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, auth_type). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, host). + -spec hostname(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). hostname(Opts) when is_map(Opts) -> gen_mod:get_opt(hostname, Opts); hostname(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, hostname). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, hosts). + -spec ip(gen_mod:opts() | global | binary()) -> 'undefined' | inet:ip_address(). ip(Opts) when is_map(Opts) -> gen_mod:get_opt(ip, Opts); ip(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, ip). + -spec max_connections(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_connections(Opts) when is_map(Opts) -> gen_mod:get_opt(max_connections, Opts); max_connections(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, max_connections). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, name). + -spec port(gen_mod:opts() | global | binary()) -> 1..1114111. port(Opts) when is_map(Opts) -> gen_mod:get_opt(port, Opts); port(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, port). + -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, ram_db_type). + -spec recbuf(gen_mod:opts() | global | binary()) -> pos_integer(). recbuf(Opts) when is_map(Opts) -> gen_mod:get_opt(recbuf, Opts); recbuf(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, recbuf). + -spec server_host(gen_mod:opts() | global | binary()) -> binary(). server_host(Opts) when is_map(Opts) -> gen_mod:get_opt(server_host, Opts); server_host(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, server_host). + -spec shaper(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(shaper, Opts); shaper(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, shaper). + -spec sndbuf(gen_mod:opts() | global | binary()) -> pos_integer(). sndbuf(Opts) when is_map(Opts) -> gen_mod:get_opt(sndbuf, Opts); sndbuf(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, sndbuf). + -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, vcard). - diff --git a/src/mod_proxy65_redis.erl b/src/mod_proxy65_redis.erl index 588bd55f3..2ada604a4 100644 --- a/src/mod_proxy65_redis.erl +++ b/src/mod_proxy65_redis.erl @@ -28,9 +28,12 @@ -include("logger.hrl"). --record(proxy65, {pid_t :: pid(), - pid_i :: pid() | undefined, - jid_i :: binary() | undefined}). +-record(proxy65, { + pid_t :: pid(), + pid_i :: pid() | undefined, + jid_i :: binary() | undefined + }). + %%%=================================================================== %%% API @@ -39,147 +42,167 @@ init() -> ?DEBUG("Cleaning Redis 'proxy65' table...", []), NodeKey = node_key(), case ejabberd_redis:smembers(NodeKey) of - {ok, SIDs} -> - SIDKeys = [sid_key(S) || S <- SIDs], - JIDs = lists:flatmap( - fun(SIDKey) -> - case ejabberd_redis:get(SIDKey) of - {ok, Val} -> - try binary_to_term(Val) of - #proxy65{jid_i = J} when is_binary(J) -> - [jid_key(J)]; - _ -> - [] - catch _:badarg -> - [] - end; - _ -> - [] - end - end, SIDKeys), - ejabberd_redis:multi( - fun() -> - if SIDs /= [] -> - ejabberd_redis:del(SIDKeys), - if JIDs /= [] -> - ejabberd_redis:del(JIDs); - true -> - ok - end; - true -> - ok - end, - ejabberd_redis:del([NodeKey]) - end), - ok; - {error, _} -> - {error, db_failure} + {ok, SIDs} -> + SIDKeys = [ sid_key(S) || S <- SIDs ], + JIDs = lists:flatmap( + fun(SIDKey) -> + case ejabberd_redis:get(SIDKey) of + {ok, Val} -> + try binary_to_term(Val) of + #proxy65{jid_i = J} when is_binary(J) -> + [jid_key(J)]; + _ -> + [] + catch + _:badarg -> + [] + end; + _ -> + [] + end + end, + SIDKeys), + ejabberd_redis:multi( + fun() -> + if + SIDs /= [] -> + ejabberd_redis:del(SIDKeys), + if + JIDs /= [] -> + ejabberd_redis:del(JIDs); + true -> + ok + end; + true -> + ok + end, + ejabberd_redis:del([NodeKey]) + end), + ok; + {error, _} -> + {error, db_failure} end. + register_stream(SID, Pid) -> SIDKey = sid_key(SID), try - {ok, Val} = ejabberd_redis:get(SIDKey), - try binary_to_term(Val) of - #proxy65{pid_i = undefined} = R -> - NewVal = term_to_binary(R#proxy65{pid_i = Pid}), - ok = ejabberd_redis:set(SIDKey, NewVal); - _ -> - {error, conflict} - catch _:badarg when Val == undefined -> - NewVal = term_to_binary(#proxy65{pid_t = Pid}), - {ok, _} = ejabberd_redis:multi( - fun() -> - ejabberd_redis:set(SIDKey, NewVal), - ejabberd_redis:sadd(node_key(), [SID]) - end), - ok; - _:badarg -> - ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", - [SIDKey, Val]), - {error, db_failure} - end - catch _:{badmatch, {error, _}} -> - {error, db_failure} + {ok, Val} = ejabberd_redis:get(SIDKey), + try binary_to_term(Val) of + #proxy65{pid_i = undefined} = R -> + NewVal = term_to_binary(R#proxy65{pid_i = Pid}), + ok = ejabberd_redis:set(SIDKey, NewVal); + _ -> + {error, conflict} + catch + _:badarg when Val == undefined -> + NewVal = term_to_binary(#proxy65{pid_t = Pid}), + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:set(SIDKey, NewVal), + ejabberd_redis:sadd(node_key(), [SID]) + end), + ok; + _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SIDKey, Val]), + {error, db_failure} + end + catch + _:{badmatch, {error, _}} -> + {error, db_failure} end. + unregister_stream(SID) -> SIDKey = sid_key(SID), NodeKey = node_key(), try - {ok, Val} = ejabberd_redis:get(SIDKey), - try binary_to_term(Val) of - #proxy65{jid_i = JID} when is_binary(JID) -> - JIDKey = jid_key(JID), - {ok, _} = ejabberd_redis:multi( - fun() -> - ejabberd_redis:del([SIDKey]), - ejabberd_redis:srem(JIDKey, [SID]), - ejabberd_redis:srem(NodeKey, [SID]) - end), - ok; - _ -> - {ok, _} = ejabberd_redis:multi( - fun() -> - ejabberd_redis:del([SIDKey]), - ejabberd_redis:srem(NodeKey, [SID]) - end), - ok - catch _:badarg when Val == undefined -> - ok; - _:badarg -> - ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", - [SIDKey, Val]), - {error, db_failure} - end - catch _:{badmatch, {error, _}} -> - {error, db_failure} + {ok, Val} = ejabberd_redis:get(SIDKey), + try binary_to_term(Val) of + #proxy65{jid_i = JID} when is_binary(JID) -> + JIDKey = jid_key(JID), + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:del([SIDKey]), + ejabberd_redis:srem(JIDKey, [SID]), + ejabberd_redis:srem(NodeKey, [SID]) + end), + ok; + _ -> + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:del([SIDKey]), + ejabberd_redis:srem(NodeKey, [SID]) + end), + ok + catch + _:badarg when Val == undefined -> + ok; + _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SIDKey, Val]), + {error, db_failure} + end + catch + _:{badmatch, {error, _}} -> + {error, db_failure} end. + activate_stream(SID, IJID, MaxConnections, _Node) -> SIDKey = sid_key(SID), JIDKey = jid_key(IJID), try - {ok, Val} = ejabberd_redis:get(SIDKey), - try binary_to_term(Val) of - #proxy65{pid_t = TPid, pid_i = IPid, - jid_i = undefined} = R when is_pid(IPid) -> - {ok, Num} = ejabberd_redis:scard(JIDKey), - if Num >= MaxConnections -> - {error, {limit, IPid, TPid}}; - true -> - NewVal = term_to_binary(R#proxy65{jid_i = IJID}), - {ok, _} = ejabberd_redis:multi( - fun() -> - ejabberd_redis:sadd(JIDKey, [SID]), - ejabberd_redis:set(SIDKey, NewVal) - end), - {ok, IPid, TPid} - end; - #proxy65{jid_i = JID} when is_binary(JID) -> - {error, conflict}; - _ -> - {error, notfound} - catch _:badarg when Val == undefined -> - {error, notfound}; - _:badarg -> - ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", - [SIDKey, Val]), - {error, db_failure} - end - catch _:{badmatch, {error, _}} -> - {error, db_failure} + {ok, Val} = ejabberd_redis:get(SIDKey), + try binary_to_term(Val) of + #proxy65{ + pid_t = TPid, + pid_i = IPid, + jid_i = undefined + } = R when is_pid(IPid) -> + {ok, Num} = ejabberd_redis:scard(JIDKey), + if + Num >= MaxConnections -> + {error, {limit, IPid, TPid}}; + true -> + NewVal = term_to_binary(R#proxy65{jid_i = IJID}), + {ok, _} = ejabberd_redis:multi( + fun() -> + ejabberd_redis:sadd(JIDKey, [SID]), + ejabberd_redis:set(SIDKey, NewVal) + end), + {ok, IPid, TPid} + end; + #proxy65{jid_i = JID} when is_binary(JID) -> + {error, conflict}; + _ -> + {error, notfound} + catch + _:badarg when Val == undefined -> + {error, notfound}; + _:badarg -> + ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", + [SIDKey, Val]), + {error, db_failure} + end + catch + _:{badmatch, {error, _}} -> + {error, db_failure} end. + %%%=================================================================== %%% Internal functions %%%=================================================================== sid_key(SID) -> <<"ejabberd:proxy65:sid:", SID/binary>>. + jid_key(JID) -> <<"ejabberd:proxy65:initiator:", JID/binary>>. + node_key() -> Node = erlang:atom_to_binary(node(), latin1), <<"ejabberd:proxy65:node:", Node/binary>>. diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index 692ad146d..c166d35b0 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -30,15 +30,27 @@ -behaviour(gen_server). %% gen_server callbacks. --export([init/1, handle_info/2, handle_call/3, - handle_cast/2, terminate/2, code_change/3]). +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3]). --export([start_link/1, reload/3, add_listener/2, process_disco_info/1, - process_disco_items/1, process_vcard/1, process_bytestreams/1, - delete_listener/1, route/1]). +-export([start_link/1, + reload/3, + add_listener/2, + process_disco_info/1, + process_disco_items/1, + process_vcard/1, + process_bytestreams/1, + delete_listener/1, + route/1]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). @@ -50,43 +62,64 @@ %%% gen_server callbacks %%%------------------------ + start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:start_link({local, Proc}, ?MODULE, [Host], []). + reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + init([Host]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, mod_proxy65), MyHosts = gen_mod:get_opt_hosts(Opts), lists:foreach( fun(MyHost) -> - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_VCARD, - ?MODULE, process_vcard), - gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_BYTESTREAMS, - ?MODULE, process_bytestreams), - ejabberd_router:register_route( - MyHost, Host, {apply, ?MODULE, route}) - end, MyHosts), + gen_iq_handler:add_iq_handler(ejabberd_local, + MyHost, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, + MyHost, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, + MyHost, + ?NS_VCARD, + ?MODULE, + process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, + MyHost, + ?NS_BYTESTREAMS, + ?MODULE, + process_bytestreams), + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}) + end, + MyHosts), {ok, #state{myhosts = MyHosts}}. + terminate(_Reason, #state{myhosts = MyHosts}) -> lists:foreach( fun(MyHost) -> - ejabberd_router:unregister_route(MyHost), - unregister_handlers(MyHost) - end, MyHosts). + ejabberd_router:unregister_route(MyHost), + unregister_handlers(MyHost) + end, + MyHosts). + handle_info({route, Packet}, State) -> - try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> + try + route(Packet) + catch + ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), @@ -97,49 +130,59 @@ handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> NewHosts = gen_mod:get_opt_hosts(NewOpts), OldHosts = gen_mod:get_opt_hosts(OldOpts), lists:foreach( fun(NewHost) -> - ejabberd_router:register_route(NewHost, ServerHost), - register_handlers(NewHost) - end, NewHosts -- OldHosts), + ejabberd_router:register_route(NewHost, ServerHost), + register_handlers(NewHost) + end, + NewHosts -- OldHosts), lists:foreach( fun(OldHost) -> - ejabberd_router:unregister_route(OldHost), - unregister_handlers(OldHost) - end, OldHosts -- NewHosts), + ejabberd_router:unregister_route(OldHost), + unregister_handlers(OldHost) + end, + OldHosts -- NewHosts), {noreply, State#state{myhosts = NewHosts}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + -spec route(stanza()) -> ok. route(#iq{} = IQ) -> ejabberd_router:process_iq(IQ); route(_) -> ok. + %%%------------------------ %%% Listener management %%%------------------------ + add_listener(Host, Opts) -> {_, IP, _} = EndPoint = get_endpoint(Host), Opts1 = gen_mod:set_opt(server_host, Host, Opts), Opts2 = gen_mod:set_opt(ip, IP, Opts1), ejabberd_listener:add_listener(EndPoint, mod_proxy65_stream, Opts2). + delete_listener(Host) -> ejabberd_listener:delete_listener(get_endpoint(Host), mod_proxy65_stream). + %%%------------------------ %%% IQ Processing %%%------------------------ @@ -150,15 +193,23 @@ process_disco_info(#iq{type = set, lang = Lang} = IQ) -> process_disco_info(#iq{type = get, to = To, lang = Lang} = IQ) -> Host = ejabberd_router:host_of_route(To#jid.lserver), Name = mod_proxy65_opt:name(Host), - Info = ejabberd_hooks:run_fold(disco_info, Host, - [], [Host, ?MODULE, <<"">>, <<"">>]), + Info = ejabberd_hooks:run_fold(disco_info, + Host, + [], + [Host, ?MODULE, <<"">>, <<"">>]), xmpp:make_iq_result( - IQ, #disco_info{xdata = Info, - identities = [#identity{category = <<"proxy">>, - type = <<"bytestreams">>, - name = translate:translate(Lang, Name)}], - features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, - ?NS_VCARD, ?NS_BYTESTREAMS]}). + IQ, + #disco_info{ + xdata = Info, + identities = [#identity{ + category = <<"proxy">>, + type = <<"bytestreams">>, + name = translate:translate(Lang, Name) + }], + features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_VCARD, ?NS_BYTESTREAMS] + }). + -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> @@ -167,6 +218,7 @@ process_disco_items(#iq{type = set, lang = Lang} = IQ) -> process_disco_items(#iq{type = get} = IQ) -> xmpp:make_iq_result(IQ, #disco_items{}). + -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -174,79 +226,96 @@ process_vcard(#iq{type = set, lang = Lang} = IQ) -> process_vcard(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), VCard = case mod_proxy65_opt:vcard(ServerHost) of - undefined -> - #vcard_temp{fn = <<"ejabberd/mod_proxy65">>, - url = ejabberd_config:get_uri(), - desc = misc:get_descr( - Lang, ?T("ejabberd SOCKS5 Bytestreams module"))}; - V -> - V - end, + undefined -> + #vcard_temp{ + fn = <<"ejabberd/mod_proxy65">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr( + Lang, ?T("ejabberd SOCKS5 Bytestreams module")) + }; + V -> + V + end, xmpp:make_iq_result(IQ, VCard). + -spec process_bytestreams(iq()) -> iq(). process_bytestreams(#iq{type = get, from = JID, to = To, lang = Lang} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), ACL = mod_proxy65_opt:access(ServerHost), case acl:match_rule(ServerHost, ACL, JID) of - allow -> - StreamHost = get_streamhost(Host, ServerHost), - xmpp:make_iq_result(IQ, #bytestreams{hosts = [StreamHost]}); - deny -> - xmpp:make_error(IQ, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)) + allow -> + StreamHost = get_streamhost(Host, ServerHost), + xmpp:make_iq_result(IQ, #bytestreams{hosts = [StreamHost]}); + deny -> + xmpp:make_error(IQ, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)) end; -process_bytestreams(#iq{type = set, lang = Lang, - sub_els = [#bytestreams{sid = SID}]} = IQ) +process_bytestreams(#iq{ + type = set, + lang = Lang, + sub_els = [#bytestreams{sid = SID}] + } = IQ) when SID == <<"">> orelse size(SID) > 128 -> Why = {bad_attr_value, <<"sid">>, <<"query">>, ?NS_BYTESTREAMS}, Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); -process_bytestreams(#iq{type = set, lang = Lang, - sub_els = [#bytestreams{activate = undefined}]} = IQ) -> +process_bytestreams(#iq{ + type = set, + lang = Lang, + sub_els = [#bytestreams{activate = undefined}] + } = IQ) -> Why = {missing_cdata, <<"">>, <<"activate">>, ?NS_BYTESTREAMS}, Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)); -process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To, - sub_els = [#bytestreams{activate = TargetJID, - sid = SID}]} = IQ) -> +process_bytestreams(#iq{ + type = set, + lang = Lang, + from = InitiatorJID, + to = To, + sub_els = [#bytestreams{ + activate = TargetJID, + sid = SID + }] + } = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), ACL = mod_proxy65_opt:access(ServerHost), case acl:match_rule(ServerHost, ACL, InitiatorJID) of - allow -> - Node = ejabberd_cluster:get_node_by_id(To#jid.lresource), - Target = jid:encode(jid:tolower(TargetJID)), - Initiator = jid:encode(jid:tolower(InitiatorJID)), - SHA1 = str:sha(<>), - Mod = gen_mod:ram_db_mod(global, mod_proxy65), - MaxConnections = max_connections(ServerHost), - case Mod:activate_stream(SHA1, Initiator, MaxConnections, Node) of - {ok, InitiatorPid, TargetPid} -> - mod_proxy65_stream:activate( - {InitiatorPid, InitiatorJID}, {TargetPid, TargetJID}), - xmpp:make_iq_result(IQ); - {error, notfound} -> - Txt = ?T("Failed to activate bytestream"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); - {error, {limit, InitiatorPid, TargetPid}} -> - mod_proxy65_stream:stop(InitiatorPid), - mod_proxy65_stream:stop(TargetPid), - Txt = ?T("Too many active bytestreams"), - xmpp:make_error(IQ, xmpp:err_resource_constraint(Txt, Lang)); - {error, conflict} -> - Txt = ?T("Bytestream already activated"), - xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); - {error, Err} -> - ?ERROR_MSG("Failed to activate bytestream from ~ts to ~ts: ~p", - [Initiator, Target, Err]), - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) - end; - deny -> - Txt = ?T("Access denied by service policy"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + allow -> + Node = ejabberd_cluster:get_node_by_id(To#jid.lresource), + Target = jid:encode(jid:tolower(TargetJID)), + Initiator = jid:encode(jid:tolower(InitiatorJID)), + SHA1 = str:sha(<>), + Mod = gen_mod:ram_db_mod(global, mod_proxy65), + MaxConnections = max_connections(ServerHost), + case Mod:activate_stream(SHA1, Initiator, MaxConnections, Node) of + {ok, InitiatorPid, TargetPid} -> + mod_proxy65_stream:activate( + {InitiatorPid, InitiatorJID}, {TargetPid, TargetJID}), + xmpp:make_iq_result(IQ); + {error, notfound} -> + Txt = ?T("Failed to activate bytestream"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); + {error, {limit, InitiatorPid, TargetPid}} -> + mod_proxy65_stream:stop(InitiatorPid), + mod_proxy65_stream:stop(TargetPid), + Txt = ?T("Too many active bytestreams"), + xmpp:make_error(IQ, xmpp:err_resource_constraint(Txt, Lang)); + {error, conflict} -> + Txt = ?T("Bytestream already activated"), + xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); + {error, Err} -> + ?ERROR_MSG("Failed to activate bytestream from ~ts to ~ts: ~p", + [Initiator, Target, Err]), + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) + end; + deny -> + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. + %%%------------------------- %%% Auxiliary functions. %%%------------------------- @@ -254,35 +323,53 @@ process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To, get_streamhost(Host, ServerHost) -> {Port, IP, _} = get_endpoint(ServerHost), HostName = case mod_proxy65_opt:hostname(ServerHost) of - undefined -> misc:ip_to_list(IP); - Val -> Val - end, + undefined -> misc:ip_to_list(IP); + Val -> Val + end, Resource = ejabberd_cluster:node_id(), - #streamhost{jid = jid:make(<<"">>, Host, Resource), - host = HostName, - port = Port}. + #streamhost{ + jid = jid:make(<<"">>, Host, Resource), + host = HostName, + port = Port + }. + -spec get_endpoint(binary()) -> {inet:port_number(), inet:ip_address(), tcp}. get_endpoint(Host) -> Port = mod_proxy65_opt:port(Host), IP = case mod_proxy65_opt:ip(Host) of - undefined -> misc:get_my_ipv4_address(); - Addr -> Addr - end, + undefined -> misc:get_my_ipv4_address(); + Addr -> Addr + end, {Port, IP, tcp}. + max_connections(ServerHost) -> mod_proxy65_opt:max_connections(ServerHost). + register_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, - ?MODULE, process_vcard), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS, - ?MODULE, process_bytestreams). + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_VCARD, + ?MODULE, + process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_BYTESTREAMS, + ?MODULE, + process_bytestreams). + unregister_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), diff --git a/src/mod_proxy65_sql.erl b/src/mod_proxy65_sql.erl index c05f055b7..c81426710 100644 --- a/src/mod_proxy65_sql.erl +++ b/src/mod_proxy65_sql.erl @@ -23,7 +23,6 @@ -module(mod_proxy65_sql). -behaviour(mod_proxy65). - %% API -export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). -export([sql_schemas/0]). @@ -31,6 +30,7 @@ -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -40,118 +40,127 @@ init() -> NodeS = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'proxy65' table...", []), case ejabberd_sql:sql_query( - ejabberd_config:get_myname(), - ?SQL("delete from proxy65 where " - "node_i=%(NodeS)s or node_t=%(NodeS)s")) of - {updated, _} -> - ok; - Err -> - ?ERROR_MSG("Failed to clean 'proxy65' table: ~p", [Err]), - Err + ejabberd_config:get_myname(), + ?SQL("delete from proxy65 where " + "node_i=%(NodeS)s or node_t=%(NodeS)s")) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("Failed to clean 'proxy65' table: ~p", [Err]), + Err end. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"proxy65">>, - columns = - [#sql_column{name = <<"sid">>, type = text}, - #sql_column{name = <<"pid_t">>, type = text}, - #sql_column{name = <<"pid_i">>, type = text}, - #sql_column{name = <<"node_t">>, type = text}, - #sql_column{name = <<"node_i">>, type = text}, - #sql_column{name = <<"jid_i">>, type = text}], - indices = [#sql_index{ - columns = [<<"sid">>], - unique = true}, - #sql_index{ - columns = [<<"jid_i">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"proxy65">>, + columns = + [#sql_column{name = <<"sid">>, type = text}, + #sql_column{name = <<"pid_t">>, type = text}, + #sql_column{name = <<"pid_i">>, type = text}, + #sql_column{name = <<"node_t">>, type = text}, + #sql_column{name = <<"node_i">>, type = text}, + #sql_column{name = <<"jid_i">>, type = text}], + indices = [#sql_index{ + columns = [<<"sid">>], + unique = true + }, + #sql_index{ + columns = [<<"jid_i">>] + }] + }] + }]. + register_stream(SID, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), F = fun() -> - case ejabberd_sql:sql_query_t( - ?SQL("update proxy65 set pid_i=%(PidS)s, " - "node_i=%(NodeS)s where sid=%(SID)s")) of - {updated, 1} -> - ok; - _ -> - ejabberd_sql:sql_query_t( - ?SQL("insert into proxy65" - "(sid, pid_t, node_t, pid_i, node_i, jid_i) " - "values (%(SID)s, %(PidS)s, %(NodeS)s, '', '', '')")) - end - end, + case ejabberd_sql:sql_query_t( + ?SQL("update proxy65 set pid_i=%(PidS)s, " + "node_i=%(NodeS)s where sid=%(SID)s")) of + {updated, 1} -> + ok; + _ -> + ejabberd_sql:sql_query_t( + ?SQL("insert into proxy65" + "(sid, pid_t, node_t, pid_i, node_i, jid_i) " + "values (%(SID)s, %(PidS)s, %(NodeS)s, '', '', '')")) + end + end, case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of - {atomic, _} -> - ok; - {aborted, Reason} -> - {error, Reason} + {atomic, _} -> + ok; + {aborted, Reason} -> + {error, Reason} end. + unregister_stream(SID) -> F = fun() -> - ejabberd_sql:sql_query_t( - ?SQL("delete from proxy65 where sid=%(SID)s")) - end, + ejabberd_sql:sql_query_t( + ?SQL("delete from proxy65 where sid=%(SID)s")) + end, case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of - {atomic, _} -> - ok; - {aborted, Reason} -> - {error, Reason} + {atomic, _} -> + ok; + {aborted, Reason} -> + {error, Reason} end. + activate_stream(SID, IJID, MaxConnections, _Node) -> F = fun() -> - case ejabberd_sql:sql_query_t( - ?SQL("select @(pid_t)s, @(node_t)s, @(pid_i)s, " - "@(node_i)s, @(jid_i)s from proxy65 where " - "sid=%(SID)s")) of - {selected, [{TPidS, TNodeS, IPidS, INodeS, <<"">>}]} - when IPidS /= <<"">> -> - try {misc:decode_pid(TPidS, TNodeS), - misc:decode_pid(IPidS, INodeS)} of - {TPid, IPid} -> - case ejabberd_sql:sql_query_t( - ?SQL("update proxy65 set jid_i=%(IJID)s " - "where sid=%(SID)s")) of - {updated, 1} when is_integer(MaxConnections) -> - case ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from proxy65 " - "where jid_i=%(IJID)s")) of - {selected, [{Num}]} when Num > MaxConnections -> - ejabberd_sql:abort({limit, IPid, TPid}); - {selected, _} -> - {ok, IPid, TPid}; - Err -> - ejabberd_sql:abort(Err) - end; - {updated, _} -> - {ok, IPid, TPid}; - Err -> - ejabberd_sql:abort(Err) - end - catch _:{bad_node, _} -> - {error, notfound} - end; - {selected, [{_, _, _, _, JID}]} when JID /= <<"">> -> - {error, conflict}; - {selected, _} -> - {error, notfound}; - Err -> - ejabberd_sql:abort(Err) - end - end, + case ejabberd_sql:sql_query_t( + ?SQL("select @(pid_t)s, @(node_t)s, @(pid_i)s, " + "@(node_i)s, @(jid_i)s from proxy65 where " + "sid=%(SID)s")) of + {selected, [{TPidS, TNodeS, IPidS, INodeS, <<"">>}]} + when IPidS /= <<"">> -> + try {misc:decode_pid(TPidS, TNodeS), + misc:decode_pid(IPidS, INodeS)} of + {TPid, IPid} -> + case ejabberd_sql:sql_query_t( + ?SQL("update proxy65 set jid_i=%(IJID)s " + "where sid=%(SID)s")) of + {updated, 1} when is_integer(MaxConnections) -> + case ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from proxy65 " + "where jid_i=%(IJID)s")) of + {selected, [{Num}]} when Num > MaxConnections -> + ejabberd_sql:abort({limit, IPid, TPid}); + {selected, _} -> + {ok, IPid, TPid}; + Err -> + ejabberd_sql:abort(Err) + end; + {updated, _} -> + {ok, IPid, TPid}; + Err -> + ejabberd_sql:abort(Err) + end + catch + _:{bad_node, _} -> + {error, notfound} + end; + {selected, [{_, _, _, _, JID}]} when JID /= <<"">> -> + {error, conflict}; + {selected, _} -> + {error, notfound}; + Err -> + ejabberd_sql:abort(Err) + end + end, case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of - {atomic, Result} -> - Result; - {aborted, {limit, _, _} = Limit} -> - {error, Limit}; - {aborted, Reason} -> - {error, Reason} + {atomic, Result} -> + Result; + {aborted, {limit, _, _} = Limit} -> + {error, Limit}; + {aborted, Reason} -> + {error, Reason} end. %%%=================================================================== diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index c12c67b63..40dcff998 100644 --- a/src/mod_proxy65_stream.erl +++ b/src/mod_proxy65_stream.erl @@ -30,16 +30,28 @@ -behaviour(ejabberd_listener). %% gen_fsm callbacks. --export([init/1, handle_event/3, handle_sync_event/4, - code_change/4, handle_info/3, terminate/3]). +-export([init/1, + handle_event/3, + handle_sync_event/4, + code_change/4, + handle_info/3, + terminate/3]). %% gen_fsm states. --export([accepting/2, wait_for_init/2, wait_for_auth/2, - wait_for_request/2, wait_for_activation/2, - stream_established/2]). +-export([accepting/2, + wait_for_init/2, + wait_for_auth/2, + wait_for_request/2, + wait_for_activation/2, + stream_established/2]). --export([start/3, stop/1, start_link/3, activate/2, - relay/3, accept/1, listen_options/0]). +-export([start/3, + stop/1, + start_link/3, + activate/2, + relay/3, + accept/1, + listen_options/0]). -include("mod_proxy65.hrl"). @@ -47,31 +59,38 @@ -define(WAIT_TIMEOUT, 60000). --record(state, - {socket :: inet:socket(), - timer = make_ref() :: reference(), - sha1 = <<"">> :: binary(), - host = <<"">> :: binary(), - auth_type = anonymous :: plain | anonymous, - shaper = none :: ejabberd_shaper:shaper()}). +-record(state, { + socket :: inet:socket(), + timer = make_ref() :: reference(), + sha1 = <<"">> :: binary(), + host = <<"">> :: binary(), + auth_type = anonymous :: plain | anonymous, + shaper = none :: ejabberd_shaper:shaper() + }). + %% Unused callbacks handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. + code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. + %%------------------------------- + start(gen_tcp, Socket, Opts) -> Host = proplists:get_value(server_host, Opts), p1_fsm:start(?MODULE, [Socket, Host], []). + start_link(gen_tcp, Socket, Opts) -> Host = proplists:get_value(server_host, Opts), p1_fsm:start_link(?MODULE, [Socket, Host], []). + init([Socket, Host]) -> process_flag(trap_exit, true), AuthType = mod_proxy65_opt:auth_type(Host), @@ -81,42 +100,52 @@ init([Socket, Host]) -> TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), inet:setopts(Socket, [{recbuf, RecvBuf}, {sndbuf, SendBuf}]), {ok, accepting, - #state{host = Host, auth_type = AuthType, - socket = Socket, shaper = Shaper, timer = TRef}}. + #state{ + host = Host, + auth_type = AuthType, + socket = Socket, + shaper = Shaper, + timer = TRef + }}. + terminate(_Reason, StateName, #state{sha1 = SHA1}) -> Mod = gen_mod:ram_db_mod(global, mod_proxy65), Mod:unregister_stream(SHA1), - if StateName == stream_established -> - ?INFO_MSG("(~w) Bytestream terminated", [self()]); - true -> ok + if + StateName == stream_established -> + ?INFO_MSG("(~w) Bytestream terminated", [self()]); + true -> ok end. + %%%------------------------------ %%% API. %%%------------------------------ accept(StreamPid) -> p1_fsm:send_event(StreamPid, accept). + stop(StreamPid) -> StreamPid ! stop. + activate({P1, J1}, {P2, J2}) -> case catch {p1_fsm:sync_send_all_state_event(P1, - get_socket), - p1_fsm:sync_send_all_state_event(P2, get_socket)} - of - {S1, S2} when is_port(S1), is_port(S2) -> - P1 ! {activate, P2, S2, J1, J2}, - P2 ! {activate, P1, S1, J1, J2}, - JID1 = jid:encode(J1), - JID2 = jid:encode(J2), - ?INFO_MSG("(~w:~w) Activated bytestream for ~ts " - "-> ~ts", - [P1, P2, JID1, JID2]), - ok; - _ -> error + get_socket), + p1_fsm:sync_send_all_state_event(P2, get_socket)} of + {S1, S2} when is_port(S1), is_port(S2) -> + P1 ! {activate, P2, S2, J1, J2}, + P2 ! {activate, P1, S1, J1, J2}, + JID1 = jid:encode(J1), + JID2 = jid:encode(J2), + ?INFO_MSG("(~w:~w) Activated bytestream for ~ts " + "-> ~ts", + [P1, P2, JID1, JID2]), + ok; + _ -> error end. + %%%----------------------- %%% States %%%----------------------- @@ -124,98 +153,109 @@ accepting(accept, State) -> inet:setopts(State#state.socket, [{active, true}]), {next_state, wait_for_init, State}. + wait_for_init(Packet, - #state{socket = Socket, auth_type = AuthType} = - StateData) -> + #state{socket = Socket, auth_type = AuthType} = + StateData) -> case mod_proxy65_lib:unpack_init_message(Packet) of - {ok, AuthMethods} -> - Method = select_auth_method(AuthType, AuthMethods), - gen_tcp:send(Socket, - mod_proxy65_lib:make_init_reply(Method)), - case Method of - ?AUTH_ANONYMOUS -> - {next_state, wait_for_request, StateData}; - ?AUTH_PLAIN -> {next_state, wait_for_auth, StateData}; - ?AUTH_NO_METHODS -> {stop, normal, StateData} - end; - error -> {stop, normal, StateData} + {ok, AuthMethods} -> + Method = select_auth_method(AuthType, AuthMethods), + gen_tcp:send(Socket, + mod_proxy65_lib:make_init_reply(Method)), + case Method of + ?AUTH_ANONYMOUS -> + {next_state, wait_for_request, StateData}; + ?AUTH_PLAIN -> {next_state, wait_for_auth, StateData}; + ?AUTH_NO_METHODS -> {stop, normal, StateData} + end; + error -> {stop, normal, StateData} end. + wait_for_auth(Packet, - #state{socket = Socket, host = Host} = StateData) -> + #state{socket = Socket, host = Host} = StateData) -> case mod_proxy65_lib:unpack_auth_request(Packet) of - {User, Pass} -> - Result = ejabberd_auth:check_password(User, <<"">>, Host, Pass), - gen_tcp:send(Socket, - mod_proxy65_lib:make_auth_reply(Result)), - case Result of - true -> {next_state, wait_for_request, StateData}; - false -> {stop, normal, StateData} - end; - _ -> {stop, normal, StateData} + {User, Pass} -> + Result = ejabberd_auth:check_password(User, <<"">>, Host, Pass), + gen_tcp:send(Socket, + mod_proxy65_lib:make_auth_reply(Result)), + case Result of + true -> {next_state, wait_for_request, StateData}; + false -> {stop, normal, StateData} + end; + _ -> {stop, normal, StateData} end. + wait_for_request(Packet, - #state{socket = Socket} = StateData) -> + #state{socket = Socket} = StateData) -> Request = mod_proxy65_lib:unpack_request(Packet), case Request of - #s5_request{sha1 = SHA1, cmd = connect} -> - Mod = gen_mod:ram_db_mod(global, mod_proxy65), - case Mod:register_stream(SHA1, self()) of - ok -> - inet:setopts(Socket, [{active, false}]), - gen_tcp:send(Socket, - mod_proxy65_lib:make_reply(Request)), - {next_state, wait_for_activation, - StateData#state{sha1 = SHA1}}; - _ -> - Err = mod_proxy65_lib:make_error_reply(Request), - gen_tcp:send(Socket, Err), - {stop, normal, StateData} - end; - #s5_request{cmd = udp} -> - Err = mod_proxy65_lib:make_error_reply(Request, - ?ERR_COMMAND_NOT_SUPPORTED), - gen_tcp:send(Socket, Err), - {stop, normal, StateData}; - _ -> {stop, normal, StateData} + #s5_request{sha1 = SHA1, cmd = connect} -> + Mod = gen_mod:ram_db_mod(global, mod_proxy65), + case Mod:register_stream(SHA1, self()) of + ok -> + inet:setopts(Socket, [{active, false}]), + gen_tcp:send(Socket, + mod_proxy65_lib:make_reply(Request)), + {next_state, wait_for_activation, + StateData#state{sha1 = SHA1}}; + _ -> + Err = mod_proxy65_lib:make_error_reply(Request), + gen_tcp:send(Socket, Err), + {stop, normal, StateData} + end; + #s5_request{cmd = udp} -> + Err = mod_proxy65_lib:make_error_reply(Request, + ?ERR_COMMAND_NOT_SUPPORTED), + gen_tcp:send(Socket, Err), + {stop, normal, StateData}; + _ -> {stop, normal, StateData} end. + wait_for_activation(_Data, StateData) -> {next_state, wait_for_activation, StateData}. + stream_established(_Data, StateData) -> {next_state, stream_established, StateData}. + %%%----------------------- %%% Callbacks processing %%%----------------------- + %% SOCKS5 packets. handle_info({tcp, _S, Data}, StateName, StateData) - when StateName /= wait_for_activation -> + when StateName /= wait_for_activation -> misc:cancel_timer(StateData#state.timer), TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), p1_fsm:send_event(self(), Data), {next_state, StateName, StateData#state{timer = TRef}}; %% Activation message. handle_info({activate, PeerPid, PeerSocket, IJid, TJid}, - wait_for_activation, StateData) -> + wait_for_activation, + StateData) -> erlang:monitor(process, PeerPid), misc:cancel_timer(StateData#state.timer), MySocket = StateData#state.socket, Shaper = StateData#state.shaper, Host = StateData#state.host, MaxRate = find_maxrate(Shaper, IJid, TJid, Host), - spawn_link(?MODULE, relay, - [MySocket, PeerSocket, MaxRate]), + spawn_link(?MODULE, + relay, + [MySocket, PeerSocket, MaxRate]), {next_state, stream_established, StateData}; %% Socket closed -handle_info({tcp_closed, _Socket}, _StateName, - StateData) -> +handle_info({tcp_closed, _Socket}, + _StateName, + StateData) -> {stop, normal, StateData}; -handle_info({tcp_error, _Socket, _Reason}, _StateName, - StateData) -> +handle_info({tcp_error, _Socket, _Reason}, + _StateName, + StateData) -> {stop, normal, StateData}; %% Got stop message. handle_info(stop, _StateName, StateData) -> @@ -223,65 +263,76 @@ handle_info(stop, _StateName, StateData) -> %% Either linked process or peer process died. handle_info({'EXIT', _, _}, _StateName, StateData) -> {stop, normal, StateData}; -handle_info({'DOWN', _, _, _, _}, _StateName, - StateData) -> +handle_info({'DOWN', _, _, _, _}, + _StateName, + StateData) -> {stop, normal, StateData}; %% Packets of no interest handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. + %% Socket request. -handle_sync_event(get_socket, _From, - wait_for_activation, StateData) -> +handle_sync_event(get_socket, + _From, + wait_for_activation, + StateData) -> Socket = StateData#state.socket, {reply, Socket, wait_for_activation, StateData}; -handle_sync_event(_Event, _From, StateName, - StateData) -> +handle_sync_event(_Event, + _From, + StateName, + StateData) -> {reply, error, StateName, StateData}. + %%%------------------------------------------------- %%% Relay Process. %%%------------------------------------------------- relay(MySocket, PeerSocket, Shaper) -> case gen_tcp:recv(MySocket, 0) of - {ok, Data} -> - case gen_tcp:send(PeerSocket, Data) of - ok -> - {NewShaper, Pause} = ejabberd_shaper:update(Shaper, byte_size(Data)), - if Pause > 0 -> timer:sleep(Pause); - true -> pass - end, - relay(MySocket, PeerSocket, NewShaper); - {error, _} = Err -> - Err - end; - {error, _} = Err -> - Err + {ok, Data} -> + case gen_tcp:send(PeerSocket, Data) of + ok -> + {NewShaper, Pause} = ejabberd_shaper:update(Shaper, byte_size(Data)), + if + Pause > 0 -> timer:sleep(Pause); + true -> pass + end, + relay(MySocket, PeerSocket, NewShaper); + {error, _} = Err -> + Err + end; + {error, _} = Err -> + Err end. + %%%------------------------ %%% Auxiliary functions %%%------------------------ select_auth_method(plain, AuthMethods) -> case lists:member(?AUTH_PLAIN, AuthMethods) of - true -> ?AUTH_PLAIN; - false -> ?AUTH_NO_METHODS + true -> ?AUTH_PLAIN; + false -> ?AUTH_NO_METHODS end; select_auth_method(anonymous, AuthMethods) -> case lists:member(?AUTH_ANONYMOUS, AuthMethods) of - true -> ?AUTH_ANONYMOUS; - false -> ?AUTH_NO_METHODS + true -> ?AUTH_ANONYMOUS; + false -> ?AUTH_NO_METHODS end. + %% Obviously, we must use shaper with maximum rate. find_maxrate(Shaper, JID1, JID2, Host) -> R1 = ejabberd_shaper:match(Host, Shaper, JID1), R2 = ejabberd_shaper:match(Host, Shaper, JID2), R = case ejabberd_shaper:get_max_rate(R1) >= ejabberd_shaper:get_max_rate(R2) of - true -> R1; - false -> R2 - end, + true -> R1; + false -> R2 + end, ejabberd_shaper:new(R). + listen_options() -> []. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 06f0af657..b8a58b778 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -42,7 +42,9 @@ -protocol({xep, 248, '0.2', '2.1.0', "complete", ""}). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("pubsub.hrl"). -include("mod_roster.hrl"). -include("translate.hrl"). @@ -54,47 +56,96 @@ -define(PEPNODE, <<"pep">>). %% exports for hooks --export([presence_probe/3, caps_add/3, caps_update/3, - in_subscription/2, out_subscription/1, - on_self_presence/1, on_user_offline/2, remove_user/2, - disco_local_identity/5, disco_local_features/5, - disco_local_items/5, disco_sm_identity/5, - disco_sm_features/5, disco_sm_items/5, - c2s_handle_info/2]). +-export([presence_probe/3, + caps_add/3, + caps_update/3, + in_subscription/2, + out_subscription/1, + on_self_presence/1, + on_user_offline/2, + remove_user/2, + disco_local_identity/5, + disco_local_features/5, + disco_local_items/5, + disco_sm_identity/5, + disco_sm_features/5, + disco_sm_items/5, + c2s_handle_info/2]). %% exported iq handlers --export([iq_sm/1, process_disco_info/1, process_disco_items/1, - process_pubsub/1, process_pubsub_owner/1, process_vcard/1, - process_commands/1]). +-export([iq_sm/1, + process_disco_info/1, + process_disco_items/1, + process_pubsub/1, + process_pubsub_owner/1, + process_vcard/1, + process_commands/1]). %% exports for console debug manual use --export([create_node/5, create_node/7, delete_node/3, - subscribe_node/5, unsubscribe_node/5, publish_item/6, publish_item/8, - delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3, - get_cached_item/2, get_configure/5, set_configure/5, - tree_action/3, node_action/4, node_call/4]). +-export([create_node/5, create_node/7, + delete_node/3, + subscribe_node/5, + unsubscribe_node/5, + publish_item/6, publish_item/8, + delete_item/4, delete_item/5, + send_items/7, + get_items/2, + get_item/3, + get_cached_item/2, + get_configure/5, + set_configure/5, + tree_action/3, + node_action/4, + node_call/4]). %% general helpers for plugins --export([extended_error/2, service_jid/1, - tree/1, tree/2, plugin/2, plugins/1, config/3, - host/1, serverhost/1]). +-export([extended_error/2, + service_jid/1, + tree/1, tree/2, + plugin/2, + plugins/1, + config/3, + host/1, + serverhost/1]). %% pubsub#errors --export([err_closed_node/0, err_configuration_required/0, - err_invalid_jid/0, err_invalid_options/0, err_invalid_payload/0, - err_invalid_subid/0, err_item_forbidden/0, err_item_required/0, - err_jid_required/0, err_max_items_exceeded/0, err_max_nodes_exceeded/0, - err_nodeid_required/0, err_not_in_roster_group/0, err_not_subscribed/0, - err_payload_too_big/0, err_payload_required/0, - err_pending_subscription/0, err_precondition_not_met/0, - err_presence_subscription_required/0, err_subid_required/0, - err_too_many_subscriptions/0, err_unsupported/1, - err_unsupported_access_model/0]). +-export([err_closed_node/0, + err_configuration_required/0, + err_invalid_jid/0, + err_invalid_options/0, + err_invalid_payload/0, + err_invalid_subid/0, + err_item_forbidden/0, + err_item_required/0, + err_jid_required/0, + err_max_items_exceeded/0, + err_max_nodes_exceeded/0, + err_nodeid_required/0, + err_not_in_roster_group/0, + err_not_subscribed/0, + err_payload_too_big/0, + err_payload_required/0, + err_pending_subscription/0, + err_precondition_not_met/0, + err_presence_subscription_required/0, + err_subid_required/0, + err_too_many_subscriptions/0, + err_unsupported/1, + err_unsupported_access_model/0]). %% API and gen_server callbacks --export([start/2, stop/1, init/1, - handle_call/3, handle_cast/2, handle_info/2, mod_doc/0, - terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]). +-export([start/2, + stop/1, + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + mod_doc/0, + terminate/2, + code_change/3, + depends/2, + mod_opt_type/1, + mod_options/1]). %% ejabberd commands -export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]). @@ -109,135 +160,127 @@ %% Description: Starts the server %%-------------------------------------------------------------------- --export_type([ - host/0, - hostPubsub/0, - hostPEP/0, - %% - nodeIdx/0, - nodeId/0, - itemId/0, - subId/0, - payload/0, - %% - nodeOption/0, - nodeOptions/0, - subOption/0, - subOptions/0, - pubOption/0, - pubOptions/0, - %% - affiliation/0, - subscription/0, - accessModel/0, - publishModel/0 - ]). +-export_type([host/0, + hostPubsub/0, + hostPEP/0, + %% + nodeIdx/0, + nodeId/0, + itemId/0, + subId/0, + payload/0, + %% + nodeOption/0, + nodeOptions/0, + subOption/0, + subOptions/0, + pubOption/0, + pubOptions/0, + %% + affiliation/0, + subscription/0, + accessModel/0, + publishModel/0]). %% -type payload() defined here because the -type xmlel() is not accessible %% from pubsub.hrl --type(payload() :: [] | [xmlel(),...]). +-type(payload() :: [] | [xmlel(), ...]). --export_type([ - pubsubNode/0, - pubsubState/0, - pubsubItem/0, - pubsubSubscription/0, - pubsubLastItem/0 - ]). +-export_type([pubsubNode/0, + pubsubState/0, + pubsubItem/0, + pubsubSubscription/0, + pubsubLastItem/0]). -type(pubsubNode() :: - #pubsub_node{ - nodeid :: {Host::mod_pubsub:host(), Node::mod_pubsub:nodeId()}, - id :: Nidx::mod_pubsub:nodeIdx(), - parents :: [Node::mod_pubsub:nodeId()], - type :: Type::binary(), - owners :: [Owner::ljid(),...], - options :: Opts::mod_pubsub:nodeOptions() - } - ). + #pubsub_node{ + nodeid :: {Host :: mod_pubsub:host(), Node :: mod_pubsub:nodeId()}, + id :: Nidx :: mod_pubsub:nodeIdx(), + parents :: [Node :: mod_pubsub:nodeId()], + type :: Type :: binary(), + owners :: [Owner :: ljid(), ...], + options :: Opts :: mod_pubsub:nodeOptions() + }). -type(pubsubState() :: - #pubsub_state{ - stateid :: {Entity::ljid(), Nidx::mod_pubsub:nodeIdx()}, - nodeidx :: Nidx::mod_pubsub:nodeIdx(), - items :: [ItemId::mod_pubsub:itemId()], - affiliation :: Affs::mod_pubsub:affiliation(), - subscriptions :: [{Sub::mod_pubsub:subscription(), SubId::mod_pubsub:subId()}] - } - ). + #pubsub_state{ + stateid :: {Entity :: ljid(), Nidx :: mod_pubsub:nodeIdx()}, + nodeidx :: Nidx :: mod_pubsub:nodeIdx(), + items :: [ItemId :: mod_pubsub:itemId()], + affiliation :: Affs :: mod_pubsub:affiliation(), + subscriptions :: [{Sub :: mod_pubsub:subscription(), SubId :: mod_pubsub:subId()}] + }). -type(pubsubItem() :: - #pubsub_item{ - itemid :: {ItemId::mod_pubsub:itemId(), Nidx::mod_pubsub:nodeIdx()}, - nodeidx :: Nidx::mod_pubsub:nodeIdx(), - creation :: {erlang:timestamp(), ljid()}, - modification :: {erlang:timestamp(), ljid()}, - payload :: mod_pubsub:payload() - } - ). + #pubsub_item{ + itemid :: {ItemId :: mod_pubsub:itemId(), Nidx :: mod_pubsub:nodeIdx()}, + nodeidx :: Nidx :: mod_pubsub:nodeIdx(), + creation :: {erlang:timestamp(), ljid()}, + modification :: {erlang:timestamp(), ljid()}, + payload :: mod_pubsub:payload() + }). -type(pubsubSubscription() :: - #pubsub_subscription{ - subid :: SubId::mod_pubsub:subId(), - options :: [] | mod_pubsub:subOptions() - } - ). + #pubsub_subscription{ + subid :: SubId :: mod_pubsub:subId(), + options :: [] | mod_pubsub:subOptions() + }). -type(pubsubLastItem() :: - #pubsub_last_item{ - nodeid :: {binary(), mod_pubsub:nodeIdx()}, - itemid :: mod_pubsub:itemId(), - creation :: {erlang:timestamp(), ljid()}, - payload :: mod_pubsub:payload() - } - ). + #pubsub_last_item{ + nodeid :: {binary(), mod_pubsub:nodeIdx()}, + itemid :: mod_pubsub:itemId(), + creation :: {erlang:timestamp(), ljid()}, + payload :: mod_pubsub:payload() + }). --record(state, - { - server_host, - hosts, - access, - pep_mapping = [], - ignore_pep_from_offline = true, - last_item_cache = false, - max_items_node = ?MAXITEMS, - max_subscriptions_node = undefined, - default_node_config = [], - nodetree = <<"nodetree_", (?STDTREE)/binary>>, - plugins = [?STDNODE], - db_type - }). +-record(state, { + server_host, + hosts, + access, + pep_mapping = [], + ignore_pep_from_offline = true, + last_item_cache = false, + max_items_node = ?MAXITEMS, + max_subscriptions_node = undefined, + default_node_config = [], + nodetree = <<"nodetree_", (?STDTREE)/binary>>, + plugins = [?STDNODE], + db_type + }). -type(state() :: - #state{ - server_host :: binary(), - hosts :: [mod_pubsub:hostPubsub()], - access :: atom(), - pep_mapping :: [{binary(), binary()}], - ignore_pep_from_offline :: boolean(), - last_item_cache :: boolean(), - max_items_node :: non_neg_integer()|unlimited, - max_subscriptions_node :: non_neg_integer()|undefined, - default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}], - nodetree :: binary(), - plugins :: [binary(),...], - db_type :: atom() - } - - ). + #state{ + server_host :: binary(), + hosts :: [mod_pubsub:hostPubsub()], + access :: atom(), + pep_mapping :: [{binary(), binary()}], + ignore_pep_from_offline :: boolean(), + last_item_cache :: boolean(), + max_items_node :: non_neg_integer() | unlimited, + max_subscriptions_node :: non_neg_integer() | undefined, + default_node_config :: [{atom(), binary() | boolean() | integer() | atom()}], + nodetree :: binary(), + plugins :: [binary(), ...], + db_type :: atom() + }). -type subs_by_depth() :: [{integer(), [{#pubsub_node{}, [{ljid(), subId(), subOptions()}]}]}]. + start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + %%==================================================================== %% gen_server callbacks %%==================================================================== + %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | @@ -245,9 +288,9 @@ stop(Host) -> %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- --spec init([binary() | [{_,_}],...]) -> {'ok',state()}. +-spec init([binary() | [{_, _}], ...]) -> {'ok', state()}. -init([ServerHost|_]) -> +init([ServerHost | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), Hosts = gen_mod:get_opt_hosts(Opts), @@ -256,113 +299,197 @@ init([ServerHost|_]) -> LastItemCache = mod_pubsub_opt:last_item_cache(Opts), MaxItemsNode = mod_pubsub_opt:max_items_node(Opts), MaxSubsNode = mod_pubsub_opt:max_subscriptions_node(Opts), - ejabberd_mnesia:create(?MODULE, pubsub_last_item, - [{ram_copies, [node()]}, - {attributes, record_info(fields, pubsub_last_item)}]), + ejabberd_mnesia:create(?MODULE, + pubsub_last_item, + [{ram_copies, [node()]}, + {attributes, record_info(fields, pubsub_last_item)}]), DBMod = gen_mod:db_mod(Opts, ?MODULE), AllPlugins = - lists:flatmap( - fun(Host) -> - DBMod:init(Host, ServerHost, Opts), - ejabberd_router:register_route( - Host, ServerHost, {apply, ?MODULE, route}), - {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), - DefaultNodeCfg = mod_pubsub_opt:default_node_config(Opts), - lists:foreach( - fun(H) -> - T = gen_mod:get_module_proc(H, config), - try - ets:new(T, [set, named_table]), - ets:insert(T, {nodetree, NodeTree}), - ets:insert(T, {plugins, Plugins}), - ets:insert(T, {last_item_cache, LastItemCache}), - ets:insert(T, {max_items_node, MaxItemsNode}), - ets:insert(T, {max_subscriptions_node, MaxSubsNode}), - ets:insert(T, {default_node_config, DefaultNodeCfg}), - ets:insert(T, {pep_mapping, PepMapping}), - ets:insert(T, {ignore_pep_from_offline, PepOffline}), - ets:insert(T, {host, Host}), - ets:insert(T, {access, Access}) - catch error:badarg when H == ServerHost -> - ok - end - end, [Host, ServerHost]), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, - ?MODULE, process_disco_info), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, - ?MODULE, process_disco_items), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB, - ?MODULE, process_pubsub), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER, - ?MODULE, process_pubsub_owner), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, - ?MODULE, process_vcard), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, - ?MODULE, process_commands), - Plugins - end, Hosts), - ejabberd_hooks:add(c2s_self_presence, ServerHost, - ?MODULE, on_self_presence, 75), - ejabberd_hooks:add(c2s_terminated, ServerHost, - ?MODULE, on_user_offline, 75), - ejabberd_hooks:add(disco_local_identity, ServerHost, - ?MODULE, disco_local_identity, 75), - ejabberd_hooks:add(disco_local_features, ServerHost, - ?MODULE, disco_local_features, 75), - ejabberd_hooks:add(disco_local_items, ServerHost, - ?MODULE, disco_local_items, 75), - ejabberd_hooks:add(presence_probe_hook, ServerHost, - ?MODULE, presence_probe, 80), - ejabberd_hooks:add(roster_in_subscription, ServerHost, - ?MODULE, in_subscription, 50), - ejabberd_hooks:add(roster_out_subscription, ServerHost, - ?MODULE, out_subscription, 50), - ejabberd_hooks:add(remove_user, ServerHost, - ?MODULE, remove_user, 50), - ejabberd_hooks:add(c2s_handle_info, ServerHost, - ?MODULE, c2s_handle_info, 50), + lists:flatmap( + fun(Host) -> + DBMod:init(Host, ServerHost, Opts), + ejabberd_router:register_route( + Host, ServerHost, {apply, ?MODULE, route}), + {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), + DefaultNodeCfg = mod_pubsub_opt:default_node_config(Opts), + lists:foreach( + fun(H) -> + T = gen_mod:get_module_proc(H, config), + try + ets:new(T, [set, named_table]), + ets:insert(T, {nodetree, NodeTree}), + ets:insert(T, {plugins, Plugins}), + ets:insert(T, {last_item_cache, LastItemCache}), + ets:insert(T, {max_items_node, MaxItemsNode}), + ets:insert(T, {max_subscriptions_node, MaxSubsNode}), + ets:insert(T, {default_node_config, DefaultNodeCfg}), + ets:insert(T, {pep_mapping, PepMapping}), + ets:insert(T, {ignore_pep_from_offline, PepOffline}), + ets:insert(T, {host, Host}), + ets:insert(T, {access, Access}) + catch + error:badarg when H == ServerHost -> + ok + end + end, + [Host, ServerHost]), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_INFO, + ?MODULE, + process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_DISCO_ITEMS, + ?MODULE, + process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_PUBSUB, + ?MODULE, + process_pubsub), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_PUBSUB_OWNER, + ?MODULE, + process_pubsub_owner), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_VCARD, + ?MODULE, + process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_COMMANDS, + ?MODULE, + process_commands), + Plugins + end, + Hosts), + ejabberd_hooks:add(c2s_self_presence, + ServerHost, + ?MODULE, + on_self_presence, + 75), + ejabberd_hooks:add(c2s_terminated, + ServerHost, + ?MODULE, + on_user_offline, + 75), + ejabberd_hooks:add(disco_local_identity, + ServerHost, + ?MODULE, + disco_local_identity, + 75), + ejabberd_hooks:add(disco_local_features, + ServerHost, + ?MODULE, + disco_local_features, + 75), + ejabberd_hooks:add(disco_local_items, + ServerHost, + ?MODULE, + disco_local_items, + 75), + ejabberd_hooks:add(presence_probe_hook, + ServerHost, + ?MODULE, + presence_probe, + 80), + ejabberd_hooks:add(roster_in_subscription, + ServerHost, + ?MODULE, + in_subscription, + 50), + ejabberd_hooks:add(roster_out_subscription, + ServerHost, + ?MODULE, + out_subscription, + 50), + ejabberd_hooks:add(remove_user, + ServerHost, + ?MODULE, + remove_user, + 50), + ejabberd_hooks:add(c2s_handle_info, + ServerHost, + ?MODULE, + c2s_handle_info, + 50), case lists:member(?PEPNODE, AllPlugins) of - true -> - ejabberd_hooks:add(caps_add, ServerHost, - ?MODULE, caps_add, 80), - ejabberd_hooks:add(caps_update, ServerHost, - ?MODULE, caps_update, 80), - ejabberd_hooks:add(disco_sm_identity, ServerHost, - ?MODULE, disco_sm_identity, 75), - ejabberd_hooks:add(disco_sm_features, ServerHost, - ?MODULE, disco_sm_features, 75), - ejabberd_hooks:add(disco_sm_items, ServerHost, - ?MODULE, disco_sm_items, 75), - gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, - ?NS_PUBSUB, ?MODULE, iq_sm), - gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, - ?NS_PUBSUB_OWNER, ?MODULE, iq_sm); - false -> - ok + true -> + ejabberd_hooks:add(caps_add, + ServerHost, + ?MODULE, + caps_add, + 80), + ejabberd_hooks:add(caps_update, + ServerHost, + ?MODULE, + caps_update, + 80), + ejabberd_hooks:add(disco_sm_identity, + ServerHost, + ?MODULE, + disco_sm_identity, + 75), + ejabberd_hooks:add(disco_sm_features, + ServerHost, + ?MODULE, + disco_sm_features, + 75), + ejabberd_hooks:add(disco_sm_items, + ServerHost, + ?MODULE, + disco_sm_items, + 75), + gen_iq_handler:add_iq_handler(ejabberd_sm, + ServerHost, + ?NS_PUBSUB, + ?MODULE, + iq_sm), + gen_iq_handler:add_iq_handler(ejabberd_sm, + ServerHost, + ?NS_PUBSUB_OWNER, + ?MODULE, + iq_sm); + false -> + ok end, ejabberd_commands:register_commands(ServerHost, ?MODULE, get_commands_spec()), NodeTree = config(ServerHost, nodetree), Plugins = config(ServerHost, plugins), PepMapping = config(ServerHost, pep_mapping), DBType = mod_pubsub_opt:db_type(ServerHost), - {ok, #state{hosts = Hosts, server_host = ServerHost, - access = Access, pep_mapping = PepMapping, - ignore_pep_from_offline = PepOffline, - last_item_cache = LastItemCache, - max_items_node = MaxItemsNode, nodetree = NodeTree, - plugins = Plugins, db_type = DBType}}. + {ok, #state{ + hosts = Hosts, + server_host = ServerHost, + access = Access, + pep_mapping = PepMapping, + ignore_pep_from_offline = PepOffline, + last_item_cache = LastItemCache, + max_items_node = MaxItemsNode, + nodetree = NodeTree, + plugins = Plugins, + db_type = DBType + }}. + depends(ServerHost, Opts) -> - [Host|_] = gen_mod:get_opt_hosts(Opts), + [Host | _] = gen_mod:get_opt_hosts(Opts), Plugins = mod_pubsub_opt:plugins(Opts), Db = mod_pubsub_opt:db_type(Opts), lists:flatmap( fun(Name) -> - Plugin = plugin(Db, Name), - try apply(Plugin, depends, [Host, ServerHost, Opts]) - catch _:undef -> [] - end - end, Plugins). + Plugin = plugin(Db, Name), + try + apply(Plugin, depends, [Host, ServerHost, Opts]) + catch + _:undef -> [] + end + end, + Plugins). + %% @doc Call the init/1 function for each plugin declared in the config file. %% The default plugin module is implicit. @@ -375,184 +502,257 @@ init_plugins(Host, ServerHost, Opts) -> Plugins = mod_pubsub_opt:plugins(Opts), PepMapping = mod_pubsub_opt:pep_mapping(Opts), PluginsOK = lists:foldl( - fun (Name, Acc) -> - Plugin = plugin(Host, Name), - apply(Plugin, init, [Host, ServerHost, Opts]), - [Name | Acc] - end, - [], Plugins), + fun(Name, Acc) -> + Plugin = plugin(Host, Name), + apply(Plugin, init, [Host, ServerHost, Opts]), + [Name | Acc] + end, + [], + Plugins), {lists:reverse(PluginsOK), TreePlugin, PepMapping}. + terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> lists:foreach( - fun (Name) -> - Plugin = plugin(Host, Name), - Plugin:terminate(Host, ServerHost) - end, - Plugins), + fun(Name) -> + Plugin = plugin(Host, Name), + Plugin:terminate(Host, ServerHost) + end, + Plugins), TreePlugin:terminate(Host, ServerHost), ok. + %% ------- %% disco hooks handling functions %% --spec disco_local_identity([identity()], jid(), jid(), - binary(), binary()) -> [identity()]. + +-spec disco_local_identity([identity()], + jid(), + jid(), + binary(), + binary()) -> [identity()]. disco_local_identity(Acc, _From, To, <<>>, _Lang) -> case lists:member(?PEPNODE, plugins(host(To#jid.lserver))) of - true -> - [#identity{category = <<"pubsub">>, type = <<"pep">>} | Acc]; - false -> - Acc + true -> + [#identity{category = <<"pubsub">>, type = <<"pep">>} | Acc]; + false -> + Acc end; disco_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec disco_local_features({error, stanza_error()} | {result, [binary()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [binary()]} | empty. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [binary()]} | empty. disco_local_features(Acc, _From, To, <<>>, _Lang) -> Host = host(To#jid.lserver), Feats = case Acc of - {result, I} -> I; - _ -> [] - end, - {result, Feats ++ [?NS_PUBSUB|[feature(F) || F <- features(Host, <<>>)]]}; + {result, I} -> I; + _ -> [] + end, + {result, Feats ++ [?NS_PUBSUB | [ feature(F) || F <- features(Host, <<>>) ]]}; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec disco_local_items({error, stanza_error()} | {result, [disco_item()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [disco_item()]} | empty. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [disco_item()]} | empty. disco_local_items(Acc, _From, _To, <<>>, _Lang) -> Acc; disco_local_items(Acc, _From, _To, _Node, _Lang) -> Acc. --spec disco_sm_identity([identity()], jid(), jid(), - binary(), binary()) -> [identity()]. + +-spec disco_sm_identity([identity()], + jid(), + jid(), + binary(), + binary()) -> [identity()]. disco_sm_identity(Acc, From, To, Node, _Lang) -> - disco_identity(jid:tolower(jid:remove_resource(To)), Node, From) - ++ Acc. + disco_identity(jid:tolower(jid:remove_resource(To)), Node, From) ++ + Acc. + -spec disco_identity(host(), binary(), jid()) -> [identity()]. disco_identity(_Host, <<>>, _From) -> [#identity{category = <<"pubsub">>, type = <<"pep">>}]; disco_identity(Host, Node, From) -> Action = - fun(#pubsub_node{id = Nidx, type = Type, - options = Options, owners = O}) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case get_allowed_items_call(Host, Nidx, From, Type, - Options, Owners) of - {result, _} -> - {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}, - #identity{category = <<"pubsub">>, type = <<"leaf">>, - name = get_option(Options, title, <<>>)}]}; - _ -> - {result, []} - end - end, + fun(#pubsub_node{ + id = Nidx, + type = Type, + options = Options, + owners = O + }) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, + Nidx, + From, + Type, + Options, + Owners) of + {result, _} -> + {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}, + #identity{ + category = <<"pubsub">>, + type = <<"leaf">>, + name = get_option(Options, title, <<>>) + }]}; + _ -> + {result, []} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] + {result, {_, Result}} -> Result; + _ -> [] end. + -spec disco_sm_features({error, stanza_error()} | {result, [binary()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [binary()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [binary()]}. disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures} = _Acc, From, To, Node, _Lang) -> {result, OtherFeatures ++ - disco_features(jid:tolower(jid:remove_resource(To)), Node, From)}; + disco_features(jid:tolower(jid:remove_resource(To)), Node, From)}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec disco_features(ljid(), binary(), jid()) -> [binary()]. disco_features(Host, <<>>, _From) -> - [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]; + [?NS_PUBSUB | [ feature(F) || F <- plugin_features(Host, <<"pep">>) ]]; disco_features(Host, Node, From) -> Action = - fun(#pubsub_node{id = Nidx, type = Type, - options = Options, owners = O}) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case get_allowed_items_call(Host, Nidx, From, - Type, Options, Owners) of - {result, _} -> - {result, - [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; - _ -> - {result, []} - end - end, + fun(#pubsub_node{ + id = Nidx, + type = Type, + options = Options, + owners = O + }) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, + Nidx, + From, + Type, + Options, + Owners) of + {result, _} -> + {result, + [?NS_PUBSUB | [ feature(F) || F <- plugin_features(Host, <<"pep">>) ]]}; + _ -> + {result, []} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] + {result, {_, Result}} -> Result; + _ -> [] end. + -spec disco_sm_items({error, stanza_error()} | {result, [disco_item()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [disco_item()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [disco_item()]}. disco_sm_items(empty, From, To, Node, Lang) -> disco_sm_items({result, []}, From, To, Node, Lang); disco_sm_items({result, OtherItems}, From, To, Node, _Lang) -> {result, lists:usort(OtherItems ++ - disco_items(jid:tolower(jid:remove_resource(To)), Node, From))}; + disco_items(jid:tolower(jid:remove_resource(To)), Node, From))}; disco_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec disco_items(ljid(), binary(), jid()) -> [disco_item()]. disco_items(Host, <<>>, From) -> MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), Action = - fun(#pubsub_node{nodeid = {_, Node}, options = Options, - type = Type, id = Nidx, owners = O}, Acc) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case get_allowed_items_call(Host, Nidx, From, - Type, Options, Owners) of - {result, _} -> - [#disco_item{node = Node, - jid = jid:make(Host), - name = get_option(Options, title, <<>>)} | Acc]; - _ -> - Acc - end - end, + fun(#pubsub_node{ + nodeid = {_, Node}, + options = Options, + type = Type, + id = Nidx, + owners = O + }, + Acc) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, + Nidx, + From, + Type, + Options, + Owners) of + {result, _} -> + [#disco_item{ + node = Node, + jid = jid:make(Host), + name = get_option(Options, title, <<>>) + } | Acc]; + _ -> + Acc + end + end, NodeBloc = fun() -> - case tree_call(Host, get_nodes, [Host, MaxNodes]) of - Nodes when is_list(Nodes) -> - {result, lists:foldl(Action, [], Nodes)}; - Error -> - Error - end - end, + case tree_call(Host, get_nodes, [Host, MaxNodes]) of + Nodes when is_list(Nodes) -> + {result, lists:foldl(Action, [], Nodes)}; + Error -> + Error + end + end, case transaction(Host, NodeBloc, sync_dirty) of - {result, Items} -> Items; - _ -> [] + {result, Items} -> Items; + _ -> [] end; disco_items(Host, Node, From) -> Action = - fun(#pubsub_node{id = Nidx, type = Type, - options = Options, owners = O}) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case get_allowed_items_call(Host, Nidx, From, - Type, Options, Owners) of - {result, Items} -> - {result, [#disco_item{jid = jid:make(Host), - name = ItemId} - || #pubsub_item{itemid = {ItemId, _}} <- Items]}; - _ -> - {result, []} - end - end, + fun(#pubsub_node{ + id = Nidx, + type = Type, + options = Options, + owners = O + }) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case get_allowed_items_call(Host, + Nidx, + From, + Type, + Options, + Owners) of + {result, Items} -> + {result, [ #disco_item{ + jid = jid:make(Host), + name = ItemId + } + || #pubsub_item{itemid = {ItemId, _}} <- Items ]}; + _ -> + {result, []} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> Result; - _ -> [] + {result, {_, Result}} -> Result; + _ -> [] end. + %% ------- %% presence and session hooks handling functions %% + -spec caps_add(jid(), jid(), [binary()]) -> ok. caps_add(JID, JID, Features) -> %% Send the owner his last PEP items. @@ -572,10 +772,12 @@ caps_add(#jid{lserver = S1} = From, #jid{lserver = S2} = To, Features) caps_add(_From, _To, _Features) -> ok. + -spec caps_update(jid(), jid(), [binary()]) -> ok. caps_update(From, To, Features) -> send_last_pep(To, From, Features). + -spec presence_probe(jid(), jid(), pid()) -> ok. presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) -> %% ignore presence_probe from my other resources @@ -586,9 +788,10 @@ presence_probe(_From, _To, _Pid) -> %% ignore presence_probe from remote contacts, those are handled via caps_add ok. --spec on_self_presence({presence(), ejabberd_c2s:state()}) - -> {presence(), ejabberd_c2s:state()}. -on_self_presence({_, #{pres_last := _}} = Acc) -> % Just a presence update. + +-spec on_self_presence({presence(), ejabberd_c2s:state()}) -> + {presence(), ejabberd_c2s:state()}. +on_self_presence({_, #{pres_last := _}} = Acc) -> % Just a presence update. Acc; on_self_presence({#presence{type = available}, #{jid := JID}} = Acc) -> send_last_items(JID), @@ -596,6 +799,7 @@ on_self_presence({#presence{type = available}, #{jid := JID}} = Acc) -> on_self_presence(Acc) -> Acc. + -spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state(). on_user_offline(#{jid := JID} = C2SState, _Reason) -> purge_offline(JID), @@ -603,20 +807,24 @@ on_user_offline(#{jid := JID} = C2SState, _Reason) -> on_user_offline(C2SState, _Reason) -> C2SState. + %% ------- %% subscription hooks handling functions %% + -spec out_subscription(presence()) -> any(). out_subscription(#presence{type = subscribed, from = From, to = To}) -> - if From#jid.lserver == To#jid.lserver -> - send_last_pep(jid:remove_resource(From), To, unknown); - true -> - ok + if + From#jid.lserver == To#jid.lserver -> + send_last_pep(jid:remove_resource(From), To, unknown); + true -> + ok end; out_subscription(_) -> ok. + -spec in_subscription(boolean(), presence()) -> true. in_subscription(_, #presence{to = To, from = Owner, type = unsubscribed}) -> unsubscribe_user(jid:remove_resource(To), Owner), @@ -624,56 +832,71 @@ in_subscription(_, #presence{to = To, from = Owner, type = unsubscribed}) -> in_subscription(_, _) -> true. + -spec unsubscribe_user(jid(), jid()) -> ok. unsubscribe_user(Entity, Owner) -> lists:foreach( fun(ServerHost) -> - unsubscribe_user(ServerHost, Entity, Owner) + unsubscribe_user(ServerHost, Entity, Owner) end, lists:usort( - lists:foldl( - fun(UserHost, Acc) -> - case gen_mod:is_loaded(UserHost, mod_pubsub) of - true -> [UserHost|Acc]; - false -> Acc - end - end, [], [Entity#jid.lserver, Owner#jid.lserver]))). + lists:foldl( + fun(UserHost, Acc) -> + case gen_mod:is_loaded(UserHost, mod_pubsub) of + true -> [UserHost | Acc]; + false -> Acc + end + end, + [], + [Entity#jid.lserver, Owner#jid.lserver]))). + -spec unsubscribe_user(binary(), jid(), jid()) -> ok. unsubscribe_user(Host, Entity, Owner) -> BJID = jid:tolower(jid:remove_resource(Owner)), lists:foreach( - fun (PType) -> - case node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]) of - {result, Subs} -> - lists:foreach( - fun({#pubsub_node{options = Options, - owners = O, - id = Nidx}, - subscribed, _, JID}) -> - Unsubscribe = match_option(Options, access_model, presence) - andalso lists:member(BJID, node_owners_action(Host, PType, Nidx, O)), - case Unsubscribe of - true -> - node_action(Host, PType, - unsubscribe_node, [Nidx, Entity, JID, all]); - false -> - ok - end; - (_) -> - ok - end, Subs); - _ -> - ok - end - end, plugins(Host)). + fun(PType) -> + case node_action(Host, + PType, + get_entity_subscriptions, + [Host, Entity]) of + {result, Subs} -> + lists:foreach( + fun({#pubsub_node{ + options = Options, + owners = O, + id = Nidx + }, + subscribed, + _, + JID}) -> + Unsubscribe = match_option(Options, access_model, presence) andalso + lists:member(BJID, node_owners_action(Host, PType, Nidx, O)), + case Unsubscribe of + true -> + node_action(Host, + PType, + unsubscribe_node, + [Nidx, Entity, JID, all]); + false -> + ok + end; + (_) -> + ok + end, + Subs); + _ -> + ok + end + end, + plugins(Host)). + %% ------- %% user remove hook handling function %% + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), @@ -683,50 +906,60 @@ remove_user(User, Server) -> HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>, lists:foreach( fun(PType) -> - case node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]) of - {result, Subs} -> - lists:foreach( - fun({#pubsub_node{id = Nidx}, _, _, JID}) -> - node_action(Host, PType, - unsubscribe_node, - [Nidx, Entity, JID, all]); - (_) -> - ok - end, Subs), - case node_action(Host, PType, - get_entity_affiliations, - [Host, Entity]) of - {result, Affs} -> - lists:foreach( - fun({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> - delete_node(H, N, Entity); - ({#pubsub_node{nodeid = {H, N}, type = Type}, owner}) - when N == HomeTreeBase, Type == <<"hometree">> -> - delete_node(H, N, Entity); - ({#pubsub_node{id = Nidx}, _}) -> - case node_action(Host, PType, - get_state, - [Nidx, jid:tolower(Entity)]) of - {result, #pubsub_state{items = ItemIds}} -> - node_action(Host, PType, - remove_extra_items, - [Nidx, 0, ItemIds]), - node_action(Host, PType, - set_affiliation, - [Nidx, Entity, none]); - _ -> - ok - end - end, Affs); - _ -> - ok - end; - _ -> - ok - end - end, plugins(Host)). + case node_action(Host, + PType, + get_entity_subscriptions, + [Host, Entity]) of + {result, Subs} -> + lists:foreach( + fun({#pubsub_node{id = Nidx}, _, _, JID}) -> + node_action(Host, + PType, + unsubscribe_node, + [Nidx, Entity, JID, all]); + (_) -> + ok + end, + Subs), + case node_action(Host, + PType, + get_entity_affiliations, + [Host, Entity]) of + {result, Affs} -> + lists:foreach( + fun({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> + delete_node(H, N, Entity); + ({#pubsub_node{nodeid = {H, N}, type = Type}, owner}) + when N == HomeTreeBase, Type == <<"hometree">> -> + delete_node(H, N, Entity); + ({#pubsub_node{id = Nidx}, _}) -> + case node_action(Host, + PType, + get_state, + [Nidx, jid:tolower(Entity)]) of + {result, #pubsub_state{items = ItemIds}} -> + node_action(Host, + PType, + remove_extra_items, + [Nidx, 0, ItemIds]), + node_action(Host, + PType, + set_affiliation, + [Nidx, Entity, none]); + _ -> + ok + end + end, + Affs); + _ -> + ok + end; + _ -> + ok + end + end, + plugins(Host)). + handle_call(server_host, _From, State) -> {reply, State#state.server_host, State}; @@ -742,77 +975,131 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({route, Packet}, State) -> - try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + try + route(Packet) + catch + ?EX_RULE(Class, Reason, St) -> + StackTrace = ?EX_STACK(St), + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, - #state{hosts = Hosts, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> + #state{hosts = Hosts, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> case lists:member(?PEPNODE, Plugins) of - true -> - ejabberd_hooks:delete(caps_add, ServerHost, - ?MODULE, caps_add, 80), - ejabberd_hooks:delete(caps_update, ServerHost, - ?MODULE, caps_update, 80), - ejabberd_hooks:delete(disco_sm_identity, ServerHost, - ?MODULE, disco_sm_identity, 75), - ejabberd_hooks:delete(disco_sm_features, ServerHost, - ?MODULE, disco_sm_features, 75), - ejabberd_hooks:delete(disco_sm_items, ServerHost, - ?MODULE, disco_sm_items, 75), - gen_iq_handler:remove_iq_handler(ejabberd_sm, - ServerHost, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_sm, - ServerHost, ?NS_PUBSUB_OWNER); - false -> - ok + true -> + ejabberd_hooks:delete(caps_add, + ServerHost, + ?MODULE, + caps_add, + 80), + ejabberd_hooks:delete(caps_update, + ServerHost, + ?MODULE, + caps_update, + 80), + ejabberd_hooks:delete(disco_sm_identity, + ServerHost, + ?MODULE, + disco_sm_identity, + 75), + ejabberd_hooks:delete(disco_sm_features, + ServerHost, + ?MODULE, + disco_sm_features, + 75), + ejabberd_hooks:delete(disco_sm_items, + ServerHost, + ?MODULE, + disco_sm_items, + 75), + gen_iq_handler:remove_iq_handler(ejabberd_sm, + ServerHost, + ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, + ServerHost, + ?NS_PUBSUB_OWNER); + false -> + ok end, - ejabberd_hooks:delete(c2s_self_presence, ServerHost, - ?MODULE, on_self_presence, 75), - ejabberd_hooks:delete(c2s_terminated, ServerHost, - ?MODULE, on_user_offline, 75), - ejabberd_hooks:delete(disco_local_identity, ServerHost, - ?MODULE, disco_local_identity, 75), - ejabberd_hooks:delete(disco_local_features, ServerHost, - ?MODULE, disco_local_features, 75), - ejabberd_hooks:delete(disco_local_items, ServerHost, - ?MODULE, disco_local_items, 75), - ejabberd_hooks:delete(presence_probe_hook, ServerHost, - ?MODULE, presence_probe, 80), - ejabberd_hooks:delete(roster_in_subscription, ServerHost, - ?MODULE, in_subscription, 50), - ejabberd_hooks:delete(roster_out_subscription, ServerHost, - ?MODULE, out_subscription, 50), - ejabberd_hooks:delete(remove_user, ServerHost, - ?MODULE, remove_user, 50), - ejabberd_hooks:delete(c2s_handle_info, ServerHost, - ?MODULE, c2s_handle_info, 50), + ejabberd_hooks:delete(c2s_self_presence, + ServerHost, + ?MODULE, + on_self_presence, + 75), + ejabberd_hooks:delete(c2s_terminated, + ServerHost, + ?MODULE, + on_user_offline, + 75), + ejabberd_hooks:delete(disco_local_identity, + ServerHost, + ?MODULE, + disco_local_identity, + 75), + ejabberd_hooks:delete(disco_local_features, + ServerHost, + ?MODULE, + disco_local_features, + 75), + ejabberd_hooks:delete(disco_local_items, + ServerHost, + ?MODULE, + disco_local_items, + 75), + ejabberd_hooks:delete(presence_probe_hook, + ServerHost, + ?MODULE, + presence_probe, + 80), + ejabberd_hooks:delete(roster_in_subscription, + ServerHost, + ?MODULE, + in_subscription, + 50), + ejabberd_hooks:delete(roster_out_subscription, + ServerHost, + ?MODULE, + out_subscription, + 50), + ejabberd_hooks:delete(remove_user, + ServerHost, + ?MODULE, + remove_user, + 50), + ejabberd_hooks:delete(c2s_handle_info, + ServerHost, + ?MODULE, + c2s_handle_info, + 50), lists:foreach( fun(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), - terminate_plugins(Host, ServerHost, Plugins, TreePlugin), - ejabberd_router:unregister_route(Host) - end, Hosts), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), + terminate_plugins(Host, ServerHost, Plugins, TreePlugin), + ejabberd_router:unregister_route(Host) + end, + Hosts), ejabberd_commands:unregister_commands(ServerHost, ?MODULE, get_commands_spec()). + %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed @@ -820,6 +1107,7 @@ terminate(_Reason, %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- @@ -827,57 +1115,71 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_info(#iq{from = From, to = To, lang = Lang, type = get, - sub_els = [#disco_info{node = Node}]} = IQ) -> +process_disco_info(#iq{ + from = From, + to = To, + lang = Lang, + type = get, + sub_els = [#disco_info{node = Node}] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, <<>>, <<>>]), + Info = ejabberd_hooks:run_fold(disco_info, + ServerHost, + [], + [ServerHost, ?MODULE, <<>>, <<>>]), case iq_disco_info(ServerHost, Host, Node, From, Lang) of - {result, IQRes} -> - XData = IQRes#disco_info.xdata ++ Info, - xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = XData}); - {error, Error} -> - xmpp:make_error(IQ, Error) + {result, IQRes} -> + XData = IQRes#disco_info.xdata ++ Info, + xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = XData}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_disco_items(#iq{type = get, from = From, to = To, - sub_els = [#disco_items{node = Node} = SubEl]} = IQ) -> +process_disco_items(#iq{ + type = get, + from = From, + to = To, + sub_els = [#disco_items{node = Node} = SubEl] + } = IQ) -> Host = To#jid.lserver, case iq_disco_items(Host, Node, From, SubEl#disco_items.rsm) of - {result, IQRes} -> - xmpp:make_iq_result(IQ, IQRes#disco_items{node = Node}); - {error, Error} -> - xmpp:make_error(IQ, Error) + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes#disco_items{node = Node}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -spec process_pubsub(iq()) -> iq(). process_pubsub(#iq{to = To} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Access = config(ServerHost, access), case iq_pubsub(Host, Access, IQ) of - {result, IQRes} -> - xmpp:make_iq_result(IQ, IQRes); - {error, Error} -> - xmpp:make_error(IQ, Error) + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -spec process_pubsub_owner(iq()) -> iq(). process_pubsub_owner(#iq{to = To} = IQ) -> Host = To#jid.lserver, case iq_pubsub_owner(Host, IQ) of - {result, IQRes} -> - xmpp:make_iq_result(IQ, IQRes); - {error, Error} -> - xmpp:make_error(IQ, Error) + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), @@ -886,554 +1188,697 @@ process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). + -spec process_commands(iq()) -> iq(). -process_commands(#iq{type = set, to = To, from = From, - sub_els = [#adhoc_command{} = Request]} = IQ) -> +process_commands(#iq{ + type = set, + to = To, + from = From, + sub_els = [#adhoc_command{} = Request] + } = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Plugins = config(ServerHost, plugins), Access = config(ServerHost, access), case adhoc_request(Host, ServerHost, From, Request, Access, Plugins) of - {error, Error} -> - xmpp:make_error(IQ, Error); - Response -> - xmpp:make_iq_result( - IQ, xmpp_util:make_adhoc_response(Request, Response)) + {error, Error} -> + xmpp:make_error(IQ, Error); + Response -> + xmpp:make_iq_result( + IQ, xmpp_util:make_adhoc_response(Request, Response)) end; process_commands(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). + -spec route(stanza()) -> ok. route(#iq{to = To} = IQ) when To#jid.lresource == <<"">> -> ejabberd_router:process_iq(IQ); route(Packet) -> To = xmpp:get_to(Packet), case To of - #jid{luser = <<>>, lresource = <<>>} -> - case Packet of - #message{type = T} when T /= error -> - case find_authorization_response(Packet) of - undefined -> - ok; - {error, Err} -> - ejabberd_router:route_error(Packet, Err); - AuthResponse -> - handle_authorization_response( - To#jid.lserver, Packet, AuthResponse) - end; - _ -> - Err = xmpp:err_service_unavailable(), - ejabberd_router:route_error(Packet, Err) - end; - _ -> - Err = xmpp:err_item_not_found(), - ejabberd_router:route_error(Packet, Err) + #jid{luser = <<>>, lresource = <<>>} -> + case Packet of + #message{type = T} when T /= error -> + case find_authorization_response(Packet) of + undefined -> + ok; + {error, Err} -> + ejabberd_router:route_error(Packet, Err); + AuthResponse -> + handle_authorization_response( + To#jid.lserver, Packet, AuthResponse) + end; + _ -> + Err = xmpp:err_service_unavailable(), + ejabberd_router:route_error(Packet, Err) + end; + _ -> + Err = xmpp:err_item_not_found(), + ejabberd_router:route_error(Packet, Err) end. + -spec command_disco_info(binary(), binary(), jid()) -> {result, disco_info()}. command_disco_info(_Host, ?NS_COMMANDS, _From) -> - {result, #disco_info{identities = [#identity{category = <<"automation">>, - type = <<"command-list">>}]}}; + {result, #disco_info{ + identities = [#identity{ + category = <<"automation">>, + type = <<"command-list">> + }] + }}; command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, _From) -> - {result, #disco_info{identities = [#identity{category = <<"automation">>, - type = <<"command-node">>}], - features = [?NS_COMMANDS]}}. + {result, #disco_info{ + identities = [#identity{ + category = <<"automation">>, + type = <<"command-node">> + }], + features = [?NS_COMMANDS] + }}. + -spec node_disco_info(binary(), binary(), jid()) -> {result, disco_info()} | - {error, stanza_error()}. + {error, stanza_error()}. node_disco_info(Host, Node, From) -> node_disco_info(Host, Node, From, true, true). + -spec node_disco_info(binary(), binary(), jid(), boolean(), boolean()) -> - {result, disco_info()} | {error, stanza_error()}. + {result, disco_info()} | {error, stanza_error()}. node_disco_info(Host, Node, _From, _Identity, _Features) -> Action = - fun(#pubsub_node{id = Nidx, type = Type, options = Options}) -> - NodeType = case get_option(Options, node_type) of - collection -> <<"collection">>; - _ -> <<"leaf">> - end, - Affs = case node_call(Host, Type, get_node_affiliations, [Nidx]) of - {result, As} -> As; - _ -> [] - end, - Subs = case node_call(Host, Type, get_node_subscriptions, [Nidx]) of - {result, Ss} -> Ss; - _ -> [] - end, - Meta = [{title, get_option(Options, title, <<>>)}, - {type, get_option(Options, type, <<>>)}, - {description, get_option(Options, description, <<>>)}, - {owner, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= owner]}, - {publisher, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= publisher]}, - {access_model, get_option(Options, access_model, open)}, + fun(#pubsub_node{id = Nidx, type = Type, options = Options}) -> + NodeType = case get_option(Options, node_type) of + collection -> <<"collection">>; + _ -> <<"leaf">> + end, + Affs = case node_call(Host, Type, get_node_affiliations, [Nidx]) of + {result, As} -> As; + _ -> [] + end, + Subs = case node_call(Host, Type, get_node_subscriptions, [Nidx]) of + {result, Ss} -> Ss; + _ -> [] + end, + Meta = [{title, get_option(Options, title, <<>>)}, + {type, get_option(Options, type, <<>>)}, + {description, get_option(Options, description, <<>>)}, + {owner, [ jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= owner ]}, + {publisher, [ jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= publisher ]}, + {access_model, get_option(Options, access_model, open)}, {publish_model, get_option(Options, publish_model, publishers)}, - {num_subscribers, length(Subs)}], - XData = #xdata{type = result, - fields = pubsub_meta_data:encode(Meta)}, - Is = [#identity{category = <<"pubsub">>, type = NodeType}], - Fs = [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, Type)]], - {result, #disco_info{identities = Is, features = Fs, xdata = [XData]}} - end, + {num_subscribers, length(Subs)}], + XData = #xdata{ + type = result, + fields = pubsub_meta_data:encode(Meta) + }, + Is = [#identity{category = <<"pubsub">>, type = NodeType}], + Fs = [?NS_PUBSUB | [ feature(F) || F <- plugin_features(Host, Type) ]], + {result, #disco_info{identities = Is, features = Fs, xdata = [XData]}} + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other + {result, {_, Result}} -> {result, Result}; + Other -> Other end. --spec iq_disco_info(binary(), binary(), binary(), jid(), binary()) - -> {result, disco_info()} | {error, stanza_error()}. + +-spec iq_disco_info(binary(), binary(), binary(), jid(), binary()) -> + {result, disco_info()} | {error, stanza_error()}. iq_disco_info(ServerHost, Host, SNode, From, Lang) -> [Node | _] = case SNode of - <<>> -> [<<>>]; - _ -> str:tokens(SNode, <<"!">>) - end, + <<>> -> [<<>>]; + _ -> str:tokens(SNode, <<"!">>) + end, case Node of - <<>> -> - Name = mod_pubsub_opt:name(ServerHost), - {result, - #disco_info{ - identities = [#identity{ - category = <<"pubsub">>, - type = <<"service">>, - name = translate:translate(Lang, Name)}], - features = [?NS_DISCO_INFO, - ?NS_DISCO_ITEMS, - ?NS_PUBSUB, - ?NS_COMMANDS, - ?NS_VCARD | - [feature(F) || F <- features(Host, Node)]]}}; - ?NS_COMMANDS -> - command_disco_info(Host, Node, From); - ?NS_PUBSUB_GET_PENDING -> - command_disco_info(Host, Node, From); - _ -> - node_disco_info(Host, Node, From) + <<>> -> + Name = mod_pubsub_opt:name(ServerHost), + {result, + #disco_info{ + identities = [#identity{ + category = <<"pubsub">>, + type = <<"service">>, + name = translate:translate(Lang, Name) + }], + features = [?NS_DISCO_INFO, + ?NS_DISCO_ITEMS, + ?NS_PUBSUB, + ?NS_COMMANDS, + ?NS_VCARD | [ feature(F) || F <- features(Host, Node) ]] + }}; + ?NS_COMMANDS -> + command_disco_info(Host, Node, From); + ?NS_PUBSUB_GET_PENDING -> + command_disco_info(Host, Node, From); + _ -> + node_disco_info(Host, Node, From) end. + -spec iq_disco_items(host(), binary(), jid(), undefined | rsm_set()) -> - {result, disco_items()} | {error, stanza_error()}. + {result, disco_items()} | {error, stanza_error()}. iq_disco_items(Host, <<>>, _From, _RSM) -> MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), case tree_action(Host, get_subnodes, [Host, <<>>, MaxNodes]) of - {error, #stanza_error{}} = Err -> - Err; - Nodes when is_list(Nodes) -> - Items = - lists:map( - fun(#pubsub_node{nodeid = {_, SubNode}, options = Options}) -> - case get_option(Options, title) of - false -> - #disco_item{jid = jid:make(Host), - node = SubNode}; - Title -> - #disco_item{jid = jid:make(Host), - name = Title, - node = SubNode} - end - end, Nodes), - {result, #disco_items{items = Items}} + {error, #stanza_error{}} = Err -> + Err; + Nodes when is_list(Nodes) -> + Items = + lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}, options = Options}) -> + case get_option(Options, title) of + false -> + #disco_item{ + jid = jid:make(Host), + node = SubNode + }; + Title -> + #disco_item{ + jid = jid:make(Host), + name = Title, + node = SubNode + } + end + end, + Nodes), + {result, #disco_items{items = Items}} end; iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) -> {result, - #disco_items{items = [#disco_item{jid = jid:make(Host), - node = ?NS_PUBSUB_GET_PENDING, - name = ?T("Get Pending")}]}}; + #disco_items{ + items = [#disco_item{ + jid = jid:make(Host), + node = ?NS_PUBSUB_GET_PENDING, + name = ?T("Get Pending") + }] + }}; iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) -> {result, #disco_items{}}; iq_disco_items(Host, Item, From, RSM) -> case str:tokens(Item, <<"!">>) of - [_Node, _ItemId] -> - {result, #disco_items{}}; - [Node] -> - MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), - Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> - Owners = node_owners_call(Host, Type, Nidx, O), - {NodeItems, RsmOut} = case get_allowed_items_call( - Host, Nidx, From, Type, Options, Owners, RSM) of - {result, R} -> R; - _ -> {[], undefined} - end, - case tree_call(Host, get_subnodes, [Host, Node, MaxNodes]) of - SubNodes when is_list(SubNodes) -> - Nodes = lists:map( - fun(#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) -> - case get_option(SubOptions, title) of - false -> - #disco_item{jid = jid:make(Host), - node = SubNode}; - Title -> - #disco_item{jid = jid:make(Host), - name = Title, - node = SubNode} - end - end, SubNodes), - Items = lists:flatmap( - fun(#pubsub_item{itemid = {RN, _}}) -> - case node_call(Host, Type, get_item_name, [Host, Node, RN]) of - {result, Name} -> - [#disco_item{jid = jid:make(Host), name = Name}]; - _ -> - [] - end - end, NodeItems), - {result, #disco_items{items = Nodes ++ Items, - rsm = RsmOut}}; - Error -> - Error - end - end, - case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other - end + [_Node, _ItemId] -> + {result, #disco_items{}}; + [Node] -> + MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), + Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + {NodeItems, RsmOut} = case get_allowed_items_call( + Host, Nidx, From, Type, Options, Owners, RSM) of + {result, R} -> R; + _ -> {[], undefined} + end, + case tree_call(Host, get_subnodes, [Host, Node, MaxNodes]) of + SubNodes when is_list(SubNodes) -> + Nodes = lists:map( + fun(#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) -> + case get_option(SubOptions, title) of + false -> + #disco_item{ + jid = jid:make(Host), + node = SubNode + }; + Title -> + #disco_item{ + jid = jid:make(Host), + name = Title, + node = SubNode + } + end + end, + SubNodes), + Items = lists:flatmap( + fun(#pubsub_item{itemid = {RN, _}}) -> + case node_call(Host, Type, get_item_name, [Host, Node, RN]) of + {result, Name} -> + [#disco_item{jid = jid:make(Host), name = Name}]; + _ -> + [] + end + end, + NodeItems), + {result, #disco_items{ + items = Nodes ++ Items, + rsm = RsmOut + }}; + Error -> + Error + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_, Result}} -> {result, Result}; + Other -> Other + end end. + -spec iq_sm(iq()) -> iq(). iq_sm(#iq{to = To, sub_els = [SubEl]} = IQ) -> LOwner = jid:tolower(jid:remove_resource(To)), Res = case xmpp:get_ns(SubEl) of - ?NS_PUBSUB -> - iq_pubsub(LOwner, all, IQ); - ?NS_PUBSUB_OWNER -> - iq_pubsub_owner(LOwner, IQ) - end, + ?NS_PUBSUB -> + iq_pubsub(LOwner, all, IQ); + ?NS_PUBSUB_OWNER -> + iq_pubsub_owner(LOwner, IQ) + end, case Res of - {result, IQRes} -> - xmpp:make_iq_result(IQ, IQRes); - {error, Error} -> - xmpp:make_error(IQ, Error) + {result, IQRes} -> + xmpp:make_iq_result(IQ, IQRes); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -spec iq_get_vcard(binary(), binary()) -> vcard_temp(). iq_get_vcard(ServerHost, Lang) -> case mod_pubsub_opt:vcard(ServerHost) of - undefined -> - Desc = misc:get_descr(Lang, ?T("ejabberd Publish-Subscribe module")), - #vcard_temp{fn = <<"ejabberd/mod_pubsub">>, - url = ejabberd_config:get_uri(), - desc = Desc}; - VCard -> - VCard + undefined -> + Desc = misc:get_descr(Lang, ?T("ejabberd Publish-Subscribe module")), + #vcard_temp{ + fn = <<"ejabberd/mod_pubsub">>, + url = ejabberd_config:get_uri(), + desc = Desc + }; + VCard -> + VCard end. + -spec iq_pubsub(binary() | ljid(), atom(), iq()) -> - {result, pubsub()} | {error, stanza_error()}. -iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang, - sub_els = [SubEl]}) -> + {result, pubsub()} | {error, stanza_error()}. +iq_pubsub(Host, + Access, + #iq{ + from = From, + type = IQType, + lang = Lang, + sub_els = [SubEl] + }) -> case {IQType, SubEl} of - {set, #pubsub{create = Node, configure = Configure, - _ = undefined}} when is_binary(Node) -> - ServerHost = serverhost(Host), - Plugins = config(ServerHost, plugins), - Config = case Configure of - {_, XData} -> decode_node_config(XData, Host, Lang); - undefined -> [] - end, - Type = hd(Plugins), - case Config of - {error, _} = Err -> - Err; - _ -> - create_node(Host, ServerHost, Node, From, Type, Access, Config) - end; - {set, #pubsub{publish = #ps_publish{node = Node, items = Items}, - publish_options = XData, configure = _, _ = undefined}} -> - ServerHost = serverhost(Host), - case Items of - [#ps_item{id = ItemId, sub_els = Payload}] -> - case decode_publish_options(XData, Lang) of - {error, _} = Err -> - Err; - PubOpts -> - publish_item(Host, ServerHost, Node, From, ItemId, - Payload, PubOpts, Access) - end; - [] -> - publish_item(Host, ServerHost, Node, From, <<>>, [], [], Access); - _ -> - {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} - end; - {set, #pubsub{retract = #ps_retract{node = Node, notify = Notify, items = Items}, - _ = undefined}} -> - case Items of - [#ps_item{id = ItemId}] -> - if ItemId /= <<>> -> - delete_item(Host, Node, From, ItemId, Notify); - true -> - {error, extended_error(xmpp:err_bad_request(), - err_item_required())} - end; - [] -> - {error, extended_error(xmpp:err_bad_request(), err_item_required())}; - _ -> - {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} - end; - {set, #pubsub{subscribe = #ps_subscribe{node = Node, jid = JID}, - options = Options, _ = undefined}} -> - Config = case Options of - #ps_options{xdata = XData, jid = undefined, node = <<>>} -> - decode_subscribe_options(XData, Lang); - #ps_options{xdata = _XData, jid = #jid{}} -> - Txt = ?T("Attribute 'jid' is not allowed here"), - {error, xmpp:err_bad_request(Txt, Lang)}; - #ps_options{xdata = _XData} -> - Txt = ?T("Attribute 'node' is not allowed here"), - {error, xmpp:err_bad_request(Txt, Lang)}; - _ -> - [] - end, - case Config of - {error, _} = Err -> - Err; - _ -> - subscribe_node(Host, Node, From, JID, Config) - end; - {set, #pubsub{unsubscribe = #ps_unsubscribe{node = Node, jid = JID, subid = SubId}, - _ = undefined}} -> - unsubscribe_node(Host, Node, From, JID, SubId); - {get, #pubsub{items = #ps_items{node = Node, - max_items = MaxItems, - subid = SubId, - items = Items}, - rsm = RSM, _ = undefined}} -> - ItemIds = [ItemId || #ps_item{id = ItemId} <- Items, ItemId /= <<>>], - get_items(Host, Node, From, SubId, MaxItems, ItemIds, RSM); - {get, #pubsub{subscriptions = {Node, _}, _ = undefined}} -> - Plugins = config(serverhost(Host), plugins), - get_subscriptions(Host, Node, From, Plugins); - {get, #pubsub{affiliations = {Node, _}, _ = undefined}} -> - Plugins = config(serverhost(Host), plugins), - get_affiliations(Host, Node, From, Plugins); - {_, #pubsub{options = #ps_options{jid = undefined}, _ = undefined}} -> - {error, extended_error(xmpp:err_bad_request(), err_jid_required())}; - {_, #pubsub{options = #ps_options{node = <<>>}, _ = undefined}} -> - {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; - {get, #pubsub{options = #ps_options{node = Node, subid = SubId, jid = JID}, - _ = undefined}} -> - get_options(Host, Node, JID, SubId, Lang); - {set, #pubsub{options = #ps_options{node = Node, subid = SubId, - jid = JID, xdata = XData}, - _ = undefined}} -> - case decode_subscribe_options(XData, Lang) of - {error, _} = Err -> - Err; - Config -> - set_options(Host, Node, JID, SubId, Config) - end; - {set, #pubsub{}} -> - {error, xmpp:err_bad_request()}; - _ -> - {error, xmpp:err_feature_not_implemented()} + {set, #pubsub{ + create = Node, + configure = Configure, + _ = undefined + }} when is_binary(Node) -> + ServerHost = serverhost(Host), + Plugins = config(ServerHost, plugins), + Config = case Configure of + {_, XData} -> decode_node_config(XData, Host, Lang); + undefined -> [] + end, + Type = hd(Plugins), + case Config of + {error, _} = Err -> + Err; + _ -> + create_node(Host, ServerHost, Node, From, Type, Access, Config) + end; + {set, #pubsub{ + publish = #ps_publish{node = Node, items = Items}, + publish_options = XData, + configure = _, + _ = undefined + }} -> + ServerHost = serverhost(Host), + case Items of + [#ps_item{id = ItemId, sub_els = Payload}] -> + case decode_publish_options(XData, Lang) of + {error, _} = Err -> + Err; + PubOpts -> + publish_item(Host, + ServerHost, + Node, + From, + ItemId, + Payload, + PubOpts, + Access) + end; + [] -> + publish_item(Host, ServerHost, Node, From, <<>>, [], [], Access); + _ -> + {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} + end; + {set, #pubsub{ + retract = #ps_retract{node = Node, notify = Notify, items = Items}, + _ = undefined + }} -> + case Items of + [#ps_item{id = ItemId}] -> + if + ItemId /= <<>> -> + delete_item(Host, Node, From, ItemId, Notify); + true -> + {error, extended_error(xmpp:err_bad_request(), + err_item_required())} + end; + [] -> + {error, extended_error(xmpp:err_bad_request(), err_item_required())}; + _ -> + {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} + end; + {set, #pubsub{ + subscribe = #ps_subscribe{node = Node, jid = JID}, + options = Options, + _ = undefined + }} -> + Config = case Options of + #ps_options{xdata = XData, jid = undefined, node = <<>>} -> + decode_subscribe_options(XData, Lang); + #ps_options{xdata = _XData, jid = #jid{}} -> + Txt = ?T("Attribute 'jid' is not allowed here"), + {error, xmpp:err_bad_request(Txt, Lang)}; + #ps_options{xdata = _XData} -> + Txt = ?T("Attribute 'node' is not allowed here"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ -> + [] + end, + case Config of + {error, _} = Err -> + Err; + _ -> + subscribe_node(Host, Node, From, JID, Config) + end; + {set, #pubsub{ + unsubscribe = #ps_unsubscribe{node = Node, jid = JID, subid = SubId}, + _ = undefined + }} -> + unsubscribe_node(Host, Node, From, JID, SubId); + {get, #pubsub{ + items = #ps_items{ + node = Node, + max_items = MaxItems, + subid = SubId, + items = Items + }, + rsm = RSM, + _ = undefined + }} -> + ItemIds = [ ItemId || #ps_item{id = ItemId} <- Items, ItemId /= <<>> ], + get_items(Host, Node, From, SubId, MaxItems, ItemIds, RSM); + {get, #pubsub{subscriptions = {Node, _}, _ = undefined}} -> + Plugins = config(serverhost(Host), plugins), + get_subscriptions(Host, Node, From, Plugins); + {get, #pubsub{affiliations = {Node, _}, _ = undefined}} -> + Plugins = config(serverhost(Host), plugins), + get_affiliations(Host, Node, From, Plugins); + {_, #pubsub{options = #ps_options{jid = undefined}, _ = undefined}} -> + {error, extended_error(xmpp:err_bad_request(), err_jid_required())}; + {_, #pubsub{options = #ps_options{node = <<>>}, _ = undefined}} -> + {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; + {get, #pubsub{ + options = #ps_options{node = Node, subid = SubId, jid = JID}, + _ = undefined + }} -> + get_options(Host, Node, JID, SubId, Lang); + {set, #pubsub{ + options = #ps_options{ + node = Node, + subid = SubId, + jid = JID, + xdata = XData + }, + _ = undefined + }} -> + case decode_subscribe_options(XData, Lang) of + {error, _} = Err -> + Err; + Config -> + set_options(Host, Node, JID, SubId, Config) + end; + {set, #pubsub{}} -> + {error, xmpp:err_bad_request()}; + _ -> + {error, xmpp:err_feature_not_implemented()} end. + -spec iq_pubsub_owner(binary() | ljid(), iq()) -> {result, pubsub_owner() | undefined} | - {error, stanza_error()}. -iq_pubsub_owner(Host, #iq{type = IQType, from = From, - lang = Lang, sub_els = [SubEl]}) -> + {error, stanza_error()}. +iq_pubsub_owner(Host, + #iq{ + type = IQType, + from = From, + lang = Lang, + sub_els = [SubEl] + }) -> case {IQType, SubEl} of - {get, #pubsub_owner{configure = {Node, undefined}, _ = undefined}} -> - ServerHost = serverhost(Host), - get_configure(Host, ServerHost, Node, From, Lang); - {set, #pubsub_owner{configure = {Node, XData}, _ = undefined}} -> - case XData of - undefined -> - {error, xmpp:err_bad_request(?T("No data form found"), Lang)}; - #xdata{type = cancel} -> - {result, #pubsub_owner{}}; - #xdata{type = submit} -> - case decode_node_config(XData, Host, Lang) of - {error, _} = Err -> - Err; - Config -> - set_configure(Host, Node, From, Config, Lang) - end; - #xdata{} -> - {error, xmpp:err_bad_request(?T("Incorrect data form"), Lang)} - end; - {get, #pubsub_owner{default = {Node, undefined}, _ = undefined}} -> - get_default(Host, Node, From, Lang); - {set, #pubsub_owner{delete = {Node, _}, _ = undefined}} -> - delete_node(Host, Node, From); - {set, #pubsub_owner{purge = Node, _ = undefined}} when Node /= undefined -> - purge_node(Host, Node, From); - {get, #pubsub_owner{subscriptions = {Node, []}, _ = undefined}} -> - get_subscriptions(Host, Node, From); - {set, #pubsub_owner{subscriptions = {Node, Subs}, _ = undefined}} -> - set_subscriptions(Host, Node, From, Subs); - {get, #pubsub_owner{affiliations = {Node, []}, _ = undefined}} -> - get_affiliations(Host, Node, From); - {set, #pubsub_owner{affiliations = {Node, Affs}, _ = undefined}} -> - set_affiliations(Host, Node, From, Affs); - {_, #pubsub_owner{}} -> - {error, xmpp:err_bad_request()}; - _ -> - {error, xmpp:err_feature_not_implemented()} + {get, #pubsub_owner{configure = {Node, undefined}, _ = undefined}} -> + ServerHost = serverhost(Host), + get_configure(Host, ServerHost, Node, From, Lang); + {set, #pubsub_owner{configure = {Node, XData}, _ = undefined}} -> + case XData of + undefined -> + {error, xmpp:err_bad_request(?T("No data form found"), Lang)}; + #xdata{type = cancel} -> + {result, #pubsub_owner{}}; + #xdata{type = submit} -> + case decode_node_config(XData, Host, Lang) of + {error, _} = Err -> + Err; + Config -> + set_configure(Host, Node, From, Config, Lang) + end; + #xdata{} -> + {error, xmpp:err_bad_request(?T("Incorrect data form"), Lang)} + end; + {get, #pubsub_owner{default = {Node, undefined}, _ = undefined}} -> + get_default(Host, Node, From, Lang); + {set, #pubsub_owner{delete = {Node, _}, _ = undefined}} -> + delete_node(Host, Node, From); + {set, #pubsub_owner{purge = Node, _ = undefined}} when Node /= undefined -> + purge_node(Host, Node, From); + {get, #pubsub_owner{subscriptions = {Node, []}, _ = undefined}} -> + get_subscriptions(Host, Node, From); + {set, #pubsub_owner{subscriptions = {Node, Subs}, _ = undefined}} -> + set_subscriptions(Host, Node, From, Subs); + {get, #pubsub_owner{affiliations = {Node, []}, _ = undefined}} -> + get_affiliations(Host, Node, From); + {set, #pubsub_owner{affiliations = {Node, Affs}, _ = undefined}} -> + set_affiliations(Host, Node, From, Affs); + {_, #pubsub_owner{}} -> + {error, xmpp:err_bad_request()}; + _ -> + {error, xmpp:err_feature_not_implemented()} end. --spec adhoc_request(binary(), binary(), jid(), adhoc_command(), - atom(), [binary()]) -> adhoc_command() | {error, stanza_error()}. -adhoc_request(Host, _ServerHost, Owner, - #adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang, - action = execute, xdata = undefined}, - _Access, Plugins) -> + +-spec adhoc_request(binary(), + binary(), + jid(), + adhoc_command(), + atom(), + [binary()]) -> adhoc_command() | {error, stanza_error()}. +adhoc_request(Host, + _ServerHost, + Owner, + #adhoc_command{ + node = ?NS_PUBSUB_GET_PENDING, + lang = Lang, + action = execute, + xdata = undefined + }, + _Access, + Plugins) -> send_pending_node_form(Host, Owner, Lang, Plugins); -adhoc_request(Host, _ServerHost, Owner, - #adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang, - action = execute, xdata = #xdata{} = XData} = Request, - _Access, _Plugins) -> +adhoc_request(Host, + _ServerHost, + Owner, + #adhoc_command{ + node = ?NS_PUBSUB_GET_PENDING, + lang = Lang, + action = execute, + xdata = #xdata{} = XData + } = Request, + _Access, + _Plugins) -> case decode_get_pending(XData, Lang) of - {error, _} = Err -> - Err; - Config -> - Node = proplists:get_value(node, Config), - case send_pending_auth_events(Host, Node, Owner, Lang) of - ok -> - xmpp_util:make_adhoc_response( - Request, #adhoc_command{status = completed}); - Err -> - Err - end + {error, _} = Err -> + Err; + Config -> + Node = proplists:get_value(node, Config), + case send_pending_auth_events(Host, Node, Owner, Lang) of + ok -> + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = completed}); + Err -> + Err + end end; -adhoc_request(_Host, _ServerHost, _Owner, - #adhoc_command{action = cancel}, _Access, _Plugins) -> +adhoc_request(_Host, + _ServerHost, + _Owner, + #adhoc_command{action = cancel}, + _Access, + _Plugins) -> #adhoc_command{status = canceled}; adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) -> ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), {error, xmpp:err_item_not_found()}. --spec send_pending_node_form(binary(), jid(), binary(), - [binary()]) -> adhoc_command() | {error, stanza_error()}. + +-spec send_pending_node_form(binary(), + jid(), + binary(), + [binary()]) -> adhoc_command() | {error, stanza_error()}. send_pending_node_form(Host, Owner, Lang, Plugins) -> - Filter = fun (Type) -> - lists:member(<<"get-pending">>, plugin_features(Host, Type)) - end, + Filter = fun(Type) -> + lists:member(<<"get-pending">>, plugin_features(Host, Type)) + end, case lists:filter(Filter, Plugins) of - [] -> - Err = extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('get-pending')), - {error, Err}; - Ps -> - case get_pending_nodes(Host, Owner, Ps) of - {ok, Nodes} -> - Form = [{node, <<>>, lists:zip(Nodes, Nodes)}], - XForm = #xdata{type = form, - fields = pubsub_get_pending:encode(Form, Lang)}, - #adhoc_command{status = executing, action = execute, - xdata = XForm}; - Err -> - Err - end + [] -> + Err = extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('get-pending')), + {error, Err}; + Ps -> + case get_pending_nodes(Host, Owner, Ps) of + {ok, Nodes} -> + Form = [{node, <<>>, lists:zip(Nodes, Nodes)}], + XForm = #xdata{ + type = form, + fields = pubsub_get_pending:encode(Form, Lang) + }, + #adhoc_command{ + status = executing, + action = execute, + xdata = XForm + }; + Err -> + Err + end end. + -spec get_pending_nodes(binary(), jid(), [binary()]) -> {ok, [binary()]} | - {error, stanza_error()}. + {error, stanza_error()}. get_pending_nodes(Host, Owner, Plugins) -> - Tr = fun (Type) -> - case node_call(Host, Type, get_pending_nodes, [Host, Owner]) of - {result, Nodes} -> Nodes; - _ -> [] - end - end, + Tr = fun(Type) -> + case node_call(Host, Type, get_pending_nodes, [Host, Owner]) of + {result, Nodes} -> Nodes; + _ -> [] + end + end, Action = fun() -> {result, lists:flatmap(Tr, Plugins)} end, case transaction(Host, Action, sync_dirty) of - {result, Res} -> {ok, Res}; - Err -> Err + {result, Res} -> {ok, Res}; + Err -> Err end. + %% @doc

Send a subscription approval form to Owner for all pending %% subscriptions on Host and Node.

--spec send_pending_auth_events(binary(), binary(), jid(), - binary()) -> ok | {error, stanza_error()}. +-spec send_pending_auth_events(binary(), + binary(), + jid(), + binary()) -> ok | {error, stanza_error()}. send_pending_auth_events(Host, Node, Owner, Lang) -> ?DEBUG("Sending pending auth events for ~ts on ~ts:~ts", - [jid:encode(Owner), Host, Node]), + [jid:encode(Owner), Host, Node]), Action = - fun(#pubsub_node{id = Nidx, type = Type}) -> - case lists:member(<<"get-pending">>, plugin_features(Host, Type)) of - true -> - case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of - {result, owner} -> - node_call(Host, Type, get_node_subscriptions, [Nidx]); - _ -> - {error, xmpp:err_forbidden( - ?T("Owner privileges required"), Lang)} - end; - false -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('get-pending'))} - end - end, + fun(#pubsub_node{id = Nidx, type = Type}) -> + case lists:member(<<"get-pending">>, plugin_features(Host, Type)) of + true -> + case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of + {result, owner} -> + node_call(Host, Type, get_node_subscriptions, [Nidx]); + _ -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), Lang)} + end; + false -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('get-pending'))} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {N, Subs}} -> - lists:foreach( - fun({J, pending, _SubId}) -> send_authorization_request(N, jid:make(J)); - ({J, pending}) -> send_authorization_request(N, jid:make(J)); - (_) -> ok - end, Subs); - Err -> - Err + {result, {N, Subs}} -> + lists:foreach( + fun({J, pending, _SubId}) -> send_authorization_request(N, jid:make(J)); + ({J, pending}) -> send_authorization_request(N, jid:make(J)); + (_) -> ok + end, + Subs); + Err -> + Err end. + %%% authorization handling -spec send_authorization_request(#pubsub_node{}, jid()) -> ok. -send_authorization_request(#pubsub_node{nodeid = {Host, Node}, - type = Type, id = Nidx, owners = O}, - Subscriber) -> +send_authorization_request(#pubsub_node{ + nodeid = {Host, Node}, + type = Type, + id = Nidx, + owners = O + }, + Subscriber) -> %% TODO: pass lang to this function Lang = <<"en">>, Fs = pubsub_subscribe_authorization:encode( - [{node, Node}, - {subscriber_jid, Subscriber}, - {allow, false}], - Lang), - X = #xdata{type = form, - title = translate:translate( - Lang, ?T("PubSub subscriber request")), - instructions = [translate:translate( - Lang, - ?T("Choose whether to approve this entity's " - "subscription."))], - fields = Fs}, + [{node, Node}, + {subscriber_jid, Subscriber}, + {allow, false}], + Lang), + X = #xdata{ + type = form, + title = translate:translate( + Lang, ?T("PubSub subscriber request")), + instructions = [translate:translate( + Lang, + ?T("Choose whether to approve this entity's " + "subscription."))], + fields = Fs + }, Stanza = #message{from = service_jid(Host), sub_els = [X]}, lists:foreach( - fun (Owner) -> - ejabberd_router:route(xmpp:set_to(Stanza, jid:make(Owner))) - end, node_owners_action(Host, Type, Nidx, O)). + fun(Owner) -> + ejabberd_router:route(xmpp:set_to(Stanza, jid:make(Owner))) + end, + node_owners_action(Host, Type, Nidx, O)). + -spec find_authorization_response(message()) -> undefined | - pubsub_subscribe_authorization:result() | - {error, stanza_error()}. + pubsub_subscribe_authorization:result() | + {error, stanza_error()}. find_authorization_response(Packet) -> case xmpp:get_subtag(Packet, #xdata{type = form}) of - #xdata{type = cancel} -> - undefined; - #xdata{type = submit, fields = Fs} -> - try pubsub_subscribe_authorization:decode(Fs) of - Result -> Result - catch _:{pubsub_subscribe_authorization, Why} -> - Lang = xmpp:get_lang(Packet), - Txt = pubsub_subscribe_authorization:format_error(Why), - {error, xmpp:err_bad_request(Txt, Lang)} - end; - #xdata{} -> - {error, xmpp:err_bad_request()}; - false -> - undefined + #xdata{type = cancel} -> + undefined; + #xdata{type = submit, fields = Fs} -> + try pubsub_subscribe_authorization:decode(Fs) of + Result -> Result + catch + _:{pubsub_subscribe_authorization, Why} -> + Lang = xmpp:get_lang(Packet), + Txt = pubsub_subscribe_authorization:format_error(Why), + {error, xmpp:err_bad_request(Txt, Lang)} + end; + #xdata{} -> + {error, xmpp:err_bad_request()}; + false -> + undefined end. + %% @doc Send a message to JID with the supplied Subscription -spec send_authorization_approval(binary(), jid(), binary(), subscribed | none) -> ok. send_authorization_approval(Host, JID, SNode, Subscription) -> - Event = #ps_event{subscription = - #ps_subscription{jid = JID, - node = SNode, - type = Subscription}}, + Event = #ps_event{ + subscription = + #ps_subscription{ + jid = JID, + node = SNode, + type = Subscription + } + }, Stanza = #message{from = service_jid(Host), to = JID, sub_els = [Event]}, ejabberd_router:route(Stanza). --spec handle_authorization_response(binary(), message(), - pubsub_subscribe_authorization:result()) -> ok. + +-spec handle_authorization_response(binary(), + message(), + pubsub_subscribe_authorization:result()) -> ok. handle_authorization_response(Host, #message{from = From} = Packet, Response) -> Node = proplists:get_value(node, Response), Subscriber = proplists:get_value(subscriber_jid, Response), @@ -1441,50 +1886,51 @@ handle_authorization_response(Host, #message{from = From} = Packet, Response) -> Lang = xmpp:get_lang(Packet), FromLJID = jid:tolower(jid:remove_resource(From)), Action = - fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case lists:member(FromLJID, Owners) of - true -> - case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of - {result, Subs} -> - update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs); - {error, _} = Err -> - Err - end; - false -> - {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)} - end - end, + fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case lists:member(FromLJID, Owners) of + true -> + case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of + {result, Subs} -> + update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs); + {error, _} = Err -> + Err + end; + false -> + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {error, Error} -> - ejabberd_router:route_error(Packet, Error); - {result, {_, _NewSubscription}} -> - %% XXX: notify about subscription state change, section 12.11 - ok + {error, Error} -> + ejabberd_router:route_error(Packet, Error); + {result, {_, _NewSubscription}} -> + %% XXX: notify about subscription state change, section 12.11 + ok end. + -spec update_auth(binary(), binary(), _, _, jid() | error, boolean(), _) -> - {result, ok} | {error, stanza_error()}. + {result, ok} | {error, stanza_error()}. update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> - Sub= lists:filter(fun - ({pending, _}) -> true; - (_) -> false - end, - Subs), + Sub = lists:filter(fun({pending, _}) -> true; + (_) -> false + end, + Subs), case Sub of - [{pending, SubId}|_] -> - NewSub = case Allow of - true -> subscribed; - false -> none - end, - node_call(Host, Type, set_subscriptions, [Nidx, Subscriber, NewSub, SubId]), - send_authorization_approval(Host, Subscriber, Node, NewSub), - {result, ok}; - _ -> - Txt = ?T("No pending subscriptions found"), - {error, xmpp:err_unexpected_request(Txt, ejabberd_option:language())} + [{pending, SubId} | _] -> + NewSub = case Allow of + true -> subscribed; + false -> none + end, + node_call(Host, Type, set_subscriptions, [Nidx, Subscriber, NewSub, SubId]), + send_authorization_approval(Host, Subscriber, Node, NewSub), + {result, ok}; + _ -> + Txt = ?T("No pending subscriptions found"), + {error, xmpp:err_unexpected_request(Txt, ejabberd_option:language())} end. + %% @doc

Create new pubsub nodes

%%

In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:

%%
    @@ -1502,99 +1948,117 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> %%
  • nodetree create_node checks if nodeid already exists
  • %%
  • node plugin create_node just sets default affiliation/subscription
  • %%
--spec create_node(host(), binary(), binary(), jid(), - binary()) -> {result, pubsub()} | {error, stanza_error()}. +-spec create_node(host(), + binary(), + binary(), + jid(), + binary()) -> {result, pubsub()} | {error, stanza_error()}. create_node(Host, ServerHost, Node, Owner, Type) -> create_node(Host, ServerHost, Node, Owner, Type, all, []). --spec create_node(host(), binary(), binary(), jid(), binary(), - atom(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. + +-spec create_node(host(), + binary(), + binary(), + jid(), + binary(), + atom(), + [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> case lists:member(<<"instant-nodes">>, plugin_features(Host, Type)) of - true -> - Node = p1_rand:get_string(), - case create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) of - {result, _} -> - {result, #pubsub{create = Node}}; - Error -> - Error - end; - false -> - {error, extended_error(xmpp:err_not_acceptable(), err_nodeid_required())} + true -> + Node = p1_rand:get_string(), + case create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) of + {result, _} -> + {result, #pubsub{create = Node}}; + Error -> + Error + end; + false -> + {error, extended_error(xmpp:err_not_acceptable(), err_nodeid_required())} end; create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Type = select_type(ServerHost, Host, Node, GivenType), NodeOptions = merge_config( - [node_config(Node, ServerHost), - Configuration, node_options(Host, Type)]), + [node_config(Node, ServerHost), + Configuration, + node_options(Host, Type)]), CreateNode = - fun() -> - Parent = case node_call(Host, Type, node_to_path, [Node]) of - {result, [Node]} -> - <<>>; - {result, Path} -> - element(2, node_call(Host, Type, path_to_node, - [lists:sublist(Path, length(Path)-1)])) - end, - Parents = case Parent of - <<>> -> []; - _ -> [Parent] - end, - case node_call(Host, Type, create_node_permission, - [Host, ServerHost, Node, Parent, Owner, Access]) of - {result, true} -> - case tree_call(Host, create_node, - [Host, Node, Type, Owner, NodeOptions, Parents]) - of - {ok, Nidx} -> - case get_node_subs_by_depth(Host, Node, Owner) of - {result, SubsByDepth} -> - case node_call(Host, Type, create_node, [Nidx, Owner]) of - {result, Result} -> {result, {Nidx, SubsByDepth, Result}}; - Error -> Error - end; - Error -> - Error - end; - {error, {virtual, Nidx}} -> - case node_call(Host, Type, create_node, [Nidx, Owner]) of - {result, Result} -> {result, {Nidx, [], Result}}; - Error -> Error - end; - Error -> - Error - end; - {result, _} -> - Txt = ?T("You're not allowed to create nodes"), - {error, xmpp:err_forbidden(Txt, ejabberd_option:language())}; - Err -> - Err - end - end, + fun() -> + Parent = case node_call(Host, Type, node_to_path, [Node]) of + {result, [Node]} -> + <<>>; + {result, Path} -> + element(2, + node_call(Host, + Type, + path_to_node, + [lists:sublist(Path, length(Path) - 1)])) + end, + Parents = case Parent of + <<>> -> []; + _ -> [Parent] + end, + case node_call(Host, + Type, + create_node_permission, + [Host, ServerHost, Node, Parent, Owner, Access]) of + {result, true} -> + case tree_call(Host, + create_node, + [Host, Node, Type, Owner, NodeOptions, Parents]) of + {ok, Nidx} -> + case get_node_subs_by_depth(Host, Node, Owner) of + {result, SubsByDepth} -> + case node_call(Host, Type, create_node, [Nidx, Owner]) of + {result, Result} -> {result, {Nidx, SubsByDepth, Result}}; + Error -> Error + end; + Error -> + Error + end; + {error, {virtual, Nidx}} -> + case node_call(Host, Type, create_node, [Nidx, Owner]) of + {result, Result} -> {result, {Nidx, [], Result}}; + Error -> Error + end; + Error -> + Error + end; + {result, _} -> + Txt = ?T("You're not allowed to create nodes"), + {error, xmpp:err_forbidden(Txt, ejabberd_option:language())}; + Err -> + Err + end + end, Reply = #pubsub{create = Node}, case transaction(Host, CreateNode, transaction) of - {result, {Nidx, SubsByDepth, {Result, broadcast}}} -> - broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth), - ejabberd_hooks:run(pubsub_create_node, ServerHost, - [ServerHost, Host, Node, Nidx, NodeOptions]), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {Nidx, _SubsByDepth, Result}} -> - ejabberd_hooks:run(pubsub_create_node, ServerHost, - [ServerHost, Host, Node, Nidx, NodeOptions]), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - Error -> - %% in case we change transaction to sync_dirty... - %% node_call(Host, Type, delete_node, [Host, Node]), - %% tree_call(Host, delete_node, [Host, Node]), - Error + {result, {Nidx, SubsByDepth, {Result, broadcast}}} -> + broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth), + ejabberd_hooks:run(pubsub_create_node, + ServerHost, + [ServerHost, Host, Node, Nidx, NodeOptions]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {Nidx, _SubsByDepth, Result}} -> + ejabberd_hooks:run(pubsub_create_node, + ServerHost, + [ServerHost, Host, Node, Nidx, NodeOptions]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + Error -> + %% in case we change transaction to sync_dirty... + %% node_call(Host, Type, delete_node, [Host, Node]), + %% tree_call(Host, delete_node, [Host, Node]), + Error end. + %% @doc

Delete specified node and all children.

%%

There are several reasons why the node deletion request might fail:

%%
    @@ -1607,77 +2071,79 @@ delete_node(_Host, <<>>, _Owner) -> {error, xmpp:err_not_allowed(?T("No node specified"), ejabberd_option:language())}; delete_node(Host, Node, Owner) -> Action = - fun(#pubsub_node{type = Type, id = Nidx}) -> - case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of - {result, owner} -> - case get_node_subs_by_depth(Host, Node, service_jid(Host)) of - {result, SubsByDepth} -> - case tree_call(Host, delete_node, [Host, Node]) of - Removed when is_list(Removed) -> - case node_call(Host, Type, delete_node, [Removed]) of - {result, Res} -> {result, {SubsByDepth, Res}}; - Error -> Error - end; - Error -> - Error - end; - Error -> - Error - end; - {result, _} -> - Lang = ejabberd_option:language(), - {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; - Error -> - Error - end - end, + fun(#pubsub_node{type = Type, id = Nidx}) -> + case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of + {result, owner} -> + case get_node_subs_by_depth(Host, Node, service_jid(Host)) of + {result, SubsByDepth} -> + case tree_call(Host, delete_node, [Host, Node]) of + Removed when is_list(Removed) -> + case node_call(Host, Type, delete_node, [Removed]) of + {result, Res} -> {result, {SubsByDepth, Res}}; + Error -> Error + end; + Error -> + Error + end; + Error -> + Error + end; + {result, _} -> + Lang = ejabberd_option:language(), + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; + Error -> + Error + end + end, Reply = undefined, ServerHost = serverhost(Host), case transaction(Host, Node, Action, transaction) of - {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} -> - lists:foreach(fun ({RNode, _RSubs}) -> - {RH, RN} = RNode#pubsub_node.nodeid, - RNidx = RNode#pubsub_node.id, - RType = RNode#pubsub_node.type, - ROptions = RNode#pubsub_node.options, - unset_cached_item(RH, RNidx), - broadcast_removed_node(RH, RN, RNidx, RType, ROptions, SubsByDepth), - ejabberd_hooks:run(pubsub_delete_node, - ServerHost, - [ServerHost, RH, RN, RNidx]) - end, - Removed), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, {_, {Result, Removed}}}} -> - lists:foreach(fun ({RNode, _RSubs}) -> - {RH, RN} = RNode#pubsub_node.nodeid, - RNidx = RNode#pubsub_node.id, - unset_cached_item(RH, RNidx), - ejabberd_hooks:run(pubsub_delete_node, - ServerHost, - [ServerHost, RH, RN, RNidx]) - end, - Removed), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {TNode, {_, Result}}} -> - Nidx = TNode#pubsub_node.id, - unset_cached_item(Host, Nidx), - ejabberd_hooks:run(pubsub_delete_node, ServerHost, - [ServerHost, Host, Node, Nidx]), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - Error -> - Error + {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} -> + lists:foreach(fun({RNode, _RSubs}) -> + {RH, RN} = RNode#pubsub_node.nodeid, + RNidx = RNode#pubsub_node.id, + RType = RNode#pubsub_node.type, + ROptions = RNode#pubsub_node.options, + unset_cached_item(RH, RNidx), + broadcast_removed_node(RH, RN, RNidx, RType, ROptions, SubsByDepth), + ejabberd_hooks:run(pubsub_delete_node, + ServerHost, + [ServerHost, RH, RN, RNidx]) + end, + Removed), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, {_, {Result, Removed}}}} -> + lists:foreach(fun({RNode, _RSubs}) -> + {RH, RN} = RNode#pubsub_node.nodeid, + RNidx = RNode#pubsub_node.id, + unset_cached_item(RH, RNidx), + ejabberd_hooks:run(pubsub_delete_node, + ServerHost, + [ServerHost, RH, RN, RNidx]) + end, + Removed), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {TNode, {_, Result}}} -> + Nidx = TNode#pubsub_node.id, + unset_cached_item(Host, Nidx), + ejabberd_hooks:run(pubsub_delete_node, + ServerHost, + [ServerHost, Host, Node, Nidx]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + Error -> + Error end. + %% @see node_hometree:subscribe_node/5 %% @doc

    Accepts or rejects subcription requests on a PubSub node.

    %%

    There are several reasons why the subscription request might fail:

    @@ -1694,100 +2160,110 @@ delete_node(Host, Node, Owner) -> %%
  • The node does not exist.
  • %%
-spec subscribe_node(host(), binary(), jid(), jid(), [{binary(), [binary()]}]) -> - {result, pubsub()} | {error, stanza_error()}. + {result, pubsub()} | {error, stanza_error()}. subscribe_node(Host, Node, From, JID, Configuration) -> SubModule = subscription_plugin(Host), SubOpts = case SubModule:parse_options_xform(Configuration) of - {result, GoodSubOpts} -> GoodSubOpts; - _ -> invalid - end, + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, Subscriber = jid:tolower(JID), - Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) -> - Features = plugin_features(Host, Type), - SubscribeFeature = lists:member(<<"subscribe">>, Features), - OptionsFeature = lists:member(<<"subscription-options">>, Features), - HasOptions = not (SubOpts == []), - SubscribeConfig = get_option(Options, subscribe), - AccessModel = get_option(Options, access_model), - SendLast = get_option(Options, send_last_published_item), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - CanSubscribe = case get_max_subscriptions_node(Host) of - Max when is_integer(Max) -> - case node_call(Host, Type, get_node_subscriptions, [Nidx]) of - {result, NodeSubs} -> - SubsNum = lists:foldl( - fun ({_, subscribed, _}, Acc) -> Acc+1; - (_, Acc) -> Acc - end, 0, NodeSubs), - SubsNum < Max; - _ -> - true - end; - _ -> - true - end, - if not SubscribeFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('subscribe'))}; - not SubscribeConfig -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('subscribe'))}; - HasOptions andalso not OptionsFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('subscription-options'))}; - SubOpts == invalid -> - {error, extended_error(xmpp:err_bad_request(), - err_invalid_options())}; - not CanSubscribe -> - %% fallback to closest XEP compatible result, assume we are not allowed to subscribe - {error, extended_error(xmpp:err_not_allowed(), - err_closed_node())}; - true -> - Owners = node_owners_call(Host, Type, Nidx, O), - {PS, RG} = get_presence_and_roster_permissions(Host, JID, - Owners, AccessModel, AllowedGroups), - node_call(Host, Type, subscribe_node, - [Nidx, From, Subscriber, AccessModel, - SendLast, PS, RG, SubOpts]) - end - end, - Reply = fun (Subscription) -> - Sub = case Subscription of - {subscribed, SubId} -> - #ps_subscription{jid = JID, type = subscribed, subid = SubId}; - Other -> - #ps_subscription{jid = JID, type = Other} - end, - #pubsub{subscription = Sub#ps_subscription{node = Node}} - end, + Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) -> + Features = plugin_features(Host, Type), + SubscribeFeature = lists:member(<<"subscribe">>, Features), + OptionsFeature = lists:member(<<"subscription-options">>, Features), + HasOptions = not (SubOpts == []), + SubscribeConfig = get_option(Options, subscribe), + AccessModel = get_option(Options, access_model), + SendLast = get_option(Options, send_last_published_item), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + CanSubscribe = case get_max_subscriptions_node(Host) of + Max when is_integer(Max) -> + case node_call(Host, Type, get_node_subscriptions, [Nidx]) of + {result, NodeSubs} -> + SubsNum = lists:foldl( + fun({_, subscribed, _}, Acc) -> Acc + 1; + (_, Acc) -> Acc + end, + 0, + NodeSubs), + SubsNum < Max; + _ -> + true + end; + _ -> + true + end, + if + not SubscribeFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscribe'))}; + not SubscribeConfig -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscribe'))}; + HasOptions andalso not OptionsFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscription-options'))}; + SubOpts == invalid -> + {error, extended_error(xmpp:err_bad_request(), + err_invalid_options())}; + not CanSubscribe -> + %% fallback to closest XEP compatible result, assume we are not allowed to subscribe + {error, extended_error(xmpp:err_not_allowed(), + err_closed_node())}; + true -> + Owners = node_owners_call(Host, Type, Nidx, O), + {PS, RG} = get_presence_and_roster_permissions(Host, + JID, + Owners, + AccessModel, + AllowedGroups), + node_call(Host, + Type, + subscribe_node, + [Nidx, From, Subscriber, AccessModel, + SendLast, PS, RG, SubOpts]) + end + end, + Reply = fun(Subscription) -> + Sub = case Subscription of + {subscribed, SubId} -> + #ps_subscription{jid = JID, type = subscribed, subid = SubId}; + Other -> + #ps_subscription{jid = JID, type = Other} + end, + #pubsub{subscription = Sub#ps_subscription{node = Node}} + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, subscribed, SubId, send_last}}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - send_items(Host, Node, Nidx, Type, Options, Subscriber, last), - ServerHost = serverhost(Host), - ejabberd_hooks:run(pubsub_subscribe_node, ServerHost, - [ServerHost, Host, Node, Subscriber, SubId]), - case Result of - default -> {result, Reply({subscribed, SubId})}; - _ -> {result, Result} - end; - {result, {_TNode, {default, subscribed, SubId}}} -> - {result, Reply({subscribed, SubId})}; - {result, {_TNode, {Result, subscribed, _SubId}}} -> - {result, Result}; - {result, {TNode, {default, pending, _SubId}}} -> - send_authorization_request(TNode, JID), - {result, Reply(pending)}; - {result, {TNode, {Result, pending}}} -> - send_authorization_request(TNode, JID), - {result, Result}; - {result, {_, Result}} -> - {result, Result}; - Error -> Error + {result, {TNode, {Result, subscribed, SubId, send_last}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + send_items(Host, Node, Nidx, Type, Options, Subscriber, last), + ServerHost = serverhost(Host), + ejabberd_hooks:run(pubsub_subscribe_node, + ServerHost, + [ServerHost, Host, Node, Subscriber, SubId]), + case Result of + default -> {result, Reply({subscribed, SubId})}; + _ -> {result, Result} + end; + {result, {_TNode, {default, subscribed, SubId}}} -> + {result, Reply({subscribed, SubId})}; + {result, {_TNode, {Result, subscribed, _SubId}}} -> + {result, Result}; + {result, {TNode, {default, pending, _SubId}}} -> + send_authorization_request(TNode, JID), + {result, Reply(pending)}; + {result, {TNode, {Result, pending}}} -> + send_authorization_request(TNode, JID), + {result, Result}; + {result, {_, Result}} -> + {result, Result}; + Error -> Error end. + %% @doc

Unsubscribe JID from the Node.

%%

There are several reasons why the unsubscribe request might fail:

%%
    @@ -1798,21 +2274,23 @@ subscribe_node(Host, Node, From, JID, Configuration) -> %%
  • The request specifies a subscription ID that is not valid or current.
  • %%
-spec unsubscribe_node(host(), binary(), jid(), jid(), binary()) -> - {result, undefined} | {error, stanza_error()}. + {result, undefined} | {error, stanza_error()}. unsubscribe_node(Host, Node, From, JID, SubId) -> Subscriber = jid:tolower(JID), - Action = fun (#pubsub_node{type = Type, id = Nidx}) -> - node_call(Host, Type, unsubscribe_node, [Nidx, From, Subscriber, SubId]) - end, + Action = fun(#pubsub_node{type = Type, id = Nidx}) -> + node_call(Host, Type, unsubscribe_node, [Nidx, From, Subscriber, SubId]) + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, default}} -> - ServerHost = serverhost(Host), - ejabberd_hooks:run(pubsub_unsubscribe_node, ServerHost, - [ServerHost, Host, Node, Subscriber, SubId]), - {result, undefined}; - Error -> Error + {result, {_, default}} -> + ServerHost = serverhost(Host), + ejabberd_hooks:run(pubsub_unsubscribe_node, + ServerHost, + [ServerHost, Host, Node, Subscriber, SubId]), + {result, undefined}; + Error -> Error end. + %% @doc

Publish item to a PubSub node.

%%

The permission to publish an item must be verified by the plugin implementation.

%%

There are several reasons why the publish request might fail:

@@ -1824,109 +2302,137 @@ unsubscribe_node(Host, Node, From, JID, SubId) -> %%
  • The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node.
  • %%
  • The request does not match the node configuration.
  • %% --spec publish_item(host(), binary(), binary(), jid(), binary(), - [xmlel()]) -> {result, pubsub()} | {error, stanza_error()}. +-spec publish_item(host(), + binary(), + binary(), + jid(), + binary(), + [xmlel()]) -> {result, pubsub()} | {error, stanza_error()}. publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, [], all). + + publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, PubOpts, Access) -> publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, PubOpts, Access); publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> - Features = plugin_features(Host, Type), - PublishFeature = lists:member(<<"publish">>, Features), - PublishModel = get_option(Options, publish_model), - DeliverPayloads = get_option(Options, deliver_payloads), - PersistItems = get_option(Options, persist_items), - MaxItems = max_items(Host, Options), - PayloadCount = payload_xmlelements(Payload), - PayloadSize = byte_size(term_to_binary(Payload)) - 2, - PayloadMaxSize = get_option(Options, max_payload_size), - PreconditionsMet = preconditions_met(PubOpts, Options), - if not PublishFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported(publish))}; - not PreconditionsMet -> - {error, extended_error(xmpp:err_conflict(), - err_precondition_not_met())}; - PayloadSize > PayloadMaxSize -> - {error, extended_error(xmpp:err_not_acceptable(), - err_payload_too_big())}; - (DeliverPayloads or PersistItems) and (PayloadCount == 0) -> - {error, extended_error(xmpp:err_bad_request(), - err_item_required())}; - (DeliverPayloads or PersistItems) and (PayloadCount > 1) -> - {error, extended_error(xmpp:err_bad_request(), - err_invalid_payload())}; - (not (DeliverPayloads or PersistItems)) and (PayloadCount > 0) -> - {error, extended_error(xmpp:err_bad_request(), - err_item_forbidden())}; - true -> - node_call(Host, Type, publish_item, - [Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, PubOpts]) - end - end, - Reply = #pubsub{publish = #ps_publish{node = Node, - items = [#ps_item{id = ItemId}]}}, + Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + PublishFeature = lists:member(<<"publish">>, Features), + PublishModel = get_option(Options, publish_model), + DeliverPayloads = get_option(Options, deliver_payloads), + PersistItems = get_option(Options, persist_items), + MaxItems = max_items(Host, Options), + PayloadCount = payload_xmlelements(Payload), + PayloadSize = byte_size(term_to_binary(Payload)) - 2, + PayloadMaxSize = get_option(Options, max_payload_size), + PreconditionsMet = preconditions_met(PubOpts, Options), + if + not PublishFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported(publish))}; + not PreconditionsMet -> + {error, extended_error(xmpp:err_conflict(), + err_precondition_not_met())}; + PayloadSize > PayloadMaxSize -> + {error, extended_error(xmpp:err_not_acceptable(), + err_payload_too_big())}; + (DeliverPayloads or PersistItems) and (PayloadCount == 0) -> + {error, extended_error(xmpp:err_bad_request(), + err_item_required())}; + (DeliverPayloads or PersistItems) and (PayloadCount > 1) -> + {error, extended_error(xmpp:err_bad_request(), + err_invalid_payload())}; + (not (DeliverPayloads or PersistItems)) and (PayloadCount > 0) -> + {error, extended_error(xmpp:err_bad_request(), + err_item_forbidden())}; + true -> + node_call(Host, + Type, + publish_item, + [Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, PubOpts]) + end + end, + Reply = #pubsub{ + publish = #ps_publish{ + node = Node, + items = [#ps_item{id = ItemId}] + } + }, case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, Broadcast, Removed}}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - BrPayload = case Broadcast of - broadcast -> Payload; - PluginPayload -> PluginPayload - end, - set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload), - case get_option(Options, deliver_notifications) of - true -> - broadcast_publish_item(Host, Node, Nidx, Type, Options, ItemId, - Publisher, BrPayload, Removed); - false -> - ok - end, - ejabberd_hooks:run(pubsub_publish_item, ServerHost, - [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {TNode, {default, Removed}}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), - set_cached_item(Host, Nidx, ItemId, Publisher, Payload), - {result, Reply}; - {result, {TNode, {Result, Removed}}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), - set_cached_item(Host, Nidx, ItemId, Publisher, Payload), - {result, Result}; - {result, {_, default}} -> - {result, Reply}; - {result, {_, Result}} -> - {result, Result}; - {error, #stanza_error{reason = 'item-not-found'}} -> - Type = select_type(ServerHost, Host, Node), - case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of - true -> - case create_node(Host, ServerHost, Node, Publisher, Type, Access, PubOpts) of - {result, #pubsub{create = NewNode}} -> - publish_item(Host, ServerHost, NewNode, Publisher, ItemId, - Payload, PubOpts, Access); - _ -> - {error, xmpp:err_item_not_found()} - end; - false -> - Txt = ?T("Automatic node creation is not enabled"), - {error, xmpp:err_item_not_found(Txt, ejabberd_option:language())} - end; - Error -> - Error + {result, {TNode, {Result, Broadcast, Removed}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + BrPayload = case Broadcast of + broadcast -> Payload; + PluginPayload -> PluginPayload + end, + set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload), + case get_option(Options, deliver_notifications) of + true -> + broadcast_publish_item(Host, + Node, + Nidx, + Type, + Options, + ItemId, + Publisher, + BrPayload, + Removed); + false -> + ok + end, + ejabberd_hooks:run(pubsub_publish_item, + ServerHost, + [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {TNode, {default, Removed}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), + set_cached_item(Host, Nidx, ItemId, Publisher, Payload), + {result, Reply}; + {result, {TNode, {Result, Removed}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, Removed), + set_cached_item(Host, Nidx, ItemId, Publisher, Payload), + {result, Result}; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + {error, #stanza_error{reason = 'item-not-found'}} -> + Type = select_type(ServerHost, Host, Node), + case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of + true -> + case create_node(Host, ServerHost, Node, Publisher, Type, Access, PubOpts) of + {result, #pubsub{create = NewNode}} -> + publish_item(Host, + ServerHost, + NewNode, + Publisher, + ItemId, + Payload, + PubOpts, + Access); + _ -> + {error, xmpp:err_item_not_found()} + end; + false -> + Txt = ?T("Automatic node creation is not enabled"), + {error, xmpp:err_item_not_found(Txt, ejabberd_option:language())} + end; + Error -> + Error end. + %% @doc

    Delete item from a PubSub node.

    %%

    The permission to delete an item must be verified by the plugin implementation.

    %%

    There are several reasons why the item retraction request might fail:

    @@ -1939,56 +2445,60 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access %%
  • The service does not support the deletion of items.
  • %% -spec delete_item(host(), binary(), jid(), binary()) -> {result, undefined} | - {error, stanza_error()}. + {error, stanza_error()}. delete_item(Host, Node, Publisher, ItemId) -> delete_item(Host, Node, Publisher, ItemId, false). + + delete_item(_, <<>>, _, _, _) -> {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> - Features = plugin_features(Host, Type), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - DeleteFeature = lists:member(<<"delete-items">>, Features), - PublishModel = get_option(Options, publish_model), - if %%-> iq_pubsub just does that matches - %% %% Request does not specify an item - %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; - not PersistentFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('persistent-items'))}; - not DeleteFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('delete-items'))}; - true -> - node_call(Host, Type, delete_item, [Nidx, Publisher, PublishModel, ItemId]) - end - end, + Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + PersistentFeature = lists:member(<<"persistent-items">>, Features), + DeleteFeature = lists:member(<<"delete-items">>, Features), + PublishModel = get_option(Options, publish_model), + if %%-> iq_pubsub just does that matches + %% %% Request does not specify an item + %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; + not PersistentFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + not DeleteFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('delete-items'))}; + true -> + node_call(Host, Type, delete_item, [Nidx, Publisher, PublishModel, ItemId]) + end + end, Reply = undefined, case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Result, broadcast}}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - ServerHost = serverhost(Host), - ejabberd_hooks:run(pubsub_delete_item, ServerHost, - [ServerHost, Node, Publisher, service_jid(Host), ItemId]), - broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, [ItemId], ForceNotify), - case get_cached_item(Host, Nidx) of - #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); - _ -> ok - end, - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, default}} -> - {result, Reply}; - {result, {_, Result}} -> - {result, Result}; - Error -> - Error + {result, {TNode, {Result, broadcast}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + ServerHost = serverhost(Host), + ejabberd_hooks:run(pubsub_delete_item, + ServerHost, + [ServerHost, Node, Publisher, service_jid(Host), ItemId]), + broadcast_retract_items(Host, Publisher, Node, Nidx, Type, Options, [ItemId], ForceNotify), + case get_cached_item(Host, Nidx) of + #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); + _ -> ok + end, + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. + %% @doc

    Delete all items of specified node owned by JID.

    %%

    There are several reasons why the node purge request might fail:

    %%
      @@ -1998,437 +2508,512 @@ delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> %%
    • The specified node does not exist.
    • %%
    -spec purge_node(mod_pubsub:host(), binary(), jid()) -> {result, undefined} | - {error, stanza_error()}. + {error, stanza_error()}. purge_node(Host, Node, Owner) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> - Features = plugin_features(Host, Type), - PurgeFeature = lists:member(<<"purge-nodes">>, Features), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - PersistentConfig = get_option(Options, persist_items), - if not PurgeFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('purge-nodes'))}; - not PersistentFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('persistent-items'))}; - not PersistentConfig -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('persistent-items'))}; - true -> node_call(Host, Type, purge_node, [Nidx, Owner]) - end - end, + Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + PurgeFeature = lists:member(<<"purge-nodes">>, Features), + PersistentFeature = lists:member(<<"persistent-items">>, Features), + PersistentConfig = get_option(Options, persist_items), + if + not PurgeFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('purge-nodes'))}; + not PersistentFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + not PersistentConfig -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + true -> node_call(Host, Type, purge_node, [Nidx, Owner]) + end + end, Reply = undefined, case transaction(Host, Node, Action, transaction) of - {result, {TNode, {Result, broadcast}}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, - broadcast_purge_node(Host, Node, Nidx, Type, Options), - unset_cached_item(Host, Nidx), - case Result of - default -> {result, Reply}; - _ -> {result, Result} - end; - {result, {_, default}} -> - {result, Reply}; - {result, {_, Result}} -> - {result, Result}; - Error -> - Error + {result, {TNode, {Result, broadcast}}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + Options = TNode#pubsub_node.options, + broadcast_purge_node(Host, Node, Nidx, Type, Options), + unset_cached_item(Host, Nidx), + case Result of + default -> {result, Reply}; + _ -> {result, Result} + end; + {result, {_, default}} -> + {result, Reply}; + {result, {_, Result}} -> + {result, Result}; + Error -> + Error end. + %% @doc

    Return the items of a given node.

    %%

    The number of items to return is limited by MaxItems.

    %%

    The permission are not checked in this function.

    --spec get_items(host(), binary(), jid(), binary(), - undefined | non_neg_integer(), [binary()], undefined | rsm_set()) -> - {result, pubsub()} | {error, stanza_error()}. +-spec get_items(host(), + binary(), + jid(), + binary(), + undefined | non_neg_integer(), + [binary()], + undefined | rsm_set()) -> + {result, pubsub()} | {error, stanza_error()}. get_items(Host, Node, From, SubId, MaxItems, ItemIds, undefined) when MaxItems =/= undefined -> - get_items(Host, Node, From, SubId, MaxItems, ItemIds, + get_items(Host, + Node, + From, + SubId, + MaxItems, + ItemIds, #rsm_set{max = MaxItems, before = <<>>}); get_items(Host, Node, From, SubId, _MaxItems, ItemIds, RSM) -> Action = - fun(#pubsub_node{options = Options, type = Type, - id = Nidx, owners = O}) -> - Features = plugin_features(Host, Type), - RetreiveFeature = lists:member(<<"retrieve-items">>, Features), - PersistentFeature = lists:member(<<"persistent-items">>, Features), - AccessModel = get_option(Options, access_model), - AllowedGroups = get_option(Options, roster_groups_allowed, []), - if not RetreiveFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('retrieve-items'))}; - not PersistentFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('persistent-items'))}; - true -> - Owners = node_owners_call(Host, Type, Nidx, O), - {PS, RG} = get_presence_and_roster_permissions( - Host, From, Owners, AccessModel, AllowedGroups), - case ItemIds of - [ItemId] -> - NotFound = xmpp:err_item_not_found(), - case node_call(Host, Type, get_item, - [Nidx, ItemId, From, AccessModel, PS, RG, undefined]) - of - {error, NotFound} -> {result, {[], undefined}}; - Result -> Result - end; - _ -> - node_call(Host, Type, get_items, - [Nidx, From, AccessModel, PS, RG, SubId, RSM]) - end - end - end, + fun(#pubsub_node{ + options = Options, + type = Type, + id = Nidx, + owners = O + }) -> + Features = plugin_features(Host, Type), + RetreiveFeature = lists:member(<<"retrieve-items">>, Features), + PersistentFeature = lists:member(<<"persistent-items">>, Features), + AccessModel = get_option(Options, access_model), + AllowedGroups = get_option(Options, roster_groups_allowed, []), + if + not RetreiveFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-items'))}; + not PersistentFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('persistent-items'))}; + true -> + Owners = node_owners_call(Host, Type, Nidx, O), + {PS, RG} = get_presence_and_roster_permissions( + Host, From, Owners, AccessModel, AllowedGroups), + case ItemIds of + [ItemId] -> + NotFound = xmpp:err_item_not_found(), + case node_call(Host, + Type, + get_item, + [Nidx, ItemId, From, AccessModel, PS, RG, undefined]) of + {error, NotFound} -> {result, {[], undefined}}; + Result -> Result + end; + _ -> + node_call(Host, + Type, + get_items, + [Nidx, From, AccessModel, PS, RG, SubId, RSM]) + end + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {TNode, {Items, RsmOut}}} -> - SendItems = case ItemIds of - [] -> - Items; - _ -> - lists:filter( - fun(#pubsub_item{itemid = {ItemId, _}}) -> - lists:member(ItemId, ItemIds) - end, Items) - end, - Options = TNode#pubsub_node.options, - {result, #pubsub{items = items_els(Node, Options, SendItems), - rsm = RsmOut}}; - {result, {TNode, Item}} -> - Options = TNode#pubsub_node.options, - {result, #pubsub{items = items_els(Node, Options, [Item])}}; - Error -> - Error + {result, {TNode, {Items, RsmOut}}} -> + SendItems = case ItemIds of + [] -> + Items; + _ -> + lists:filter( + fun(#pubsub_item{itemid = {ItemId, _}}) -> + lists:member(ItemId, ItemIds) + end, + Items) + end, + Options = TNode#pubsub_node.options, + {result, #pubsub{ + items = items_els(Node, Options, SendItems), + rsm = RsmOut + }}; + {result, {TNode, Item}} -> + Options = TNode#pubsub_node.options, + {result, #pubsub{items = items_els(Node, Options, [Item])}}; + Error -> + Error end. + %% Seems like this function broken get_items(Host, Node) -> - Action = fun (#pubsub_node{type = Type, id = Nidx}) -> - node_call(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) - end, + Action = fun(#pubsub_node{type = Type, id = Nidx}) -> + node_call(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, {Items, _}}} -> Items; - Error -> Error + {result, {_, {Items, _}}} -> Items; + Error -> Error end. + %% This function is broken too? get_item(Host, Node, ItemId) -> - Action = fun (#pubsub_node{type = Type, id = Nidx}) -> - node_call(Host, Type, get_item, [Nidx, ItemId]) - end, + Action = fun(#pubsub_node{type = Type, id = Nidx}) -> + node_call(Host, Type, get_item, [Nidx, ItemId]) + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Items}} -> Items; - Error -> Error + {result, {_, Items}} -> Items; + Error -> Error end. --spec get_allowed_items_call(host(), nodeIdx(), jid(), - binary(), nodeOptions(), [ljid()]) -> {result, [#pubsub_item{}]} | - {error, stanza_error()}. + +-spec get_allowed_items_call(host(), + nodeIdx(), + jid(), + binary(), + nodeOptions(), + [ljid()]) -> {result, [#pubsub_item{}]} | + {error, stanza_error()}. get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) -> case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, undefined) of - {result, {Items, _RSM}} -> {result, Items}; - Error -> Error + {result, {Items, _RSM}} -> {result, Items}; + Error -> Error end. --spec get_allowed_items_call(host(), nodeIdx(), jid(), - binary(), nodeOptions(), [ljid()], - undefined | rsm_set()) -> - {result, {[#pubsub_item{}], undefined | rsm_set()}} | - {error, stanza_error()}. + +-spec get_allowed_items_call(host(), + nodeIdx(), + jid(), + binary(), + nodeOptions(), + [ljid()], + undefined | rsm_set()) -> + {result, {[#pubsub_item{}], undefined | rsm_set()}} | + {error, stanza_error()}. get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) -> AccessModel = get_option(Options, access_model), AllowedGroups = get_option(Options, roster_groups_allowed, []), {PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups), node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]). + -spec get_last_items(host(), binary(), nodeIdx(), ljid(), last | integer()) -> [#pubsub_item{}]. get_last_items(Host, Type, Nidx, LJID, last) -> % hack to handle section 6.1.7 of XEP-0060 get_last_items(Host, Type, Nidx, LJID, 1); get_last_items(Host, Type, Nidx, LJID, 1) -> case get_cached_item(Host, Nidx) of - undefined -> - case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of - {result, Items} -> Items; - _ -> [] - end; - LastItem -> - [LastItem] + undefined -> + case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of + {result, Items} -> Items; + _ -> [] + end; + LastItem -> + [LastItem] end; get_last_items(Host, Type, Nidx, LJID, Count) when Count > 1 -> case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of - {result, Items} -> Items; - _ -> [] + {result, Items} -> Items; + _ -> [] end; get_last_items(_Host, _Type, _Nidx, _LJID, _Count) -> []. + -spec get_only_item(host(), binary(), nodeIdx(), ljid()) -> [#pubsub_item{}]. get_only_item(Host, Type, Nidx, LJID) -> case get_cached_item(Host, Nidx) of - undefined -> - case node_action(Host, Type, get_only_item, [Nidx, LJID]) of - {result, Items} when length(Items) < 2 -> - Items; - {result, Items} -> - [hd(lists:keysort(#pubsub_item.modification, Items))]; - _ -> [] - end; - LastItem -> - [LastItem] + undefined -> + case node_action(Host, Type, get_only_item, [Nidx, LJID]) of + {result, Items} when length(Items) < 2 -> + Items; + {result, Items} -> + [hd(lists:keysort(#pubsub_item.modification, Items))]; + _ -> [] + end; + LastItem -> + [LastItem] end. + %% @doc

    Return the list of affiliations as an XMPP response.

    -spec get_affiliations(host(), binary(), jid(), [binary()]) -> - {result, pubsub()} | {error, stanza_error()}. + {result, pubsub()} | {error, stanza_error()}. get_affiliations(Host, Node, JID, Plugins) when is_list(Plugins) -> Result = - lists:foldl( - fun(Type, {Status, Acc}) -> - Features = plugin_features(Host, Type), - RetrieveFeature = lists:member(<<"retrieve-affiliations">>, Features), - if not RetrieveFeature -> - {{error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('retrieve-affiliations'))}, - Acc}; - true -> - case node_action(Host, Type, - get_entity_affiliations, - [Host, JID]) of - {result, Affs} -> - {Status, [Affs | Acc]}; - {error, _} = Err -> - {Err, Acc} - end - end - end, {ok, []}, Plugins), + lists:foldl( + fun(Type, {Status, Acc}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"retrieve-affiliations">>, Features), + if + not RetrieveFeature -> + {{error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-affiliations'))}, + Acc}; + true -> + case node_action(Host, + Type, + get_entity_affiliations, + [Host, JID]) of + {result, Affs} -> + {Status, [Affs | Acc]}; + {error, _} = Err -> + {Err, Acc} + end + end + end, + {ok, []}, + Plugins), case Result of - {ok, Affs} -> - Entities = lists:flatmap( - fun({_, none}) -> - []; - ({#pubsub_node{nodeid = {_, NodeId}}, Aff}) -> - if (Node == <<>>) or (Node == NodeId) -> - [#ps_affiliation{node = NodeId, - type = Aff}]; - true -> - [] - end; - (_) -> - [] - end, lists:usort(lists:flatten(Affs))), - {result, #pubsub{affiliations = {<<>>, Entities}}}; - {Error, _} -> - Error + {ok, Affs} -> + Entities = lists:flatmap( + fun({_, none}) -> + []; + ({#pubsub_node{nodeid = {_, NodeId}}, Aff}) -> + if + (Node == <<>>) or (Node == NodeId) -> + [#ps_affiliation{ + node = NodeId, + type = Aff + }]; + true -> + [] + end; + (_) -> + [] + end, + lists:usort(lists:flatten(Affs))), + {result, #pubsub{affiliations = {<<>>, Entities}}}; + {Error, _} -> + Error end. + -spec get_affiliations(host(), binary(), jid()) -> - {result, pubsub_owner()} | {error, stanza_error()}. + {result, pubsub_owner()} | {error, stanza_error()}. get_affiliations(Host, Node, JID) -> Action = - fun(#pubsub_node{type = Type, id = Nidx}) -> - Features = plugin_features(Host, Type), - RetrieveFeature = lists:member(<<"modify-affiliations">>, Features), - {result, Affiliation} = node_call(Host, Type, get_affiliation, [Nidx, JID]), - if not RetrieveFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('modify-affiliations'))}; - Affiliation /= owner -> - {error, xmpp:err_forbidden(?T("Owner privileges required"), ejabberd_option:language())}; - true -> - node_call(Host, Type, get_node_affiliations, [Nidx]) - end - end, + fun(#pubsub_node{type = Type, id = Nidx}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"modify-affiliations">>, Features), + {result, Affiliation} = node_call(Host, Type, get_affiliation, [Nidx, JID]), + if + not RetrieveFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('modify-affiliations'))}; + Affiliation /= owner -> + {error, xmpp:err_forbidden(?T("Owner privileges required"), ejabberd_option:language())}; + true -> + node_call(Host, Type, get_node_affiliations, [Nidx]) + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, []}} -> - {error, xmpp:err_item_not_found()}; - {result, {_, Affs}} -> - Entities = lists:flatmap( - fun({_, none}) -> - []; - ({AJID, Aff}) -> - [#ps_affiliation{jid = AJID, type = Aff}] - end, Affs), - {result, #pubsub_owner{affiliations = {Node, Entities}}}; - Error -> - Error + {result, {_, []}} -> + {error, xmpp:err_item_not_found()}; + {result, {_, Affs}} -> + Entities = lists:flatmap( + fun({_, none}) -> + []; + ({AJID, Aff}) -> + [#ps_affiliation{jid = AJID, type = Aff}] + end, + Affs), + {result, #pubsub_owner{affiliations = {Node, Entities}}}; + Error -> + Error end. + -spec set_affiliations(host(), binary(), jid(), [ps_affiliation()]) -> - {result, undefined} | {error, stanza_error()}. + {result, undefined} | {error, stanza_error()}. set_affiliations(Host, Node, From, Affs) -> Owner = jid:tolower(jid:remove_resource(From)), Action = - fun(#pubsub_node{type = Type, id = Nidx, owners = O, options = Options} = N) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case lists:member(Owner, Owners) of - true -> - AccessModel = get_option(Options, access_model), - OwnerJID = jid:make(Owner), - FilteredAffs = - case Owners of - [Owner] -> - [Aff || Aff <- Affs, - Aff#ps_affiliation.jid /= OwnerJID]; - _ -> - Affs - end, - lists:foreach( - fun(#ps_affiliation{jid = JID, type = Affiliation}) -> - node_call(Host, Type, set_affiliation, [Nidx, JID, Affiliation]), - case Affiliation of - owner -> - NewOwner = jid:tolower(jid:remove_resource(JID)), - NewOwners = [NewOwner | Owners], - tree_call(Host, - set_node, - [N#pubsub_node{owners = NewOwners}]); - none -> - OldOwner = jid:tolower(jid:remove_resource(JID)), - case lists:member(OldOwner, Owners) of - true -> - NewOwners = Owners -- [OldOwner], - tree_call(Host, - set_node, - [N#pubsub_node{owners = NewOwners}]); - _ -> - ok - end; - _ -> - ok - end, - case AccessModel of - whitelist when Affiliation /= owner, - Affiliation /= publisher, - Affiliation /= member -> - node_action(Host, Type, - unsubscribe_node, - [Nidx, OwnerJID, JID, - all]); - _ -> - ok - end - end, FilteredAffs), - {result, undefined}; - _ -> - {error, xmpp:err_forbidden( - ?T("Owner privileges required"), ejabberd_option:language())} - end - end, + fun(#pubsub_node{type = Type, id = Nidx, owners = O, options = Options} = N) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case lists:member(Owner, Owners) of + true -> + AccessModel = get_option(Options, access_model), + OwnerJID = jid:make(Owner), + FilteredAffs = + case Owners of + [Owner] -> + [ Aff || Aff <- Affs, + Aff#ps_affiliation.jid /= OwnerJID ]; + _ -> + Affs + end, + lists:foreach( + fun(#ps_affiliation{jid = JID, type = Affiliation}) -> + node_call(Host, Type, set_affiliation, [Nidx, JID, Affiliation]), + case Affiliation of + owner -> + NewOwner = jid:tolower(jid:remove_resource(JID)), + NewOwners = [NewOwner | Owners], + tree_call(Host, + set_node, + [N#pubsub_node{owners = NewOwners}]); + none -> + OldOwner = jid:tolower(jid:remove_resource(JID)), + case lists:member(OldOwner, Owners) of + true -> + NewOwners = Owners -- [OldOwner], + tree_call(Host, + set_node, + [N#pubsub_node{owners = NewOwners}]); + _ -> + ok + end; + _ -> + ok + end, + case AccessModel of + whitelist when Affiliation /= owner, + Affiliation /= publisher, + Affiliation /= member -> + node_action(Host, + Type, + unsubscribe_node, + [Nidx, OwnerJID, JID, + all]); + _ -> + ok + end + end, + FilteredAffs), + {result, undefined}; + _ -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), ejabberd_option:language())} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other + {result, {_, Result}} -> {result, Result}; + Other -> Other end. + -spec get_options(binary(), binary(), jid(), binary(), binary()) -> - {result, xdata()} | {error, stanza_error()}. + {result, xdata()} | {error, stanza_error()}. get_options(Host, Node, JID, SubId, Lang) -> - Action = fun (#pubsub_node{type = Type, id = Nidx}) -> - case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of - true -> - get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type); - false -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('subscription-options'))} - end - end, + Action = fun(#pubsub_node{type = Type, id = Nidx}) -> + case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of + true -> + get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type); + false -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscription-options'))} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_Node, XForm}} -> {result, XForm}; - Error -> Error + {result, {_Node, XForm}} -> {result, XForm}; + Error -> Error end. --spec get_options_helper(binary(), jid(), binary(), binary(), _, binary(), - binary()) -> {result, pubsub()} | {error, stanza_error()}. + +-spec get_options_helper(binary(), + jid(), + binary(), + binary(), + _, + binary(), + binary()) -> {result, pubsub()} | {error, stanza_error()}. get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type) -> Subscriber = jid:tolower(JID), case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of - {result, Subs} -> - SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed], - case {SubId, SubIds} of - {_, []} -> - {error, extended_error(xmpp:err_not_acceptable(), - err_not_subscribed())}; - {<<>>, [SID]} -> - read_sub(Host, Node, Nidx, Subscriber, SID, Lang); - {<<>>, _} -> - {error, extended_error(xmpp:err_not_acceptable(), - err_subid_required())}; - {_, _} -> - ValidSubId = lists:member(SubId, SubIds), - if ValidSubId -> - read_sub(Host, Node, Nidx, Subscriber, SubId, Lang); - true -> - {error, extended_error(xmpp:err_not_acceptable(), - err_invalid_subid())} - end - end; - {error, _} = Error -> - Error + {result, Subs} -> + SubIds = [ Id || {Sub, Id} <- Subs, Sub == subscribed ], + case {SubId, SubIds} of + {_, []} -> + {error, extended_error(xmpp:err_not_acceptable(), + err_not_subscribed())}; + {<<>>, [SID]} -> + read_sub(Host, Node, Nidx, Subscriber, SID, Lang); + {<<>>, _} -> + {error, extended_error(xmpp:err_not_acceptable(), + err_subid_required())}; + {_, _} -> + ValidSubId = lists:member(SubId, SubIds), + if + ValidSubId -> + read_sub(Host, Node, Nidx, Subscriber, SubId, Lang); + true -> + {error, extended_error(xmpp:err_not_acceptable(), + err_invalid_subid())} + end + end; + {error, _} = Error -> + Error end. + -spec read_sub(binary(), binary(), nodeIdx(), ljid(), binary(), binary()) -> {result, pubsub()}. read_sub(Host, Node, Nidx, Subscriber, SubId, Lang) -> SubModule = subscription_plugin(Host), XData = case SubModule:get_subscription(Subscriber, Nidx, SubId) of - {error, notfound} -> - undefined; - {result, #pubsub_subscription{options = Options}} -> - {result, X} = SubModule:get_options_xform(Lang, Options), - X - end, - {result, #pubsub{options = #ps_options{jid = jid:make(Subscriber), - subid = SubId, - node = Node, - xdata = XData}}}. + {error, notfound} -> + undefined; + {result, #pubsub_subscription{options = Options}} -> + {result, X} = SubModule:get_options_xform(Lang, Options), + X + end, + {result, #pubsub{ + options = #ps_options{ + jid = jid:make(Subscriber), + subid = SubId, + node = Node, + xdata = XData + } + }}. --spec set_options(binary(), binary(), jid(), binary(), - [{binary(), [binary()]}]) -> - {result, undefined} | {error, stanza_error()}. + +-spec set_options(binary(), + binary(), + jid(), + binary(), + [{binary(), [binary()]}]) -> + {result, undefined} | {error, stanza_error()}. set_options(Host, Node, JID, SubId, Configuration) -> - Action = fun (#pubsub_node{type = Type, id = Nidx}) -> - case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of - true -> - set_options_helper(Host, Configuration, JID, Nidx, SubId, Type); - false -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('subscription-options'))} - end - end, + Action = fun(#pubsub_node{type = Type, id = Nidx}) -> + case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of + true -> + set_options_helper(Host, Configuration, JID, Nidx, SubId, Type); + false -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('subscription-options'))} + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_Node, Result}} -> {result, Result}; - Error -> Error + {result, {_Node, Result}} -> {result, Result}; + Error -> Error end. --spec set_options_helper(binary(), [{binary(), [binary()]}], jid(), - nodeIdx(), binary(), binary()) -> - {result, undefined} | {error, stanza_error()}. + +-spec set_options_helper(binary(), + [{binary(), [binary()]}], + jid(), + nodeIdx(), + binary(), + binary()) -> + {result, undefined} | {error, stanza_error()}. set_options_helper(Host, Configuration, JID, Nidx, SubId, Type) -> SubModule = subscription_plugin(Host), SubOpts = case SubModule:parse_options_xform(Configuration) of - {result, GoodSubOpts} -> GoodSubOpts; - _ -> invalid - end, + {result, GoodSubOpts} -> GoodSubOpts; + _ -> invalid + end, Subscriber = jid:tolower(JID), case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of - {result, Subs} -> - SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed], - case {SubId, SubIds} of - {_, []} -> - {error, extended_error(xmpp:err_not_acceptable(), err_not_subscribed())}; - {<<>>, [SID]} -> - write_sub(Host, Nidx, Subscriber, SID, SubOpts); - {<<>>, _} -> - {error, extended_error(xmpp:err_not_acceptable(), err_subid_required())}; - {_, _} -> - write_sub(Host, Nidx, Subscriber, SubId, SubOpts) - end; - {error, _} = Err -> - Err + {result, Subs} -> + SubIds = [ Id || {Sub, Id} <- Subs, Sub == subscribed ], + case {SubId, SubIds} of + {_, []} -> + {error, extended_error(xmpp:err_not_acceptable(), err_not_subscribed())}; + {<<>>, [SID]} -> + write_sub(Host, Nidx, Subscriber, SID, SubOpts); + {<<>>, _} -> + {error, extended_error(xmpp:err_not_acceptable(), err_subid_required())}; + {_, _} -> + write_sub(Host, Nidx, Subscriber, SubId, SubOpts) + end; + {error, _} = Err -> + Err end. + -spec write_sub(binary(), nodeIdx(), ljid(), binary(), _) -> {result, undefined} | - {error, stanza_error()}. + {error, stanza_error()}. write_sub(_Host, _Nidx, _Subscriber, _SubId, invalid) -> {error, extended_error(xmpp:err_bad_request(), err_invalid_options())}; write_sub(_Host, _Nidx, _Subscriber, _SubId, []) -> @@ -2436,259 +3021,306 @@ write_sub(_Host, _Nidx, _Subscriber, _SubId, []) -> write_sub(Host, Nidx, Subscriber, SubId, Options) -> SubModule = subscription_plugin(Host), case SubModule:set_subscription(Subscriber, Nidx, SubId, Options) of - {result, _} -> {result, undefined}; - {error, _} -> {error, extended_error(xmpp:err_not_acceptable(), - err_invalid_subid())} + {result, _} -> {result, undefined}; + {error, _} -> + {error, extended_error(xmpp:err_not_acceptable(), + err_invalid_subid())} end. + %% @doc

    Return the list of subscriptions as an XMPP response.

    -spec get_subscriptions(host(), binary(), jid(), [binary()]) -> - {result, pubsub()} | {error, stanza_error()}. + {result, pubsub()} | {error, stanza_error()}. get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> - Result = lists:foldl(fun (Type, {Status, Acc}) -> - Features = plugin_features(Host, Type), - RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features), - if not RetrieveFeature -> - {{error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('retrieve-subscriptions'))}, - Acc}; - true -> - Subscriber = jid:remove_resource(JID), - case node_action(Host, Type, - get_entity_subscriptions, - [Host, Subscriber]) of - {result, Subs} -> - {Status, [Subs | Acc]}; - {error, _} = Err -> - {Err, Acc} - end - end - end, {ok, []}, Plugins), + Result = lists:foldl(fun(Type, {Status, Acc}) -> + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features), + if + not RetrieveFeature -> + {{error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-subscriptions'))}, + Acc}; + true -> + Subscriber = jid:remove_resource(JID), + case node_action(Host, + Type, + get_entity_subscriptions, + [Host, Subscriber]) of + {result, Subs} -> + {Status, [Subs | Acc]}; + {error, _} = Err -> + {Err, Acc} + end + end + end, + {ok, []}, + Plugins), case Result of - {ok, Subs} -> - Entities = lists:flatmap(fun - ({#pubsub_node{nodeid = {_, SubsNode}}, Sub}) -> - case Node of - <<>> -> - [#ps_subscription{jid = jid:remove_resource(JID), - node = SubsNode, type = Sub}]; - SubsNode -> - [#ps_subscription{jid = jid:remove_resource(JID), - type = Sub}]; - _ -> - [] - end; - ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}) -> - case Node of - <<>> -> - [#ps_subscription{jid = SubJID, - subid = SubId, - type = Sub, - node = SubsNode}]; - SubsNode -> - [#ps_subscription{jid = SubJID, - subid = SubId, - type = Sub}]; - _ -> - [] - end; - ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}) -> - case Node of - <<>> -> - [#ps_subscription{jid = SubJID, - type = Sub, - node = SubsNode}]; - SubsNode -> - [#ps_subscription{jid = SubJID, type = Sub}]; - _ -> - [] - end - end, - lists:usort(lists:flatten(Subs))), - {result, #pubsub{subscriptions = {<<>>, Entities}}}; - {Error, _} -> - Error + {ok, Subs} -> + Entities = lists:flatmap(fun({#pubsub_node{nodeid = {_, SubsNode}}, Sub}) -> + case Node of + <<>> -> + [#ps_subscription{ + jid = jid:remove_resource(JID), + node = SubsNode, + type = Sub + }]; + SubsNode -> + [#ps_subscription{ + jid = jid:remove_resource(JID), + type = Sub + }]; + _ -> + [] + end; + ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}) -> + case Node of + <<>> -> + [#ps_subscription{ + jid = SubJID, + subid = SubId, + type = Sub, + node = SubsNode + }]; + SubsNode -> + [#ps_subscription{ + jid = SubJID, + subid = SubId, + type = Sub + }]; + _ -> + [] + end; + ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}) -> + case Node of + <<>> -> + [#ps_subscription{ + jid = SubJID, + type = Sub, + node = SubsNode + }]; + SubsNode -> + [#ps_subscription{jid = SubJID, type = Sub}]; + _ -> + [] + end + end, + lists:usort(lists:flatten(Subs))), + {result, #pubsub{subscriptions = {<<>>, Entities}}}; + {Error, _} -> + Error end. + -spec get_subscriptions(host(), binary(), jid()) -> {result, pubsub_owner()} | - {error, stanza_error()}. + {error, stanza_error()}. get_subscriptions(Host, Node, JID) -> Action = fun(#pubsub_node{type = Type, id = Nidx}) -> - Features = plugin_features(Host, Type), - RetrieveFeature = lists:member(<<"manage-subscriptions">>, Features), - case node_call(Host, Type, get_affiliation, [Nidx, JID]) of - {result, Affiliation} -> - if not RetrieveFeature -> - {error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('manage-subscriptions'))}; - Affiliation /= owner -> - Lang = ejabberd_option:language(), - {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; - true -> - node_call(Host, Type, get_node_subscriptions, [Nidx]) - end; - Error -> - Error - end - end, + Features = plugin_features(Host, Type), + RetrieveFeature = lists:member(<<"manage-subscriptions">>, Features), + case node_call(Host, Type, get_affiliation, [Nidx, JID]) of + {result, Affiliation} -> + if + not RetrieveFeature -> + {error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('manage-subscriptions'))}; + Affiliation /= owner -> + Lang = ejabberd_option:language(), + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; + true -> + node_call(Host, Type, get_node_subscriptions, [Nidx]) + end; + Error -> + Error + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Subs}} -> - Entities = - lists:flatmap( - fun({_, none}) -> - []; - ({_, pending, _}) -> - []; - ({AJID, Sub}) -> - [#ps_subscription{jid = AJID, type = Sub}]; - ({AJID, Sub, SubId}) -> - [#ps_subscription{jid = AJID, type = Sub, subid = SubId}] - end, Subs), - {result, #pubsub_owner{subscriptions = {Node, Entities}}}; - Error -> - Error + {result, {_, Subs}} -> + Entities = + lists:flatmap( + fun({_, none}) -> + []; + ({_, pending, _}) -> + []; + ({AJID, Sub}) -> + [#ps_subscription{jid = AJID, type = Sub}]; + ({AJID, Sub, SubId}) -> + [#ps_subscription{jid = AJID, type = Sub, subid = SubId}] + end, + Subs), + {result, #pubsub_owner{subscriptions = {Node, Entities}}}; + Error -> + Error end. + -spec get_subscriptions_for_send_last(host(), binary(), atom(), jid(), ljid(), ljid()) -> - [{#pubsub_node{}, subId(), ljid()}]. + [{#pubsub_node{}, subId(), ljid()}]. get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) -> - case node_action(Host, PType, - get_entity_subscriptions_for_send_last, - [Host, JID]) of - {result, Subs} -> - [{Node, SubId, SubJID} - || {Node, Sub, SubId, SubJID} <- Subs, - Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)]; - _ -> - [] + case node_action(Host, + PType, + get_entity_subscriptions_for_send_last, + [Host, JID]) of + {result, Subs} -> + [ {Node, SubId, SubJID} + || {Node, Sub, SubId, SubJID} <- Subs, + Sub =:= subscribed, + (SubJID == LJID) or (SubJID == BJID) ]; + _ -> + [] end; %% sql version already filter result by on_sub_and_presence get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) -> - case node_action(Host, PType, - get_entity_subscriptions, - [Host, JID]) of - {result, Subs} -> - [{Node, SubId, SubJID} - || {Node, Sub, SubId, SubJID} <- Subs, - Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID), - match_option(Node, send_last_published_item, on_sub_and_presence)]; - _ -> - [] + case node_action(Host, + PType, + get_entity_subscriptions, + [Host, JID]) of + {result, Subs} -> + [ {Node, SubId, SubJID} + || {Node, Sub, SubId, SubJID} <- Subs, + Sub =:= subscribed, + (SubJID == LJID) or (SubJID == BJID), + match_option(Node, send_last_published_item, on_sub_and_presence) ]; + _ -> + [] end. + -spec set_subscriptions(host(), binary(), jid(), [ps_subscription()]) -> - {result, undefined} | {error, stanza_error()}. + {result, undefined} | {error, stanza_error()}. set_subscriptions(Host, Node, From, Entities) -> Owner = jid:tolower(jid:remove_resource(From)), Notify = fun(#ps_subscription{jid = JID, type = Sub}) -> - Stanza = #message{ - from = service_jid(Host), - to = JID, - sub_els = [#ps_event{ - subscription = #ps_subscription{ - jid = JID, - type = Sub, - node = Node}}]}, - ejabberd_router:route(Stanza) - end, + Stanza = #message{ + from = service_jid(Host), + to = JID, + sub_els = [#ps_event{ + subscription = #ps_subscription{ + jid = JID, + type = Sub, + node = Node + } + }] + }, + ejabberd_router:route(Stanza) + end, Action = - fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> - Owners = node_owners_call(Host, Type, Nidx, O), - case lists:member(Owner, Owners) of - true -> - Result = - lists:foldl( - fun(_, {error, _} = Err) -> - Err; - (#ps_subscription{jid = JID, type = Sub, - subid = SubId} = Entity, _) -> - case node_call(Host, Type, - set_subscriptions, - [Nidx, JID, Sub, SubId]) of - {error, _} = Err -> - Err; - _ -> - Notify(Entity) - end - end, ok, Entities), - case Result of - ok -> {result, undefined}; - {error, _} = Err -> Err - end; - _ -> - {error, xmpp:err_forbidden( - ?T("Owner privileges required"), ejabberd_option:language())} + fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> + Owners = node_owners_call(Host, Type, Nidx, O), + case lists:member(Owner, Owners) of + true -> + Result = + lists:foldl( + fun(_, {error, _} = Err) -> + Err; + (#ps_subscription{ + jid = JID, + type = Sub, + subid = SubId + } = Entity, + _) -> + case node_call(Host, + Type, + set_subscriptions, + [Nidx, JID, Sub, SubId]) of + {error, _} = Err -> + Err; + _ -> + Notify(Entity) + end + end, + ok, + Entities), + case Result of + ok -> {result, undefined}; + {error, _} = Err -> Err + end; + _ -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), ejabberd_option:language())} - end - end, + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other + {result, {_, Result}} -> {result, Result}; + Other -> Other end. --spec get_presence_and_roster_permissions( - host(), jid(), [ljid()], accessModel(), - [binary()]) -> {boolean(), boolean()}. + +-spec get_presence_and_roster_permissions(host(), + jid(), + [ljid()], + accessModel(), + [binary()]) -> {boolean(), boolean()}. get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups) -> - if (AccessModel == presence) or (AccessModel == roster) -> - case Host of - {User, Server, _} -> - get_roster_info(User, Server, From, AllowedGroups); - _ -> - [{OUser, OServer, _} | _] = Owners, - get_roster_info(OUser, OServer, From, AllowedGroups) - end; - true -> - {true, true} + if + (AccessModel == presence) or (AccessModel == roster) -> + case Host of + {User, Server, _} -> + get_roster_info(User, Server, From, AllowedGroups); + _ -> + [{OUser, OServer, _} | _] = Owners, + get_roster_info(OUser, OServer, From, AllowedGroups) + end; + true -> + {true, true} end. + -spec get_roster_info(binary(), binary(), ljid() | jid(), [binary()]) -> {boolean(), boolean()}. get_roster_info(_, _, {<<>>, <<>>, _}, _) -> {false, false}; get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> LJID = {SubscriberUser, SubscriberServer, <<>>}, {Subscription, _Ask, Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, - OwnerServer, {none, none, []}, - [OwnerUser, OwnerServer, LJID]), + OwnerServer, + {none, none, []}, + [OwnerUser, OwnerServer, LJID]), PresenceSubscription = Subscription == both orelse - Subscription == from orelse - {OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer}, - RosterGroup = lists:any(fun (Group) -> - lists:member(Group, AllowedGroups) - end, - Groups), + Subscription == from orelse + {OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer}, + RosterGroup = lists:any(fun(Group) -> + lists:member(Group, AllowedGroups) + end, + Groups), {PresenceSubscription, RosterGroup}; get_roster_info(OwnerUser, OwnerServer, JID, AllowedGroups) -> get_roster_info(OwnerUser, OwnerServer, jid:tolower(JID), AllowedGroups). + -spec preconditions_met(pubsub_publish_options:result(), - pubsub_node_config:result()) -> boolean(). + pubsub_node_config:result()) -> boolean(). preconditions_met(PubOpts, NodeOpts) -> lists:all(fun(Opt) -> lists:member(Opt, NodeOpts) end, PubOpts). + -spec service_jid(jid() | ljid() | binary()) -> jid(). service_jid(#jid{} = Jid) -> Jid; service_jid({U, S, R}) -> jid:make(U, S, R); service_jid(Host) -> jid:make(Host). + %% @doc

    Check if a notification must be delivered or not based on %% node and subscription options.

    -spec is_to_deliver(ljid(), items | nodes, integer(), nodeOptions(), subOptions()) -> boolean(). is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> - sub_to_deliver(LJID, NotifyType, Depth, SubOptions) - andalso node_to_deliver(LJID, NodeOptions). + sub_to_deliver(LJID, NotifyType, Depth, SubOptions) andalso + node_to_deliver(LJID, NodeOptions). + -spec sub_to_deliver(ljid(), items | nodes, integer(), subOptions()) -> boolean(). sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> - lists:all(fun (Option) -> - sub_option_can_deliver(NotifyType, Depth, Option) - end, - SubOptions). + lists:all(fun(Option) -> + sub_option_can_deliver(NotifyType, Depth, Option) + end, + SubOptions). + -spec node_to_deliver(ljid(), nodeOptions()) -> boolean(). node_to_deliver(LJID, NodeOptions) -> presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)). + -spec sub_option_can_deliver(items | nodes, integer(), _) -> boolean(). sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; @@ -2698,71 +3330,77 @@ sub_option_can_deliver(_, _, {deliver, false}) -> false; sub_option_can_deliver(_, _, {expire, When}) -> erlang:timestamp() < When; sub_option_can_deliver(_, _, _) -> true. + -spec presence_can_deliver(ljid(), boolean()) -> boolean(). presence_can_deliver(_, false) -> true; presence_can_deliver({User, Server, Resource}, true) -> case ejabberd_sm:get_user_present_resources(User, Server) of - [] -> - false; - Ss -> - lists:foldl(fun - (_, true) -> - true; - ({_, R}, _Acc) -> - case Resource of - <<>> -> true; - R -> true; - _ -> false - end - end, - false, Ss) + [] -> + false; + Ss -> + lists:foldl(fun(_, true) -> + true; + ({_, R}, _Acc) -> + case Resource of + <<>> -> true; + R -> true; + _ -> false + end + end, + false, + Ss) end. + -spec state_can_deliver(ljid(), subOptions()) -> [ljid()]. state_can_deliver({U, S, R}, []) -> [{U, S, R}]; state_can_deliver({U, S, R}, SubOptions) -> case lists:keysearch(show_values, 1, SubOptions) of - %% If not in suboptions, item can be delivered, case doesn't apply - false -> [{U, S, R}]; - %% If in a suboptions ... - {_, {_, ShowValues}} -> - Resources = case R of - %% If the subscriber JID is a bare one, get all its resources - <<>> -> user_resources(U, S); - %% If the subscriber JID is a full one, use its resource - R -> [R] - end, - lists:foldl(fun (Resource, Acc) -> - get_resource_state({U, S, Resource}, ShowValues, Acc) - end, - [], Resources) + %% If not in suboptions, item can be delivered, case doesn't apply + false -> [{U, S, R}]; + %% If in a suboptions ... + {_, {_, ShowValues}} -> + Resources = case R of + %% If the subscriber JID is a bare one, get all its resources + <<>> -> user_resources(U, S); + %% If the subscriber JID is a full one, use its resource + R -> [R] + end, + lists:foldl(fun(Resource, Acc) -> + get_resource_state({U, S, Resource}, ShowValues, Acc) + end, + [], + Resources) end. + -spec get_resource_state(ljid(), [binary()], [ljid()]) -> [ljid()]. get_resource_state({U, S, R}, ShowValues, JIDs) -> case ejabberd_sm:get_session_pid(U, S, R) of - none -> - %% If no PID, item can be delivered - lists:append([{U, S, R}], JIDs); - Pid -> - Show = case ejabberd_c2s:get_presence(Pid) of - #presence{type = unavailable} -> <<"unavailable">>; - #presence{show = undefined} -> <<"online">>; - #presence{show = Sh} -> atom_to_binary(Sh, latin1) - end, - case lists:member(Show, ShowValues) of - %% If yes, item can be delivered - true -> lists:append([{U, S, R}], JIDs); - %% If no, item can't be delivered - false -> JIDs - end + none -> + %% If no PID, item can be delivered + lists:append([{U, S, R}], JIDs); + Pid -> + Show = case ejabberd_c2s:get_presence(Pid) of + #presence{type = unavailable} -> <<"unavailable">>; + #presence{show = undefined} -> <<"online">>; + #presence{show = Sh} -> atom_to_binary(Sh, latin1) + end, + case lists:member(Show, ShowValues) of + %% If yes, item can be delivered + true -> lists:append([{U, S, R}], JIDs); + %% If no, item can't be delivered + false -> JIDs + end end. + -spec payload_xmlelements([xmlel()]) -> non_neg_integer(). payload_xmlelements(Payload) -> payload_xmlelements(Payload, 0). + -spec payload_xmlelements([xmlel()], non_neg_integer()) -> non_neg_integer(). payload_xmlelements([], Count) -> Count; payload_xmlelements([#xmlel{} | Tail], Count) -> @@ -2770,134 +3408,212 @@ payload_xmlelements([#xmlel{} | Tail], Count) -> payload_xmlelements([_ | Tail], Count) -> payload_xmlelements(Tail, Count). + -spec items_els(binary(), nodeOptions(), [#pubsub_item{}]) -> ps_items(). items_els(Node, Options, Items) -> Els = case get_option(Options, itemreply) of - publisher -> - [#ps_item{id = ItemId, sub_els = Payload, publisher = jid:encode(USR)} - || #pubsub_item{itemid = {ItemId, _}, payload = Payload, modification = {_, USR}} - <- Items]; - _ -> - [#ps_item{id = ItemId, sub_els = Payload} - || #pubsub_item{itemid = {ItemId, _}, payload = Payload} - <- Items] - end, + publisher -> + [ #ps_item{id = ItemId, sub_els = Payload, publisher = jid:encode(USR)} + || #pubsub_item{itemid = {ItemId, _}, payload = Payload, modification = {_, USR}} <- Items ]; + _ -> + [ #ps_item{id = ItemId, sub_els = Payload} + || #pubsub_item{itemid = {ItemId, _}, payload = Payload} <- Items ] + end, #ps_items{node = Node, items = Els}. + %%%%%% broadcast functions --spec broadcast_publish_item(host(), binary(), nodeIdx(), binary(), - nodeOptions(), binary(), jid(), [xmlel()], _) -> - {result, boolean()}. + +-spec broadcast_publish_item(host(), + binary(), + nodeIdx(), + binary(), + nodeOptions(), + binary(), + jid(), + [xmlel()], + _) -> + {result, boolean()}. broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payload, Removed) -> case get_collection_subscriptions(Host, Node) of - {result, SubsByDepth} -> - ItemPublisher = case get_option(NodeOptions, itemreply) of - publisher -> jid:encode(From); - _ -> <<>> - end, - ItemPayload = case get_option(NodeOptions, deliver_payloads) of - true -> Payload; - false -> [] - end, - ItemsEls = #ps_items{node = Node, - items = [#ps_item{id = ItemId, - publisher = ItemPublisher, - sub_els = ItemPayload}]}, - Stanza = #message{ sub_els = [#ps_event{items = ItemsEls}]}, - broadcast_stanza(Host, From, Node, Nidx, Type, - NodeOptions, SubsByDepth, items, Stanza, true), - case Removed of - [] -> - ok; - _ -> - case get_option(NodeOptions, notify_retract) of - true -> - RetractStanza = #message{ - sub_els = - [#ps_event{ - items = #ps_items{ - node = Node, - retract = Removed}}]}, - broadcast_stanza(Host, Node, Nidx, Type, - NodeOptions, SubsByDepth, - items, RetractStanza, true); - _ -> - ok - end - end, - {result, true}; - _ -> - {result, false} + {result, SubsByDepth} -> + ItemPublisher = case get_option(NodeOptions, itemreply) of + publisher -> jid:encode(From); + _ -> <<>> + end, + ItemPayload = case get_option(NodeOptions, deliver_payloads) of + true -> Payload; + false -> [] + end, + ItemsEls = #ps_items{ + node = Node, + items = [#ps_item{ + id = ItemId, + publisher = ItemPublisher, + sub_els = ItemPayload + }] + }, + Stanza = #message{sub_els = [#ps_event{items = ItemsEls}]}, + broadcast_stanza(Host, + From, + Node, + Nidx, + Type, + NodeOptions, + SubsByDepth, + items, + Stanza, + true), + case Removed of + [] -> + ok; + _ -> + case get_option(NodeOptions, notify_retract) of + true -> + RetractStanza = #message{ + sub_els = + [#ps_event{ + items = #ps_items{ + node = Node, + retract = Removed + } + }] + }, + broadcast_stanza(Host, + Node, + Nidx, + Type, + NodeOptions, + SubsByDepth, + items, + RetractStanza, + true); + _ -> + ok + end + end, + {result, true}; + _ -> + {result, false} end. --spec broadcast_retract_items(host(), jid(), binary(), nodeIdx(), binary(), - nodeOptions(), [itemId()]) -> {result, boolean()}. + +-spec broadcast_retract_items(host(), + jid(), + binary(), + nodeIdx(), + binary(), + nodeOptions(), + [itemId()]) -> {result, boolean()}. broadcast_retract_items(Host, Publisher, Node, Nidx, Type, NodeOptions, ItemIds) -> broadcast_retract_items(Host, Publisher, Node, Nidx, Type, NodeOptions, ItemIds, false). --spec broadcast_retract_items(host(), jid(), binary(), nodeIdx(), binary(), - nodeOptions(), [itemId()], boolean()) -> {result, boolean()}. + +-spec broadcast_retract_items(host(), + jid(), + binary(), + nodeIdx(), + binary(), + nodeOptions(), + [itemId()], + boolean()) -> {result, boolean()}. broadcast_retract_items(_Host, _Publisher, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) -> {result, false}; broadcast_retract_items(Host, Publisher, Node, Nidx, Type, NodeOptions, ItemIds, ForceNotify) -> case (get_option(NodeOptions, notify_retract) or ForceNotify) of - true -> - case get_collection_subscriptions(Host, Node) of - {result, SubsByDepth} -> - Stanza = #message{ - sub_els = - [#ps_event{ - items = #ps_items{ - node = Node, - retract = ItemIds}}]}, - broadcast_stanza(Host, Publisher, Node, Nidx, Type, - NodeOptions, SubsByDepth, items, Stanza, true), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} + true -> + case get_collection_subscriptions(Host, Node) of + {result, SubsByDepth} -> + Stanza = #message{ + sub_els = + [#ps_event{ + items = #ps_items{ + node = Node, + retract = ItemIds + } + }] + }, + broadcast_stanza(Host, + Publisher, + Node, + Nidx, + Type, + NodeOptions, + SubsByDepth, + items, + Stanza, + true), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} end. + -spec broadcast_purge_node(host(), binary(), nodeIdx(), binary(), nodeOptions()) -> {result, boolean()}. broadcast_purge_node(Host, Node, Nidx, Type, NodeOptions) -> case get_option(NodeOptions, notify_retract) of - true -> - case get_collection_subscriptions(Host, Node) of - {result, SubsByDepth} -> - Stanza = #message{sub_els = [#ps_event{purge = Node}]}, - broadcast_stanza(Host, Node, Nidx, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} + true -> + case get_collection_subscriptions(Host, Node) of + {result, SubsByDepth} -> + Stanza = #message{sub_els = [#ps_event{purge = Node}]}, + broadcast_stanza(Host, + Node, + Nidx, + Type, + NodeOptions, + SubsByDepth, + nodes, + Stanza, + false), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} end. --spec broadcast_removed_node(host(), binary(), nodeIdx(), binary(), - nodeOptions(), subs_by_depth()) -> {result, boolean()}. + +-spec broadcast_removed_node(host(), + binary(), + nodeIdx(), + binary(), + nodeOptions(), + subs_by_depth()) -> {result, boolean()}. broadcast_removed_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> case get_option(NodeOptions, notify_delete) of - true -> - case SubsByDepth of - [] -> - {result, false}; - _ -> - Stanza = #message{sub_els = [#ps_event{delete = {Node, <<>>}}]}, - broadcast_stanza(Host, Node, Nidx, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), - {result, true} - end; - _ -> - {result, false} + true -> + case SubsByDepth of + [] -> + {result, false}; + _ -> + Stanza = #message{sub_els = [#ps_event{delete = {Node, <<>>}}]}, + broadcast_stanza(Host, + Node, + Nidx, + Type, + NodeOptions, + SubsByDepth, + nodes, + Stanza, + false), + {result, true} + end; + _ -> + {result, false} end. --spec broadcast_created_node(host(), binary(), nodeIdx(), binary(), - nodeOptions(), subs_by_depth()) -> {result, boolean()}. + +-spec broadcast_created_node(host(), + binary(), + nodeIdx(), + binary(), + nodeOptions(), + subs_by_depth()) -> {result, boolean()}. broadcast_created_node(_, _, _, _, _, []) -> {result, false}; broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> @@ -2905,132 +3621,176 @@ broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, true), {result, true}. --spec broadcast_config_notification(host(), binary(), nodeIdx(), binary(), - nodeOptions(), binary()) -> {result, boolean()}. + +-spec broadcast_config_notification(host(), + binary(), + nodeIdx(), + binary(), + nodeOptions(), + binary()) -> {result, boolean()}. broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) -> case get_option(NodeOptions, notify_config) of - true -> - case get_collection_subscriptions(Host, Node) of - {result, SubsByDepth} -> - Content = case get_option(NodeOptions, deliver_payloads) of - true -> - #xdata{type = result, - fields = get_configure_xfields( - Type, NodeOptions, Lang, [])}; - false -> - undefined - end, - Stanza = #message{ - sub_els = [#ps_event{ - configuration = {Node, Content}}]}, - broadcast_stanza(Host, Node, Nidx, Type, - NodeOptions, SubsByDepth, nodes, Stanza, false), - {result, true}; - _ -> - {result, false} - end; - _ -> - {result, false} + true -> + case get_collection_subscriptions(Host, Node) of + {result, SubsByDepth} -> + Content = case get_option(NodeOptions, deliver_payloads) of + true -> + #xdata{ + type = result, + fields = get_configure_xfields( + Type, NodeOptions, Lang, []) + }; + false -> + undefined + end, + Stanza = #message{ + sub_els = [#ps_event{ + configuration = {Node, Content} + }] + }, + broadcast_stanza(Host, + Node, + Nidx, + Type, + NodeOptions, + SubsByDepth, + nodes, + Stanza, + false), + {result, true}; + _ -> + {result, false} + end; + _ -> + {result, false} end. + -spec get_collection_subscriptions(host(), nodeId()) -> {result, subs_by_depth()} | - {error, stanza_error()}. + {error, stanza_error()}. get_collection_subscriptions(Host, Node) -> Action = fun() -> get_node_subs_by_depth(Host, Node, service_jid(Host)) end, transaction(Host, Action, sync_dirty). + -spec get_node_subs_by_depth(host(), nodeId(), jid()) -> {result, subs_by_depth()} | - {error, stanza_error()}. + {error, stanza_error()}. get_node_subs_by_depth(Host, Node, From) -> case tree_call(Host, get_parentnodes_tree, [Host, Node, From]) of - ParentTree when is_list(ParentTree) -> - {result, - lists:filtermap( - fun({Depth, Nodes}) -> - case lists:filtermap( - fun(N) -> - case get_node_subs(Host, N) of - {result, Result} -> {true, {N, Result}}; - _ -> false - end - end, Nodes) of - [] -> false; - Subs -> {true, {Depth, Subs}} - end - end, ParentTree)}; - Error -> - Error + ParentTree when is_list(ParentTree) -> + {result, + lists:filtermap( + fun({Depth, Nodes}) -> + case lists:filtermap( + fun(N) -> + case get_node_subs(Host, N) of + {result, Result} -> {true, {N, Result}}; + _ -> false + end + end, + Nodes) of + [] -> false; + Subs -> {true, {Depth, Subs}} + end + end, + ParentTree)}; + Error -> + Error end. + -spec get_node_subs(host(), #pubsub_node{}) -> {result, [{ljid(), subId(), subOptions()}]} | - {error, stanza_error()}. + {error, stanza_error()}. get_node_subs(Host, #pubsub_node{type = Type, id = Nidx}) -> - WithOptions = lists:member(<<"subscription-options">>, plugin_features(Host, Type)), + WithOptions = lists:member(<<"subscription-options">>, plugin_features(Host, Type)), case node_call(Host, Type, get_node_subscriptions, [Nidx]) of - {result, Subs} -> {result, get_options_for_subs(Host, Nidx, Subs, WithOptions)}; - Other -> Other + {result, Subs} -> {result, get_options_for_subs(Host, Nidx, Subs, WithOptions)}; + Other -> Other end. --spec get_options_for_subs(host(), nodeIdx(), - [{ljid(), subscription(), subId()}], - boolean()) -> - [{ljid(), subId(), subOptions()}]. + +-spec get_options_for_subs(host(), + nodeIdx(), + [{ljid(), subscription(), subId()}], + boolean()) -> + [{ljid(), subId(), subOptions()}]. get_options_for_subs(_Host, _Nidx, Subs, false) -> lists:foldl(fun({JID, subscribed, SubID}, Acc) -> - [{JID, SubID, []} | Acc]; - (_, Acc) -> - Acc - end, [], Subs); + [{JID, SubID, []} | Acc]; + (_, Acc) -> + Acc + end, + [], + Subs); get_options_for_subs(Host, Nidx, Subs, true) -> SubModule = subscription_plugin(Host), lists:foldl(fun({JID, subscribed, SubID}, Acc) -> - case SubModule:get_subscription(JID, Nidx, SubID) of - #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; - {error, notfound} -> [{JID, SubID, []} | Acc] - end; - (_, Acc) -> - Acc - end, [], Subs). + case SubModule:get_subscription(JID, Nidx, SubID) of + #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; + {error, notfound} -> [{JID, SubID, []} | Acc] + end; + (_, Acc) -> + Acc + end, + [], + Subs). --spec broadcast_stanza(host(), nodeId(), nodeIdx(), binary(), - nodeOptions(), subs_by_depth(), - items | nodes, stanza(), boolean()) -> ok. + +-spec broadcast_stanza(host(), + nodeId(), + nodeIdx(), + binary(), + nodeOptions(), + subs_by_depth(), + items | nodes, + stanza(), + boolean()) -> ok. broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> NotificationType = get_option(NodeOptions, notification_type, headline), - BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but useful + BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but useful Stanza = add_message_type( - xmpp:set_from(BaseStanza, service_jid(Host)), - NotificationType), + xmpp:set_from(BaseStanza, service_jid(Host)), + NotificationType), %% Handles explicit subscriptions SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), - lists:foreach(fun ({LJID, _NodeName, SubIDs}) -> - LJIDs = case BroadcastAll of - true -> - {U, S, _} = LJID, - [{U, S, R} || R <- user_resources(U, S)]; - false -> - [LJID] - end, - %% Determine if the stanza should have SHIM ('SubID' and 'name') headers - StanzaToSend = case {SHIM, SubIDs} of - {false, _} -> - Stanza; - %% If there's only one SubID, don't add it - {true, [_]} -> - Stanza; - {true, SubIDs} -> - add_shim_headers(Stanza, subid_shim(SubIDs)) - end, - lists:foreach(fun(To) -> - ejabberd_router:route( - xmpp:set_to(xmpp:put_meta(StanzaToSend, ignore_sm_bounce, true), - jid:make(To))) - end, LJIDs) - end, SubIDsByJID). + lists:foreach(fun({LJID, _NodeName, SubIDs}) -> + LJIDs = case BroadcastAll of + true -> + {U, S, _} = LJID, + [ {U, S, R} || R <- user_resources(U, S) ]; + false -> + [LJID] + end, + %% Determine if the stanza should have SHIM ('SubID' and 'name') headers + StanzaToSend = case {SHIM, SubIDs} of + {false, _} -> + Stanza; + %% If there's only one SubID, don't add it + {true, [_]} -> + Stanza; + {true, SubIDs} -> + add_shim_headers(Stanza, subid_shim(SubIDs)) + end, + lists:foreach(fun(To) -> + ejabberd_router:route( + xmpp:set_to(xmpp:put_meta(StanzaToSend, ignore_sm_bounce, true), + jid:make(To))) + end, + LJIDs) + end, + SubIDsByJID). --spec broadcast_stanza(host(), jid(), nodeId(), nodeIdx(), binary(), - nodeOptions(), subs_by_depth(), items | nodes, - stanza(), boolean()) -> ok. + +-spec broadcast_stanza(host(), + jid(), + nodeId(), + nodeIdx(), + binary(), + nodeOptions(), + subs_by_depth(), + items | nodes, + stanza(), + boolean()) -> ok. broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> broadcast_stanza({LUser, LServer, <<>>}, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM), %% Handles implicit presence subscriptions @@ -3042,94 +3802,109 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeO Owner = jid:make(LUser, LServer), FromBareJid = xmpp:set_from(BaseStanza, Owner), Stanza = add_extended_headers( - add_message_type(FromBareJid, NotificationType), - extended_headers([Publisher])), + add_message_type(FromBareJid, NotificationType), + extended_headers([Publisher])), Pred = fun(To) -> delivery_permitted(Owner, To, NodeOptions) end, ejabberd_sm:route(jid:make(LUser, LServer, SenderResource), - {pep_message, <<((Node))/binary, "+notify">>, Stanza, Pred}); + {pep_message, <<((Node))/binary, "+notify">>, Stanza, Pred}); broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM). + -spec c2s_handle_info(ejabberd_c2s:state(), term()) -> ejabberd_c2s:state(). c2s_handle_info(#{lserver := LServer} = C2SState, - {pep_message, Feature, Packet, Pred}) when is_function(Pred) -> - [maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) - || {USR, Caps} <- mod_caps:list_features(C2SState), Pred(USR)], + {pep_message, Feature, Packet, Pred}) when is_function(Pred) -> + [ maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) + || {USR, Caps} <- mod_caps:list_features(C2SState), Pred(USR) ], {stop, C2SState}; c2s_handle_info(#{lserver := LServer} = C2SState, - {pep_message, Feature, Packet, {_, _, _} = USR}) -> + {pep_message, Feature, Packet, {_, _, _} = USR}) -> case mod_caps:get_user_caps(USR, C2SState) of - {ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet); - error -> ok + {ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet); + error -> ok end, {stop, C2SState}; c2s_handle_info(C2SState, _) -> C2SState. --spec send_items(host(), nodeId(), nodeIdx(), binary(), - nodeOptions(), ljid(), last | integer()) -> ok. + +-spec send_items(host(), + nodeId(), + nodeIdx(), + binary(), + nodeOptions(), + ljid(), + last | integer()) -> ok. send_items(Host, Node, Nidx, Type, Options, LJID, Number) -> send_items(Host, Node, Nidx, Type, Options, Host, LJID, LJID, Number). + + send_items(Host, Node, Nidx, Type, Options, Publisher, SubLJID, ToLJID, Number) -> Items = case max_items(Host, Options) of - 1 -> - get_only_item(Host, Type, Nidx, SubLJID); - _ -> - get_last_items(Host, Type, Nidx, SubLJID, Number) - end, + 1 -> + get_only_item(Host, Type, Nidx, SubLJID); + _ -> + get_last_items(Host, Type, Nidx, SubLJID, Number) + end, case Items of - [] -> - ok; - Items -> - Delay = case Number of - last -> % handle section 6.1.7 of XEP-0060 - [Last] = Items, - {Stamp, _USR} = Last#pubsub_item.modification, - [#delay{stamp = Stamp}]; - _ -> - [] - end, - Stanza = #message{ - sub_els = [#ps_event{items = items_els(Node, Options, Items)} - | Delay]}, - NotificationType = get_option(Options, notification_type, headline), - send_stanza(Publisher, ToLJID, Node, - add_message_type(Stanza, NotificationType)) + [] -> + ok; + Items -> + Delay = case Number of + last -> % handle section 6.1.7 of XEP-0060 + [Last] = Items, + {Stamp, _USR} = Last#pubsub_item.modification, + [#delay{stamp = Stamp}]; + _ -> + [] + end, + Stanza = #message{ + sub_els = [#ps_event{items = items_els(Node, Options, Items)} | Delay] + }, + NotificationType = get_option(Options, notification_type, headline), + send_stanza(Publisher, + ToLJID, + Node, + add_message_type(Stanza, NotificationType)) end. + -spec send_stanza(host(), ljid(), binary(), stanza()) -> ok. send_stanza({LUser, LServer, _} = Publisher, USR, Node, BaseStanza) -> Stanza = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)), USRs = case USR of - {PUser, PServer, <<>>} -> - [{PUser, PServer, PRessource} - || PRessource <- user_resources(PUser, PServer)]; - _ -> - [USR] - end, + {PUser, PServer, <<>>} -> + [ {PUser, PServer, PRessource} + || PRessource <- user_resources(PUser, PServer) ]; + _ -> + [USR] + end, lists:foreach( fun(To) -> - ejabberd_sm:route( - jid:make(Publisher), - {pep_message, <<((Node))/binary, "+notify">>, - add_extended_headers( - Stanza, extended_headers([jid:make(Publisher)])), - To}) - end, USRs); + ejabberd_sm:route( + jid:make(Publisher), + {pep_message, <<((Node))/binary, "+notify">>, + add_extended_headers( + Stanza, extended_headers([jid:make(Publisher)])), + To}) + end, + USRs); send_stanza(Host, USR, _Node, Stanza) -> ejabberd_router:route( xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))). + -spec maybe_send_pep_stanza(binary(), ljid(), caps(), binary(), stanza()) -> ok. maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) -> Features = mod_caps:get_features(LServer, Caps), case lists:member(Feature, Features) of - true -> - ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR))); - false -> - ok + true -> + ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR))); + false -> + ok end. + -spec send_last_items(jid()) -> ok. send_last_items(JID) -> ServerHost = JID#jid.lserver, @@ -3139,17 +3914,24 @@ send_last_items(JID) -> BJID = jid:remove_resource(LJID), lists:foreach( fun(PType) -> - Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), - lists:foreach( - fun({#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, - options = Options}, _, SubJID}) - when Type == PType-> - send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1); - (_) -> - ok - end, - lists:usort(Subs)) - end, config(ServerHost, plugins)). + Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), + lists:foreach( + fun({#pubsub_node{ + nodeid = {_, Node}, + type = Type, + id = Nidx, + options = Options + }, + _, + SubJID}) + when Type == PType -> + send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1); + (_) -> + ok + end, + lists:usort(Subs)) + end, + config(ServerHost, plugins)). % pep_from_offline hack can not work anymore, as sender c2s does not % exists when sender is offline, so we can't get match receiver caps % does it make sens to send PEP from an offline contact anyway ? @@ -3171,115 +3953,135 @@ send_last_items(JID) -> % true -> % ok % end. + + send_last_pep(From, To, Features) -> ServerHost = From#jid.lserver, Host = host(ServerHost), Publisher = jid:tolower(From), Owner = jid:remove_resource(Publisher), NotifyNodes = - case Features of - _ when is_list(Features) -> - lists:filtermap( - fun(V) -> - Vs = byte_size(V) - 7, - case V of - <> -> - {true, NotNode}; - _ -> - false - end - end, Features); - _ -> - unknown - end, + case Features of + _ when is_list(Features) -> + lists:filtermap( + fun(V) -> + Vs = byte_size(V) - 7, + case V of + <> -> + {true, NotNode}; + _ -> + false + end + end, + Features); + _ -> + unknown + end, case tree_action(Host, get_nodes, [Owner, infinity]) of Nodes when is_list(Nodes) -> lists:foreach( - fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> - MaybeNotify = - case NotifyNodes of - unknown -> true; - _ -> lists:member(Node, NotifyNodes) - end, - case MaybeNotify andalso match_option(Options, send_last_published_item, on_sub_and_presence) of - true -> - case delivery_permitted(From, To, Options) of - true -> - LJID = jid:tolower(To), - send_items(Owner, Node, Nidx, Type, Options, - Publisher, LJID, LJID, 1); - false -> - ok - end; - _ -> - ok - end - end, Nodes); + fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> + MaybeNotify = + case NotifyNodes of + unknown -> true; + _ -> lists:member(Node, NotifyNodes) + end, + case MaybeNotify andalso match_option(Options, send_last_published_item, on_sub_and_presence) of + true -> + case delivery_permitted(From, To, Options) of + true -> + LJID = jid:tolower(To), + send_items(Owner, + Node, + Nidx, + Type, + Options, + Publisher, + LJID, + LJID, + 1); + false -> + ok + end; + _ -> + ok + end + end, + Nodes); _ -> ok end. + -spec subscribed_nodes_by_jid(items | nodes, subs_by_depth()) -> [{ljid(), binary(), subId()}]. subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> - NodesToDeliver = fun (Depth, Node, Subs, Acc) -> - NodeName = case Node#pubsub_node.nodeid of - {_, N} -> N; - Other -> Other - end, - NodeOptions = Node#pubsub_node.options, - lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> - case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of - true -> - case state_can_deliver(LJID, SubOptions) of - [] -> {JIDs, Recipients}; - [LJID] -> {JIDs, [{LJID, NodeName, [SubID]} | Recipients]}; - JIDsToDeliver -> - lists:foldl( - fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> - case lists:member(JIDToDeliver, JIDs) of - %% check if the JIDs co-accumulator contains the Subscription Jid, - false -> - %% - if not, - %% - add the Jid to JIDs list co-accumulator ; - %% - create a tuple of the Jid, Nidx, and SubID (as list), - %% and add the tuple to the Recipients list co-accumulator - {[JIDToDeliver | JIDsAcc], - [{JIDToDeliver, NodeName, [SubID]} - | RecipientsAcc]}; - true -> - %% - if the JIDs co-accumulator contains the Jid - %% get the tuple containing the Jid from the Recipient list co-accumulator - {_, {JIDToDeliver, NodeName1, SubIDs}} = - lists:keysearch(JIDToDeliver, 1, RecipientsAcc), - %% delete the tuple from the Recipients list - % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), - % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), - %% add the SubID to the SubIDs list in the tuple, - %% and add the tuple back to the Recipients list co-accumulator - % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} - % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} - % v2: {JIDs, Recipients1} - {JIDsAcc, - lists:keyreplace(JIDToDeliver, 1, - RecipientsAcc, - {JIDToDeliver, NodeName1, - [SubID | SubIDs]})} - end - end, {JIDs, Recipients}, JIDsToDeliver) - end; - false -> - {JIDs, Recipients} - end - end, Acc, Subs) - end, + NodesToDeliver = fun(Depth, Node, Subs, Acc) -> + NodeName = case Node#pubsub_node.nodeid of + {_, N} -> N; + Other -> Other + end, + NodeOptions = Node#pubsub_node.options, + lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> + case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of + true -> + case state_can_deliver(LJID, SubOptions) of + [] -> {JIDs, Recipients}; + [LJID] -> {JIDs, [{LJID, NodeName, [SubID]} | Recipients]}; + JIDsToDeliver -> + lists:foldl( + fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> + case lists:member(JIDToDeliver, JIDs) of + %% check if the JIDs co-accumulator contains the Subscription Jid, + false -> + %% - if not, + %% - add the Jid to JIDs list co-accumulator ; + %% - create a tuple of the Jid, Nidx, and SubID (as list), + %% and add the tuple to the Recipients list co-accumulator + {[JIDToDeliver | JIDsAcc], + [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]}; + true -> + %% - if the JIDs co-accumulator contains the Jid + %% get the tuple containing the Jid from the Recipient list co-accumulator + {_, {JIDToDeliver, NodeName1, SubIDs}} = + lists:keysearch(JIDToDeliver, 1, RecipientsAcc), + %% delete the tuple from the Recipients list + % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), + % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), + %% add the SubID to the SubIDs list in the tuple, + %% and add the tuple back to the Recipients list co-accumulator + % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} + % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} + % v2: {JIDs, Recipients1} + {JIDsAcc, + lists:keyreplace(JIDToDeliver, + 1, + RecipientsAcc, + {JIDToDeliver, + NodeName1, + [SubID | SubIDs]})} + end + end, + {JIDs, Recipients}, + JIDsToDeliver) + end; + false -> + {JIDs, Recipients} + end + end, + Acc, + Subs) + end, DepthsToDeliver = fun({Depth, SubsByNode}, Acc1) -> - lists:foldl(fun({Node, Subs}, Acc2) -> - NodesToDeliver(Depth, Node, Subs, Acc2) - end, Acc1, SubsByNode) - end, + lists:foldl(fun({Node, Subs}, Acc2) -> + NodesToDeliver(Depth, Node, Subs, Acc2) + end, + Acc1, + SubsByNode) + end, {_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth), JIDSubs. + -spec delivery_permitted(jid() | ljid(), jid() | ljid(), nodeOptions()) -> boolean(). delivery_permitted(From, To, Options) -> LFrom = jid:tolower(From), @@ -3288,53 +4090,61 @@ delivery_permitted(From, To, Options) -> %% TODO: Fix the 'whitelist'/'authorize' cases for last PEP notifications. %% Currently, only node owners receive those. case get_option(Options, access_model) of - open -> true; - presence -> true; - whitelist -> RecipientIsOwner; - authorize -> RecipientIsOwner; - roster -> - Grps = get_option(Options, roster_groups_allowed, []), - {LUser, LServer, _} = LFrom, - {_, IsInGrp} = get_roster_info(LUser, LServer, LTo, Grps), - IsInGrp + open -> true; + presence -> true; + whitelist -> RecipientIsOwner; + authorize -> RecipientIsOwner; + roster -> + Grps = get_option(Options, roster_groups_allowed, []), + {LUser, LServer, _} = LFrom, + {_, IsInGrp} = get_roster_info(LUser, LServer, LTo, Grps), + IsInGrp end. + -spec user_resources(binary(), binary()) -> [binary()]. user_resources(User, Server) -> ejabberd_sm:get_user_resources(User, Server). + -spec user_resource(binary(), binary(), binary()) -> binary(). user_resource(User, Server, <<>>) -> case user_resources(User, Server) of - [R | _] -> R; - _ -> <<>> + [R | _] -> R; + _ -> <<>> end; user_resource(_, _, Resource) -> Resource. + %%%%%%% Configuration handling --spec get_configure(host(), binary(), binary(), jid(), - binary()) -> {error, stanza_error()} | {result, pubsub_owner()}. +-spec get_configure(host(), + binary(), + binary(), + jid(), + binary()) -> {error, stanza_error()} | {result, pubsub_owner()}. get_configure(Host, ServerHost, Node, From, Lang) -> - Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> - case node_call(Host, Type, get_affiliation, [Nidx, From]) of - {result, owner} -> - Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), - Fs = get_configure_xfields(Type, Options, Lang, Groups), - {result, #pubsub_owner{ - configure = - {Node, #xdata{type = form, fields = Fs}}}}; - {result, _} -> - {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; - Error -> - Error - end - end, + Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx}) -> + case node_call(Host, Type, get_affiliation, [Nidx, From]) of + {result, owner} -> + Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), + Fs = get_configure_xfields(Type, Options, Lang, Groups), + {result, #pubsub_owner{ + configure = + {Node, #xdata{type = form, fields = Fs}} + }}; + {result, _} -> + {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; + Error -> + Error + end + end, case transaction(Host, Node, Action, sync_dirty) of - {result, {_, Result}} -> {result, Result}; - Other -> Other + {result, {_, Result}} -> {result, Result}; + Other -> Other end. + -spec get_default(host(), binary(), jid(), binary()) -> {result, pubsub_owner()}. get_default(Host, Node, _From, Lang) -> Type = select_type(serverhost(Host), Host, Node), @@ -3342,6 +4152,7 @@ get_default(Host, Node, _From, Lang) -> Fs = get_configure_xfields(Type, Options, Lang, []), {result, #pubsub_owner{default = {<<>>, #xdata{type = form, fields = Fs}}}}. + -spec match_option(#pubsub_node{} | [{atom(), any()}], atom(), any()) -> boolean(). match_option(Node, Var, Val) when is_record(Node, pubsub_node) -> match_option(Node#pubsub_node.options, Var, Val); @@ -3350,115 +4161,130 @@ match_option(Options, Var, Val) when is_list(Options) -> match_option(_, _, _) -> false. + -spec get_option([{atom(), any()}], atom()) -> any(). get_option([], _) -> false; get_option(Options, Var) -> get_option(Options, Var, false). + -spec get_option([{atom(), any()}], atom(), any()) -> any(). get_option(Options, Var, Def) -> case lists:keysearch(Var, 1, Options) of - {value, {_Val, Ret}} -> Ret; - _ -> Def + {value, {_Val, Ret}} -> Ret; + _ -> Def end. + -spec node_options(host(), binary()) -> [{atom(), any()}]. node_options(Host, Type) -> ConfigOpts = config(Host, default_node_config), PluginOpts = node_plugin_options(Host, Type), merge_config([ConfigOpts, PluginOpts]). + -spec node_plugin_options(host(), binary()) -> [{atom(), any()}]. node_plugin_options(Host, Type) -> Module = plugin(Host, Type), case {lists:member(Type, config(Host, plugins)), catch Module:options()} of - {true, Opts} when is_list(Opts) -> - Opts; - {_, _} -> - DefaultModule = plugin(Host, ?STDNODE), - DefaultModule:options() + {true, Opts} when is_list(Opts) -> + Opts; + {_, _} -> + DefaultModule = plugin(Host, ?STDNODE), + DefaultModule:options() end. + -spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. node_owners_action(Host, Type, Nidx, []) -> case node_action(Host, Type, get_node_affiliations, [Nidx]) of - {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; - _ -> [] + {result, Affs} -> [ LJID || {LJID, Aff} <- Affs, Aff =:= owner ]; + _ -> [] end; node_owners_action(_Host, _Type, _Nidx, Owners) -> Owners. + -spec node_owners_call(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. node_owners_call(Host, Type, Nidx, []) -> case node_call(Host, Type, get_node_affiliations, [Nidx]) of - {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; - _ -> [] + {result, Affs} -> [ LJID || {LJID, Aff} <- Affs, Aff =:= owner ]; + _ -> [] end; node_owners_call(_Host, _Type, _Nidx, Owners) -> Owners. + node_config(Node, ServerHost) -> Opts = mod_pubsub_opt:force_node_config(ServerHost), node_config(Node, ServerHost, Opts). -node_config(Node, ServerHost, [{RE, Opts}|NodeOpts]) -> + +node_config(Node, ServerHost, [{RE, Opts} | NodeOpts]) -> case re:run(Node, RE) of - {match, _} -> - Opts; - nomatch -> - node_config(Node, ServerHost, NodeOpts) + {match, _} -> + Opts; + nomatch -> + node_config(Node, ServerHost, NodeOpts) end; node_config(_, _, []) -> []. + %% @doc

    Return the maximum number of items for a given node.

    %%

    Unlimited means that there is no limit in the number of items that can %% be stored.

    -spec max_items(host(), [{atom(), any()}]) -> non_neg_integer() | unlimited. max_items(Host, Options) -> case get_option(Options, persist_items) of - true -> - case get_option(Options, max_items) of - I when is_integer(I), I < 0 -> 0; - I when is_integer(I) -> I; - _ -> get_max_items_node(Host) - end; - false -> - case get_option(Options, send_last_published_item) of - never -> - 0; - _ -> - case is_last_item_cache_enabled(Host) of - true -> 0; - false -> 1 - end - end + true -> + case get_option(Options, max_items) of + I when is_integer(I), I < 0 -> 0; + I when is_integer(I) -> I; + _ -> get_max_items_node(Host) + end; + false -> + case get_option(Options, send_last_published_item) of + never -> + 0; + _ -> + case is_last_item_cache_enabled(Host) of + true -> 0; + false -> 1 + end + 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) + 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()]. + +-spec get_configure_xfields(_, + pubsub_node_config:result(), + binary(), + [binary()]) -> [xdata_field()]. get_configure_xfields(_Type, Options, Lang, Groups) -> pubsub_node_config:encode( lists:filtermap( - fun({roster_groups_allowed, Value}) -> - {true, {roster_groups_allowed, Value, Groups}}; - ({sql, _}) -> false; - ({rsm, _}) -> false; - ({Item, infinity}) when Item == max_items; - Item == item_expire; - Item == children_max -> - {true, {Item, max}}; - (_) -> true - end, Options), + fun({roster_groups_allowed, Value}) -> + {true, {roster_groups_allowed, Value, Groups}}; + ({sql, _}) -> false; + ({rsm, _}) -> false; + ({Item, infinity}) when Item == max_items; + Item == item_expire; + Item == children_max -> + {true, {Item, max}}; + (_) -> true + end, + Options), Lang). + %%

    There are several reasons why the node configuration request might fail:

    %%
      %%
    • The service does not support node configuration.
    • @@ -3467,300 +4293,345 @@ get_configure_xfields(_Type, Options, Lang, Groups) -> %%
    • The node has no configuration options.
    • %%
    • The specified node does not exist.
    • %%
    --spec set_configure(host(), binary(), jid(), [{binary(), [binary()]}], - binary()) -> {result, undefined} | {error, stanza_error()}. +-spec set_configure(host(), + binary(), + jid(), + [{binary(), [binary()]}], + binary()) -> {result, undefined} | {error, stanza_error()}. set_configure(_Host, <<>>, _From, _Config, _Lang) -> {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; set_configure(Host, Node, From, Config, Lang) -> Action = - fun(#pubsub_node{options = Options, type = Type, id = Nidx} = N) -> - case node_call(Host, Type, get_affiliation, [Nidx, From]) of - {result, owner} -> - OldOpts = case Options of - [] -> node_options(Host, Type); - _ -> Options - end, - NewOpts = merge_config( - [node_config(Node, serverhost(Host)), - Config, OldOpts]), - case tree_call(Host, - set_node, - [N#pubsub_node{options = NewOpts}]) of - {result, Nidx} -> {result, NewOpts}; - ok -> {result, NewOpts}; - Err -> Err - end; - {result, _} -> - {error, xmpp:err_forbidden( - ?T("Owner privileges required"), Lang)}; - Error -> - Error - end - end, + fun(#pubsub_node{options = Options, type = Type, id = Nidx} = N) -> + case node_call(Host, Type, get_affiliation, [Nidx, From]) of + {result, owner} -> + OldOpts = case Options of + [] -> node_options(Host, Type); + _ -> Options + end, + NewOpts = merge_config( + [node_config(Node, serverhost(Host)), + Config, + OldOpts]), + case tree_call(Host, + set_node, + [N#pubsub_node{options = NewOpts}]) of + {result, Nidx} -> {result, NewOpts}; + ok -> {result, NewOpts}; + Err -> Err + end; + {result, _} -> + {error, xmpp:err_forbidden( + ?T("Owner privileges required"), Lang)}; + Error -> + Error + end + end, case transaction(Host, Node, Action, transaction) of - {result, {TNode, Options}} -> - Nidx = TNode#pubsub_node.id, - Type = TNode#pubsub_node.type, - broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang), - {result, undefined}; - Other -> - Other + {result, {TNode, Options}} -> + Nidx = TNode#pubsub_node.id, + Type = TNode#pubsub_node.type, + broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang), + {result, undefined}; + Other -> + Other end. + -spec merge_config([[proplists:property()]]) -> [proplists:property()]. merge_config(ListOfConfigs) -> lists:ukeysort(1, lists:flatten(ListOfConfigs)). + -spec decode_node_config(undefined | xdata(), binary(), binary()) -> - pubsub_node_config:result() | - {error, stanza_error()}. + pubsub_node_config:result() | + {error, stanza_error()}. decode_node_config(undefined, _, _) -> []; decode_node_config(#xdata{fields = Fs}, Host, Lang) -> try - Config = pubsub_node_config:decode(Fs), - 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} -> - Config; - {true, true, false} -> - erlang:error( - {pubsub_node_config, - {bad_var_value, <<"pubsub#max_payload_size">>, - ?NS_PUBSUB_NODE_CONFIG}}); - {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">>, - ?NS_PUBSUB_NODE_CONFIG}}) - end - catch _:{pubsub_node_config, Why} -> - Txt = pubsub_node_config:format_error(Why), - {error, xmpp:err_resource_constraint(Txt, Lang)} + Config = pubsub_node_config:decode(Fs), + 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} -> + Config; + {true, true, false} -> + erlang:error( + {pubsub_node_config, + {bad_var_value, <<"pubsub#max_payload_size">>, + ?NS_PUBSUB_NODE_CONFIG}}); + {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">>, + ?NS_PUBSUB_NODE_CONFIG}}) + end + catch + _:{pubsub_node_config, Why} -> + Txt = pubsub_node_config:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} end. + -spec decode_subscribe_options(undefined | xdata(), binary()) -> - pubsub_subscribe_options:result() | - {error, stanza_error()}. + pubsub_subscribe_options:result() | + {error, stanza_error()}. decode_subscribe_options(undefined, _) -> []; decode_subscribe_options(#xdata{fields = Fs}, Lang) -> - try pubsub_subscribe_options:decode(Fs) - catch _:{pubsub_subscribe_options, Why} -> - Txt = pubsub_subscribe_options:format_error(Why), - {error, xmpp:err_resource_constraint(Txt, Lang)} + try + pubsub_subscribe_options:decode(Fs) + catch + _:{pubsub_subscribe_options, Why} -> + Txt = pubsub_subscribe_options:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} end. + -spec decode_publish_options(undefined | xdata(), binary()) -> - pubsub_publish_options:result() | - {error, stanza_error()}. + pubsub_publish_options:result() | + {error, stanza_error()}. decode_publish_options(undefined, _) -> []; decode_publish_options(#xdata{fields = Fs}, Lang) -> - try pubsub_publish_options:decode(Fs) - catch _:{pubsub_publish_options, Why} -> - Txt = pubsub_publish_options:format_error(Why), - {error, xmpp:err_resource_constraint(Txt, Lang)} + try + pubsub_publish_options:decode(Fs) + catch + _:{pubsub_publish_options, Why} -> + Txt = pubsub_publish_options:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} end. + -spec decode_get_pending(xdata(), binary()) -> - pubsub_get_pending:result() | - {error, stanza_error()}. + pubsub_get_pending:result() | + {error, stanza_error()}. decode_get_pending(#xdata{fields = Fs}, Lang) -> - try pubsub_get_pending:decode(Fs) - catch _:{pubsub_get_pending, Why} -> - Txt = pubsub_get_pending:format_error(Why), - {error, xmpp:err_resource_constraint(Txt, Lang)} + try + pubsub_get_pending:decode(Fs) + catch + _:{pubsub_get_pending, Why} -> + Txt = pubsub_get_pending:format_error(Why), + {error, xmpp:err_resource_constraint(Txt, Lang)} end. --spec check_opt_range(atom(), [proplists:property()], - non_neg_integer() | unlimited | infinity) -> boolean(). + +-spec check_opt_range(atom(), + [proplists:property()], + 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 + max -> true; + Val -> Val =< Max end. + -spec get_max_items_node(host()) -> unlimited | non_neg_integer(). get_max_items_node(Host) -> 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) -> config(Host, max_subscriptions_node, undefined). + %%%% last item cache handling -spec is_last_item_cache_enabled(host()) -> boolean(). is_last_item_cache_enabled(Host) -> config(Host, last_item_cache, false). + -spec set_cached_item(host(), nodeIdx(), binary(), jid(), [xmlel()]) -> ok. set_cached_item({_, ServerHost, _}, Nidx, ItemId, Publisher, Payload) -> set_cached_item(ServerHost, Nidx, ItemId, Publisher, Payload); set_cached_item(Host, Nidx, ItemId, Publisher, Payload) -> case is_last_item_cache_enabled(Host) of - true -> - Stamp = {erlang:timestamp(), jid:tolower(jid:remove_resource(Publisher))}, - Item = #pubsub_last_item{nodeid = {Host, Nidx}, - itemid = ItemId, - creation = Stamp, - payload = Payload}, - mnesia:dirty_write(Item); - _ -> - ok + true -> + Stamp = {erlang:timestamp(), jid:tolower(jid:remove_resource(Publisher))}, + Item = #pubsub_last_item{ + nodeid = {Host, Nidx}, + itemid = ItemId, + creation = Stamp, + payload = Payload + }, + mnesia:dirty_write(Item); + _ -> + ok end. + -spec unset_cached_item(host(), nodeIdx()) -> ok. unset_cached_item({_, ServerHost, _}, Nidx) -> unset_cached_item(ServerHost, Nidx); unset_cached_item(Host, Nidx) -> case is_last_item_cache_enabled(Host) of - true -> mnesia:dirty_delete({pubsub_last_item, {Host, Nidx}}); - _ -> ok + true -> mnesia:dirty_delete({pubsub_last_item, {Host, Nidx}}); + _ -> ok end. + -spec get_cached_item(host(), nodeIdx()) -> undefined | #pubsub_item{}. get_cached_item({_, ServerHost, _}, Nidx) -> get_cached_item(ServerHost, Nidx); get_cached_item(Host, Nidx) -> case is_last_item_cache_enabled(Host) of - true -> - case mnesia:dirty_read({pubsub_last_item, {Host, Nidx}}) of - [#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] -> - #pubsub_item{itemid = {ItemId, Nidx}, - payload = Payload, creation = Creation, - modification = Creation}; - _ -> - undefined - end; - _ -> - undefined + true -> + case mnesia:dirty_read({pubsub_last_item, {Host, Nidx}}) of + [#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] -> + #pubsub_item{ + itemid = {ItemId, Nidx}, + payload = Payload, + creation = Creation, + modification = Creation + }; + _ -> + undefined + end; + _ -> + undefined end. + %%%% plugin handling -spec host(binary()) -> binary(). host(ServerHost) -> config(ServerHost, host, <<"pubsub.", ServerHost/binary>>). + -spec serverhost(host()) -> binary(). -serverhost({_U, ServerHost, _R})-> +serverhost({_U, ServerHost, _R}) -> serverhost(ServerHost); serverhost(Host) -> ejabberd_router:host_of_route(Host). + -spec tree(host()) -> atom(). tree(Host) -> case config(Host, nodetree) of - undefined -> tree(Host, ?STDTREE); - Tree -> Tree + undefined -> tree(Host, ?STDTREE); + Tree -> Tree end. + -spec tree(host() | atom(), binary()) -> atom(). tree(_Host, <<"virtual">>) -> - nodetree_virtual; % special case, virtual does not use any backend + nodetree_virtual; % special case, virtual does not use any backend tree(Host, Name) -> submodule(Host, <<"nodetree">>, Name). + -spec plugin(host() | atom(), binary()) -> atom(). plugin(Host, Name) -> submodule(Host, <<"node">>, Name). + -spec plugins(host()) -> [binary()]. plugins(Host) -> case config(Host, plugins) of - undefined -> [?STDNODE]; - [] -> [?STDNODE]; - Plugins -> Plugins + undefined -> [?STDNODE]; + [] -> [?STDNODE]; + Plugins -> Plugins end. + -spec subscription_plugin(host() | atom()) -> atom(). subscription_plugin(Host) -> submodule(Host, <<"pubsub">>, <<"subscription">>). + -spec submodule(host() | atom(), binary(), binary()) -> atom(). submodule(Db, Type, Name) when is_atom(Db) -> case Db of - mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]); - _ -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)]) + mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]); + _ -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)]) end; submodule(Host, Type, Name) -> Db = mod_pubsub_opt:db_type(serverhost(Host)), submodule(Db, Type, Name). + -spec config(binary(), any()) -> any(). config(ServerHost, Key) -> config(ServerHost, Key, undefined). + -spec config(host(), any(), any()) -> any(). config({_User, Host, _Resource}, Key, Default) -> config(Host, Key, Default); config(ServerHost, Key, Default) -> case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), Key) of - [{Key, Value}] -> Value; - _ -> Default + [{Key, Value}] -> Value; + _ -> Default end. + -spec select_type(binary(), host(), binary(), binary()) -> binary(). select_type(ServerHost, {_User, _Server, _Resource}, Node, _Type) -> case config(ServerHost, pep_mapping) of - undefined -> ?PEPNODE; - Mapping -> proplists:get_value(Node, Mapping, ?PEPNODE) + undefined -> ?PEPNODE; + Mapping -> proplists:get_value(Node, Mapping, ?PEPNODE) end; select_type(ServerHost, _Host, _Node, Type) -> case config(ServerHost, plugins) of - undefined -> - Type; - Plugins -> - case lists:member(Type, Plugins) of - true -> Type; - false -> hd(Plugins) - end + undefined -> + Type; + Plugins -> + case lists:member(Type, Plugins) of + true -> Type; + false -> hd(Plugins) + end end. + -spec select_type(binary(), host(), binary()) -> binary(). select_type(ServerHost, Host, Node) -> select_type(ServerHost, Host, Node, hd(plugins(Host))). + -spec feature(binary()) -> binary(). feature(<<"rsm">>) -> ?NS_RSM; feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>. + -spec features() -> [binary()]. features() -> - [% see plugin "access-authorize", % OPTIONAL - <<"access-open">>, % OPTIONAL this relates to access_model option in node_hometree - <<"access-presence">>, % OPTIONAL this relates to access_model option in node_pep - <<"access-whitelist">>, % OPTIONAL - <<"collections">>, % RECOMMENDED - <<"config-node">>, % RECOMMENDED + [ % see plugin "access-authorize", % OPTIONAL + <<"access-open">>, % OPTIONAL this relates to access_model option in node_hometree + <<"access-presence">>, % OPTIONAL this relates to access_model option in node_pep + <<"access-whitelist">>, % OPTIONAL + <<"collections">>, % RECOMMENDED + <<"config-node">>, % RECOMMENDED <<"config-node-max">>, - <<"create-and-configure">>, % RECOMMENDED - <<"item-ids">>, % RECOMMENDED - <<"last-published">>, % RECOMMENDED - <<"member-affiliation">>, % RECOMMENDED - <<"presence-notifications">>, % OPTIONAL - <<"presence-subscribe">>, % RECOMMENDED - <<"publisher-affiliation">>, % RECOMMENDED - <<"publish-only-affiliation">>, % OPTIONAL - <<"publish-options">>, % OPTIONAL + <<"create-and-configure">>, % RECOMMENDED + <<"item-ids">>, % RECOMMENDED + <<"last-published">>, % RECOMMENDED + <<"member-affiliation">>, % RECOMMENDED + <<"presence-notifications">>, % OPTIONAL + <<"presence-subscribe">>, % RECOMMENDED + <<"publisher-affiliation">>, % RECOMMENDED + <<"publish-only-affiliation">>, % OPTIONAL + <<"publish-options">>, % OPTIONAL <<"retrieve-default">>, - <<"shim">>]. % RECOMMENDED + <<"shim">>]. % RECOMMENDED + % see plugin "retrieve-items", % RECOMMENDED % see plugin "retrieve-subscriptions", % RECOMMENDED @@ -3771,25 +4642,28 @@ features() -> plugin_features(Host, Type) -> Module = plugin(Host, Type), case catch Module:features() of - {'EXIT', {undef, _}} -> []; - Result -> Result + {'EXIT', {undef, _}} -> []; + Result -> Result end. + -spec features(binary(), binary()) -> [binary()]. features(Host, <<>>) -> - lists:usort(lists:foldl(fun (Plugin, Acc) -> - Acc ++ plugin_features(Host, Plugin) - end, - features(), plugins(Host))); + lists:usort(lists:foldl(fun(Plugin, Acc) -> + Acc ++ plugin_features(Host, Plugin) + end, + features(), + plugins(Host))); features(Host, Node) when is_binary(Node) -> - Action = fun (#pubsub_node{type = Type}) -> - {result, plugin_features(Host, Type)} - end, + Action = fun(#pubsub_node{type = Type}) -> + {result, plugin_features(Host, Type)} + end, case transaction(Host, Node, Action, sync_dirty) of - {result, Features} -> lists:usort(features() ++ Features); - _ -> features() + {result, Features} -> lists:usort(features() ++ Features); + _ -> features() end. + %% @doc

    node tree plugin call.

    -spec tree_call(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). tree_call({_User, Server, _Resource}, Function, Args) -> @@ -3800,46 +4674,50 @@ tree_call(Host, Function, Args) -> Res = apply(Tree, Function, Args), Res2 = ejabberd_hooks:run_fold(pubsub_tree_call, Host, Res, [Tree, Function, Args]), case Res2 of - {error, #stanza_error{}} = Err -> - Err; - {error, {virtual, _}} = Err -> - Err; - {error, _} -> - ErrTxt = ?T("Database failure"), - Lang = ejabberd_option:language(), - {error, xmpp:err_internal_server_error(ErrTxt, Lang)}; - Other -> - Other + {error, #stanza_error{}} = Err -> + Err; + {error, {virtual, _}} = Err -> + Err; + {error, _} -> + ErrTxt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(ErrTxt, Lang)}; + Other -> + Other end. + -spec tree_action(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). tree_action(Host, Function, Args) -> ?DEBUG("Tree_action ~p ~p ~p", [Host, Function, Args]), ServerHost = serverhost(Host), DBType = mod_pubsub_opt:db_type(ServerHost), - Fun = fun () -> - try tree_call(Host, Function, Args) - catch ?EX_RULE(Class, Reason, St) when DBType == sql -> - StackTrace = ?EX_STACK(St), - ejabberd_sql:abort({exception, Class, Reason, StackTrace}) - end - end, + Fun = fun() -> + try + tree_call(Host, Function, Args) + catch + ?EX_RULE(Class, Reason, St) when DBType == sql -> + StackTrace = ?EX_STACK(St), + ejabberd_sql:abort({exception, Class, Reason, StackTrace}) + end + end, Ret = case DBType of - mnesia -> - mnesia:sync_dirty(Fun); - sql -> - ejabberd_sql:sql_bloc(ServerHost, Fun); - _ -> - Fun() - end, + mnesia -> + mnesia:sync_dirty(Fun); + sql -> + ejabberd_sql:sql_bloc(ServerHost, Fun); + _ -> + Fun() + end, get_tree_action_result(Ret). + -spec get_tree_action_result(any()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). get_tree_action_result({atomic, Result}) -> Result; get_tree_action_result({aborted, {exception, Class, Reason, StackTrace}}) -> ?ERROR_MSG("Transaction aborted:~n** ~ts", - [misc:format_exception(2, Class, Reason, StackTrace)]), + [misc:format_exception(2, Class, Reason, StackTrace)]), get_tree_action_result({error, db_failure}); get_tree_action_result({aborted, Reason}) -> ?ERROR_MSG("Transaction aborted: ~p~n", [Reason]), @@ -3856,94 +4734,102 @@ get_tree_action_result(Other) -> %% This is very risky, but tree plugins design is really bad Other. + %% @doc

    node plugin call.

    -spec node_call(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}. node_call(Host, Type, Function, Args) -> ?DEBUG("Node_call ~p ~p ~p", [Type, Function, Args]), Module = plugin(Host, Type), case erlang:function_exported(Module, Function, length(Args)) of - true -> - case apply(Module, Function, Args) of - {result, Result} -> - {result, Result}; - #pubsub_state{} = Result -> - {result, Result}; - {error, #stanza_error{}} = Err -> - Err; - {error, _} -> - ErrTxt = ?T("Database failure"), - Lang = ejabberd_option:language(), - {error, xmpp:err_internal_server_error(ErrTxt, Lang)} - end; - false when Type /= ?STDNODE -> - node_call(Host, ?STDNODE, Function, Args); - false -> - %% Let it crash with the stacktrace - apply(Module, Function, Args) + true -> + case apply(Module, Function, Args) of + {result, Result} -> + {result, Result}; + #pubsub_state{} = Result -> + {result, Result}; + {error, #stanza_error{}} = Err -> + Err; + {error, _} -> + ErrTxt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(ErrTxt, Lang)} + end; + false when Type /= ?STDNODE -> + node_call(Host, ?STDNODE, Function, Args); + false -> + %% Let it crash with the stacktrace + apply(Module, Function, Args) end. + -spec node_action(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}. node_action(Host, Type, Function, Args) -> ?DEBUG("Node_action ~p ~p ~p ~p", [Host, Type, Function, Args]), transaction(Host, fun() -> node_call(Host, Type, Function, Args) end, sync_dirty). + %% @doc

    plugin transaction handling.

    -spec transaction(host(), binary(), fun((#pubsub_node{}) -> _), transaction | sync_dirty) -> - {result, any()} | {error, stanza_error()}. + {result, any()} | {error, stanza_error()}. transaction(Host, Node, Action, Trans) -> transaction( Host, fun() -> - case tree_call(Host, get_node, [Host, Node]) of - N when is_record(N, pubsub_node) -> - case Action(N) of - {result, Result} -> {result, {N, Result}}; - {atomic, {result, Result}} -> {result, {N, Result}}; - Other -> Other - end; - Error -> - Error - end + case tree_call(Host, get_node, [Host, Node]) of + N when is_record(N, pubsub_node) -> + case Action(N) of + {result, Result} -> {result, {N, Result}}; + {atomic, {result, Result}} -> {result, {N, Result}}; + Other -> Other + end; + Error -> + Error + end end, Trans). + -spec transaction(host(), fun(), transaction | sync_dirty) -> - {result, any()} | {error, stanza_error()}. + {result, any()} | {error, stanza_error()}. transaction(Host, Fun, Trans) -> ServerHost = serverhost(Host), DBType = mod_pubsub_opt:db_type(ServerHost), do_transaction(ServerHost, Fun, Trans, DBType). + -spec do_transaction(binary(), fun(), transaction | sync_dirty, atom()) -> - {result, any()} | {error, stanza_error()}. + {result, any()} | {error, stanza_error()}. do_transaction(ServerHost, Fun, Trans, DBType) -> F = fun() -> - try Fun() - catch ?EX_RULE(Class, Reason, St) when (DBType == mnesia andalso - Trans == transaction) orelse - DBType == sql -> - StackTrace = ?EX_STACK(St), - Ex = {exception, Class, Reason, StackTrace}, - case DBType of - mnesia -> mnesia:abort(Ex); - sql -> ejabberd_sql:abort(Ex) - end - end - end, + try + Fun() + catch + ?EX_RULE(Class, Reason, St) when (DBType == mnesia andalso + Trans == transaction) orelse + DBType == sql -> + StackTrace = ?EX_STACK(St), + Ex = {exception, Class, Reason, StackTrace}, + case DBType of + mnesia -> mnesia:abort(Ex); + sql -> ejabberd_sql:abort(Ex) + end + end + end, Res = case DBType of - mnesia -> - mnesia:Trans(F); - sql -> - SqlFun = case Trans of - transaction -> sql_transaction; - _ -> sql_bloc - end, - ejabberd_sql:SqlFun(ServerHost, F); - _ -> - F() - end, + mnesia -> + mnesia:Trans(F); + sql -> + SqlFun = case Trans of + transaction -> sql_transaction; + _ -> sql_bloc + end, + ejabberd_sql:SqlFun(ServerHost, F); + _ -> + F() + end, get_transaction_response(Res). + -spec get_transaction_response(any()) -> {result, any()} | {error, stanza_error()}. get_transaction_response({result, _} = Result) -> Result; @@ -3958,313 +4844,377 @@ get_transaction_response({error, _}) -> {error, xmpp:err_internal_server_error(?T("Database failure"), Lang)}; get_transaction_response({exception, Class, Reason, StackTrace}) -> ?ERROR_MSG("Transaction aborted:~n** ~ts", - [misc:format_exception(2, Class, Reason, StackTrace)]), + [misc:format_exception(2, Class, Reason, StackTrace)]), get_transaction_response({error, db_failure}); get_transaction_response(Err) -> ?ERROR_MSG("Transaction error: ~p", [Err]), get_transaction_response({error, db_failure}). + %%%% helpers + %% Add pubsub-specific error element -spec extended_error(stanza_error(), ps_error()) -> stanza_error(). extended_error(StanzaErr, PubSubErr) -> StanzaErr#stanza_error{sub_els = [PubSubErr]}. + -spec err_closed_node() -> ps_error(). err_closed_node() -> #ps_error{type = 'closed-node'}. + -spec err_configuration_required() -> ps_error(). err_configuration_required() -> #ps_error{type = 'configuration-required'}. + -spec err_invalid_jid() -> ps_error(). err_invalid_jid() -> #ps_error{type = 'invalid-jid'}. + -spec err_invalid_options() -> ps_error(). err_invalid_options() -> #ps_error{type = 'invalid-options'}. + -spec err_invalid_payload() -> ps_error(). err_invalid_payload() -> #ps_error{type = 'invalid-payload'}. + -spec err_invalid_subid() -> ps_error(). err_invalid_subid() -> #ps_error{type = 'invalid-subid'}. + -spec err_item_forbidden() -> ps_error(). err_item_forbidden() -> #ps_error{type = 'item-forbidden'}. + -spec err_item_required() -> ps_error(). err_item_required() -> #ps_error{type = 'item-required'}. + -spec err_jid_required() -> ps_error(). err_jid_required() -> #ps_error{type = 'jid-required'}. + -spec err_max_items_exceeded() -> ps_error(). err_max_items_exceeded() -> #ps_error{type = 'max-items-exceeded'}. + -spec err_max_nodes_exceeded() -> ps_error(). err_max_nodes_exceeded() -> #ps_error{type = 'max-nodes-exceeded'}. + -spec err_nodeid_required() -> ps_error(). err_nodeid_required() -> #ps_error{type = 'nodeid-required'}. + -spec err_not_in_roster_group() -> ps_error(). err_not_in_roster_group() -> #ps_error{type = 'not-in-roster-group'}. + -spec err_not_subscribed() -> ps_error(). err_not_subscribed() -> #ps_error{type = 'not-subscribed'}. + -spec err_payload_too_big() -> ps_error(). err_payload_too_big() -> #ps_error{type = 'payload-too-big'}. + -spec err_payload_required() -> ps_error(). err_payload_required() -> #ps_error{type = 'payload-required'}. + -spec err_pending_subscription() -> ps_error(). err_pending_subscription() -> #ps_error{type = 'pending-subscription'}. + -spec err_precondition_not_met() -> ps_error(). err_precondition_not_met() -> #ps_error{type = 'precondition-not-met'}. + -spec err_presence_subscription_required() -> ps_error(). err_presence_subscription_required() -> #ps_error{type = 'presence-subscription-required'}. + -spec err_subid_required() -> ps_error(). err_subid_required() -> #ps_error{type = 'subid-required'}. + -spec err_too_many_subscriptions() -> ps_error(). err_too_many_subscriptions() -> #ps_error{type = 'too-many-subscriptions'}. + -spec err_unsupported(ps_feature()) -> ps_error(). err_unsupported(Feature) -> #ps_error{type = 'unsupported', feature = Feature}. + -spec err_unsupported_access_model() -> ps_error(). err_unsupported_access_model() -> #ps_error{type = 'unsupported-access-model'}. + -spec uniqid() -> mod_pubsub:itemId(). uniqid() -> {T1, T2, T3} = erlang:timestamp(), (str:format("~.16B~.16B~.16B", [T1, T2, T3])). + -spec add_message_type(message(), message_type()) -> message(). add_message_type(#message{} = Message, Type) -> Message#message{type = Type}. + %% Place of changed at the bottom of the stanza %% cf. http://xmpp.org/extensions/xep-0060.html#publisher-publish-success-subid %% %% "[SHIM Headers] SHOULD be included after the event notification information %% (i.e., as the last child of the stanza)". + -spec add_shim_headers(stanza(), [{binary(), binary()}]) -> stanza(). add_shim_headers(Stanza, Headers) -> xmpp:set_subtag(Stanza, #shim{headers = Headers}). + -spec add_extended_headers(stanza(), [address()]) -> stanza(). add_extended_headers(Stanza, Addrs) -> xmpp:set_subtag(Stanza, #addresses{list = Addrs}). + -spec subid_shim([binary()]) -> [{binary(), binary()}]. subid_shim(SubIds) -> - [{<<"SubId">>, SubId} || SubId <- SubIds]. + [ {<<"SubId">>, SubId} || SubId <- SubIds ]. + %% The argument is a list of Jids because this function could be used %% with the 'pubsub#replyto' (type=jid-multi) node configuration. + -spec extended_headers([jid()]) -> [address()]. extended_headers(Jids) -> - [#address{type = replyto, jid = Jid} || Jid <- Jids]. + [ #address{type = replyto, jid = Jid} || Jid <- Jids ]. + -spec purge_offline(jid()) -> ok. purge_offline(#jid{lserver = Host} = JID) -> Plugins = plugins(Host), Result = lists:foldl( - fun(Type, {Status, Acc}) -> - Features = plugin_features(Host, Type), - case lists:member(<<"retrieve-affiliations">>, plugin_features(Host, Type)) of - false -> - {{error, extended_error(xmpp:err_feature_not_implemented(), - err_unsupported('retrieve-affiliations'))}, - Acc}; - true -> - Items = lists:member(<<"retract-items">>, Features) - andalso lists:member(<<"persistent-items">>, Features), - if Items -> - case node_action(Host, Type, - get_entity_affiliations, [Host, JID]) of - {result, Affs} -> - {Status, [Affs | Acc]}; - {error, _} = Err -> - {Err, Acc} - end; - true -> - {Status, Acc} - end - end - end, {ok, []}, Plugins), + fun(Type, {Status, Acc}) -> + Features = plugin_features(Host, Type), + case lists:member(<<"retrieve-affiliations">>, plugin_features(Host, Type)) of + false -> + {{error, extended_error(xmpp:err_feature_not_implemented(), + err_unsupported('retrieve-affiliations'))}, + Acc}; + true -> + Items = lists:member(<<"retract-items">>, Features) andalso + lists:member(<<"persistent-items">>, Features), + if + Items -> + case node_action(Host, + Type, + get_entity_affiliations, + [Host, JID]) of + {result, Affs} -> + {Status, [Affs | Acc]}; + {error, _} = Err -> + {Err, Acc} + end; + true -> + {Status, Acc} + end + end + end, + {ok, []}, + Plugins), case Result of - {ok, Affs} -> - lists:foreach( - fun ({Node, Affiliation}) -> - Options = Node#pubsub_node.options, - Publisher = lists:member(Affiliation, [owner,publisher,publish_only]), - Open = (get_option(Options, publish_model) == open), - Purge = (get_option(Options, purge_offline) - andalso get_option(Options, persist_items)), - if (Publisher or Open) and Purge -> - purge_offline(Host, JID, Node); - true -> - ok - end - end, lists:usort(lists:flatten(Affs))); - _ -> - ok + {ok, Affs} -> + lists:foreach( + fun({Node, Affiliation}) -> + Options = Node#pubsub_node.options, + Publisher = lists:member(Affiliation, [owner, publisher, publish_only]), + Open = (get_option(Options, publish_model) == open), + Purge = (get_option(Options, purge_offline) andalso + get_option(Options, persist_items)), + if + (Publisher or Open) and Purge -> + purge_offline(Host, JID, Node); + true -> + ok + end + end, + lists:usort(lists:flatten(Affs))); + _ -> + ok end. + -spec purge_offline(host(), jid(), #pubsub_node{}) -> ok | {error, stanza_error()}. purge_offline(Host, #jid{luser = User, lserver = Server, lresource = Resource} = JID, Node) -> Nidx = Node#pubsub_node.id, Type = Node#pubsub_node.type, Options = Node#pubsub_node.options, case node_action(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) of - {result, {[], _}} -> - ok; - {result, {Items, _}} -> - PublishModel = get_option(Options, publish_model), - ForceNotify = get_option(Options, notify_retract), - {_, NodeId} = Node#pubsub_node.nodeid, - lists:foreach( - fun(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}}) - when (U == User) and (S == Server) and (R == Resource) -> - case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of - {result, {_, broadcast}} -> - broadcast_retract_items(Host, JID, NodeId, Nidx, Type, Options, [ItemId], ForceNotify), - case get_cached_item(Host, Nidx) of - #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); - _ -> ok - end; - _ -> - ok - end; - (_) -> - true - end, Items); - {error, #stanza_error{}} = Err -> - Err; - _ -> - Txt = ?T("Database failure"), - Lang = ejabberd_option:language(), - {error, xmpp:err_internal_server_error(Txt, Lang)} + {result, {[], _}} -> + ok; + {result, {Items, _}} -> + PublishModel = get_option(Options, publish_model), + ForceNotify = get_option(Options, notify_retract), + {_, NodeId} = Node#pubsub_node.nodeid, + lists:foreach( + fun(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}}) + when (U == User) and (S == Server) and (R == Resource) -> + case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of + {result, {_, broadcast}} -> + broadcast_retract_items(Host, JID, NodeId, Nidx, Type, Options, [ItemId], ForceNotify), + case get_cached_item(Host, Nidx) of + #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); + _ -> ok + end; + _ -> + ok + end; + (_) -> + true + end, + Items); + {error, #stanza_error{}} = Err -> + Err; + _ -> + Txt = ?T("Database failure"), + Lang = ejabberd_option:language(), + {error, xmpp:err_internal_server_error(Txt, Lang)} end. + -spec delete_old_items(non_neg_integer()) -> ok | error. delete_old_items(N) -> Results = lists:flatmap( - fun(Host) -> - case tree_action(Host, get_all_nodes, [Host]) of - Nodes when is_list(Nodes) -> - lists:map( - fun(#pubsub_node{id = Nidx, type = Type}) -> - case node_action(Host, Type, - remove_extra_items, - [Nidx, N]) of - {result, _} -> - ok; - {error, _} -> - error - end - end, Nodes); - _ -> - [error] - end - end, ejabberd_option:hosts()), + fun(Host) -> + case tree_action(Host, get_all_nodes, [Host]) of + Nodes when is_list(Nodes) -> + lists:map( + fun(#pubsub_node{id = Nidx, type = Type}) -> + case node_action(Host, + Type, + remove_extra_items, + [Nidx, N]) of + {result, _} -> + ok; + {error, _} -> + error + end + end, + Nodes); + _ -> + [error] + end + end, + ejabberd_option:hosts()), case lists:member(error, Results) of - true -> - error; - false -> - ok + true -> + error; + false -> + ok end. + -spec delete_expired_items() -> ok | error. delete_expired_items() -> Results = lists:flatmap( - fun(Host) -> - case tree_action(Host, get_all_nodes, [Host]) of - Nodes when is_list(Nodes) -> - lists:map( - fun(#pubsub_node{id = Nidx, type = Type, - options = Options}) -> - case item_expire(Host, Options) of - infinity -> - ok; - Seconds -> - case node_action( - Host, Type, - remove_expired_items, - [Nidx, Seconds]) of - {result, []} -> - ok; - {result, [_|_]} -> - unset_cached_item( - Host, Nidx); - {error, _} -> - error - end - end - end, Nodes); - _ -> - [error] - end - end, ejabberd_option:hosts()), + 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 + 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}]. + [#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(). mod_opt_type(access_createnode) -> @@ -4287,17 +5237,17 @@ mod_opt_type(force_node_config) -> econf:map( econf:glob(), econf:map( - econf:atom(), - econf:either( - econf:int(), - econf:atom()), - [{return, orddict}, unique])); + econf:atom(), + econf:either( + econf:int(), + econf:atom()), + [{return, orddict}, unique])); mod_opt_type(default_node_config) -> econf:map( econf:atom(), econf:either( - econf:int(), - econf:atom()), + econf:int(), + econf:atom()), [unique]); mod_opt_type(nodetree) -> econf:binary(); @@ -4316,6 +5266,7 @@ mod_opt_type(db_type) -> mod_opt_type(vcard) -> econf:vcard_temp(). + mod_options(Host) -> [{access_createnode, all}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, @@ -4335,164 +5286,196 @@ mod_options(Host) -> {default_node_config, []}, {force_node_config, []}]. + mod_doc() -> - #{desc => - [?T("This module offers a service for " - "https://xmpp.org/extensions/xep-0060.html" - "[XEP-0060: Publish-Subscribe]. The functionality in " - "'mod_pubsub' can be extended using plugins. " - "The plugin that implements PEP " - "(https://xmpp.org/extensions/xep-0163.html" - "[XEP-0163: Personal Eventing via Pubsub]) " - "is enabled in the default ejabberd configuration file, " - "and it requires _`mod_caps`_.")], + #{ + desc => + [?T("This module offers a service for " + "https://xmpp.org/extensions/xep-0060.html" + "[XEP-0060: Publish-Subscribe]. The functionality in " + "'mod_pubsub' can be extended using plugins. " + "The plugin that implements PEP " + "(https://xmpp.org/extensions/xep-0163.html" + "[XEP-0163: Personal Eventing via Pubsub]) " + "is enabled in the default ejabberd configuration file, " + "and it requires _`mod_caps`_.")], opts => - [{access_createnode, - #{value => "AccessName", - desc => - ?T("This option restricts which users are allowed to " - "create pubsub nodes using 'acl' and 'access'. " - "By default any account in the local ejabberd server " - "is allowed to create pubsub nodes. " - "The default value is: 'all'.")}}, - {db_type, - #{value => "mnesia | sql", - desc => - ?T("Same as top-level _`default_db`_ option, but applied to " - "this module only.")}}, - {default_node_config, - #{value => "List of Key:Value", - desc => - ?T("To override default node configuration, regardless " - "of node plugin. Value is a list of key-value " - "definition. Node configuration still uses default " - "configuration defined by node plugin, and overrides " - "any items by value defined in this configurable list.")}}, - {force_node_config, - #{value => "List of Node and the list of its Key:Value", - desc => - ?T("Define the configuration for given nodes. " - "The default value is: '[]'."), - example => - ["force_node_config:", - " ## Avoid buggy clients to make their bookmarks public", - " storage:bookmarks:", - " access_model: whitelist"]}}, - {host, - #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, - {hosts, - #{value => ?T("[Host, ...]"), - desc => - ?T("This option defines the Jabber IDs of the service. " - "If the 'hosts' option is not specified, the only Jabber " - "ID will be the hostname of the virtual host with the " - "prefix \"pubsub.\". The keyword '@HOST@' is replaced " - "with the real virtual host name.")}}, - {ignore_pep_from_offline, - #{value => "false | true", - desc => - ?T("To specify whether or not we should get last " - "published PEP items from users in our roster which " - "are offline when we connect. Value is 'true' or " - "'false'. If not defined, pubsub assumes true so we " - "only get last items of online contacts.")}}, - {last_item_cache, - #{value => "false | true", - desc => - ?T("To specify whether or not pubsub should cache last " - "items. Value is 'true' or 'false'. If not defined, " - "pubsub does not cache last items. On systems with not" - " so many nodes, caching last items speeds up pubsub " - "and allows you to raise the 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: '1000'.")}}, - {max_nodes_discoitems, - #{value => "pos_integer() | infinity", - desc => - ?T("The maximum number of nodes to return in a " - "discoitem response. The default value is: '100'.")}}, - {max_subscriptions_node, - #{value => "MaxSubs", - desc => - ?T("Define the maximum number of subscriptions managed " - "by a node. " - "Default value is no limitation: 'undefined'.")}}, - {name, - #{value => ?T("Name"), - desc => - ?T("The value of the service name. This name is only visible " - "in some clients that support " - "https://xmpp.org/extensions/xep-0030.html" - "[XEP-0030: Service Discovery]. " - "The default is 'vCard User Search'.")}}, - {nodetree, - #{value => "Nodetree", - desc => - [?T("To specify which nodetree to use. If not defined, the " - "default pubsub nodetree is used: 'tree'. Only one " - "nodetree can be used per host, and is shared by all " - "node plugins."), - ?T("- 'tree' nodetree store node configuration and " - "relations on the database. 'flat' nodes are stored " - "without any relationship, and 'hometree' nodes can " - "have child nodes."), - ?T("- 'virtual' nodetree does not store nodes on database. " - "This saves resources on systems with tons of nodes. " - "If using the 'virtual' nodetree, you can only enable " - "those node plugins: '[flat, pep]' or '[flat]'; any " - "other plugins configuration will not work. Also, all " - "nodes will have the default configuration, and this " - "can not be changed. Using 'virtual' nodetree requires " - "to start from a clean database, it will not work if " - "you used the default 'tree' nodetree before.")]}}, - {pep_mapping, - #{value => "List of Key:Value", - desc => - ?T("In this option you can provide a list of key-value to choose " - "defined node plugins on given PEP namespace. " - "The following example will use 'node_tune' instead of " - "'node_pep' for every PEP node with the tune namespace:"), - example => - ["modules:", - " ...", - " mod_pubsub:", - " pep_mapping:", - " http://jabber.org/protocol/tune: tune", - " ..."] - }}, - {plugins, - #{value => "[Plugin, ...]", - desc => [?T("To specify which pubsub node plugins to use. " - "The first one in the list is used by default. " - "If this option is not defined, the default plugins " - "list is: '[flat]'. PubSub clients can define which " - "plugin to use when creating a node: " - "add 'type=\'plugin-name\'' attribute " - "to the 'create' stanza element."), - ?T("- 'flat' plugin handles the default behaviour and " - "follows standard XEP-0060 implementation."), - ?T("- 'pep' plugin adds extension to handle Personal " - "Eventing Protocol (XEP-0163) to the PubSub engine. " - "When enabled, PEP is handled automatically.")]}}, - {vcard, - #{value => ?T("vCard"), - desc => - ?T("A custom vCard of the server that will be displayed by " - "some XMPP clients in Service Discovery. The value of " - "'vCard' is a YAML map constructed from an XML " - "representation of vCard. Since the representation has " - "no attributes, the mapping is straightforward."), - example => + [{access_createnode, + #{ + value => "AccessName", + desc => + ?T("This option restricts which users are allowed to " + "create pubsub nodes using 'acl' and 'access'. " + "By default any account in the local ejabberd server " + "is allowed to create pubsub nodes. " + "The default value is: 'all'.") + }}, + {db_type, + #{ + value => "mnesia | sql", + desc => + ?T("Same as top-level _`default_db`_ option, but applied to " + "this module only.") + }}, + {default_node_config, + #{ + value => "List of Key:Value", + desc => + ?T("To override default node configuration, regardless " + "of node plugin. Value is a list of key-value " + "definition. Node configuration still uses default " + "configuration defined by node plugin, and overrides " + "any items by value defined in this configurable list.") + }}, + {force_node_config, + #{ + value => "List of Node and the list of its Key:Value", + desc => + ?T("Define the configuration for given nodes. " + "The default value is: '[]'."), + example => + ["force_node_config:", + " ## Avoid buggy clients to make their bookmarks public", + " storage:bookmarks:", + " access_model: whitelist"] + }}, + {host, + #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, + {hosts, + #{ + value => ?T("[Host, ...]"), + desc => + ?T("This option defines the Jabber IDs of the service. " + "If the 'hosts' option is not specified, the only Jabber " + "ID will be the hostname of the virtual host with the " + "prefix \"pubsub.\". The keyword '@HOST@' is replaced " + "with the real virtual host name.") + }}, + {ignore_pep_from_offline, + #{ + value => "false | true", + desc => + ?T("To specify whether or not we should get last " + "published PEP items from users in our roster which " + "are offline when we connect. Value is 'true' or " + "'false'. If not defined, pubsub assumes true so we " + "only get last items of online contacts.") + }}, + {last_item_cache, + #{ + value => "false | true", + desc => + ?T("To specify whether or not pubsub should cache last " + "items. Value is 'true' or 'false'. If not defined, " + "pubsub does not cache last items. On systems with not" + " so many nodes, caching last items speeds up pubsub " + "and allows you to raise the 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: '1000'.") + }}, + {max_nodes_discoitems, + #{ + value => "pos_integer() | infinity", + desc => + ?T("The maximum number of nodes to return in a " + "discoitem response. The default value is: '100'.") + }}, + {max_subscriptions_node, + #{ + value => "MaxSubs", + desc => + ?T("Define the maximum number of subscriptions managed " + "by a node. " + "Default value is no limitation: 'undefined'.") + }}, + {name, + #{ + value => ?T("Name"), + desc => + ?T("The value of the service name. This name is only visible " + "in some clients that support " + "https://xmpp.org/extensions/xep-0030.html" + "[XEP-0030: Service Discovery]. " + "The default is 'vCard User Search'.") + }}, + {nodetree, + #{ + value => "Nodetree", + desc => + [?T("To specify which nodetree to use. If not defined, the " + "default pubsub nodetree is used: 'tree'. Only one " + "nodetree can be used per host, and is shared by all " + "node plugins."), + ?T("- 'tree' nodetree store node configuration and " + "relations on the database. 'flat' nodes are stored " + "without any relationship, and 'hometree' nodes can " + "have child nodes."), + ?T("- 'virtual' nodetree does not store nodes on database. " + "This saves resources on systems with tons of nodes. " + "If using the 'virtual' nodetree, you can only enable " + "those node plugins: '[flat, pep]' or '[flat]'; any " + "other plugins configuration will not work. Also, all " + "nodes will have the default configuration, and this " + "can not be changed. Using 'virtual' nodetree requires " + "to start from a clean database, it will not work if " + "you used the default 'tree' nodetree before.")] + }}, + {pep_mapping, + #{ + value => "List of Key:Value", + desc => + ?T("In this option you can provide a list of key-value to choose " + "defined node plugins on given PEP namespace. " + "The following example will use 'node_tune' instead of " + "'node_pep' for every PEP node with the tune namespace:"), + example => + ["modules:", + " ...", + " mod_pubsub:", + " pep_mapping:", + " http://jabber.org/protocol/tune: tune", + " ..."] + }}, + {plugins, + #{ + value => "[Plugin, ...]", + desc => [?T("To specify which pubsub node plugins to use. " + "The first one in the list is used by default. " + "If this option is not defined, the default plugins " + "list is: '[flat]'. PubSub clients can define which " + "plugin to use when creating a node: " + "add 'type=\'plugin-name\'' attribute " + "to the 'create' stanza element."), + ?T("- 'flat' plugin handles the default behaviour and " + "follows standard XEP-0060 implementation."), + ?T("- 'pep' plugin adds extension to handle Personal " + "Eventing Protocol (XEP-0163) to the PubSub engine. " + "When enabled, PEP is handled automatically.")] + }}, + {vcard, + #{ + value => ?T("vCard"), + desc => + ?T("A custom vCard of the server that will be displayed by " + "some XMPP clients in Service Discovery. The value of " + "'vCard' is a YAML map constructed from an XML " + "representation of vCard. Since the representation has " + "no attributes, the mapping is straightforward."), + example => ["# This XML representation of vCard:", "# ", "# Conferences", @@ -4508,33 +5491,33 @@ mod_doc() -> " adr:", " -", " work: true", - " street: Elm Street"]}} - ], + " street: Elm Street"] + }}], example => - [{?T("Example of configuration that uses flat nodes as default, " - "and allows use of flat, hometree and pep nodes:"), - ["modules:", - " mod_pubsub:", - " access_createnode: pubsub_createnode", - " max_subscriptions_node: 100", - " default_node_config:", - " notification_type: normal", - " notify_retract: false", - " max_items: 4", - " plugins:", - " - flat", - " - pep"]}, - {?T("Using relational database requires using mod_pubsub with " - "db_type 'sql'. Only flat, hometree and pep plugins supports " - "SQL. The following example shows previous configuration " - "with SQL usage:"), - ["modules:", - " mod_pubsub:", - " db_type: sql", - " access_createnode: pubsub_createnode", - " ignore_pep_from_offline: true", - " last_item_cache: false", - " plugins:", - " - flat", - " - pep"]} - ]}. + [{?T("Example of configuration that uses flat nodes as default, " + "and allows use of flat, hometree and pep nodes:"), + ["modules:", + " mod_pubsub:", + " access_createnode: pubsub_createnode", + " max_subscriptions_node: 100", + " default_node_config:", + " notification_type: normal", + " notify_retract: false", + " max_items: 4", + " plugins:", + " - flat", + " - pep"]}, + {?T("Using relational database requires using mod_pubsub with " + "db_type 'sql'. Only flat, hometree and pep plugins supports " + "SQL. The following example shows previous configuration " + "with SQL usage:"), + ["modules:", + " mod_pubsub:", + " db_type: sql", + " access_createnode: pubsub_createnode", + " ignore_pep_from_offline: true", + " last_item_cache: false", + " plugins:", + " - flat", + " - pep"]}] + }. diff --git a/src/mod_pubsub_mnesia.erl b/src/mod_pubsub_mnesia.erl index a1dbc2ff3..ed23b2f06 100644 --- a/src/mod_pubsub_mnesia.erl +++ b/src/mod_pubsub_mnesia.erl @@ -21,6 +21,7 @@ %% API -export([init/3]). + %%%=================================================================== %%% API %%%=================================================================== diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl index 612abf35b..f51a21717 100644 --- a/src/mod_pubsub_opt.erl +++ b/src/mod_pubsub_opt.erl @@ -21,105 +21,121 @@ -export([plugins/1]). -export([vcard/1]). + -spec access_createnode(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_createnode(Opts) when is_map(Opts) -> gen_mod:get_opt(access_createnode, Opts); access_createnode(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, access_createnode). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, db_type). --spec default_node_config(gen_mod:opts() | global | binary()) -> [{atom(),atom() | integer()}]. + +-spec default_node_config(gen_mod:opts() | global | binary()) -> [{atom(), atom() | integer()}]. default_node_config(Opts) when is_map(Opts) -> gen_mod:get_opt(default_node_config, Opts); default_node_config(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, default_node_config). --spec force_node_config(gen_mod:opts() | global | binary()) -> [{misc:re_mp(),[{atom(),atom() | integer()}]}]. + +-spec force_node_config(gen_mod:opts() | global | binary()) -> [{misc:re_mp(), [{atom(), atom() | integer()}]}]. force_node_config(Opts) when is_map(Opts) -> gen_mod:get_opt(force_node_config, Opts); force_node_config(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, force_node_config). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, host). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, hosts). + -spec ignore_pep_from_offline(gen_mod:opts() | global | binary()) -> boolean(). ignore_pep_from_offline(Opts) when is_map(Opts) -> gen_mod:get_opt(ignore_pep_from_offline, Opts); ignore_pep_from_offline(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, ignore_pep_from_offline). + -spec last_item_cache(gen_mod:opts() | global | binary()) -> boolean(). last_item_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(last_item_cache, Opts); last_item_cache(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache). + -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) -> gen_mod:get_module_opt(Host, mod_pubsub, max_items_node). + -spec max_nodes_discoitems(gen_mod:opts() | global | binary()) -> 'infinity' | non_neg_integer(). max_nodes_discoitems(Opts) when is_map(Opts) -> gen_mod:get_opt(max_nodes_discoitems, Opts); max_nodes_discoitems(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, max_nodes_discoitems). + -spec max_subscriptions_node(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). max_subscriptions_node(Opts) when is_map(Opts) -> gen_mod:get_opt(max_subscriptions_node, Opts); max_subscriptions_node(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, max_subscriptions_node). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, name). + -spec nodetree(gen_mod:opts() | global | binary()) -> binary(). nodetree(Opts) when is_map(Opts) -> gen_mod:get_opt(nodetree, Opts); nodetree(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, nodetree). --spec pep_mapping(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec pep_mapping(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. pep_mapping(Opts) when is_map(Opts) -> gen_mod:get_opt(pep_mapping, Opts); pep_mapping(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, pep_mapping). + -spec plugins(gen_mod:opts() | global | binary()) -> [binary()]. plugins(Opts) when is_map(Opts) -> gen_mod:get_opt(plugins, Opts); plugins(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, plugins). + -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, vcard). - diff --git a/src/mod_pubsub_serverinfo.erl b/src/mod_pubsub_serverinfo.erl index 45a24a31e..add976a45 100644 --- a/src/mod_pubsub_serverinfo.erl +++ b/src/mod_pubsub_serverinfo.erl @@ -42,12 +42,13 @@ -export([in_auth_result/3, out_auth_result/2, get_info/5]). -define(NS_URN_SERVERINFO, <<"urn:xmpp:serverinfo:0">>). --define(PUBLIC_HOSTS_URL, <<"https://data.xmpp.net/providers/v2/providers-Ds.json">>). +-define(PUBLIC_HOSTS_URL, <<"https://data.xmpp.net/providers/v2/providers-Ds.json">>). -record(state, {host, pubsub_host, node, monitors = #{}, timer = undefined, public_hosts = []}). %% @format-begin + start(Host, Opts) -> case pubsub_host(Host, Opts) of {error, _Reason} = Error -> @@ -60,6 +61,7 @@ start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, PubsubHost) end. + stop(Host) -> ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 50), ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50), @@ -67,34 +69,41 @@ stop(Host) -> ejabberd_hooks:delete(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50), gen_mod:stop_child(?MODULE, Host). + init([Host, PubsubHost]) -> TRef = timer:send_interval( - timer:minutes(5), self(), update_pubsub), + timer:minutes(5), self(), update_pubsub), Monitors = init_monitors(Host), PublicHosts = fetch_public_hosts(), State = - #state{host = Host, - pubsub_host = PubsubHost, - node = <<"serverinfo">>, - timer = TRef, - monitors = Monitors, - public_hosts = PublicHosts}, + #state{ + host = Host, + pubsub_host = PubsubHost, + node = <<"serverinfo">>, + timer = TRef, + monitors = Monitors, + public_hosts = PublicHosts + }, self() ! update_pubsub, {ok, State}. + -spec init_monitors(binary()) -> map(). init_monitors(Host) -> lists:foldl(fun(Domain, Monitors) -> - RefIn = make_ref(), % just dummies - RefOut = make_ref(), - maps:merge(#{RefIn => {incoming, {Host, Domain, true}}, - RefOut => {outgoing, {Host, Domain, true}}}, - Monitors) + RefIn = make_ref(), % just dummies + RefOut = make_ref(), + maps:merge(#{ + RefIn => {incoming, {Host, Domain, true}}, + RefOut => {outgoing, {Host, Domain, true}} + }, + Monitors) end, #{}, ejabberd_option:hosts() -- [Host]). + -spec fetch_public_hosts() -> list(). fetch_public_hosts() -> try @@ -113,8 +122,9 @@ fetch_public_hosts() -> [] end. + handle_cast({Event, Domain, Pid}, #state{host = Host, monitors = Mons} = State) - when Event == register_in; Event == register_out -> + when Event == register_in; Event == register_out -> Ref = monitor(process, Pid), IsPublic = check_if_public(Domain, State), NewMons = maps:put(Ref, {event_to_dir(Event), {Host, Domain, IsPublic}}, Mons), @@ -122,16 +132,19 @@ handle_cast({Event, Domain, Pid}, #state{host = Host, monitors = Mons} = State) handle_cast(_, State) -> {noreply, State}. + event_to_dir(register_in) -> incoming; event_to_dir(register_out) -> outgoing. + handle_call(pubsub_host, _From, #state{pubsub_host = PubsubHost} = State) -> {reply, {ok, PubsubHost}, State}; handle_call(_Request, _From, State) -> {noreply, State}. + handle_info({iq_reply, IQReply, {LServer, RServer}}, #state{monitors = Mons} = State) -> case IQReply of #iq{type = result, sub_els = [El]} -> @@ -140,12 +153,12 @@ handle_info({iq_reply, IQReply, {LServer, RServer}}, #state{monitors = Mons} = S case lists:member(?NS_URN_SERVERINFO, Features) of true -> NewMons = - maps:fold(fun (Ref, {Dir, {LServer0, RServer0, _}}, Acc) - when LServer == LServer0, RServer == RServer0 -> + maps:fold(fun(Ref, {Dir, {LServer0, RServer0, _}}, Acc) + when LServer == LServer0, RServer == RServer0 -> maps:put(Ref, {Dir, {LServer, RServer, true}}, Acc); - (Ref, Other, Acc) -> + (Ref, Other, Acc) -> maps:put(Ref, Other, Acc) end, #{}, @@ -168,6 +181,7 @@ handle_info({'DOWN', Mon, process, _Pid, _Info}, #state{monitors = Mons} = State handle_info(_Request, State) -> {noreply, State}. + terminate(_Reason, #state{monitors = Mons, timer = Timer}) -> case is_reference(Timer) of true -> @@ -176,8 +190,9 @@ terminate(_Reason, #state{monitors = Mons, timer = Timer}) -> receive {timeout, Timer, _} -> ok - after 0 -> - ok + after + 0 -> + ok end; _ -> ok @@ -187,17 +202,22 @@ terminate(_Reason, #state{monitors = Mons, timer = Timer}) -> end, maps:fold(fun(Mon, _, _) -> demonitor(Mon) end, ok, Mons). + depends(_Host, _Opts) -> [{mod_pubsub, hard}]. + mod_options(_Host) -> [{pubsub_host, undefined}]. + mod_opt_type(pubsub_host) -> econf:either(undefined, econf:host()). + mod_doc() -> - #{desc => + #{ + desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0485.html[XEP-0485: PubSub Server Information] " "to expose S2S information over the Pub/Sub service."), @@ -218,94 +238,110 @@ mod_doc() -> note => "added in 25.07", opts => [{pubsub_host, - #{value => "undefined | string()", + #{ + value => "undefined | string()", desc => ?T("Use this local PubSub host to advertise S2S connections. " "This must be a host local to this service handled by _`mod_pubsub`_. " "This option is only needed if your configuration has more than one host in mod_pubsub's 'hosts' option. " - "The default value is the first host defined in mod_pubsub 'hosts' option.")}}], + "The default value is the first host defined in mod_pubsub 'hosts' option.") + }}], example => - ["modules:", " mod_pubsub_serverinfo:", " pubsub_host: custom.pubsub.domain.local"]}. + ["modules:", " mod_pubsub_serverinfo:", " pubsub_host: custom.pubsub.domain.local"] + }. + in_auth_result(#{server_host := Host, remote_server := RServer} = State, true, _Server) -> gen_server:cast( - gen_mod:get_module_proc(Host, ?MODULE), {register_in, RServer, self()}), + gen_mod:get_module_proc(Host, ?MODULE), {register_in, RServer, self()}), State; in_auth_result(State, _, _) -> State. + out_auth_result(#{server_host := Host, remote_server := RServer} = State, true) -> gen_server:cast( - gen_mod:get_module_proc(Host, ?MODULE), {register_out, RServer, self()}), + gen_mod:get_module_proc(Host, ?MODULE), {register_out, RServer, self()}), State; out_auth_result(State, _) -> State. + check_if_public(Domain, State) -> maybe_send_disco_info(is_public(Domain, State) orelse is_monitored(Domain, State), Domain, State). + is_public(Domain, #state{public_hosts = PublicHosts}) -> lists:member(Domain, PublicHosts). + is_monitored(Domain, #state{host = Host, monitors = Mons}) -> maps:size( - maps:filter(fun (_Ref, {_Dir, {LServer, RServer, IsPublic}}) - when LServer == Host, RServer == Domain -> - IsPublic; - (_Ref, _Other) -> - false - end, - Mons)) - =/= 0. + maps:filter(fun(_Ref, {_Dir, {LServer, RServer, IsPublic}}) + when LServer == Host, RServer == Domain -> + IsPublic; + (_Ref, _Other) -> + false + end, + Mons)) =/= + 0. + maybe_send_disco_info(true, _Domain, _State) -> true; maybe_send_disco_info(false, Domain, #state{host = Host}) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), - IQ = #iq{type = get, - from = jid:make(Host), - to = jid:make(Domain), - sub_els = [#disco_info{}]}, + IQ = #iq{ + type = get, + from = jid:make(Host), + to = jid:make(Domain), + sub_els = [#disco_info{}] + }, ejabberd_router:route_iq(IQ, {Host, Domain}, Proc), false. -update_pubsub(#state{host = Host, - pubsub_host = PubsubHost, - node = Node, - monitors = Mons}) -> + +update_pubsub(#state{ + host = Host, + pubsub_host = PubsubHost, + node = Node, + monitors = Mons + }) -> Map = maps:fold(fun(_, {Dir, {MyDomain, Target, IsPublic}}, Acc) -> - maps:update_with(MyDomain, - fun(Acc2) -> - maps:update_with(Target, - fun({Types, _}) -> - {Types#{Dir => true}, IsPublic} - end, - {#{Dir => true}, IsPublic}, - Acc2) - end, - #{Target => {#{Dir => true}, IsPublic}}, - Acc) + maps:update_with(MyDomain, + fun(Acc2) -> + maps:update_with(Target, + fun({Types, _}) -> + {Types#{Dir => true}, IsPublic} + end, + {#{Dir => true}, IsPublic}, + Acc2) + end, + #{Target => {#{Dir => true}, IsPublic}}, + Acc) end, #{}, Mons), Domains = maps:fold(fun(MyDomain, Targets, Acc) -> - Remote = - maps:fold(fun (Remote, {Types, true}, Acc2) -> - [#pubsub_serverinfo_remote_domain{name = Remote, - type = - maps:keys(Types)} - | Acc2]; - (_HiddenRemote, {Types, false}, Acc2) -> - [#pubsub_serverinfo_remote_domain{type = - maps:keys(Types)} - | Acc2] - end, - [], - Targets), - [#pubsub_serverinfo_domain{name = MyDomain, remote_domain = Remote} | Acc] + Remote = + maps:fold(fun(Remote, {Types, true}, Acc2) -> + [#pubsub_serverinfo_remote_domain{ + name = Remote, + type = + maps:keys(Types) + } | Acc2]; + (_HiddenRemote, {Types, false}, Acc2) -> + [#pubsub_serverinfo_remote_domain{ + type = + maps:keys(Types) + } | Acc2] + end, + [], + Targets), + [#pubsub_serverinfo_domain{name = MyDomain, remote_domain = Remote} | Acc] end, [], Map), @@ -321,6 +357,7 @@ update_pubsub(#state{host = Host, PubOpts, all). + get_local_features({error, _} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_features(Acc, _From, _To, Node, _Lang) when Node == <<>> -> @@ -333,37 +370,47 @@ get_local_features(Acc, _From, _To, Node, _Lang) when Node == <<>> -> get_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. + get_info(Acc, Host, Mod, Node, Lang) - when Mod == undefined orelse Mod == mod_disco, Node == <<"">> -> + when Mod == undefined orelse Mod == mod_disco, Node == <<"">> -> case mod_disco:get_info(Acc, Host, Mod, Node, Lang) of [#xdata{fields = Fields} = XD | Rest] -> PubsubHost = pubsub_host(Host), NodeField = - #xdata_field{var = <<"serverinfo-pubsub-node">>, - values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}, + #xdata_field{ + var = <<"serverinfo-pubsub-node">>, + values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>] + }, {stop, [XD#xdata{fields = Fields ++ [NodeField]} | Rest]}; _ -> Acc end; get_info(Acc, Host, Mod, Node, _Lang) when Node == <<"">>, is_atom(Mod) -> PubsubHost = pubsub_host(Host), - [#xdata{type = result, - fields = - [#xdata_field{type = hidden, - var = <<"FORM_TYPE">>, - values = [?NS_SERVERINFO]}, - #xdata_field{var = <<"serverinfo-pubsub-node">>, - values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}]} - | Acc]; + [#xdata{ + type = result, + fields = + [#xdata_field{ + type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_SERVERINFO] + }, + #xdata_field{ + var = <<"serverinfo-pubsub-node">>, + values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>] + }] + } | Acc]; get_info(Acc, _Host, _Mod, _Node, _Lang) -> Acc. + pubsub_host(Host) -> {ok, PubsubHost} = gen_server:call( - gen_mod:get_module_proc(Host, ?MODULE), pubsub_host), + gen_mod:get_module_proc(Host, ?MODULE), pubsub_host), PubsubHost. + pubsub_host(Host, Opts) -> case gen_mod:get_opt(pubsub_host, Opts) of undefined -> @@ -379,9 +426,11 @@ pubsub_host(Host, Opts) -> end end. + check_pubsub_host_exists(Host, PubsubHost) -> lists:member(PubsubHost, get_mod_pubsub_hosts(Host)). + get_mod_pubsub_hosts(Host) -> case gen_mod:get_module_opt(Host, mod_pubsub, hosts) of [] -> diff --git a/src/mod_pubsub_serverinfo_opt.erl b/src/mod_pubsub_serverinfo_opt.erl index 731715f3c..a74418604 100644 --- a/src/mod_pubsub_serverinfo_opt.erl +++ b/src/mod_pubsub_serverinfo_opt.erl @@ -5,9 +5,9 @@ -export([pubsub_host/1]). + -spec pubsub_host(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). pubsub_host(Opts) when is_map(Opts) -> gen_mod:get_opt(pubsub_host, Opts); pubsub_host(Host) -> gen_mod:get_module_opt(Host, mod_pubsub_serverinfo, pubsub_host). - diff --git a/src/mod_pubsub_sql.erl b/src/mod_pubsub_sql.erl index 59b22c110..692f03568 100644 --- a/src/mod_pubsub_sql.erl +++ b/src/mod_pubsub_sql.erl @@ -24,6 +24,7 @@ -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -31,87 +32,131 @@ init(_Host, ServerHost, _Opts) -> ejabberd_sql_schema:update_schema(ServerHost, ?MODULE, sql_schemas()), ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"pubsub_node">>, - columns = - [#sql_column{name = <<"host">>, type = text}, - #sql_column{name = <<"node">>, type = text}, - #sql_column{name = <<"parent">>, type = text, - default = true}, - #sql_column{name = <<"plugin">>, type = text}, - #sql_column{name = <<"nodeid">>, type = bigserial}], - indices = [#sql_index{ - columns = [<<"nodeid">>], - unique = true}, - #sql_index{ - columns = [<<"parent">>]}, - #sql_index{ - columns = [<<"host">>, <<"node">>], - unique = true}]}, - #sql_table{ - name = <<"pubsub_node_option">>, - columns = - [#sql_column{name = <<"nodeid">>, type = bigint, - opts = [#sql_references{ - table = <<"pubsub_node">>, - column = <<"nodeid">>}]}, - #sql_column{name = <<"name">>, type = text}, - #sql_column{name = <<"val">>, type = text}], - indices = [#sql_index{columns = [<<"nodeid">>]}]}, - #sql_table{ - name = <<"pubsub_node_owner">>, - columns = - [#sql_column{name = <<"nodeid">>, type = bigint, - opts = [#sql_references{ - table = <<"pubsub_node">>, - column = <<"nodeid">>}]}, - #sql_column{name = <<"owner">>, type = text}], - indices = [#sql_index{columns = [<<"nodeid">>]}]}, - #sql_table{ - name = <<"pubsub_state">>, - columns = - [#sql_column{name = <<"nodeid">>, type = bigint, - opts = [#sql_references{ - table = <<"pubsub_node">>, - column = <<"nodeid">>}]}, - #sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"affiliation">>, type = {char, 1}}, - #sql_column{name = <<"subscriptions">>, type = text, - default = true}, - #sql_column{name = <<"stateid">>, type = bigserial}], - indices = [#sql_index{columns = [<<"stateid">>], - unique = true}, - #sql_index{columns = [<<"jid">>]}, - #sql_index{columns = [<<"nodeid">>, <<"jid">>], - unique = true}]}, - #sql_table{ - name = <<"pubsub_item">>, - columns = - [#sql_column{name = <<"nodeid">>, type = bigint, - opts = [#sql_references{ - table = <<"pubsub_node">>, - column = <<"nodeid">>}]}, - #sql_column{name = <<"itemid">>, type = text}, - #sql_column{name = <<"publisher">>, type = text}, - #sql_column{name = <<"creation">>, type = {text, 32}}, - #sql_column{name = <<"modification">>, type = {text, 32}}, - #sql_column{name = <<"payload">>, type = {text, big}, - default = true}], - indices = [#sql_index{columns = [<<"nodeid">>, <<"itemid">>], - unique = true}, - #sql_index{columns = [<<"itemid">>]}]}, - #sql_table{ - name = <<"pubsub_subscription_opt">>, - columns = - [#sql_column{name = <<"subid">>, type = text}, - #sql_column{name = <<"opt_name">>, type = {text, 32}}, - #sql_column{name = <<"opt_value">>, type = text}], - indices = [#sql_index{columns = [<<"subid">>, <<"opt_name">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"pubsub_node">>, + columns = + [#sql_column{name = <<"host">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{ + name = <<"parent">>, + type = text, + default = true + }, + #sql_column{name = <<"plugin">>, type = text}, + #sql_column{name = <<"nodeid">>, type = bigserial}], + indices = [#sql_index{ + columns = [<<"nodeid">>], + unique = true + }, + #sql_index{ + columns = [<<"parent">>] + }, + #sql_index{ + columns = [<<"host">>, <<"node">>], + unique = true + }] + }, + #sql_table{ + name = <<"pubsub_node_option">>, + columns = + [#sql_column{ + name = <<"nodeid">>, + type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">> + }] + }, + #sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"val">>, type = text}], + indices = [#sql_index{columns = [<<"nodeid">>]}] + }, + #sql_table{ + name = <<"pubsub_node_owner">>, + columns = + [#sql_column{ + name = <<"nodeid">>, + type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">> + }] + }, + #sql_column{name = <<"owner">>, type = text}], + indices = [#sql_index{columns = [<<"nodeid">>]}] + }, + #sql_table{ + name = <<"pubsub_state">>, + columns = + [#sql_column{ + name = <<"nodeid">>, + type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">> + }] + }, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"affiliation">>, type = {char, 1}}, + #sql_column{ + name = <<"subscriptions">>, + type = text, + default = true + }, + #sql_column{name = <<"stateid">>, type = bigserial}], + indices = [#sql_index{ + columns = [<<"stateid">>], + unique = true + }, + #sql_index{columns = [<<"jid">>]}, + #sql_index{ + columns = [<<"nodeid">>, <<"jid">>], + unique = true + }] + }, + #sql_table{ + name = <<"pubsub_item">>, + columns = + [#sql_column{ + name = <<"nodeid">>, + type = bigint, + opts = [#sql_references{ + table = <<"pubsub_node">>, + column = <<"nodeid">> + }] + }, + #sql_column{name = <<"itemid">>, type = text}, + #sql_column{name = <<"publisher">>, type = text}, + #sql_column{name = <<"creation">>, type = {text, 32}}, + #sql_column{name = <<"modification">>, type = {text, 32}}, + #sql_column{ + name = <<"payload">>, + type = {text, big}, + default = true + }], + indices = [#sql_index{ + columns = [<<"nodeid">>, <<"itemid">>], + unique = true + }, + #sql_index{columns = [<<"itemid">>]}] + }, + #sql_table{ + name = <<"pubsub_subscription_opt">>, + columns = + [#sql_column{name = <<"subid">>, type = text}, + #sql_column{name = <<"opt_name">>, type = {text, 32}}, + #sql_column{name = <<"opt_value">>, type = text}], + indices = [#sql_index{ + columns = [<<"subid">>, <<"opt_name">>], + unique = true + }] + }] + }]. diff --git a/src/mod_push.erl b/src/mod_push.erl index fb5ba1be4..9866b4be6 100644 --- a/src/mod_push.erl +++ b/src/mod_push.erl @@ -33,9 +33,15 @@ -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. --export([disco_sm_features/5, c2s_session_pending/1, c2s_copy_session/2, - c2s_session_resumed/1, c2s_handle_cast/2, c2s_stanza/3, mam_message/7, - offline_message/1, remove_user/2]). +-export([disco_sm_features/5, + c2s_session_pending/1, + c2s_copy_session/2, + c2s_session_resumed/1, + c2s_handle_cast/2, + c2s_stanza/3, + mam_message/7, + offline_message/1, + remove_user/2]). %% gen_iq_handler callback. -export([process_iq/1]). @@ -51,7 +57,9 @@ -include("ejabberd_commands.hrl"). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -define(PUSH_CACHE, push_cache). @@ -62,32 +70,38 @@ -type err_reason() :: notfound | db_failure. -type direction() :: send | recv | undefined. --callback init(binary(), gen_mod:opts()) - -> any(). --callback store_session(binary(), binary(), push_session_id(), jid(), binary(), - xdata()) - -> {ok, push_session()} | {error, err_reason()}. --callback lookup_session(binary(), binary(), jid(), binary()) - -> {ok, push_session()} | {error, err_reason()}. --callback lookup_session(binary(), binary(), push_session_id()) - -> {ok, push_session()} | {error, err_reason()}. --callback lookup_sessions(binary(), binary(), jid()) - -> {ok, [push_session()]} | {error, err_reason()}. --callback lookup_sessions(binary(), binary()) - -> {ok, [push_session()]} | {error, err_reason()}. --callback lookup_sessions(binary()) - -> {ok, [push_session()]} | {error, err_reason()}. --callback delete_session(binary(), binary(), push_session_id()) - -> ok | {error, err_reason()}. --callback delete_old_sessions(binary() | global, erlang:timestamp()) - -> ok | {error, err_reason()}. --callback use_cache(binary()) - -> boolean(). --callback cache_nodes(binary()) - -> [node()]. + +-callback init(binary(), gen_mod:opts()) -> + any(). +-callback store_session(binary(), + binary(), + push_session_id(), + jid(), + binary(), + xdata()) -> + {ok, push_session()} | {error, err_reason()}. +-callback lookup_session(binary(), binary(), jid(), binary()) -> + {ok, push_session()} | {error, err_reason()}. +-callback lookup_session(binary(), binary(), push_session_id()) -> + {ok, push_session()} | {error, err_reason()}. +-callback lookup_sessions(binary(), binary(), jid()) -> + {ok, [push_session()]} | {error, err_reason()}. +-callback lookup_sessions(binary(), binary()) -> + {ok, [push_session()]} | {error, err_reason()}. +-callback lookup_sessions(binary()) -> + {ok, [push_session()]} | {error, err_reason()}. +-callback delete_session(binary(), binary(), push_session_id()) -> + ok | {error, err_reason()}. +-callback delete_old_sessions(binary() | global, erlang:timestamp()) -> + ok | {error, err_reason()}. +-callback use_cache(binary()) -> + boolean(). +-callback cache_nodes(binary()) -> + [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). + %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- @@ -97,7 +111,7 @@ start(Host, Opts) -> Mod:init(Host, Opts), init_cache(Mod, Host, Opts), {ok, [{commands, get_commands_spec()}, - {iq_handler, ejabberd_sm, ?NS_PUSH_0, process_iq}, + {iq_handler, ejabberd_sm, ?NS_PUSH_0, process_iq}, {hook, disco_sm_features, disco_sm_features, 50}, {hook, c2s_session_pending, c2s_session_pending, 50}, {hook, c2s_copy_session, c2s_copy_session, 50}, @@ -113,22 +127,26 @@ start(Host, Opts) -> stop(_Host) -> ok. + -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts), ok. + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. + -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(notify_on) -> econf:enum([messages, all]); @@ -149,6 +167,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(Host) -> [{notify_on, all}, @@ -160,8 +179,10 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module implements the XMPP server's part of " "the push notification solution specified in " "https://xmpp.org/extensions/xep-0357.html" @@ -174,7 +195,8 @@ mod_doc() -> "platform-dependent backend services such as FCM or APNS."), opts => [{notify_on, - #{value => "messages | all", + #{ + value => "messages | all", note => "added in 23.10", desc => ?T("If this option is set to 'messages', notifications are " @@ -182,16 +204,20 @@ mod_doc() -> "(or some encrypted payload). If it's set to 'all', any " "kind of XMPP stanza will trigger a notification. If " "unsure, it's strongly recommended to stick to 'all', " - "which is the default value.")}}, + "which is the default value.") + }}, {include_sender, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If this option is set to 'true', the sender's JID " "is included with push notifications generated for " "incoming messages with a body. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {include_body, - #{value => "true | false | Text", + #{ + value => "true | false | Text", desc => ?T("If this option is set to 'true', the message text " "is included with push notifications generated for " @@ -202,38 +228,56 @@ mod_doc() -> "whether the notification was triggered by a message " "with body (as opposed to other types of traffic) " "without leaking actual message contents. " - "The default value is \"New message\".")}}, + "The default value is \"New message\".") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}] + }. + %%-------------------------------------------------------------------- %% ejabberd command callback. %%-------------------------------------------------------------------- -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> - [#ejabberd_commands{name = delete_old_push_sessions, tags = [purge], - desc = "Remove push sessions older than DAYS", - module = ?MODULE, function = delete_old_sessions, - args = [{days, integer}], - result = {res, rescode}}]. + [#ejabberd_commands{ + name = delete_old_push_sessions, + tags = [purge], + desc = "Remove push sessions older than DAYS", + module = ?MODULE, + function = delete_old_sessions, + args = [{days, integer}], + result = {res, rescode} + }]. + -spec delete_old_sessions(non_neg_integer()) -> ok | any(). delete_old_sessions(Days) -> @@ -241,43 +285,52 @@ delete_old_sessions(Days) -> Diff = Days * 24 * 60 * 60 * 1000000, TimeStamp = misc:usec_to_now(CurrentTime - Diff), DBTypes = lists:usort( - lists:map( - fun(Host) -> - case mod_push_opt:db_type(Host) of - sql -> {sql, Host}; - Other -> {Other, global} - end - end, ejabberd_option:hosts())), + lists:map( + fun(Host) -> + case mod_push_opt:db_type(Host) of + sql -> {sql, Host}; + Other -> {Other, global} + end + end, + ejabberd_option:hosts())), Results = lists:map( - fun({DBType, Host}) -> - Mod = gen_mod:db_mod(DBType, ?MODULE), - Mod:delete_old_sessions(Host, TimeStamp) - end, DBTypes), + fun({DBType, Host}) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:delete_old_sessions(Host, TimeStamp) + end, + DBTypes), ets_cache:clear(?PUSH_CACHE, ejabberd_cluster:get_nodes()), case lists:filter(fun(Res) -> Res /= ok end, Results) of - [] -> - ?INFO_MSG("Deleted push sessions older than ~B days", [Days]), - ok; - [{error, Reason} | _] -> - ?ERROR_MSG("Error while deleting old push sessions: ~p", [Reason]), - Reason + [] -> + ?INFO_MSG("Deleted push sessions older than ~B days", [Days]), + ok; + [{error, Reason} | _] -> + ?ERROR_MSG("Error while deleting old push sessions: ~p", [Reason]), + Reason end. + %%-------------------------------------------------------------------- %% Service discovery. %%-------------------------------------------------------------------- -spec disco_sm_features(empty | {result, [binary()]} | {error, stanza_error()}, - jid(), jid(), binary(), binary()) - -> {result, [binary()]} | {error, stanza_error()}. + jid(), + jid(), + binary(), + binary()) -> + {result, [binary()]} | {error, stanza_error()}. disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures}, - #jid{luser = U, lserver = S}, - #jid{luser = U, lserver = S}, <<"">>, _Lang) -> + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, + <<"">>, + _Lang) -> {result, [?NS_PUSH_0 | OtherFeatures]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. + %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- @@ -288,82 +341,100 @@ process_iq(#iq{type = get, lang = Lang} = IQ) -> process_iq(#iq{lang = Lang, sub_els = [#push_enable{node = <<>>}]} = IQ) -> Txt = ?T("Enabling push without 'node' attribute is not supported"), xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); -process_iq(#iq{from = #jid{lserver = LServer} = JID, - to = #jid{lserver = LServer}, - lang = Lang, - sub_els = [#push_enable{jid = PushJID, - node = Node, - xdata = XData}]} = IQ) -> +process_iq(#iq{ + from = #jid{lserver = LServer} = JID, + to = #jid{lserver = LServer}, + lang = Lang, + sub_els = [#push_enable{ + jid = PushJID, + node = Node, + xdata = XData + }] + } = IQ) -> case enable(JID, PushJID, Node, XData) of - ok -> - xmpp:make_iq_result(IQ); - {error, db_failure} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); - {error, notfound} -> - Txt = ?T("User session not found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + ok -> + xmpp:make_iq_result(IQ); + {error, db_failure} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + {error, notfound} -> + Txt = ?T("User session not found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end; -process_iq(#iq{from = #jid{lserver = LServer} = JID, - to = #jid{lserver = LServer}, - lang = Lang, - sub_els = [#push_disable{jid = PushJID, - node = Node}]} = IQ) -> +process_iq(#iq{ + from = #jid{lserver = LServer} = JID, + to = #jid{lserver = LServer}, + lang = Lang, + sub_els = [#push_disable{ + jid = PushJID, + node = Node + }] + } = IQ) -> case disable(JID, PushJID, Node) of - ok -> - xmpp:make_iq_result(IQ); - {error, db_failure} -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); - {error, notfound} -> - Txt = ?T("Push record not found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + ok -> + xmpp:make_iq_result(IQ); + {error, db_failure} -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + {error, notfound} -> + Txt = ?T("Push record not found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end; process_iq(IQ) -> xmpp:make_error(IQ, xmpp:err_not_allowed()). + -spec enable(jid(), jid(), binary(), xdata()) -> ok | {error, err_reason()}. enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, - PushJID, Node, XData) -> + PushJID, + Node, + XData) -> case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of - {ID, PID} -> - case store_session(LUser, LServer, ID, PushJID, Node, XData) of - {ok, _} -> - ?INFO_MSG("Enabling push notifications for ~ts", - [jid:encode(JID)]), - ejabberd_c2s:cast(PID, {push_enable, ID}), - ejabberd_sm:set_user_info(LUser, LServer, LResource, - push_id, ID); - {error, _} = Err -> - ?ERROR_MSG("Cannot enable push for ~ts: database error", - [jid:encode(JID)]), - Err - end; - none -> - ?WARNING_MSG("Cannot enable push for ~ts: session not found", - [jid:encode(JID)]), - {error, notfound} + {ID, PID} -> + case store_session(LUser, LServer, ID, PushJID, Node, XData) of + {ok, _} -> + ?INFO_MSG("Enabling push notifications for ~ts", + [jid:encode(JID)]), + ejabberd_c2s:cast(PID, {push_enable, ID}), + ejabberd_sm:set_user_info(LUser, + LServer, + LResource, + push_id, + ID); + {error, _} = Err -> + ?ERROR_MSG("Cannot enable push for ~ts: database error", + [jid:encode(JID)]), + Err + end; + none -> + ?WARNING_MSG("Cannot enable push for ~ts: session not found", + [jid:encode(JID)]), + {error, notfound} end. + -spec disable(jid(), jid(), binary() | undefined) -> ok | {error, err_reason()}. disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, - PushJID, Node) -> + PushJID, + Node) -> case ejabberd_sm:get_session_pid(LUser, LServer, LResource) of - PID when is_pid(PID) -> - ?INFO_MSG("Disabling push notifications for ~ts", - [jid:encode(JID)]), - ejabberd_sm:del_user_info(LUser, LServer, LResource, push_id), - ejabberd_c2s:cast(PID, push_disable); - none -> - ?WARNING_MSG("Session not found while disabling push for ~ts", - [jid:encode(JID)]) + PID when is_pid(PID) -> + ?INFO_MSG("Disabling push notifications for ~ts", + [jid:encode(JID)]), + ejabberd_sm:del_user_info(LUser, LServer, LResource, push_id), + ejabberd_c2s:cast(PID, push_disable); + none -> + ?WARNING_MSG("Session not found while disabling push for ~ts", + [jid:encode(JID)]) end, - if Node /= <<>> -> - delete_session(LUser, LServer, PushJID, Node); - true -> - delete_sessions(LUser, LServer, PushJID) + if + Node /= <<>> -> + delete_session(LUser, LServer, PushJID, Node); + true -> + delete_sessions(LUser, LServer, PushJID) end. + %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- @@ -371,87 +442,114 @@ disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, c2s_stanza(State, #stream_error{}, _SendResult) -> State; c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, - Pkt, _SendResult) -> + Pkt, + _SendResult) -> ?DEBUG("Notifying client of stanza", []), notify(State, Pkt, get_direction(Pkt)), State; c2s_stanza(State, _Pkt, _SendResult) -> State. --spec mam_message(message() | drop, binary(), binary(), jid(), - binary(), chat | groupchat, recv | send) -> message(). + +-spec mam_message(message() | drop, + binary(), + binary(), + jid(), + binary(), + chat | groupchat, + recv | send) -> message(). mam_message(#message{} = Pkt, LUser, LServer, _Peer, _Nick, chat, Dir) -> case lookup_sessions(LUser, LServer) of - {ok, [_|_] = Clients} -> - case drop_online_sessions(LUser, LServer, Clients) of - [_|_] = Clients1 -> - ?DEBUG("Notifying ~ts@~ts of MAM message", [LUser, LServer]), - notify(LUser, LServer, Clients1, Pkt, Dir); - [] -> - ok - end; - _ -> - ok + {ok, [_ | _] = Clients} -> + case drop_online_sessions(LUser, LServer, Clients) of + [_ | _] = Clients1 -> + ?DEBUG("Notifying ~ts@~ts of MAM message", [LUser, LServer]), + notify(LUser, LServer, Clients1, Pkt, Dir); + [] -> + ok + end; + _ -> + ok end, Pkt; mam_message(Pkt, _LUser, _LServer, _Peer, _Nick, _Type, _Dir) -> Pkt. + -spec offline_message({any(), message()}) -> {any(), message()}. offline_message({offlined, #message{meta = #{mam_archived := true}}} = Acc) -> - Acc; % Push notification was triggered via MAM. + Acc; % Push notification was triggered via MAM. offline_message({offlined, - #message{to = #jid{luser = LUser, - lserver = LServer}} = Pkt} = Acc) -> + #message{ + to = #jid{ + luser = LUser, + lserver = LServer + } + } = Pkt} = Acc) -> case lookup_sessions(LUser, LServer) of - {ok, [_|_] = Clients} -> - ?DEBUG("Notifying ~ts@~ts of offline message", [LUser, LServer]), - notify(LUser, LServer, Clients, Pkt, recv); - _ -> - ok + {ok, [_ | _] = Clients} -> + ?DEBUG("Notifying ~ts@~ts of offline message", [LUser, LServer]), + notify(LUser, LServer, Clients, Pkt, recv); + _ -> + ok end, Acc; offline_message(Acc) -> Acc. + -spec c2s_session_pending(c2s_state()) -> c2s_state(). c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> case p1_queue:len(Queue) of - Len when Len > 0 -> - ?DEBUG("Notifying client of unacknowledged stanza(s)", []), - {Pkt, Dir} = case mod_stream_mgmt:queue_find( - fun is_incoming_chat_msg/1, Queue) of - none -> {none, undefined}; - Pkt0 -> {Pkt0, get_direction(Pkt0)} - end, - notify(State, Pkt, Dir), - State; - 0 -> - State + Len when Len > 0 -> + ?DEBUG("Notifying client of unacknowledged stanza(s)", []), + {Pkt, Dir} = case mod_stream_mgmt:queue_find( + fun is_incoming_chat_msg/1, Queue) of + none -> {none, undefined}; + Pkt0 -> {Pkt0, get_direction(Pkt0)} + end, + notify(State, Pkt, Dir), + State; + 0 -> + State end; c2s_session_pending(State) -> State. + -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). -c2s_copy_session(State, #{push_enabled := true, - push_session_id := ID}) -> - State#{push_enabled => true, - push_session_id => ID}; +c2s_copy_session(State, + #{ + push_enabled := true, + push_session_id := ID + }) -> + State#{ + push_enabled => true, + push_session_id => ID + }; c2s_copy_session(State, _) -> State. + -spec c2s_session_resumed(c2s_state()) -> c2s_state(). -c2s_session_resumed(#{push_session_id := ID, - user := U, server := S, resource := R} = State) -> +c2s_session_resumed(#{ + push_session_id := ID, + user := U, + server := S, + resource := R + } = State) -> ejabberd_sm:set_user_info(U, S, R, push_id, ID), State; c2s_session_resumed(State) -> State. + -spec c2s_handle_cast(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. c2s_handle_cast(State, {push_enable, ID}) -> - {stop, State#{push_enabled => true, - push_session_id => ID}}; + {stop, State#{ + push_enabled => true, + push_session_id => ID + }}; c2s_handle_cast(State, push_disable) -> State1 = maps:remove(push_enabled, State), State2 = maps:remove(push_session_id, State1), @@ -459,6 +557,7 @@ c2s_handle_cast(State, push_disable) -> c2s_handle_cast(State, _Msg) -> State. + -spec remove_user(binary(), binary()) -> ok | {error, err_reason()}. remove_user(LUser, LServer) -> ?INFO_MSG("Removing any push sessions of ~ts@~ts", [LUser, LServer]), @@ -466,317 +565,377 @@ remove_user(LUser, LServer) -> LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer) end, delete_sessions(LUser, LServer, LookupFun, Mod). + %%-------------------------------------------------------------------- %% Generate push notifications. %%-------------------------------------------------------------------- -spec notify(c2s_state(), xmpp_element() | xmlel() | none, direction()) -> ok. notify(#{jid := #jid{luser = LUser, lserver = LServer}} = State, Pkt, Dir) -> case lookup_session(LUser, LServer, State) of - {ok, Client} -> - notify(LUser, LServer, [Client], Pkt, Dir); - _Err -> - ok + {ok, Client} -> + notify(LUser, LServer, [Client], Pkt, Dir); + _Err -> + ok end. --spec notify(binary(), binary(), [push_session()], - xmpp_element() | xmlel() | none, direction()) -> ok. + +-spec notify(binary(), + binary(), + [push_session()], + xmpp_element() | xmlel() | none, + direction()) -> ok. notify(LUser, LServer, Clients, Pkt, Dir) -> lists:foreach( fun({ID, PushLJID, Node, XData}) -> - HandleResponse = - fun(#iq{type = result}) -> - ?DEBUG("~ts accepted notification for ~ts@~ts (~ts)", - [jid:encode(PushLJID), LUser, LServer, Node]); - (#iq{type = error} = IQ) -> - case inspect_error(IQ) of - {wait, Reason} -> - ?INFO_MSG("~ts rejected notification for " - "~ts@~ts (~ts) temporarily: ~ts", - [jid:encode(PushLJID), LUser, - LServer, Node, Reason]); - {Type, Reason} -> - spawn(?MODULE, delete_session, - [LUser, LServer, ID]), - ?WARNING_MSG("~ts rejected notification for " - "~ts@~ts (~ts), disabling push: ~ts " - "(~ts)", - [jid:encode(PushLJID), LUser, - LServer, Node, Reason, Type]) - end; - (timeout) -> - ?DEBUG("Timeout sending notification for ~ts@~ts (~ts) " - "to ~ts", - [LUser, LServer, Node, jid:encode(PushLJID)]), - ok % Hmm. - end, - notify(LServer, PushLJID, Node, XData, Pkt, Dir, HandleResponse) - end, Clients). + HandleResponse = + fun(#iq{type = result}) -> + ?DEBUG("~ts accepted notification for ~ts@~ts (~ts)", + [jid:encode(PushLJID), LUser, LServer, Node]); + (#iq{type = error} = IQ) -> + case inspect_error(IQ) of + {wait, Reason} -> + ?INFO_MSG("~ts rejected notification for " + "~ts@~ts (~ts) temporarily: ~ts", + [jid:encode(PushLJID), + LUser, + LServer, + Node, + Reason]); + {Type, Reason} -> + spawn(?MODULE, + delete_session, + [LUser, LServer, ID]), + ?WARNING_MSG("~ts rejected notification for " + "~ts@~ts (~ts), disabling push: ~ts " + "(~ts)", + [jid:encode(PushLJID), + LUser, + LServer, + Node, + Reason, + Type]) + end; + (timeout) -> + ?DEBUG("Timeout sending notification for ~ts@~ts (~ts) " + "to ~ts", + [LUser, LServer, Node, jid:encode(PushLJID)]), + ok % Hmm. + end, + notify(LServer, PushLJID, Node, XData, Pkt, Dir, HandleResponse) + end, + Clients). --spec notify(binary(), ljid(), binary(), xdata(), - xmpp_element() | xmlel() | none, direction(), - fun((iq() | timeout) -> any())) -> ok. + +-spec notify(binary(), + ljid(), + binary(), + xdata(), + xmpp_element() | xmlel() | none, + direction(), + fun((iq() | timeout) -> any())) -> ok. notify(LServer, PushLJID, Node, XData, Pkt0, Dir, HandleResponse) -> Pkt = unwrap_message(Pkt0), From = jid:make(LServer), case {make_summary(LServer, Pkt, Dir), mod_push_opt:notify_on(LServer)} of - {undefined, messages} -> - ?DEBUG("Suppressing notification for stanza without payload", []), - ok; - {Summary, _NotifyOn} -> - Item = #ps_item{sub_els = [#push_notification{xdata = Summary}]}, - PubSub = #pubsub{publish = #ps_publish{node = Node, items = [Item]}, - publish_options = XData}, - IQ = #iq{type = set, - from = From, - to = jid:make(PushLJID), - id = p1_rand:get_string(), - sub_els = [PubSub]}, - ejabberd_router:route_iq(IQ, HandleResponse) + {undefined, messages} -> + ?DEBUG("Suppressing notification for stanza without payload", []), + ok; + {Summary, _NotifyOn} -> + Item = #ps_item{sub_els = [#push_notification{xdata = Summary}]}, + PubSub = #pubsub{ + publish = #ps_publish{node = Node, items = [Item]}, + publish_options = XData + }, + IQ = #iq{ + type = set, + from = From, + to = jid:make(PushLJID), + id = p1_rand:get_string(), + sub_els = [PubSub] + }, + ejabberd_router:route_iq(IQ, HandleResponse) end. + %%-------------------------------------------------------------------- %% Miscellaneous. %%-------------------------------------------------------------------- -spec is_incoming_chat_msg(stanza()) -> boolean(). is_incoming_chat_msg(#message{} = Msg) -> case get_direction(Msg) of - recv -> get_body_text(unwrap_message(Msg)) /= none; - send -> false + recv -> get_body_text(unwrap_message(Msg)) /= none; + send -> false end; is_incoming_chat_msg(_Stanza) -> false. + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- --spec store_session(binary(), binary(), push_session_id(), jid(), binary(), - xdata()) -> {ok, push_session()} | {error, err_reason()}. +-spec store_session(binary(), + binary(), + push_session_id(), + jid(), + binary(), + xdata()) -> {ok, push_session()} | {error, err_reason()}. store_session(LUser, LServer, ID, PushJID, Node, XData) -> Mod = gen_mod:db_mod(LServer, ?MODULE), delete_session(LUser, LServer, PushJID, Node), case use_cache(Mod, LServer) of - true -> - ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)), - ets_cache:update( - ?PUSH_CACHE, - {LUser, LServer, ID}, {ok, {ID, PushJID, Node, XData}}, - fun() -> - Mod:store_session(LUser, LServer, ID, PushJID, Node, - XData) - end, cache_nodes(Mod, LServer)); - false -> - Mod:store_session(LUser, LServer, ID, PushJID, Node, XData) + true -> + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:update( + ?PUSH_CACHE, + {LUser, LServer, ID}, + {ok, {ID, PushJID, Node, XData}}, + fun() -> + Mod:store_session(LUser, + LServer, + ID, + PushJID, + Node, + XData) + end, + cache_nodes(Mod, LServer)); + false -> + Mod:store_session(LUser, LServer, ID, PushJID, Node, XData) end. --spec lookup_session(binary(), binary(), c2s_state()) - -> {ok, push_session()} | error | {error, err_reason()}. + +-spec lookup_session(binary(), binary(), c2s_state()) -> + {ok, push_session()} | error | {error, err_reason()}. lookup_session(LUser, LServer, #{push_session_id := ID}) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?PUSH_CACHE, {LUser, LServer, ID}, - fun() -> Mod:lookup_session(LUser, LServer, ID) end); - false -> - Mod:lookup_session(LUser, LServer, ID) + true -> + ets_cache:lookup( + ?PUSH_CACHE, + {LUser, LServer, ID}, + fun() -> Mod:lookup_session(LUser, LServer, ID) end); + false -> + Mod:lookup_session(LUser, LServer, ID) end. + -spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | {error, err_reason()}. lookup_sessions(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?PUSH_CACHE, {LUser, LServer}, - fun() -> Mod:lookup_sessions(LUser, LServer) end); - false -> - Mod:lookup_sessions(LUser, LServer) + true -> + ets_cache:lookup( + ?PUSH_CACHE, + {LUser, LServer}, + fun() -> Mod:lookup_sessions(LUser, LServer) end); + false -> + Mod:lookup_sessions(LUser, LServer) end. --spec delete_session(binary(), binary(), push_session_id()) - -> ok | {error, db_failure}. + +-spec delete_session(binary(), binary(), push_session_id()) -> + ok | {error, db_failure}. delete_session(LUser, LServer, ID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:delete_session(LUser, LServer, ID) of - ok -> - case use_cache(Mod, LServer) of - true -> - ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)), - ets_cache:delete(?PUSH_CACHE, {LUser, LServer, ID}, - cache_nodes(Mod, LServer)); - false -> - ok - end; - {error, _} = Err -> - Err + ok -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer, ID}, + cache_nodes(Mod, LServer)); + false -> + ok + end; + {error, _} = Err -> + Err end. + -spec delete_session(binary(), binary(), jid(), binary()) -> ok | {error, err_reason()}. delete_session(LUser, LServer, PushJID, Node) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:lookup_session(LUser, LServer, PushJID, Node) of - {ok, {ID, _, _, _}} -> - delete_session(LUser, LServer, ID); - error -> - {error, notfound}; - {error, _} = Err -> - Err + {ok, {ID, _, _, _}} -> + delete_session(LUser, LServer, ID); + error -> + {error, notfound}; + {error, _} = Err -> + Err end. + -spec delete_sessions(binary(), binary(), jid()) -> ok | {error, err_reason()}. delete_sessions(LUser, LServer, PushJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer, PushJID) end, delete_sessions(LUser, LServer, LookupFun, Mod). --spec delete_sessions(binary(), binary(), fun(() -> any()), module()) - -> ok | {error, err_reason()}. + +-spec delete_sessions(binary(), binary(), fun(() -> any()), module()) -> + ok | {error, err_reason()}. delete_sessions(LUser, LServer, LookupFun, Mod) -> case LookupFun() of - {ok, []} -> - {error, notfound}; - {ok, Clients} -> - case use_cache(Mod, LServer) of - true -> - ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)); - false -> - ok - end, - lists:foreach( - fun({ID, _, _, _}) -> - ok = Mod:delete_session(LUser, LServer, ID), - case use_cache(Mod, LServer) of - true -> - ets_cache:delete(?PUSH_CACHE, - {LUser, LServer, ID}, - cache_nodes(Mod, LServer)); - false -> - ok - end - end, Clients); - {error, _} = Err -> - Err + {ok, []} -> + {error, notfound}; + {ok, Clients} -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok + end, + lists:foreach( + fun({ID, _, _, _}) -> + ok = Mod:delete_session(LUser, LServer, ID), + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, + {LUser, LServer, ID}, + cache_nodes(Mod, LServer)); + false -> + ok + end + end, + Clients); + {error, _} = Err -> + Err end. --spec drop_online_sessions(binary(), binary(), [push_session()]) - -> [push_session()]. + +-spec drop_online_sessions(binary(), binary(), [push_session()]) -> + [push_session()]. drop_online_sessions(LUser, LServer, Clients) -> OnlineIDs = lists:filtermap( - fun({_, Info}) -> - case proplists:get_value(push_id, Info) of - OnlineID = {_, _, _} -> - {true, OnlineID}; - undefined -> - false - end - end, ejabberd_sm:get_user_info(LUser, LServer)), - [Client || {ID, _, _, _} = Client <- Clients, - not lists:member(ID, OnlineIDs)]. + fun({_, Info}) -> + case proplists:get_value(push_id, Info) of + OnlineID = {_, _, _} -> + {true, OnlineID}; + undefined -> + false + end + end, + ejabberd_sm:get_user_info(LUser, LServer)), + [ Client || {ID, _, _, _} = Client <- Clients, + not lists:member(ID, OnlineIDs) ]. --spec make_summary(binary(), xmpp_element() | xmlel() | none, direction()) - -> xdata() | undefined. + +-spec make_summary(binary(), xmpp_element() | xmlel() | none, direction()) -> + xdata() | undefined. make_summary(Host, #message{from = From0} = Pkt, recv) -> case {mod_push_opt:include_sender(Host), - mod_push_opt:include_body(Host)} of - {false, false} -> - undefined; - {IncludeSender, IncludeBody} -> - case get_body_text(Pkt) of - none -> - undefined; - Text -> - Fields1 = case IncludeBody of - StaticText when is_binary(StaticText) -> - [{'last-message-body', StaticText}]; - true -> - [{'last-message-body', Text}]; - false -> - [] - end, - Fields2 = case IncludeSender of - true -> - From = jid:remove_resource(From0), - [{'last-message-sender', From} | Fields1]; - false -> - Fields1 - end, - #xdata{type = submit, fields = push_summary:encode(Fields2)} - end + mod_push_opt:include_body(Host)} of + {false, false} -> + undefined; + {IncludeSender, IncludeBody} -> + case get_body_text(Pkt) of + none -> + undefined; + Text -> + Fields1 = case IncludeBody of + StaticText when is_binary(StaticText) -> + [{'last-message-body', StaticText}]; + true -> + [{'last-message-body', Text}]; + false -> + [] + end, + Fields2 = case IncludeSender of + true -> + From = jid:remove_resource(From0), + [{'last-message-sender', From} | Fields1]; + false -> + Fields1 + end, + #xdata{type = submit, fields = push_summary:encode(Fields2)} + end end; make_summary(_Host, _Pkt, _Dir) -> undefined. + -spec unwrap_message(Stanza) -> Stanza when Stanza :: stanza() | none. unwrap_message(#message{meta = #{carbon_copy := true}} = Msg) -> misc:unwrap_carbon(Msg); unwrap_message(#message{type = normal} = Msg) -> case misc:unwrap_mucsub_message(Msg) of - #message{} = InnerMsg -> - InnerMsg; - false -> - Msg + #message{} = InnerMsg -> + InnerMsg; + false -> + Msg end; unwrap_message(Stanza) -> Stanza. + -spec get_direction(stanza()) -> direction(). -get_direction(#message{meta = #{carbon_copy := true}, - from = #jid{luser = U, lserver = S}, - to = #jid{luser = U, lserver = S}}) -> +get_direction(#message{ + meta = #{carbon_copy := true}, + from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S} + }) -> send; get_direction(#message{}) -> recv; get_direction(_Stanza) -> undefined. + -spec get_body_text(message()) -> binary() | none. get_body_text(#message{body = Body} = Msg) -> case xmpp:get_text(Body) of - Text when byte_size(Text) > 0 -> - Text; - <<>> -> - case body_is_encrypted(Msg) of - true -> - <<"(encrypted)">>; - false -> - none - end + Text when byte_size(Text) > 0 -> + Text; + <<>> -> + case body_is_encrypted(Msg) of + true -> + <<"(encrypted)">>; + false -> + none + end end. + -spec body_is_encrypted(message()) -> boolean(). body_is_encrypted(#message{sub_els = MsgEls}) -> case lists:keyfind(<<"encrypted">>, #xmlel.name, MsgEls) of - #xmlel{children = EncEls} -> - lists:keyfind(<<"payload">>, #xmlel.name, EncEls) /= false; - false -> - false + #xmlel{children = EncEls} -> + lists:keyfind(<<"payload">>, #xmlel.name, EncEls) /= false; + false -> + false end. + -spec inspect_error(iq()) -> {atom(), binary()}. inspect_error(IQ) -> case xmpp:get_error(IQ) of - #stanza_error{type = Type} = Err -> - {Type, xmpp:format_stanza_error(Err)}; - undefined -> - {undefined, <<"unrecognized error">>} + #stanza_error{type = Type} = Err -> + {Type, xmpp:format_stanza_error(Err)}; + undefined -> + {undefined, <<"unrecognized error">>} end. + %%-------------------------------------------------------------------- %% Caching. %%-------------------------------------------------------------------- -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?PUSH_CACHE, CacheOpts); - false -> - ets_cache:delete(?PUSH_CACHE) + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?PUSH_CACHE, CacheOpts); + false -> + ets_cache:delete(?PUSH_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_push_opt:cache_size(Opts), @@ -784,16 +943,18 @@ cache_opts(Opts) -> LifeTime = mod_push_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(Host); - false -> mod_push_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_push_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl index 33bd2b53e..b500cf677 100644 --- a/src/mod_push_keepalive.erl +++ b/src/mod_push_keepalive.erl @@ -32,18 +32,25 @@ -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. --export([ejabberd_started/0, c2s_session_pending/1, c2s_session_resumed/1, - c2s_copy_session/2, c2s_handle_cast/2, c2s_handle_info/2, - c2s_stanza/3]). +-export([ejabberd_started/0, + c2s_session_pending/1, + c2s_session_resumed/1, + c2s_copy_session/2, + c2s_handle_cast/2, + c2s_handle_info/2, + c2s_stanza/3]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). --define(PUSH_BEFORE_TIMEOUT_PERIOD, 120000). % 2 minutes. +-define(PUSH_BEFORE_TIMEOUT_PERIOD, 120000). % 2 minutes. -type c2s_state() :: ejabberd_c2s:state(). + %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- @@ -60,20 +67,24 @@ start(_Host, _Opts) -> %% don't initiate s2s connections before certificates are loaded: {hook, ejabberd_started, ejabberd_started, 90, global}]}. + -spec stop(binary()) -> ok. stop(_Host) -> ok. + -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> [{mod_push, hard}, {mod_client_state, soft}, {mod_stream_mgmt, soft}]. + -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(resume_timeout) -> econf:either( @@ -84,24 +95,29 @@ mod_opt_type(wake_on_start) -> mod_opt_type(wake_on_timeout) -> econf:bool(). + mod_options(_Host) -> [{resume_timeout, timer:hours(72)}, {wake_on_start, false}, {wake_on_timeout, true}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module tries to keep the stream management " "session (see _`mod_stream_mgmt`_) of a disconnected " "mobile client alive if the client enabled push " "notifications for that session. However, the normal " "session resumption timeout is restored once a push " "notification is issued, so the session will be closed " - "if the client doesn't respond to push notifications."), "", + "if the client doesn't respond to push notifications."), + "", ?T("The module depends on _`mod_push`_.")], opts => [{resume_timeout, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("This option specifies the period of time until " "the session of a disconnected push client times out. " @@ -109,9 +125,11 @@ mod_doc() -> "notification is issued. Once that happened, the " "resumption timeout configured for _`mod_stream_mgmt`_ " "is restored. " - "The default value is '72' hours.")}}, + "The default value is '72' hours.") + }}, {wake_on_start, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If this option is set to 'true', notifications " "are generated for **all** registered push clients " @@ -119,94 +137,118 @@ mod_doc() -> "enabled on servers with many push clients as it " "can generate significant load on the involved push " "services and the server itself. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {wake_on_timeout, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If this option is set to 'true', a notification " "is generated shortly before the session would time " "out as per the 'resume_timeout' option. " - "The default value is 'true'.")}}]}. + "The default value is 'true'.") + }}] + }. + %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- -spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, - Pkt, _SendResult) -> + Pkt, + _SendResult) -> case mod_push:is_incoming_chat_msg(Pkt) of - true -> - maybe_restore_resume_timeout(State); - false -> - State + true -> + maybe_restore_resume_timeout(State); + false -> + State end; c2s_stanza(State, _Pkt, _SendResult) -> State. + -spec c2s_session_pending(c2s_state()) -> c2s_state(). c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> case mod_stream_mgmt:queue_find(fun mod_push:is_incoming_chat_msg/1, - Queue) of - none -> - State1 = maybe_adjust_resume_timeout(State), - maybe_start_wakeup_timer(State1); - _Msg -> - State + Queue) of + none -> + State1 = maybe_adjust_resume_timeout(State), + maybe_start_wakeup_timer(State1); + _Msg -> + State end; c2s_session_pending(State) -> State. + -spec c2s_session_resumed(c2s_state()) -> c2s_state(). c2s_session_resumed(#{push_enabled := true} = State) -> maybe_restore_resume_timeout(State); c2s_session_resumed(State) -> State. + -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). -c2s_copy_session(State, #{push_enabled := true, - push_resume_timeout := ResumeTimeout, - push_wake_on_timeout := WakeOnTimeout} = OldState) -> +c2s_copy_session(State, + #{ + push_enabled := true, + push_resume_timeout := ResumeTimeout, + push_wake_on_timeout := WakeOnTimeout + } = OldState) -> State1 = case maps:find(push_resume_timeout_orig, OldState) of - {ok, Val} -> - State#{push_resume_timeout_orig => Val}; - error -> - State - end, - State1#{push_resume_timeout => ResumeTimeout, - push_wake_on_timeout => WakeOnTimeout}; + {ok, Val} -> + State#{push_resume_timeout_orig => Val}; + error -> + State + end, + State1#{ + push_resume_timeout => ResumeTimeout, + push_wake_on_timeout => WakeOnTimeout + }; c2s_copy_session(State, _) -> State. + -spec c2s_handle_cast(c2s_state(), any()) -> c2s_state(). c2s_handle_cast(#{lserver := LServer} = State, {push_enable, _ID}) -> ResumeTimeout = mod_push_keepalive_opt:resume_timeout(LServer), WakeOnTimeout = mod_push_keepalive_opt:wake_on_timeout(LServer), - State#{push_resume_timeout => ResumeTimeout, - push_wake_on_timeout => WakeOnTimeout}; + State#{ + push_resume_timeout => ResumeTimeout, + push_wake_on_timeout => WakeOnTimeout + }; c2s_handle_cast(State, push_disable) -> State1 = maps:remove(push_resume_timeout, State), maps:remove(push_wake_on_timeout, State1); c2s_handle_cast(State, _Msg) -> State. + -spec c2s_handle_info(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. -c2s_handle_info(#{push_enabled := true, mgmt_state := pending, - jid := JID} = State, {timeout, _, push_keepalive}) -> +c2s_handle_info(#{ + push_enabled := true, + mgmt_state := pending, + jid := JID + } = State, + {timeout, _, push_keepalive}) -> ?INFO_MSG("Waking ~ts before session times out", [jid:encode(JID)]), mod_push:notify(State, none, undefined), {stop, State}; c2s_handle_info(State, _) -> State. + -spec ejabberd_started() -> ok. ejabberd_started() -> Pred = fun(Host) -> - gen_mod:is_loaded(Host, ?MODULE) andalso - mod_push_keepalive_opt:wake_on_start(Host) - end, - [wake_all(Host) || Host <- ejabberd_config:get_option(hosts), Pred(Host)], + gen_mod:is_loaded(Host, ?MODULE) andalso + mod_push_keepalive_opt:wake_on_start(Host) + end, + [ wake_all(Host) || Host <- ejabberd_config:get_option(hosts), Pred(Host) ], ok. + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- @@ -219,6 +261,7 @@ maybe_adjust_resume_timeout(#{push_resume_timeout := Timeout} = State) -> State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), State1#{push_resume_timeout_orig => OrigTimeout}. + -spec maybe_restore_resume_timeout(c2s_state()) -> c2s_state(). maybe_restore_resume_timeout(#{push_resume_timeout_orig := Timeout} = State) -> ?DEBUG("Restoring resume timeout to ~B seconds", [Timeout div 1000]), @@ -227,9 +270,12 @@ maybe_restore_resume_timeout(#{push_resume_timeout_orig := Timeout} = State) -> maybe_restore_resume_timeout(State) -> State. + -spec maybe_start_wakeup_timer(c2s_state()) -> c2s_state(). -maybe_start_wakeup_timer(#{push_wake_on_timeout := true, - push_resume_timeout := ResumeTimeout} = State) +maybe_start_wakeup_timer(#{ + push_wake_on_timeout := true, + push_resume_timeout := ResumeTimeout + } = State) when is_integer(ResumeTimeout), ResumeTimeout > ?PUSH_BEFORE_TIMEOUT_PERIOD -> WakeTimeout = ResumeTimeout - ?PUSH_BEFORE_TIMEOUT_PERIOD, ?DEBUG("Scheduling wake-up timer to fire in ~B seconds", [WakeTimeout div 1000]), @@ -238,18 +284,24 @@ maybe_start_wakeup_timer(#{push_wake_on_timeout := true, maybe_start_wakeup_timer(State) -> State. + -spec wake_all(binary()) -> ok. wake_all(LServer) -> ?INFO_MSG("Waking all push clients on ~ts", [LServer]), Mod = gen_mod:db_mod(LServer, mod_push), case Mod:lookup_sessions(LServer) of - {ok, Sessions} -> - IgnoreResponse = fun(_) -> ok end, - lists:foreach(fun({_, PushLJID, Node, XData}) -> - mod_push:notify(LServer, PushLJID, Node, - XData, none, undefined, - IgnoreResponse) - end, Sessions); - error -> - ok + {ok, Sessions} -> + IgnoreResponse = fun(_) -> ok end, + lists:foreach(fun({_, PushLJID, Node, XData}) -> + mod_push:notify(LServer, + PushLJID, + Node, + XData, + none, + undefined, + IgnoreResponse) + end, + Sessions); + error -> + ok end. diff --git a/src/mod_push_keepalive_opt.erl b/src/mod_push_keepalive_opt.erl index 82b1d51bb..3b7dfb6c0 100644 --- a/src/mod_push_keepalive_opt.erl +++ b/src/mod_push_keepalive_opt.erl @@ -7,21 +7,23 @@ -export([wake_on_start/1]). -export([wake_on_timeout/1]). + -spec resume_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). resume_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(resume_timeout, Opts); resume_timeout(Host) -> gen_mod:get_module_opt(Host, mod_push_keepalive, resume_timeout). + -spec wake_on_start(gen_mod:opts() | global | binary()) -> boolean(). wake_on_start(Opts) when is_map(Opts) -> gen_mod:get_opt(wake_on_start, Opts); wake_on_start(Host) -> gen_mod:get_module_opt(Host, mod_push_keepalive, wake_on_start). + -spec wake_on_timeout(gen_mod:opts() | global | binary()) -> boolean(). wake_on_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(wake_on_timeout, Opts); wake_on_timeout(Host) -> gen_mod:get_module_opt(Host, mod_push_keepalive, wake_on_timeout). - diff --git a/src/mod_push_mnesia.erl b/src/mod_push_mnesia.erl index 6a5f068b9..e1196be50 100644 --- a/src/mod_push_mnesia.erl +++ b/src/mod_push_mnesia.erl @@ -29,183 +29,215 @@ -behaviour(mod_push). %% API --export([init/2, store_session/6, lookup_session/4, lookup_session/3, - lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, - delete_session/3, delete_old_sessions/2, transform/1]). +-export([init/2, + store_session/6, + lookup_session/4, lookup_session/3, + lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, + delete_session/3, + delete_old_sessions/2, + transform/1]). -include_lib("stdlib/include/ms_transform.hrl"). + -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_push.hrl"). + %%%------------------------------------------------------------------- %%% API %%%------------------------------------------------------------------- init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, push_session, - [{disc_only_copies, [node()]}, - {type, bag}, - {attributes, record_info(fields, push_session)}]). + ejabberd_mnesia:create(?MODULE, + push_session, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, push_session)}]). + store_session(LUser, LServer, TS, PushJID, Node, XData) -> US = {LUser, LServer}, PushLJID = jid:tolower(PushJID), MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), F = fun() -> - enforce_max_sessions(US, MaxSessions), - mnesia:write(#push_session{us = US, - timestamp = TS, - service = PushLJID, - node = Node, - xml = encode_xdata(XData)}) - end, + enforce_max_sessions(US, MaxSessions), + mnesia:write(#push_session{ + us = US, + timestamp = TS, + service = PushLJID, + node = Node, + xml = encode_xdata(XData) + }) + end, case mnesia:transaction(F) of - {atomic, ok} -> - {ok, {TS, PushLJID, Node, XData}}; - {aborted, E} -> - ?ERROR_MSG("Cannot store push session for ~ts@~ts: ~p", - [LUser, LServer, E]), - {error, db_failure} + {atomic, ok} -> + {ok, {TS, PushLJID, Node, XData}}; + {aborted, E} -> + ?ERROR_MSG("Cannot store push session for ~ts@~ts: ~p", + [LUser, LServer, E]), + {error, db_failure} end. + lookup_session(LUser, LServer, PushJID, Node) -> PushLJID = jid:tolower(PushJID), MatchSpec = ets:fun2ms( - fun(#push_session{us = {U, S}, service = P, node = N} = Rec) - when U == LUser, - S == LServer, - P == PushLJID, - N == Node -> - Rec - end), + fun(#push_session{us = {U, S}, service = P, node = N} = Rec) + when U == LUser, + S == LServer, + P == PushLJID, + N == Node -> + Rec + end), case mnesia:dirty_select(push_session, MatchSpec) of - [#push_session{timestamp = TS, xml = El}] -> - {ok, {TS, PushLJID, Node, decode_xdata(El)}}; - [] -> - ?DEBUG("No push session found for ~ts@~ts (~p, ~ts)", - [LUser, LServer, PushJID, Node]), - {error, notfound} + [#push_session{timestamp = TS, xml = El}] -> + {ok, {TS, PushLJID, Node, decode_xdata(El)}}; + [] -> + ?DEBUG("No push session found for ~ts@~ts (~p, ~ts)", + [LUser, LServer, PushJID, Node]), + {error, notfound} end. + lookup_session(LUser, LServer, TS) -> MatchSpec = ets:fun2ms( - fun(#push_session{us = {U, S}, timestamp = T} = Rec) - when U == LUser, - S == LServer, - T == TS -> - Rec - end), + fun(#push_session{us = {U, S}, timestamp = T} = Rec) + when U == LUser, + S == LServer, + T == TS -> + Rec + end), case mnesia:dirty_select(push_session, MatchSpec) of - [#push_session{service = PushLJID, node = Node, xml = El}] -> - {ok, {TS, PushLJID, Node, decode_xdata(El)}}; - [] -> - ?DEBUG("No push session found for ~ts@~ts (~p)", - [LUser, LServer, TS]), - {error, notfound} + [#push_session{service = PushLJID, node = Node, xml = El}] -> + {ok, {TS, PushLJID, Node, decode_xdata(El)}}; + [] -> + ?DEBUG("No push session found for ~ts@~ts (~p)", + [LUser, LServer, TS]), + {error, notfound} end. + lookup_sessions(LUser, LServer, PushJID) -> PushLJID = jid:tolower(PushJID), MatchSpec = ets:fun2ms( - fun(#push_session{us = {U, S}, service = P} = Rec) - when U == LUser, - S == LServer, - P == PushLJID -> - Rec - end), + fun(#push_session{us = {U, S}, service = P} = Rec) + when U == LUser, + S == LServer, + P == PushLJID -> + Rec + end), Records = mnesia:dirty_select(push_session, MatchSpec), {ok, records_to_sessions(Records)}. + lookup_sessions(LUser, LServer) -> Records = mnesia:dirty_read(push_session, {LUser, LServer}), {ok, records_to_sessions(Records)}. + lookup_sessions(LServer) -> MatchSpec = ets:fun2ms( - fun(#push_session{us = {_U, S}} = Rec) - when S == LServer -> - Rec - end), + fun(#push_session{us = {_U, S}} = Rec) + when S == LServer -> + Rec + end), Records = mnesia:dirty_select(push_session, MatchSpec), {ok, records_to_sessions(Records)}. + delete_session(LUser, LServer, TS) -> MatchSpec = ets:fun2ms( - fun(#push_session{us = {U, S}, timestamp = T} = Rec) - when U == LUser, - S == LServer, - T == TS -> - Rec - end), + fun(#push_session{us = {U, S}, timestamp = T} = Rec) + when U == LUser, + S == LServer, + T == TS -> + Rec + end), F = fun() -> - Recs = mnesia:select(push_session, MatchSpec), - lists:foreach(fun mnesia:delete_object/1, Recs) - end, + Recs = mnesia:select(push_session, MatchSpec), + lists:foreach(fun mnesia:delete_object/1, Recs) + end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, E} -> - ?ERROR_MSG("Cannot delete push session of ~ts@~ts: ~p", - [LUser, LServer, E]), - {error, db_failure} + {atomic, ok} -> + ok; + {aborted, E} -> + ?ERROR_MSG("Cannot delete push session of ~ts@~ts: ~p", + [LUser, LServer, E]), + {error, db_failure} end. + delete_old_sessions(_LServer, Time) -> DelIfOld = fun(#push_session{timestamp = T} = Rec, ok) when T < Time -> - mnesia:delete_object(Rec); - (_Rec, ok) -> - ok - end, + mnesia:delete_object(Rec); + (_Rec, ok) -> + ok + end, F = fun() -> - mnesia:foldl(DelIfOld, ok, push_session) - end, + mnesia:foldl(DelIfOld, ok, push_session) + end, case mnesia:transaction(F) of - {atomic, ok} -> - ok; - {aborted, E} -> - ?ERROR_MSG("Cannot delete old push sessions: ~p", [E]), - {error, db_failure} + {atomic, ok} -> + ok; + {aborted, E} -> + ?ERROR_MSG("Cannot delete old push sessions: ~p", [E]), + {error, db_failure} end. + transform({push_session, US, TS, Service, Node, XData}) -> ?INFO_MSG("Transforming push_session Mnesia table", []), - #push_session{us = US, timestamp = TS, service = Service, - node = Node, xml = encode_xdata(XData)}. + #push_session{ + us = US, + timestamp = TS, + service = Service, + node = Node, + xml = encode_xdata(XData) + }. + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- --spec enforce_max_sessions({binary(), binary()}, non_neg_integer() | infinity) - -> ok. +-spec enforce_max_sessions({binary(), binary()}, non_neg_integer() | infinity) -> + ok. enforce_max_sessions(_US, infinity) -> ok; enforce_max_sessions({U, S} = US, MaxSessions) -> case mnesia:wread({push_session, US}) of - Recs when length(Recs) >= MaxSessions -> - Recs1 = lists:sort(fun(#push_session{timestamp = TS1}, - #push_session{timestamp = TS2}) -> - TS1 >= TS2 - end, Recs), - OldRecs = lists:nthtail(MaxSessions - 1, Recs1), - ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", [U, S]), - lists:foreach(fun(Rec) -> mnesia:delete_object(Rec) end, OldRecs); - _ -> - ok + Recs when length(Recs) >= MaxSessions -> + Recs1 = lists:sort(fun(#push_session{timestamp = TS1}, + #push_session{timestamp = TS2}) -> + TS1 >= TS2 + end, + Recs), + OldRecs = lists:nthtail(MaxSessions - 1, Recs1), + ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", [U, S]), + lists:foreach(fun(Rec) -> mnesia:delete_object(Rec) end, OldRecs); + _ -> + ok end. + decode_xdata(undefined) -> undefined; decode_xdata(El) -> xmpp:decode(El). + encode_xdata(undefined) -> undefined; encode_xdata(XData) -> xmpp:encode(XData). + records_to_sessions(Records) -> - [{TS, PushLJID, Node, decode_xdata(El)} - || #push_session{timestamp = TS, - service = PushLJID, - node = Node, - xml = El} <- Records]. + [ {TS, PushLJID, Node, decode_xdata(El)} + || #push_session{ + timestamp = TS, + service = PushLJID, + node = Node, + xml = El + } <- Records ]. diff --git a/src/mod_push_opt.erl b/src/mod_push_opt.erl index db6c55389..2222e0f89 100644 --- a/src/mod_push_opt.erl +++ b/src/mod_push_opt.erl @@ -12,51 +12,58 @@ -export([notify_on/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_push, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_push, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_push, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_push, db_type). + -spec include_body(gen_mod:opts() | global | binary()) -> boolean() | binary(). include_body(Opts) when is_map(Opts) -> gen_mod:get_opt(include_body, Opts); include_body(Host) -> gen_mod:get_module_opt(Host, mod_push, include_body). + -spec include_sender(gen_mod:opts() | global | binary()) -> boolean(). include_sender(Opts) when is_map(Opts) -> gen_mod:get_opt(include_sender, Opts); include_sender(Host) -> gen_mod:get_module_opt(Host, mod_push, include_sender). + -spec notify_on(gen_mod:opts() | global | binary()) -> 'all' | 'messages'. notify_on(Opts) when is_map(Opts) -> gen_mod:get_opt(notify_on, Opts); notify_on(Host) -> gen_mod:get_module_opt(Host, mod_push, notify_on). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_push, use_cache). - diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl index a36e50f8e..219c65524 100644 --- a/src/mod_push_sql.erl +++ b/src/mod_push_sql.erl @@ -27,16 +27,22 @@ -behaviour(mod_push). %% API --export([init/2, store_session/6, lookup_session/4, lookup_session/3, - lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, - delete_session/3, delete_old_sessions/2, export/1]). +-export([init/2, + store_session/6, + lookup_session/4, lookup_session/3, + lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, + delete_session/3, + delete_old_sessions/2, + export/1]). -export([sql_schemas/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("mod_push.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -44,27 +50,36 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"push_session">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"timestamp">>, type = bigint}, - #sql_column{name = <<"service">>, type = text}, - #sql_column{name = <<"node">>, type = text}, - #sql_column{name = <<"xml">>, type = text}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, - <<"timestamp">>], - unique = true}, - #sql_index{ - columns = [<<"server_host">>, <<"username">>, - <<"service">>, <<"node">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"push_session">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"timestamp">>, type = bigint}, + #sql_column{name = <<"service">>, type = text}, + #sql_column{name = <<"node">>, type = text}, + #sql_column{name = <<"xml">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"username">>, + <<"timestamp">>], + unique = true + }, + #sql_index{ + columns = [<<"server_host">>, + <<"username">>, + <<"service">>, + <<"node">>], + unique = true + }] + }] + }]. + store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> XML = encode_xdata(XData), @@ -73,162 +88,178 @@ store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> Service = jid:encode(PushLJID), MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), enforce_max_sessions(LUser, LServer, MaxSessions), - case ?SQL_UPSERT(LServer, "push_session", - ["!username=%(LUser)s", + case ?SQL_UPSERT(LServer, + "push_session", + ["!username=%(LUser)s", "!server_host=%(LServer)s", - "timestamp=%(TS)d", - "!service=%(Service)s", - "!node=%(Node)s", - "xml=%(XML)s"]) of - ok -> - {ok, {NowTS, PushLJID, Node, XData}}; - _Err -> - {error, db_failure} + "timestamp=%(TS)d", + "!service=%(Service)s", + "!node=%(Node)s", + "xml=%(XML)s"]) of + ok -> + {ok, {NowTS, PushLJID, Node, XData}}; + _Err -> + {error, db_failure} end. + lookup_session(LUser, LServer, PushJID, Node) -> PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(timestamp)d, @(xml)s from push_session " - "where username=%(LUser)s and %(LServer)H " + LServer, + ?SQL("select @(timestamp)d, @(xml)s from push_session " + "where username=%(LUser)s and %(LServer)H " "and service=%(Service)s " - "and node=%(Node)s")) of - {selected, [{TS, XML}]} -> - NowTS = misc:usec_to_now(TS), - XData = decode_xdata(XML, LUser, LServer), - {ok, {NowTS, PushLJID, Node, XData}}; - {selected, []} -> - {error, notfound}; - _Err -> - {error, db_failure} + "and node=%(Node)s")) of + {selected, [{TS, XML}]} -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + {ok, {NowTS, PushLJID, Node, XData}}; + {selected, []} -> + {error, notfound}; + _Err -> + {error, db_failure} end. + lookup_session(LUser, LServer, NowTS) -> TS = misc:now_to_usec(NowTS), case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(service)s, @(node)s, @(xml)s " - "from push_session where username=%(LUser)s and %(LServer)H " - "and timestamp=%(TS)d")) of - {selected, [{Service, Node, XML}]} -> - PushLJID = jid:tolower(jid:decode(Service)), - XData = decode_xdata(XML, LUser, LServer), - {ok, {NowTS, PushLJID, Node, XData}}; - {selected, []} -> - {error, notfound}; - _Err -> - {error, db_failure} + LServer, + ?SQL("select @(service)s, @(node)s, @(xml)s " + "from push_session where username=%(LUser)s and %(LServer)H " + "and timestamp=%(TS)d")) of + {selected, [{Service, Node, XML}]} -> + PushLJID = jid:tolower(jid:decode(Service)), + XData = decode_xdata(XML, LUser, LServer), + {ok, {NowTS, PushLJID, Node, XData}}; + {selected, []} -> + {error, notfound}; + _Err -> + {error, db_failure} end. + lookup_sessions(LUser, LServer, PushJID) -> PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(timestamp)d, @(xml)s, @(node)s from push_session " - "where username=%(LUser)s and %(LServer)H " + LServer, + ?SQL("select @(timestamp)d, @(xml)s, @(node)s from push_session " + "where username=%(LUser)s and %(LServer)H " "and service=%(Service)s")) of - {selected, Rows} -> - {ok, lists:map( - fun({TS, XML, Node}) -> - NowTS = misc:usec_to_now(TS), - XData = decode_xdata(XML, LUser, LServer), - {NowTS, PushLJID, Node, XData} - end, Rows)}; - _Err -> - {error, db_failure} + {selected, Rows} -> + {ok, lists:map( + fun({TS, XML, Node}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + {NowTS, PushLJID, Node, XData} + end, + Rows)}; + _Err -> + {error, db_failure} end. + lookup_sessions(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(timestamp)d, @(xml)s, @(node)s, @(service)s " - "from push_session " + LServer, + ?SQL("select @(timestamp)d, @(xml)s, @(node)s, @(service)s " + "from push_session " "where username=%(LUser)s and %(LServer)H")) of - {selected, Rows} -> - {ok, lists:map( - fun({TS, XML, Node, Service}) -> - NowTS = misc:usec_to_now(TS), - XData = decode_xdata(XML, LUser, LServer), - PushLJID = jid:tolower(jid:decode(Service)), - {NowTS, PushLJID,Node, XData} - end, Rows)}; - _Err -> - {error, db_failure} + {selected, Rows} -> + {ok, lists:map( + fun({TS, XML, Node, Service}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + PushLJID = jid:tolower(jid:decode(Service)), + {NowTS, PushLJID, Node, XData} + end, + Rows)}; + _Err -> + {error, db_failure} end. + lookup_sessions(LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(username)s, @(timestamp)d, @(xml)s, " - "@(node)s, @(service)s from push_session " + LServer, + ?SQL("select @(username)s, @(timestamp)d, @(xml)s, " + "@(node)s, @(service)s from push_session " "where %(LServer)H")) of - {selected, Rows} -> - {ok, lists:map( - fun({LUser, TS, XML, Node, Service}) -> - NowTS = misc:usec_to_now(TS), - XData = decode_xdata(XML, LUser, LServer), - PushLJID = jid:tolower(jid:decode(Service)), - {NowTS, PushLJID, Node, XData} - end, Rows)}; - _Err -> - {error, db_failure} + {selected, Rows} -> + {ok, lists:map( + fun({LUser, TS, XML, Node, Service}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + PushLJID = jid:tolower(jid:decode(Service)), + {NowTS, PushLJID, Node, XData} + end, + Rows)}; + _Err -> + {error, db_failure} end. + delete_session(LUser, LServer, NowTS) -> TS = misc:now_to_usec(NowTS), case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from push_session where " - "username=%(LUser)s and %(LServer)H and timestamp=%(TS)d")) of - {updated, _} -> - ok; - _Err -> - {error, db_failure} + LServer, + ?SQL("delete from push_session where " + "username=%(LUser)s and %(LServer)H and timestamp=%(TS)d")) of + {updated, _} -> + ok; + _Err -> + {error, db_failure} end. + delete_old_sessions(LServer, Time) -> TS = misc:now_to_usec(Time), case ejabberd_sql:sql_query( - LServer, - ?SQL("delete from push_session where timestamp<%(TS)d " + LServer, + ?SQL("delete from push_session where timestamp<%(TS)d " "and %(LServer)H")) of - {updated, _} -> - ok; - _Err -> - {error, db_failure} + {updated, _} -> + ok; + _Err -> + {error, db_failure} end. + export(_Server) -> [{push_session, - fun(Host, #push_session{us = {LUser, LServer}, - timestamp = NowTS, - service = PushLJID, - node = Node, - xml = XData}) - when LServer == Host -> - TS = misc:now_to_usec(NowTS), - Service = jid:encode(PushLJID), - XML = encode_xdata(XData), - [?SQL("delete from push_session where " - "username=%(LUser)s and %(LServer)H and " + fun(Host, + #push_session{ + us = {LUser, LServer}, + timestamp = NowTS, + service = PushLJID, + node = Node, + xml = XData + }) + when LServer == Host -> + TS = misc:now_to_usec(NowTS), + Service = jid:encode(PushLJID), + XML = encode_xdata(XData), + [?SQL("delete from push_session where " + "username=%(LUser)s and %(LServer)H and " "timestamp=%(TS)d and " - "service=%(Service)s and node=%(Node)s and " - "xml=%(XML)s;"), - ?SQL_INSERT( - "push_session", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "timestamp=%(TS)d", - "service=%(Service)s", - "node=%(Node)s", - "xml=%(XML)s"])]; - (_Host, _R) -> - [] + "service=%(Service)s and node=%(Node)s and " + "xml=%(XML)s;"), + ?SQL_INSERT( + "push_session", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "service=%(Service)s", + "node=%(Node)s", + "xml=%(XML)s"])]; + (_Host, _R) -> + [] end}]. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -236,39 +267,45 @@ enforce_max_sessions(_LUser, _LServer, infinity) -> ok; enforce_max_sessions(LUser, LServer, MaxSessions) -> case lookup_sessions(LUser, LServer) of - {ok, Sessions} when length(Sessions) >= MaxSessions -> - ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", - [LUser, LServer]), - Sessions1 = lists:sort(fun({TS1, _, _, _}, {TS2, _, _, _}) -> - TS1 >= TS2 - end, Sessions), - OldSessions = lists:nthtail(MaxSessions - 1, Sessions1), - lists:foreach(fun({TS, _, _, _}) -> - delete_session(LUser, LServer, TS) - end, OldSessions); - _ -> - ok + {ok, Sessions} when length(Sessions) >= MaxSessions -> + ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", + [LUser, LServer]), + Sessions1 = lists:sort(fun({TS1, _, _, _}, {TS2, _, _, _}) -> + TS1 >= TS2 + end, + Sessions), + OldSessions = lists:nthtail(MaxSessions - 1, Sessions1), + lists:foreach(fun({TS, _, _, _}) -> + delete_session(LUser, LServer, TS) + end, + OldSessions); + _ -> + ok end. + decode_xdata(<<>>, _LUser, _LServer) -> undefined; decode_xdata(XML, LUser, LServer) -> case fxml_stream:parse_element(XML) of - #xmlel{} = El -> - try xmpp:decode(El) - catch _:{xmpp_codec, Why} -> - ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts " - "from table 'push_session': ~ts", - [XML, LUser, LServer, xmpp:format_error(Why)]), - undefined - end; - Err -> - ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts from " - "table 'push_session': ~p", - [XML, LUser, LServer, Err]), - undefined + #xmlel{} = El -> + try + xmpp:decode(El) + catch + _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts " + "from table 'push_session': ~ts", + [XML, LUser, LServer, xmpp:format_error(Why)]), + undefined + end; + Err -> + ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts from " + "table 'push_session': ~p", + [XML, LUser, LServer, Err]), + undefined end. + encode_xdata(undefined) -> <<>>; encode_xdata(XData) -> diff --git a/src/mod_register.erl b/src/mod_register.erl index 793c3c54d..e18911cf3 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -31,107 +31,145 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, stream_feature_register/2, - 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]). +-export([start/2, + stop/1, + reload/3, + stream_feature_register/2, + 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"). + start(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, mod_register_ip, - [{ram_copies, [node()]}, {local_content, true}, - {attributes, [key, value]}]), + ejabberd_mnesia:create(?MODULE, + mod_register_ip, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, [key, value]}]), {ok, [{iq_handler, ejabberd_local, ?NS_REGISTER, process_iq}, {iq_handler, ejabberd_sm, ?NS_REGISTER, process_iq}, {hook, c2s_pre_auth_features, stream_feature_register, 50}, {hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + -spec stream_feature_register([xmpp_element()], binary()) -> [xmpp_element()]. stream_feature_register(Acc, Host) -> case {mod_register_opt:access(Host), - mod_register_opt:ip_access(Host), - mod_register_opt:redirect_url(Host)} of - {none, _, undefined} -> Acc; - {_, none, undefined} -> Acc; - {_, _, _} -> [#feature_register{}|Acc] + mod_register_opt:ip_access(Host), + mod_register_opt:redirect_url(Host)} of + {none, _, undefined} -> Acc; + {_, none, undefined} -> Acc; + {_, _, _} -> [#feature_register{} | Acc] end. + c2s_unauthenticated_packet(#{ip := IP, server := Server} = State, - #iq{type = T, sub_els = [_]} = IQ) + #iq{type = T, sub_els = [_]} = IQ) when T == set; T == get -> try xmpp:try_subtag(IQ, #register{}) of - #register{} = Register -> - {Address, _} = IP, - IQ1 = xmpp:set_els(IQ, [Register]), - IQ2 = xmpp:set_from_to(IQ1, jid:make(<<>>), jid:make(Server)), - ResIQ = process_iq(IQ2, Address), - ResIQ1 = xmpp:set_from_to(ResIQ, jid:make(Server), undefined), - {stop, ejabberd_c2s:send(State, ResIQ1)}; - false -> - State - catch _:{xmpp_codec, Why} -> - Txt = xmpp:io_format_error(Why), - Lang = maps:get(lang, State), - Err = make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)), - {stop, ejabberd_c2s:send(State, Err)} + #register{} = Register -> + {Address, _} = IP, + IQ1 = xmpp:set_els(IQ, [Register]), + IQ2 = xmpp:set_from_to(IQ1, jid:make(<<>>), jid:make(Server)), + ResIQ = process_iq(IQ2, Address), + ResIQ1 = xmpp:set_from_to(ResIQ, jid:make(Server), undefined), + {stop, ejabberd_c2s:send(State, ResIQ1)}; + false -> + State + catch + _:{xmpp_codec, Why} -> + Txt = xmpp:io_format_error(Why), + Lang = maps:get(lang, State), + Err = make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)), + {stop, ejabberd_c2s:send(State, Err)} end; c2s_unauthenticated_packet(State, _) -> State. + process_iq(#iq{from = From} = IQ) -> process_iq(IQ, jid:tolower(From)). + process_iq(#iq{from = From, to = To} = IQ, Source) -> IsCaptchaEnabled = - case mod_register_opt:captcha_protected(To#jid.lserver) of - true -> true; - false -> false - end, + case mod_register_opt:captcha_protected(To#jid.lserver) of + true -> true; + false -> false + end, Server = To#jid.lserver, Access = mod_register_opt:access_remove(Server), Remove = case {acl:match_rule(Server, Access, From), From#jid.lserver} of - {allow, Server} -> - allow; - {_, _} -> - deny - end, + {allow, Server} -> + allow; + {_, _} -> + deny + end, process_iq(IQ, Source, IsCaptchaEnabled, Remove == allow). -process_iq(#iq{type = set, lang = Lang, - sub_els = [#register{remove = true}]} = IQ, - _Source, _IsCaptchaEnabled, _AllowRemove = false) -> + +process_iq(#iq{ + type = set, + lang = Lang, + sub_els = [#register{remove = true}] + } = IQ, + _Source, + _IsCaptchaEnabled, + _AllowRemove = false) -> Txt = ?T("Access denied by service policy"), make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang)); -process_iq(#iq{type = set, lang = Lang, to = To, from = From, - sub_els = [#register{remove = true, - username = User, - password = Password}]} = IQ, - _Source, _IsCaptchaEnabled, _AllowRemove = true) -> +process_iq(#iq{ + type = set, + lang = Lang, + to = To, + from = From, + sub_els = [#register{ + remove = true, + username = User, + password = Password + }] + } = IQ, + _Source, + _IsCaptchaEnabled, + _AllowRemove = true) -> Server = To#jid.lserver, - if is_binary(User) -> - case From of - #jid{user = User, lserver = Server} -> + if + is_binary(User) -> + case From of + #jid{user = User, lserver = Server} -> ResIQ = xmpp:make_iq_result(IQ), ejabberd_router:route(ResIQ), - ejabberd_auth:remove_user(User, Server), + ejabberd_auth:remove_user(User, Server), ignore; - _ -> - if is_binary(Password) -> + _ -> + if + is_binary(Password) -> case ejabberd_auth:check_password( User, <<"">>, Server, Password) of true -> @@ -144,183 +182,227 @@ process_iq(#iq{type = set, lang = Lang, to = To, from = From, make_stripped_error( IQ, xmpp:err_forbidden(Txt, Lang)) end; - true -> - Txt = ?T("No 'password' found in this query"), - make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) - end - end; - true -> - case From of - #jid{luser = LUser, lserver = Server} -> - ResIQ = xmpp:make_iq_result(IQ), - ejabberd_router:route(xmpp:set_from_to(ResIQ, From, From)), - ejabberd_auth:remove_user(LUser, Server), - ignore; - _ -> - Txt = ?T("The query is only allowed from local users"), - make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)) - end + true -> + Txt = ?T("No 'password' found in this query"), + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end + end; + true -> + case From of + #jid{luser = LUser, lserver = Server} -> + ResIQ = xmpp:make_iq_result(IQ), + ejabberd_router:route(xmpp:set_from_to(ResIQ, From, From)), + ejabberd_auth:remove_user(LUser, Server), + ignore; + _ -> + Txt = ?T("The query is only allowed from local users"), + make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + end end; -process_iq(#iq{type = set, to = To, - sub_els = [#register{username = User, - password = Password}]} = IQ, - Source, IsCaptchaEnabled, _AllowRemove) when is_binary(User), - is_binary(Password) -> +process_iq(#iq{ + type = set, + to = To, + sub_els = [#register{ + username = User, + password = Password + }] + } = IQ, + Source, + IsCaptchaEnabled, + _AllowRemove) when is_binary(User), + is_binary(Password) -> Server = To#jid.lserver, try_register_or_set_password( User, Server, Password, IQ, Source, not IsCaptchaEnabled); -process_iq(#iq{type = set, to = To, - lang = Lang, sub_els = [#register{xdata = #xdata{} = X}]} = IQ, - Source, true, _AllowRemove) -> +process_iq(#iq{ + type = set, + to = To, + lang = Lang, + sub_els = [#register{xdata = #xdata{} = X}] + } = IQ, + Source, + true, + _AllowRemove) -> Server = To#jid.lserver, XdataC = xmpp_util:set_xdata_field( - #xdata_field{ - var = <<"FORM_TYPE">>, - type = hidden, values = [?NS_CAPTCHA]}, - X), + #xdata_field{ + var = <<"FORM_TYPE">>, + type = hidden, + values = [?NS_CAPTCHA] + }, + X), case ejabberd_captcha:process_reply(XdataC) of - ok -> - case process_xdata_submit(X) of - {ok, User, Password} -> - try_register_or_set_password( - User, Server, Password, IQ, Source, true); - _ -> - Txt = ?T("Incorrect data form"), - make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) - end; - {error, malformed} -> - Txt = ?T("Incorrect CAPTCHA submit"), - make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)); - _ -> - ErrText = ?T("The CAPTCHA verification has failed"), - make_stripped_error(IQ, xmpp:err_not_allowed(ErrText, Lang)) + ok -> + case process_xdata_submit(X) of + {ok, User, Password} -> + try_register_or_set_password( + User, Server, Password, IQ, Source, true); + _ -> + Txt = ?T("Incorrect data form"), + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end; + {error, malformed} -> + Txt = ?T("Incorrect CAPTCHA submit"), + make_stripped_error(IQ, xmpp:err_bad_request(Txt, Lang)); + _ -> + ErrText = ?T("The CAPTCHA verification has failed"), + make_stripped_error(IQ, xmpp:err_not_allowed(ErrText, Lang)) end; process_iq(#iq{type = set} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove) -> make_stripped_error(IQ, xmpp:err_bad_request()); process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ, - Source, IsCaptchaEnabled, _AllowRemove) -> + Source, + IsCaptchaEnabled, + _AllowRemove) -> Server = To#jid.lserver, {IsRegistered, Username} = - case From of - #jid{user = User, lserver = Server} -> - case ejabberd_auth:user_exists(User, Server) of - true -> - {true, User}; - false -> - {false, User} - end; - _ -> - {false, <<"">>} - end, + case From of + #jid{user = User, lserver = Server} -> + case ejabberd_auth:user_exists(User, Server) of + true -> + {true, User}; + false -> + {false, User} + end; + _ -> + {false, <<"">>} + end, Instr = translate:translate( - Lang, ?T("Choose a username and password to register " - "with this server")), + Lang, + ?T("Choose a username and password to register " + "with this server")), URL = mod_register_opt:redirect_url(Server), - if (URL /= undefined) and not IsRegistered -> - Desc = str:translate_and_format(Lang, ?T("To register, visit ~s"), [URL]), - xmpp:make_iq_result( - IQ, #register{instructions = Desc, - sub_els = [#oob_x{url = URL}]}); - IsCaptchaEnabled and not IsRegistered -> - TopInstr = translate:translate( - Lang, ?T("You need a client that supports x:data " - "and CAPTCHA to register")), - UField = #xdata_field{type = 'text-single', - label = translate:translate(Lang, ?T("User")), - var = <<"username">>, - required = true}, - PField = #xdata_field{type = 'text-private', - label = translate:translate(Lang, ?T("Password")), - var = <<"password">>, - required = true}, - X = #xdata{type = form, instructions = [Instr], - fields = [UField, PField]}, - case ejabberd_captcha:create_captcha_x(ID, To, Lang, Source, X) of - {ok, CaptchaEls} -> + if + (URL /= undefined) and not IsRegistered -> + Desc = str:translate_and_format(Lang, ?T("To register, visit ~s"), [URL]), + xmpp:make_iq_result( + IQ, + #register{ + instructions = Desc, + sub_els = [#oob_x{url = URL}] + }); + IsCaptchaEnabled and not IsRegistered -> + TopInstr = translate:translate( + Lang, + ?T("You need a client that supports x:data " + "and CAPTCHA to register")), + UField = #xdata_field{ + type = 'text-single', + label = translate:translate(Lang, ?T("User")), + var = <<"username">>, + required = true + }, + PField = #xdata_field{ + type = 'text-private', + label = translate:translate(Lang, ?T("Password")), + var = <<"password">>, + required = true + }, + X = #xdata{ + type = form, + instructions = [Instr], + fields = [UField, PField] + }, + case ejabberd_captcha:create_captcha_x(ID, To, Lang, Source, X) of + {ok, CaptchaEls} -> {value, XdataC, CaptchaEls2} = lists:keytake(xdata, 1, CaptchaEls), Xdata = xmpp_util:set_xdata_field( - #xdata_field{ - var = <<"FORM_TYPE">>, - type = hidden, values = [?NS_REGISTER]}, - XdataC), - xmpp:make_iq_result( - IQ, #register{instructions = TopInstr, - sub_els = [Xdata | CaptchaEls2]}); - {error, limit} -> - ErrText = ?T("Too many CAPTCHA requests"), - make_stripped_error( - IQ, xmpp:err_resource_constraint(ErrText, Lang)); - _Err -> - ErrText = ?T("Unable to generate a CAPTCHA"), - make_stripped_error( - IQ, xmpp:err_internal_server_error(ErrText, Lang)) - end; - true -> - xmpp:make_iq_result( - IQ, - #register{instructions = Instr, - username = Username, - password = <<"">>, - registered = IsRegistered}) + #xdata_field{ + var = <<"FORM_TYPE">>, + type = hidden, + values = [?NS_REGISTER] + }, + XdataC), + xmpp:make_iq_result( + IQ, + #register{ + instructions = TopInstr, + sub_els = [Xdata | CaptchaEls2] + }); + {error, limit} -> + ErrText = ?T("Too many CAPTCHA requests"), + make_stripped_error( + IQ, xmpp:err_resource_constraint(ErrText, Lang)); + _Err -> + ErrText = ?T("Unable to generate a CAPTCHA"), + make_stripped_error( + IQ, xmpp:err_internal_server_error(ErrText, Lang)) + end; + true -> + xmpp:make_iq_result( + IQ, + #register{ + instructions = Instr, + username = Username, + password = <<"">>, + registered = IsRegistered + }) end. -try_register_or_set_password(User, Server, Password, - #iq{from = From, lang = Lang} = IQ, - Source, CaptchaSucceed) -> + +try_register_or_set_password(User, + Server, + Password, + #iq{from = From, lang = Lang} = IQ, + Source, + CaptchaSucceed) -> case {jid:nodeprep(User), From} of - {error, _} -> - Err = xmpp:err_jid_malformed(format_error(invalid_jid), Lang), - make_stripped_error(IQ, Err); - {UserP, #jid{user = User2, lserver = Server}} when UserP == User2 -> - try_set_password(User, Server, Password, IQ); - _ when CaptchaSucceed -> - case check_from(From, Server) of - allow -> - case try_register(User, Server, Password, Source, ?MODULE, Lang) of - ok -> - xmpp:make_iq_result(IQ); - {error, Error} -> - make_stripped_error(IQ, Error) - end; - deny -> - Txt = ?T("Access denied by service policy"), - make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang)) - end; - _ -> - make_stripped_error(IQ, xmpp:err_not_allowed()) + {error, _} -> + Err = xmpp:err_jid_malformed(format_error(invalid_jid), Lang), + make_stripped_error(IQ, Err); + {UserP, #jid{user = User2, lserver = Server}} when UserP == User2 -> + try_set_password(User, Server, Password, IQ); + _ when CaptchaSucceed -> + case check_from(From, Server) of + allow -> + case try_register(User, Server, Password, Source, ?MODULE, Lang) of + ok -> + xmpp:make_iq_result(IQ); + {error, Error} -> + make_stripped_error(IQ, Error) + end; + deny -> + Txt = ?T("Access denied by service policy"), + make_stripped_error(IQ, xmpp:err_forbidden(Txt, Lang)) + end; + _ -> + make_stripped_error(IQ, xmpp:err_not_allowed()) end. + try_set_password(User, Server, Password) -> case is_strong_password(Server, Password) of - true -> - ejabberd_auth:set_password(User, Server, Password); - error_preparing_password -> - {error, invalid_password}; - false -> - {error, weak_password} + true -> + ejabberd_auth:set_password(User, Server, Password); + error_preparing_password -> + {error, invalid_password}; + false -> + {error, weak_password} end. + try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) -> case try_set_password(User, Server, Password) of - ok -> - ?INFO_MSG("~ts has changed password from ~ts", - [jid:encode({User, Server, <<"">>}), - ejabberd_config:may_hide_data( - misc:ip_to_list(maps:get(ip, M, {0,0,0,0})))]), - xmpp:make_iq_result(IQ); - {error, not_allowed} -> - Txt = ?T("Changing password is not allowed"), - make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)); - {error, invalid_jid = Why} -> - make_stripped_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang)); - {error, invalid_password = Why} -> - make_stripped_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang)); - {error, weak_password = Why} -> - make_stripped_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang)); - {error, db_failure = Why} -> - make_stripped_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang)) + ok -> + ?INFO_MSG("~ts has changed password from ~ts", + [jid:encode({User, Server, <<"">>}), + ejabberd_config:may_hide_data( + misc:ip_to_list(maps:get(ip, M, {0, 0, 0, 0})))]), + xmpp:make_iq_result(IQ); + {error, not_allowed} -> + Txt = ?T("Changing password is not allowed"), + make_stripped_error(IQ, xmpp:err_not_allowed(Txt, Lang)); + {error, invalid_jid = Why} -> + make_stripped_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang)); + {error, invalid_password = Why} -> + make_stripped_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang)); + {error, weak_password = Why} -> + make_stripped_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang)); + {error, db_failure = Why} -> + make_stripped_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 @@ -328,69 +410,72 @@ try_register(User, Server, Password, SourceRaw, Module) -> false -> {error, eaccess} end. + try_register(User, Server, Password, SourceRaw) -> case jid:is_nodename(User) of - false -> - {error, invalid_jid}; - true -> - case check_access(User, Server, SourceRaw) of - deny -> - {error, eaccess}; - allow -> - Source = may_remove_resource(SourceRaw), - case check_timeout(Source) of - true -> - case is_strong_password(Server, Password) of - true -> - case ejabberd_auth:try_register( - User, Server, Password) of - ok -> - ok; - {error, _} = Err -> - remove_timeout(Source), - Err - end; - false -> - remove_timeout(Source), - {error, weak_password}; - error_preparing_password -> - remove_timeout(Source), - {error, invalid_password} - end; - false -> - {error, wait} - end - end + false -> + {error, invalid_jid}; + true -> + case check_access(User, Server, SourceRaw) of + deny -> + {error, eaccess}; + allow -> + Source = may_remove_resource(SourceRaw), + case check_timeout(Source) of + true -> + case is_strong_password(Server, Password) of + true -> + case ejabberd_auth:try_register( + User, Server, Password) of + ok -> + ok; + {error, _} = Err -> + remove_timeout(Source), + Err + end; + false -> + remove_timeout(Source), + {error, weak_password}; + error_preparing_password -> + remove_timeout(Source), + {error, invalid_password} + end; + false -> + {error, wait} + end + end end. + 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), - ?INFO_MSG("The account ~ts was registered from IP address ~ts", - [jid:encode({User, Server, <<"">>}), - ejabberd_config:may_hide_data(ip_to_string(Source))]), - send_welcome_message(JID), - send_registration_notifications(?MODULE, JID, Source); - {error, invalid_jid = Why} -> - {error, xmpp:err_jid_malformed(format_error(Why), Lang)}; - {error, eaccess = Why} -> - {error, xmpp:err_forbidden(format_error(Why), Lang)}; - {error, wait = Why} -> - {error, xmpp:err_resource_constraint(format_error(Why), Lang)}; - {error, weak_password = Why} -> - {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; - {error, invalid_password = Why} -> - {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; - {error, not_allowed = Why} -> - {error, xmpp:err_not_allowed(format_error(Why), Lang)}; - {error, exists = Why} -> - {error, xmpp:err_conflict(format_error(Why), Lang)}; - {error, db_failure = Why} -> - {error, xmpp:err_internal_server_error(format_error(Why), Lang)} + ok -> + JID = jid:make(User, Server), + Source = may_remove_resource(SourceRaw), + ?INFO_MSG("The account ~ts was registered from IP address ~ts", + [jid:encode({User, Server, <<"">>}), + ejabberd_config:may_hide_data(ip_to_string(Source))]), + send_welcome_message(JID), + send_registration_notifications(?MODULE, JID, Source); + {error, invalid_jid = Why} -> + {error, xmpp:err_jid_malformed(format_error(Why), Lang)}; + {error, eaccess = Why} -> + {error, xmpp:err_forbidden(format_error(Why), Lang)}; + {error, wait = Why} -> + {error, xmpp:err_resource_constraint(format_error(Why), Lang)}; + {error, weak_password = Why} -> + {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; + {error, invalid_password = Why} -> + {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; + {error, not_allowed = Why} -> + {error, xmpp:err_not_allowed(format_error(Why), Lang)}; + {error, exists = Why} -> + {error, xmpp:err_conflict(format_error(Why), Lang)}; + {error, db_failure = Why} -> + {error, xmpp:err_internal_server_error(format_error(Why), Lang)} end. + format_error(invalid_jid) -> ?T("Malformed username"); format_error(eaccess) -> @@ -410,19 +495,23 @@ format_error(db_failure) -> format_error(Unexpected) -> list_to_binary(io_lib:format(?T("Unexpected error condition: ~p"), [Unexpected])). + send_welcome_message(JID) -> Host = JID#jid.lserver, case mod_register_opt:welcome_message(Host) of - {<<"">>, <<"">>} -> ok; - {Subj, Body} -> - ejabberd_router:route( - #message{from = jid:make(Host), - to = JID, - type = chat, - subject = xmpp:mk_text(Subj), - body = xmpp:mk_text(Body)}) + {<<"">>, <<"">>} -> ok; + {Subj, Body} -> + ejabberd_router:route( + #message{ + from = jid:make(Host), + to = JID, + type = chat, + subject = xmpp:mk_text(Subj), + body = xmpp:mk_text(Body) + }) end. + send_registration_notifications(Mod, UJID, Source) -> Host = UJID#jid.lserver, case mod_register_opt:registration_watchers(Host) of @@ -430,98 +519,110 @@ send_registration_notifications(Mod, UJID, Source) -> JIDs when is_list(JIDs) -> Body = (str:format("[~s] The account ~s was registered from " - "IP address ~s on node ~w using ~p.", - [get_time_string(), - jid:encode(UJID), - ejabberd_config:may_hide_data( - ip_to_string(Source)), - node(), Mod])), + "IP address ~s on node ~w using ~p.", + [get_time_string(), + jid:encode(UJID), + ejabberd_config:may_hide_data( + ip_to_string(Source)), + node(), + Mod])), lists:foreach( fun(JID) -> ejabberd_router:route( - #message{from = jid:make(Host), - to = JID, - type = chat, - body = xmpp:mk_text(Body)}) - end, JIDs) + #message{ + from = jid:make(Host), + to = JID, + type = chat, + body = xmpp:mk_text(Body) + }) + end, + JIDs) end. + check_from(#jid{user = <<"">>, server = <<"">>}, - _Server) -> + _Server) -> allow; check_from(JID, Server) -> Access = mod_register_opt:access_from(Server), acl:match_rule(Server, Access, JID). + check_timeout(undefined) -> true; check_timeout(Source) -> Timeout = ejabberd_option:registration_timeout(), - if is_integer(Timeout) -> - Priority = -erlang:system_time(millisecond), - CleanPriority = Priority + Timeout, - F = fun () -> - Treap = case mnesia:read(mod_register_ip, treap, write) - of - [] -> treap:empty(); - [{mod_register_ip, treap, T}] -> T - end, - Treap1 = clean_treap(Treap, CleanPriority), - case treap:lookup(Source, Treap1) of - error -> - Treap2 = treap:insert(Source, Priority, [], - Treap1), - mnesia:write({mod_register_ip, treap, Treap2}), - true; - {ok, _, _} -> - mnesia:write({mod_register_ip, treap, Treap1}), - false - end - end, - case mnesia:transaction(F) of - {atomic, Res} -> Res; - {aborted, Reason} -> - ?ERROR_MSG("timeout check error: ~p~n", [Reason]), - true - end; - true -> true + if + is_integer(Timeout) -> + Priority = -erlang:system_time(millisecond), + CleanPriority = Priority + Timeout, + F = fun() -> + Treap = case mnesia:read(mod_register_ip, treap, write) of + [] -> treap:empty(); + [{mod_register_ip, treap, T}] -> T + end, + Treap1 = clean_treap(Treap, CleanPriority), + case treap:lookup(Source, Treap1) of + error -> + Treap2 = treap:insert(Source, + Priority, + [], + Treap1), + mnesia:write({mod_register_ip, treap, Treap2}), + true; + {ok, _, _} -> + mnesia:write({mod_register_ip, treap, Treap1}), + false + end + end, + case mnesia:transaction(F) of + {atomic, Res} -> Res; + {aborted, Reason} -> + ?ERROR_MSG("timeout check error: ~p~n", [Reason]), + true + end; + true -> true end. + clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of - true -> Treap; - false -> - {_Key, Priority, _Value} = treap:get_root(Treap), - if Priority > CleanPriority -> - clean_treap(treap:delete_root(Treap), CleanPriority); - true -> Treap - end + true -> Treap; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if + Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority); + true -> Treap + end end. + remove_timeout(undefined) -> true; remove_timeout(Source) -> Timeout = ejabberd_option:registration_timeout(), - if is_integer(Timeout) -> - F = fun () -> - Treap = case mnesia:read(mod_register_ip, treap, write) - of - [] -> treap:empty(); - [{mod_register_ip, treap, T}] -> T - end, - Treap1 = treap:delete(Source, Treap), - mnesia:write({mod_register_ip, treap, Treap1}), - ok - end, - case mnesia:transaction(F) of - {atomic, ok} -> ok; - {aborted, Reason} -> - ?ERROR_MSG("Mod_register: timeout remove error: " - "~p~n", - [Reason]), - ok - end; - true -> ok + if + is_integer(Timeout) -> + F = fun() -> + Treap = case mnesia:read(mod_register_ip, treap, write) of + [] -> treap:empty(); + [{mod_register_ip, treap, T}] -> T + end, + Treap1 = treap:delete(Source, Treap), + mnesia:write({mod_register_ip, treap, Treap1}), + ok + end, + case mnesia:transaction(F) of + {atomic, ok} -> ok; + {aborted, Reason} -> + ?ERROR_MSG("Mod_register: timeout remove error: " + "~p~n", + [Reason]), + ok + end; + true -> ok end. + ip_to_string({_, _, _} = USR) -> jid:encode(USR); ip_to_string(Source) when is_tuple(Source) -> @@ -529,28 +630,33 @@ ip_to_string(Source) when is_tuple(Source) -> ip_to_string(undefined) -> <<"undefined">>; ip_to_string(_) -> <<"unknown">>. + get_time_string() -> write_time(erlang:localtime()). %% Function copied from ejabberd_logger_h.erl and customized + write_time({{Y, Mo, D}, {H, Mi, S}}) -> io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Y, Mo, D, H, Mi, S]). + [Y, Mo, D, H, Mi, S]). + process_xdata_submit(X) -> case {xmpp_util:get_xdata_values(<<"username">>, X), - xmpp_util:get_xdata_values(<<"password">>, X)} of - {[User], [Pass]} -> {ok, User, Pass}; - _ -> error + xmpp_util:get_xdata_values(<<"password">>, X)} of + {[User], [Pass]} -> {ok, User, Pass}; + _ -> error end. + is_strong_password(Server, Password) -> case jid:resourceprep(Password) of - PP when is_binary(PP) -> - is_strong_password2(Server, Password); - error -> - error_preparing_password + PP when is_binary(PP) -> + is_strong_password2(Server, Password); + error -> + error_preparing_password end. + is_strong_password2(Server, Password) -> LServer = jid:nameprep(Server), case mod_register_opt:password_strength(LServer) of @@ -560,20 +666,25 @@ is_strong_password2(Server, Password) -> ejabberd_auth:entropy(Password) >= Entropy end. + make_stripped_error(IQ, Err) -> xmpp:make_error(xmpp:remove_subtag(IQ, #register{}), Err). + %%% %%% ip_access management %%% + may_remove_resource({_, _, _} = From) -> jid:remove_resource(From); may_remove_resource(From) -> From. + get_ip_access(Host) -> mod_register_opt:ip_access(Host). + check_ip_access(Server, {User, Server, Resource}, IPAccess) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of {IPAddress, _PortNumber} -> @@ -586,15 +697,17 @@ check_ip_access(_Server, undefined, _IPAccess) -> check_ip_access(Server, IPAddress, IPAccess) -> acl:match_rule(Server, IPAccess, IPAddress). + check_access(User, Server, Source) -> JID = jid:make(User, Server), Access = mod_register_opt:access(Server), IPAccess = get_ip_access(Server), case acl:match_rule(Server, Access, JID) of - allow -> check_ip_access(Server, Source, IPAccess); - deny -> deny + allow -> check_ip_access(Server, Source, IPAccess); + deny -> deny end. + mod_opt_type(access) -> econf:acl(); mod_opt_type(access_from) -> @@ -614,17 +727,20 @@ mod_opt_type(registration_watchers) -> mod_opt_type(welcome_message) -> econf:and_then( econf:options( - #{subject => econf:binary(), - body => econf:binary()}), + #{ + subject => econf:binary(), + body => econf:binary() + }), fun(Opts) -> - {proplists:get_value(subject, Opts, <<>>), - proplists:get_value(body, Opts, <<>>)} + {proplists:get_value(subject, Opts, <<>>), + proplists:get_value(body, Opts, <<>>)} end); mod_opt_type(redirect_url) -> econf:url(). + -spec mod_options(binary()) -> [{welcome_message, {binary(), binary()}} | - {atom(), term()}]. + {atom(), term()}]. mod_options(_Host) -> [{access, all}, {access_from, none}, @@ -637,20 +753,27 @@ mod_options(_Host) -> {redirect_url, undefined}, {welcome_message, {<<>>, <<>>}}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module adds support for https://xmpp.org/extensions/xep-0077.html" "[XEP-0077: In-Band Registration]. " - "This protocol enables end users to use an XMPP client to:"), "", - ?T("* Register a new account on the server."), "", - ?T("* Change the password from an existing account on the server."), "", - ?T("* Delete an existing account on the server."), "", + "This protocol enables end users to use an XMPP client to:"), + "", + ?T("* Register a new account on the server."), + "", + ?T("* Change the password from an existing account on the server."), + "", + ?T("* Delete an existing account on the server."), + "", ?T("This module reads also the top-level _`registration_timeout`_ " "option defined globally for the server, " "so please check that option documentation too.")], opts => [{access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Specify rules to restrict what usernames can be registered. " "If a rule returns 'deny' on the requested username, " @@ -658,67 +781,86 @@ mod_doc() -> "restrictions by default. " "If 'AccessName' is 'none', then registering new accounts using " "In-Band Registration is disabled and the corresponding " - "stream feature is not announced to clients.")}}, + "stream feature is not announced to clients.") + }}, {access_from, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("By default, ejabberd doesn't allow the client to register new accounts " "from s2s or existing c2s sessions. You can change it by defining " "access rule in this option. Use with care: allowing registration " - "from s2s leads to uncontrolled massive accounts creation by rogue users.")}}, + "from s2s leads to uncontrolled massive accounts creation by rogue users.") + }}, {access_remove, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Specify rules to restrict access for user unregistration. " - "By default any user is able to unregister their account.")}}, + "By default any user is able to unregister their account.") + }}, {allow_modules, - #{value => "all | [Module, ...]", + #{ + 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]'.")}}, + "something like '[mod_register, mod_register_web]'.") + }}, {captcha_protected, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Protect registrations with _`basic.md#captcha|CAPTCHA`_. " - "The default is 'false'.")}}, + "The default is 'false'.") + }}, {ip_access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("Define rules to allow or deny account registration depending " "on the IP address of the XMPP client. The 'AccessName' should " - "be of type 'ip'. The default value is 'all'.")}}, + "be of type 'ip'. The default value is 'all'.") + }}, {password_strength, - #{value => "Entropy", + #{ + value => "Entropy", desc => ?T("This option sets the minimum " "https://en.wikipedia.org/wiki/Entropy_(information_theory)" "[Shannon entropy] for passwords. The value 'Entropy' is a " "number of bits of entropy. The recommended minimum is 32 bits. " - "The default is '0', i.e. no checks are performed.")}}, + "The default is '0', i.e. no checks are performed.") + }}, {registration_watchers, - #{value => "[JID, ...]", + #{ + value => "[JID, ...]", desc => ?T("This option defines a list of JIDs which will be notified each " - "time a new account is registered.")}}, + "time a new account is registered.") + }}, {redirect_url, - #{value => ?T("URL"), + #{ + value => ?T("URL"), desc => ?T("This option enables registration redirection as described in " "https://xmpp.org/extensions/xep-0077.html#redirect" - "[XEP-0077: In-Band Registration: Redirection].")}}, + "[XEP-0077: In-Band Registration: Redirection].") + }}, {welcome_message, - #{value => "{subject: Subject, body: Body}", + #{ + value => "{subject: Subject, body: Body}", desc => ?T("Set a welcome message that is sent to each newly registered account. " "The message will have subject 'Subject' and text 'Body'."), example => - ["modules:", - " mod_register:", - " welcome_message:", - " subject: \"Welcome!\"", - " body: |-", - " Hi!", - " Welcome to this XMPP server"]}} - ]}. + ["modules:", + " mod_register:", + " welcome_message:", + " subject: \"Welcome!\"", + " body: |-", + " Hi!", + " Welcome to this XMPP server"] + }}] + }. diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl index e7236424c..e9fe8beb8 100644 --- a/src/mod_register_opt.erl +++ b/src/mod_register_opt.erl @@ -14,63 +14,72 @@ -export([registration_watchers/1]). -export([welcome_message/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_register, access). + -spec access_from(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access_from(Opts) when is_map(Opts) -> gen_mod:get_opt(access_from, Opts); access_from(Host) -> gen_mod:get_module_opt(Host, mod_register, access_from). + -spec access_remove(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_remove(Opts) when is_map(Opts) -> gen_mod:get_opt(access_remove, 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); captcha_protected(Host) -> gen_mod:get_module_opt(Host, mod_register, captcha_protected). + -spec ip_access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). ip_access(Opts) when is_map(Opts) -> gen_mod:get_opt(ip_access, Opts); ip_access(Host) -> gen_mod:get_module_opt(Host, mod_register, ip_access). + -spec password_strength(gen_mod:opts() | global | binary()) -> number(). password_strength(Opts) when is_map(Opts) -> gen_mod:get_opt(password_strength, Opts); password_strength(Host) -> gen_mod:get_module_opt(Host, mod_register, password_strength). + -spec redirect_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). redirect_url(Opts) when is_map(Opts) -> gen_mod:get_opt(redirect_url, Opts); redirect_url(Host) -> gen_mod:get_module_opt(Host, mod_register, redirect_url). + -spec registration_watchers(gen_mod:opts() | global | binary()) -> [jid:jid()]. registration_watchers(Opts) when is_map(Opts) -> gen_mod:get_opt(registration_watchers, Opts); registration_watchers(Host) -> gen_mod:get_module_opt(Host, mod_register, registration_watchers). --spec welcome_message(gen_mod:opts() | global | binary()) -> {binary(),binary()}. + +-spec welcome_message(gen_mod:opts() | global | binary()) -> {binary(), binary()}. welcome_message(Opts) when is_map(Opts) -> gen_mod:get_opt(welcome_message, Opts); welcome_message(Host) -> gen_mod:get_module_opt(Host, mod_register, welcome_message). - diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index b91b4637b..448eeebe4 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -46,545 +46,666 @@ %%% gen_mod callbacks %%%---------------------------------------------------------------------- + start(_Host, _Opts) -> %% case mod_register_web_opt:docroot(Opts, fun(A) -> A end, undefined) of ok. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> [{mod_register, hard}]. + %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- + process(Path, #request{raw_path = RawPath} = Request) -> Continue = case Path of - [E] -> - binary:match(E, <<".">>) /= nomatch; - _ -> - false - end, + [E] -> + binary:match(E, <<".">>) /= nomatch; + _ -> + false + end, case Continue orelse binary:at(RawPath, size(RawPath) - 1) == $/ of - true -> - process2(Path, Request); - _ -> - {301, [{<<"Location">>, <>}], <<>>} + true -> + process2(Path, Request); + _ -> + {301, [{<<"Location">>, <>}], <<>>} end. + process2([], #request{method = 'GET', lang = Lang}) -> index_page(Lang); process2([<<"register.css">>], - #request{method = 'GET'}) -> + #request{method = 'GET'}) -> serve_css(); process2([Section], - #request{method = 'GET', lang = Lang, host = Host, - ip = {Addr, _Port}}) -> + #request{ + method = 'GET', + lang = Lang, + host = Host, + ip = {Addr, _Port} + }) -> Host2 = case ejabberd_router:is_my_host(Host) of - true -> - Host; - false -> - <<"">> - end, + true -> + Host; + false -> + <<"">> + end, case Section of - <<"new">> -> form_new_get(Host2, Lang, Addr); - <<"delete">> -> form_del_get(Host2, Lang); - <<"change_password">> -> form_changepass_get(Host2, Lang); - _ -> {404, [], "Not Found"} + <<"new">> -> form_new_get(Host2, Lang, Addr); + <<"delete">> -> form_del_get(Host2, Lang); + <<"change_password">> -> form_changepass_get(Host2, Lang); + _ -> {404, [], "Not Found"} end; process2([<<"new">>], - #request{method = 'POST', q = Q, ip = {Ip, _Port}, - lang = Lang, host = _HTTPHost}) -> + #request{ + method = 'POST', + q = Q, + ip = {Ip, _Port}, + lang = Lang, + host = _HTTPHost + }) -> 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), - Text = translate:translate(Lang, ?T("Your XMPP account was successfully registered.")), - {200, [], Text}; - Error -> - ErrorText = + {success, ok, {Username, Host, _Password}} -> + Jid = jid:make(Username, Host), + mod_register:send_registration_notifications(?MODULE, Jid, Ip), + Text = translate:translate(Lang, ?T("Your XMPP account was successfully registered.")), + {200, [], Text}; + Error -> + ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error creating the account: ")), translate:translate(Lang, get_error_text(Error))]), - {404, [], ErrorText} + {404, [], ErrorText} end; process2([<<"delete">>], - #request{method = 'POST', q = Q, lang = Lang, - host = _HTTPHost}) -> + #request{ + method = 'POST', + q = Q, + lang = Lang, + host = _HTTPHost + }) -> case form_del_post(Q) of - {atomic, ok} -> - Text = translate:translate(Lang, ?T("Your XMPP account was successfully unregistered.")), - {200, [], Text}; - Error -> - ErrorText = + {atomic, ok} -> + Text = translate:translate(Lang, ?T("Your XMPP account was successfully unregistered.")), + {200, [], Text}; + Error -> + ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error deleting the account: ")), translate:translate(Lang, get_error_text(Error))]), - {404, [], ErrorText} + {404, [], ErrorText} end; %% TODO: Currently only the first vhost is usable. The web request record %% should include the host where the POST was sent. process2([<<"change_password">>], - #request{method = 'POST', q = Q, lang = Lang, - host = _HTTPHost}) -> + #request{ + method = 'POST', + q = Q, + lang = Lang, + host = _HTTPHost + }) -> case form_changepass_post(Q) of - {atomic, ok} -> - Text = translate:translate(Lang, ?T("The password of your XMPP account was successfully changed.")), - {200, [], Text}; - Error -> - ErrorText = + {atomic, ok} -> + Text = translate:translate(Lang, ?T("The password of your XMPP account was successfully changed.")), + {200, [], Text}; + Error -> + ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error changing the password: ")), translate:translate(Lang, get_error_text(Error))]), - {404, [], ErrorText} + {404, [], ErrorText} end; process2(_Path, _Request) -> {404, [], "Not Found"}. + %%%---------------------------------------------------------------------- %%% CSS %%%---------------------------------------------------------------------- + serve_css() -> case css() of - {ok, CSS} -> - {200, - [{<<"Content-Type">>, <<"text/css">>}, last_modified(), - cache_control_public()], CSS}; - error -> - {404, [], "CSS not found"} + {ok, CSS} -> + {200, + [{<<"Content-Type">>, <<"text/css">>}, + last_modified(), + cache_control_public()], + CSS}; + error -> + {404, [], "CSS not found"} end. + last_modified() -> {<<"Last-Modified">>, <<"Mon, 25 Feb 2008 13:23:30 GMT">>}. + cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. + -spec css() -> {ok, binary()} | error. css() -> Dir = misc:css_dir(), File = filename:join(Dir, "register.css"), case file:read_file(File) of - {ok, Data} -> - {ok, Data}; - {error, Why} -> - ?ERROR_MSG("Failed to read ~ts: ~ts", [File, file:format_error(Why)]), - error + {ok, Data} -> + {ok, Data}; + {error, Why} -> + ?ERROR_MSG("Failed to read ~ts: ~ts", [File, file:format_error(Why)]), + error end. + meta() -> ?XA(<<"meta">>, - [{<<"name">>, <<"viewport">>}, - {<<"content">>, <<"width=device-width, initial-scale=1">>}]). + [{<<"name">>, <<"viewport">>}, + {<<"content">>, <<"width=device-width, initial-scale=1">>}]). + %%%---------------------------------------------------------------------- %%% Index page %%%---------------------------------------------------------------------- + index_page(Lang) -> HeadEls = [meta(), - ?XCT(<<"title">>, - ?T("XMPP Account Registration")), - ?XA(<<"link">>, - [{<<"href">>, <<"register.css">>}, - {<<"type">>, <<"text/css">>}, - {<<"rel">>, <<"stylesheet">>}])], + ?XCT(<<"title">>, + ?T("XMPP Account Registration")), + ?XA(<<"link">>, + [{<<"href">>, <<"register.css">>}, + {<<"type">>, <<"text/css">>}, + {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, - [{<<"class">>, <<"title">>}, - {<<"style">>, <<"text-align:center;">>}], - ?T("XMPP Account Registration")), - ?XE(<<"ul">>, - [?XE(<<"li">>, - [?ACT(<<"new/">>, ?T("Register an XMPP account"))]), - ?XE(<<"li">>, - [?ACT(<<"change_password/">>, ?T("Change Password"))]), - ?XE(<<"li">>, - [?ACT(<<"delete/">>, - ?T("Unregister an XMPP account"))])])], + [{<<"class">>, <<"title">>}, + {<<"style">>, <<"text-align:center;">>}], + ?T("XMPP Account Registration")), + ?XE(<<"ul">>, + [?XE(<<"li">>, + [?ACT(<<"new/">>, ?T("Register an XMPP account"))]), + ?XE(<<"li">>, + [?ACT(<<"change_password/">>, ?T("Change Password"))]), + ?XE(<<"li">>, + [?ACT(<<"delete/">>, + ?T("Unregister an XMPP account"))])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. + %%%---------------------------------------------------------------------- %%% Formulary new account GET %%%---------------------------------------------------------------------- + form_new_get(Host, Lang, IP) -> try build_captcha_li_list(Lang, IP) of - CaptchaEls -> - form_new_get2(Host, Lang, CaptchaEls) - catch - throw:Result -> - ?DEBUG("Unexpected result when creating a captcha: ~p", [Result]), - ejabberd_web:error(not_allowed) + CaptchaEls -> + form_new_get2(Host, Lang, CaptchaEls) + catch + throw:Result -> + ?DEBUG("Unexpected result when creating a captcha: ~p", [Result]), + ejabberd_web:error(not_allowed) end. + form_new_get2(Host, Lang, CaptchaEls) -> HeadEls = [meta(), - ?XCT(<<"title">>, - ?T("Register an XMPP account")), - ?XA(<<"link">>, - [{<<"href">>, <<"../register.css">>}, - {<<"type">>, <<"text/css">>}, - {<<"rel">>, <<"stylesheet">>}])], + ?XCT(<<"title">>, + ?T("Register an XMPP account")), + ?XA(<<"link">>, + [{<<"href">>, <<"../register.css">>}, + {<<"type">>, <<"text/css">>}, + {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, - [{<<"class">>, <<"title">>}, - {<<"style">>, <<"text-align:center;">>}], - ?T("Register an XMPP account")), - ?XCT(<<"p">>, - ?T("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.")), - ?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XE(<<"ol">>, - ([?XE(<<"li">>, - [?CT(?T("Username:")), ?C(<<" ">>), - ?INPUTS(<<"text">>, <<"username">>, <<"">>, - <<"20">>), - ?BR, - ?XE(<<"ul">>, - [?XCT(<<"li">>, - ?T("This is case insensitive: macbeth is " - "the same that MacBeth and Macbeth.")), - ?XC(<<"li">>, - <<(translate:translate(Lang, ?T("Characters not allowed:")))/binary, - " \" & ' / : < > @ ">>)])]), - ?XE(<<"li">>, - [?CT(?T("Server:")), ?C(<<" ">>), - ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("Password:")), ?C(<<" ">>), - ?INPUTS(<<"password">>, <<"password">>, <<"">>, - <<"20">>), - ?BR, - ?XE(<<"ul">>, - [?XCT(<<"li">>, - ?T("Don't tell your password to anybody, " - "not even the administrators of the XMPP " - "server.")), - ?XCT(<<"li">>, - ?T("You can later change your password using " - "an XMPP client.")), - ?XCT(<<"li">>, - ?T("Some XMPP clients can store your password " - "in the computer, but you should do this only " - "in your personal computer for safety reasons.")), - ?XCT(<<"li">>, - ?T("Memorize your password, or write it " - "in a paper placed in a safe place. In " - "XMPP there isn't an automated way " - "to recover your password if you forget " - "it."))])]), - ?XE(<<"li">>, - [?CT(?T("Password Verification:")), ?C(<<" ">>), - ?INPUTS(<<"password">>, <<"password2">>, <<"">>, - <<"20">>)])] - ++ - CaptchaEls ++ - [?XE(<<"li">>, - [?INPUTT(<<"submit">>, <<"register">>, - ?T("Register"))])]))])], + [{<<"class">>, <<"title">>}, + {<<"style">>, <<"text-align:center;">>}], + ?T("Register an XMPP account")), + ?XCT(<<"p">>, + ?T("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.")), + ?XAE(<<"form">>, + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [?XE(<<"ol">>, + ([?XE(<<"li">>, + [?CT(?T("Username:")), + ?C(<<" ">>), + ?INPUTS(<<"text">>, + <<"username">>, + <<"">>, + <<"20">>), + ?BR, + ?XE(<<"ul">>, + [?XCT(<<"li">>, + ?T("This is case insensitive: macbeth is " + "the same that MacBeth and Macbeth.")), + ?XC(<<"li">>, + <<(translate:translate(Lang, ?T("Characters not allowed:")))/binary, + " \" & ' / : < > @ ">>)])]), + ?XE(<<"li">>, + [?CT(?T("Server:")), + ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("Password:")), + ?C(<<" ">>), + ?INPUTS(<<"password">>, + <<"password">>, + <<"">>, + <<"20">>), + ?BR, + ?XE(<<"ul">>, + [?XCT(<<"li">>, + ?T("Don't tell your password to anybody, " + "not even the administrators of the XMPP " + "server.")), + ?XCT(<<"li">>, + ?T("You can later change your password using " + "an XMPP client.")), + ?XCT(<<"li">>, + ?T("Some XMPP clients can store your password " + "in the computer, but you should do this only " + "in your personal computer for safety reasons.")), + ?XCT(<<"li">>, + ?T("Memorize your password, or write it " + "in a paper placed in a safe place. In " + "XMPP there isn't an automated way " + "to recover your password if you forget " + "it."))])]), + ?XE(<<"li">>, + [?CT(?T("Password Verification:")), + ?C(<<" ">>), + ?INPUTS(<<"password">>, + <<"password2">>, + <<"">>, + <<"20">>)])] ++ + CaptchaEls ++ + [?XE(<<"li">>, + [?INPUTT(<<"submit">>, + <<"register">>, + ?T("Register"))])]))])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. + %% Copied from mod_register.erl %% Function copied from ejabberd_logger_h.erl and customized %%%---------------------------------------------------------------------- %%% Formulary new POST %%%---------------------------------------------------------------------- + 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}, Ip); - [_Username, _Host, _Password, _Password2, false, false] -> - {error, passwords_not_identical}; - [_Username, _Host, _Password, _Password2, Id, Key] -> - ejabberd_captcha:check_captcha(Id, Key), - {error, passwords_not_identical}; - _ -> {error, wrong_parameters} + [Username, Host, Password, 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] -> + ejabberd_captcha:check_captcha(Id, Key), + {error, passwords_not_identical}; + _ -> {error, wrong_parameters} end. + get_register_parameters(Q) -> - lists:map(fun (Key) -> - case lists:keysearch(Key, 1, Q) of - {value, {_Key, Value}} -> Value; - false -> false - end - end, - [<<"username">>, <<"host">>, <<"password">>, <<"password2">>, - <<"id">>, <<"key">>]). + lists:map(fun(Key) -> + case lists:keysearch(Key, 1, Q) of + {value, {_Key, Value}} -> Value; + false -> false + end + end, + [<<"username">>, + <<"host">>, + <<"password">>, + <<"password2">>, + <<"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, Ip); - captcha_non_valid -> {error, captcha_non_valid}; - captcha_not_found -> {error, captcha_non_valid} + captcha_valid -> + register_account(Username, Host, Password, Ip); + captcha_non_valid -> {error, captcha_non_valid}; + captcha_not_found -> {error, captcha_non_valid} end. + %%%---------------------------------------------------------------------- %%% Formulary Captcha support for new GET/POST %%%---------------------------------------------------------------------- + build_captcha_li_list(Lang, IP) -> case ejabberd_captcha:is_feature_available() of - true -> build_captcha_li_list2(Lang, IP); - false -> [] + true -> build_captcha_li_list2(Lang, IP); + false -> [] end. + build_captcha_li_list2(Lang, IP) -> SID = <<"">>, - From = #jid{user = <<"">>, server = <<"test">>, - resource = <<"">>}, - To = #jid{user = <<"">>, server = <<"test">>, - resource = <<"">>}, + From = #jid{ + user = <<"">>, + server = <<"test">>, + resource = <<"">> + }, + To = #jid{ + user = <<"">>, + server = <<"test">>, + resource = <<"">> + }, Args = [], case ejabberd_captcha:create_captcha( - SID, From, To, Lang, IP, Args) of - {ok, Id, _, _} -> - case ejabberd_captcha:build_captcha_html(Id, Lang) of - {_, {CImg, CText, CId, CKey}} -> - [?XE(<<"li">>, - [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; - Error -> - throw(Error) - end; - Error -> - throw(Error) + SID, From, To, Lang, IP, Args) of + {ok, Id, _, _} -> + case ejabberd_captcha:build_captcha_html(Id, Lang) of + {_, {CImg, CText, CId, CKey}} -> + [?XE(<<"li">>, + [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; + Error -> + throw(Error) + end; + Error -> + throw(Error) end. + %%%---------------------------------------------------------------------- %%% Formulary change password GET %%%---------------------------------------------------------------------- + form_changepass_get(Host, Lang) -> HeadEls = [meta(), - ?XCT(<<"title">>, ?T("Change Password")), - ?XA(<<"link">>, - [{<<"href">>, <<"../register.css">>}, - {<<"type">>, <<"text/css">>}, - {<<"rel">>, <<"stylesheet">>}])], + ?XCT(<<"title">>, ?T("Change Password")), + ?XA(<<"link">>, + [{<<"href">>, <<"../register.css">>}, + {<<"type">>, <<"text/css">>}, + {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, - [{<<"class">>, <<"title">>}, - {<<"style">>, <<"text-align:center;">>}], - ?T("Change Password")), - ?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XE(<<"ol">>, - [?XE(<<"li">>, - [?CT(?T("Username:")), ?C(<<" ">>), - ?INPUTS(<<"text">>, <<"username">>, <<"">>, - <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("Server:")), ?C(<<" ">>), - ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("Old Password:")), ?C(<<" ">>), - ?INPUTS(<<"password">>, <<"passwordold">>, <<"">>, - <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("New Password:")), ?C(<<" ">>), - ?INPUTS(<<"password">>, <<"password">>, <<"">>, - <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("Password Verification:")), ?C(<<" ">>), - ?INPUTS(<<"password">>, <<"password2">>, <<"">>, - <<"20">>)]), - ?XE(<<"li">>, - [?INPUTT(<<"submit">>, <<"changepass">>, - ?T("Change Password"))])])])], + [{<<"class">>, <<"title">>}, + {<<"style">>, <<"text-align:center;">>}], + ?T("Change Password")), + ?XAE(<<"form">>, + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [?XE(<<"ol">>, + [?XE(<<"li">>, + [?CT(?T("Username:")), + ?C(<<" ">>), + ?INPUTS(<<"text">>, + <<"username">>, + <<"">>, + <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("Server:")), + ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("Old Password:")), + ?C(<<" ">>), + ?INPUTS(<<"password">>, + <<"passwordold">>, + <<"">>, + <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("New Password:")), + ?C(<<" ">>), + ?INPUTS(<<"password">>, + <<"password">>, + <<"">>, + <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("Password Verification:")), + ?C(<<" ">>), + ?INPUTS(<<"password">>, + <<"password2">>, + <<"">>, + <<"20">>)]), + ?XE(<<"li">>, + [?INPUTT(<<"submit">>, + <<"changepass">>, + ?T("Change Password"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. + %%%---------------------------------------------------------------------- %%% Formulary change password POST %%%---------------------------------------------------------------------- + form_changepass_post(Q) -> case catch get_changepass_parameters(Q) of - [Username, Host, PasswordOld, Password, Password] -> - try_change_password(Username, Host, PasswordOld, - Password); - [_Username, _Host, _PasswordOld, _Password, _Password2] -> - {error, passwords_not_identical}; - _ -> {error, wrong_parameters} + [Username, Host, PasswordOld, Password, Password] -> + try_change_password(Username, + Host, + PasswordOld, + Password); + [_Username, _Host, _PasswordOld, _Password, _Password2] -> + {error, passwords_not_identical}; + _ -> {error, wrong_parameters} end. + get_changepass_parameters(Q) -> -%% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | -%% {error, account_doesnt_exist} | -%% {error, password_not_changed} | -%% {error, password_incorrect} - lists:map(fun (Key) -> - {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), - Value - end, - [<<"username">>, <<"host">>, <<"passwordold">>, <<"password">>, - <<"password2">>]). + %% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | + %% {error, account_doesnt_exist} | + %% {error, password_not_changed} | + %% {error, password_incorrect} + lists:map(fun(Key) -> + {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), + Value + end, + [<<"username">>, + <<"host">>, + <<"passwordold">>, + <<"password">>, + <<"password2">>]). -try_change_password(Username, Host, PasswordOld, - Password) -> - try change_password(Username, Host, PasswordOld, - Password) - of - {atomic, ok} -> {atomic, ok} + +try_change_password(Username, + Host, + PasswordOld, + Password) -> + try change_password(Username, + Host, + PasswordOld, + Password) of + {atomic, ok} -> {atomic, ok} catch - error:{badmatch, Error} -> {error, Error} + error:{badmatch, Error} -> {error, Error} end. -change_password(Username, Host, PasswordOld, - Password) -> + +change_password(Username, + Host, + PasswordOld, + Password) -> account_exists = check_account_exists(Username, Host), - password_correct = check_password(Username, Host, - PasswordOld), - ok = ejabberd_auth:set_password(Username, Host, - Password), + password_correct = check_password(Username, + Host, + PasswordOld), + ok = ejabberd_auth:set_password(Username, + Host, + Password), case check_password(Username, Host, Password) of - password_correct -> {atomic, ok}; - password_incorrect -> {error, password_not_changed} + password_correct -> {atomic, ok}; + password_incorrect -> {error, password_not_changed} end. + check_account_exists(Username, Host) -> case ejabberd_auth:user_exists(Username, Host) of - true -> account_exists; - false -> account_doesnt_exist + true -> account_exists; + false -> account_doesnt_exist end. + check_password(Username, Host, Password) -> - case ejabberd_auth:check_password(Username, <<"">>, Host, - Password) - of - true -> password_correct; - false -> password_incorrect + case ejabberd_auth:check_password(Username, + <<"">>, + Host, + Password) of + true -> password_correct; + false -> password_incorrect end. + %%%---------------------------------------------------------------------- %%% Formulary delete account GET %%%---------------------------------------------------------------------- + form_del_get(Host, Lang) -> HeadEls = [meta(), - ?XCT(<<"title">>, - ?T("Unregister an XMPP account")), - ?XA(<<"link">>, - [{<<"href">>, <<"../register.css">>}, - {<<"type">>, <<"text/css">>}, - {<<"rel">>, <<"stylesheet">>}])], + ?XCT(<<"title">>, + ?T("Unregister an XMPP account")), + ?XA(<<"link">>, + [{<<"href">>, <<"../register.css">>}, + {<<"type">>, <<"text/css">>}, + {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, - [{<<"class">>, <<"title">>}, - {<<"style">>, <<"text-align:center;">>}], - ?T("Unregister an XMPP account")), - ?XCT(<<"p">>, - ?T("This page allows to unregister an XMPP " - "account in this XMPP server.")), - ?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], - [?XE(<<"ol">>, - [?XE(<<"li">>, - [?CT(?T("Username:")), ?C(<<" ">>), - ?INPUTS(<<"text">>, <<"username">>, <<"">>, - <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("Server:")), ?C(<<" ">>), - ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), - ?XE(<<"li">>, - [?CT(?T("Password:")), ?C(<<" ">>), - ?INPUTS(<<"password">>, <<"password">>, <<"">>, - <<"20">>)]), - ?XE(<<"li">>, - [?INPUTT(<<"submit">>, <<"unregister">>, - ?T("Unregister"))])])])], + [{<<"class">>, <<"title">>}, + {<<"style">>, <<"text-align:center;">>}], + ?T("Unregister an XMPP account")), + ?XCT(<<"p">>, + ?T("This page allows to unregister an XMPP " + "account in this XMPP server.")), + ?XAE(<<"form">>, + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [?XE(<<"ol">>, + [?XE(<<"li">>, + [?CT(?T("Username:")), + ?C(<<" ">>), + ?INPUTS(<<"text">>, + <<"username">>, + <<"">>, + <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("Server:")), + ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), + ?XE(<<"li">>, + [?CT(?T("Password:")), + ?C(<<" ">>), + ?INPUTS(<<"password">>, + <<"password">>, + <<"">>, + <<"20">>)]), + ?XE(<<"li">>, + [?INPUTT(<<"submit">>, + <<"unregister">>, + ?T("Unregister"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. + %% @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, Ip) -> try mod_register_opt:access(Host) of - Access -> - case jid:make(Username, Host) of - error -> {error, invalid_jid}; - JID -> - case acl:match_rule(Host, Access, JID) of - deny -> {error, not_allowed}; - allow -> register_account2(Username, Host, Password, Ip) - end - end - catch _:{module_not_loaded, mod_register, _Host} -> - {error, host_unknown} + Access -> + case jid:make(Username, Host) of + error -> {error, invalid_jid}; + JID -> + case acl:match_rule(Host, Access, JID) of + deny -> {error, not_allowed}; + allow -> register_account2(Username, Host, Password, Ip) + end + end + catch + _:{module_not_loaded, mod_register, _Host} -> + {error, host_unknown} end. + register_account2(Username, Host, Password, Ip) -> - case mod_register:try_register(Username, Host, Password, Ip, ?MODULE) - of - ok -> - {success, ok, {Username, Host, Password}}; - Other -> Other + case mod_register:try_register(Username, Host, Password, Ip, ?MODULE) of + ok -> + {success, ok, {Username, Host, Password}}; + Other -> Other end. + %%%---------------------------------------------------------------------- %%% Formulary delete POST %%%---------------------------------------------------------------------- + form_del_post(Q) -> case catch get_unregister_parameters(Q) of - [Username, Host, Password] -> - try_unregister_account(Username, Host, Password); - _ -> {error, wrong_parameters} + [Username, Host, Password] -> + try_unregister_account(Username, Host, Password); + _ -> {error, wrong_parameters} end. + get_unregister_parameters(Q) -> -%% @spec(Username, Host, Password) -> {atomic, ok} | -%% {error, account_doesnt_exist} | -%% {error, account_exists} | -%% {error, password_incorrect} - lists:map(fun (Key) -> - {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), - Value - end, - [<<"username">>, <<"host">>, <<"password">>]). + %% @spec(Username, Host, Password) -> {atomic, ok} | + %% {error, account_doesnt_exist} | + %% {error, account_exists} | + %% {error, password_incorrect} + lists:map(fun(Key) -> + {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), + Value + end, + [<<"username">>, <<"host">>, <<"password">>]). + try_unregister_account(Username, Host, Password) -> try unregister_account(Username, Host, Password) of - {atomic, ok} -> {atomic, ok} + {atomic, ok} -> {atomic, ok} catch - error:{badmatch, Error} -> {error, Error} + error:{badmatch, Error} -> {error, Error} end. + unregister_account(Username, Host, Password) -> account_exists = check_account_exists(Username, Host), - password_correct = check_password(Username, Host, - Password), - ok = ejabberd_auth:remove_user(Username, Host, - Password), + password_correct = check_password(Username, + Host, + Password), + ok = ejabberd_auth:remove_user(Username, + Host, + Password), account_doesnt_exist = check_account_exists(Username, - Host), + Host), {atomic, ok}. + %%%---------------------------------------------------------------------- %%% Error texts %%%---------------------------------------------------------------------- + get_error_text({error, captcha_non_valid}) -> ?T("The captcha you entered is wrong"); get_error_text({error, exists}) -> @@ -606,36 +727,46 @@ get_error_text({error, wrong_parameters}) -> get_error_text({error, Why}) -> mod_register:format_error(Why). + mod_options(_) -> []. + mod_doc() -> - #{desc => - [?T("This module provides a web page where users can:"), "", - ?T("- Register a new account on the server."), "", - ?T("- Change the password from an existing account on the server."), "", - ?T("- Unregister an existing account on the server."), "", - ?T("This module supports _`basic.md#captcha|CAPTCHA`_ " + #{ + desc => + [?T("This module provides a web page where users can:"), + "", + ?T("- Register a new account on the server."), + "", + ?T("- Change the password from an existing account on the server."), + "", + ?T("- Unregister an existing account on the server."), + "", + ?T("This module supports _`basic.md#captcha|CAPTCHA`_ " "to register a new account. " - "To enable this feature, configure the " + "To enable this feature, configure the " "top-level _`captcha_cmd`_ and " - "top-level _`captcha_url`_ options."), "", - ?T("As an example usage, the users of the host 'localhost' can " - "visit the page: 'https://localhost:5280/register/' It is " - "important to include the last / character in the URL, " - "otherwise the subpages URL will be incorrect."), "", + "top-level _`captcha_url`_ options."), + "", + ?T("As an example usage, the users of the host 'localhost' can " + "visit the page: 'https://localhost:5280/register/' It is " + "important to include the last / character in the URL, " + "otherwise the subpages URL will be incorrect."), + "", ?T("This module is enabled in 'listen' -> 'ejabberd_http' -> " "_`listen-options.md#request_handlers|request_handlers`_, " "no need to enable in 'modules'."), ?T("The module depends on _`mod_register`_ where all the " "configuration is performed.")], - example => - ["listen:", - " -", - " port: 5280", - " module: ejabberd_http", - " request_handlers:", - " /register: mod_register_web", - "", - "modules:", - " mod_register: {}"]}. + example => + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /register: mod_register_web", + "", + "modules:", + " mod_register: {}"] + }. diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 7765fc255..8056e9669 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -40,46 +40,69 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_iq/1, export/1, - import_info/0, process_local_iq/1, get_user_roster_items/2, - import/5, get_roster/2, push_item/3, - import_start/2, import_stop/2, is_subscribed/2, - c2s_self_presence/1, in_subscription/2, - out_subscription/1, set_items/3, remove_user/2, - get_jid_info/4, encode_item/1, get_versioning_feature/2, - roster_version/2, mod_doc/0, - mod_opt_type/1, mod_options/1, set_roster/1, del_roster/3, - process_rosteritems/5, - depends/2, set_item_and_notify_clients/3]). +-export([start/2, + stop/1, + reload/3, + process_iq/1, + export/1, + import_info/0, + process_local_iq/1, + get_user_roster_items/2, + import/5, + get_roster/2, + push_item/3, + import_start/2, + import_stop/2, + is_subscribed/2, + c2s_self_presence/1, + in_subscription/2, + out_subscription/1, + set_items/3, + remove_user/2, + get_jid_info/4, + encode_item/1, + get_versioning_feature/2, + roster_version/2, + mod_doc/0, + mod_opt_type/1, + mod_options/1, + set_roster/1, + del_roster/3, + process_rosteritems/5, + depends/2, + set_item_and_notify_clients/3]). -export([webadmin_page_hostuser/4, webadmin_menu_hostuser/4, webadmin_user/4]). -import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/4]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_roster.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("ejabberd_stacktrace.hrl"). -include("translate.hrl"). --define(ROSTER_CACHE, roster_cache). --define(ROSTER_ITEM_CACHE, roster_item_cache). +-define(ROSTER_CACHE, roster_cache). +-define(ROSTER_ITEM_CACHE, roster_item_cache). -define(ROSTER_VERSION_CACHE, roster_version_cache). --define(SM_MIX_ANNOTATE, roster_mix_annotate). +-define(SM_MIX_ANNOTATE, roster_mix_annotate). -type c2s_state() :: ejabberd_c2s:state(). -export_type([subscription/0]). + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), #roster{} | [binary()]) -> ok. -callback read_roster_version(binary(), binary()) -> {ok, binary()} | error. -callback write_roster_version(binary(), binary(), boolean(), binary()) -> any(). -callback get_roster(binary(), binary()) -> {ok, [#roster{}]} | error. -callback get_roster_item(binary(), binary(), ljid()) -> {ok, #roster{}} | error. --callback read_subscription_and_groups(binary(), binary(), ljid()) - -> {ok, {subscription(), ask(), [binary()]}} | error. +-callback read_subscription_and_groups(binary(), binary(), ljid()) -> + {ok, {subscription(), ask(), [binary()]}} | error. -callback roster_subscribe(binary(), binary(), ljid(), #roster{}) -> any(). -callback transaction(binary(), fun(() -> T)) -> {atomic, T} | {aborted, any()}. -callback remove_user(binary(), binary()) -> any(). @@ -90,6 +113,7 @@ -optional_callbacks([use_cache/2, cache_nodes/1]). + start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), @@ -106,146 +130,181 @@ start(Host, Opts) -> {hook, webadmin_user, webadmin_user, 50}, {iq_handler, ejabberd_sm, ?NS_ROSTER, process_iq}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), - if NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + if + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts). + depends(_Host, _Opts) -> []. + -spec process_iq(iq()) -> iq(). -process_iq(#iq{from = #jid{luser = U, lserver = S}, - to = #jid{luser = U, lserver = S}} = IQ) -> +process_iq(#iq{ + from = #jid{luser = U, lserver = S}, + to = #jid{luser = U, lserver = S} + } = IQ) -> process_local_iq(IQ); process_iq(#iq{lang = Lang, to = To} = IQ) -> case ejabberd_hooks:run_fold(roster_remote_access, - To#jid.lserver, false, [IQ]) of - false -> - Txt = ?T("Query to another users is forbidden"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); - {true, IQ1} -> - process_local_iq(IQ1) + To#jid.lserver, + false, + [IQ]) of + false -> + Txt = ?T("Query to another users is forbidden"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); + {true, IQ1} -> + process_local_iq(IQ1) end. + -spec process_local_iq(iq()) -> iq(). -process_local_iq(#iq{type = set,lang = Lang, - sub_els = [#roster_query{ - items = [#roster_item{ask = Ask}]}]} = IQ) +process_local_iq(#iq{ + type = set, + lang = Lang, + sub_els = [#roster_query{ + items = [#roster_item{ask = Ask}] + }] + } = IQ) when Ask /= undefined -> Txt = ?T("Possessing 'ask' attribute is not allowed by RFC6121"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); -process_local_iq(#iq{type = set, from = From, lang = Lang, - sub_els = [#roster_query{ - items = [#roster_item{} = Item]}]} = IQ) -> +process_local_iq(#iq{ + type = set, + from = From, + lang = Lang, + sub_els = [#roster_query{ + items = [#roster_item{} = Item] + }] + } = IQ) -> case has_duplicated_groups(Item#roster_item.groups) of - true -> - Txt = ?T("Duplicated groups are not allowed by RFC6121"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); - false -> - From1 = case xmpp:get_meta(IQ, privilege_from, none) of - #jid{} = PrivFrom -> - PrivFrom; - none -> - From - end, - #jid{lserver = LServer} = From1, - Access = mod_roster_opt:access(LServer), - case acl:match_rule(LServer, Access, From) of - deny -> - Txt = ?T("Access denied by service policy"), - xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); - allow -> - process_iq_set(IQ) - end + true -> + Txt = ?T("Duplicated groups are not allowed by RFC6121"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); + false -> + From1 = case xmpp:get_meta(IQ, privilege_from, none) of + #jid{} = PrivFrom -> + PrivFrom; + none -> + From + end, + #jid{lserver = LServer} = From1, + Access = mod_roster_opt:access(LServer), + case acl:match_rule(LServer, Access, From) of + deny -> + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); + allow -> + process_iq_set(IQ) + end end; -process_local_iq(#iq{type = set, lang = Lang, - sub_els = [#roster_query{items = [_|_]}]} = IQ) -> +process_local_iq(#iq{ + type = set, + lang = Lang, + sub_els = [#roster_query{items = [_ | _]}] + } = IQ) -> Txt = ?T("Multiple elements are not allowed by RFC6121"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); -process_local_iq(#iq{type = get, lang = Lang, - sub_els = [#roster_query{items = Items}]} = IQ) -> +process_local_iq(#iq{ + type = get, + lang = Lang, + sub_els = [#roster_query{items = Items}] + } = IQ) -> case Items of - [] -> - process_iq_get(IQ); - [_|_] -> - Txt = ?T("The query must not contain elements"), - xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + [] -> + process_iq_get(IQ); + [_ | _] -> + Txt = ?T("The query must not contain elements"), + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; process_local_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec roster_hash([#roster{}]) -> binary(). roster_hash(Items) -> - str:sha(term_to_binary(lists:sort([R#roster_item{groups = lists:sort(Grs)} - || R = #roster_item{groups = Grs} - <- Items]))). + str:sha(term_to_binary(lists:sort([ R#roster_item{groups = lists:sort(Grs)} + || R = #roster_item{groups = Grs} <- Items ]))). + %% Returns a list that may contain an xmlelement with the XEP-237 feature if it's enabled. -spec get_versioning_feature([xmpp_element()], binary()) -> [xmpp_element()]. get_versioning_feature(Acc, Host) -> case gen_mod:is_loaded(Host, ?MODULE) of - true -> - case mod_roster_opt:versioning(Host) of - true -> - [#rosterver_feature{}|Acc]; - false -> - Acc - end; - false -> - Acc + true -> + case mod_roster_opt:versioning(Host) of + true -> + [#rosterver_feature{} | Acc]; + false -> + Acc + end; + false -> + Acc end. + -spec roster_version(binary(), binary()) -> undefined | binary(). roster_version(LServer, LUser) -> case mod_roster_opt:store_current_id(LServer) of - true -> - case read_roster_version(LUser, LServer) of - error -> undefined; - {ok, V} -> V - end; - false -> - roster_hash(run_roster_get_hook(LUser, LServer)) + true -> + case read_roster_version(LUser, LServer) of + error -> undefined; + {ok, V} -> V + end; + false -> + roster_hash(run_roster_get_hook(LUser, LServer)) end. + -spec read_roster_version(binary(), binary()) -> {ok, binary()} | error. read_roster_version(LUser, LServer) -> ets_cache:lookup( - ?ROSTER_VERSION_CACHE, {LUser, LServer}, + ?ROSTER_VERSION_CACHE, + {LUser, LServer}, fun() -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:read_roster_version(LUser, LServer) + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_roster_version(LUser, LServer) end). + -spec write_roster_version(binary(), binary()) -> binary(). write_roster_version(LUser, LServer) -> write_roster_version(LUser, LServer, false). + -spec write_roster_version_t(binary(), binary()) -> binary(). write_roster_version_t(LUser, LServer) -> write_roster_version(LUser, LServer, true). + -spec write_roster_version(binary(), binary(), boolean()) -> binary(). write_roster_version(LUser, LServer, InTransaction) -> Ver = str:sha(term_to_binary(erlang:unique_integer())), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:write_roster_version(LUser, LServer, InTransaction, Ver), - if InTransaction -> ok; - true -> - ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)) + if + InTransaction -> ok; + true -> + ets_cache:delete(?ROSTER_VERSION_CACHE, + {LUser, LServer}, + cache_nodes(Mod, LServer)) end, Ver. + %% Load roster from DB only if necessary. %% It is necessary if %% - roster versioning is disabled in server OR @@ -253,249 +312,298 @@ write_roster_version(LUser, LServer, InTransaction) -> %% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR %% - the roster version from client don't match current version. -spec process_iq_get(iq()) -> iq(). -process_iq_get(#iq{to = To, from = From, - sub_els = [#roster_query{ver = RequestedVersion, mix_annotate = MixEnabled}]} = IQ) -> +process_iq_get(#iq{ + to = To, + from = From, + sub_els = [#roster_query{ver = RequestedVersion, mix_annotate = MixEnabled}] + } = IQ) -> LUser = To#jid.luser, LServer = To#jid.lserver, {ItemsToSend, VersionToSend} = - case {mod_roster_opt:versioning(LServer), - mod_roster_opt:store_current_id(LServer)} of - {true, true} when RequestedVersion /= undefined -> - case read_roster_version(LUser, LServer) of - error -> - RosterVersion = write_roster_version(LUser, LServer), - {run_roster_get_hook(LUser, LServer), RosterVersion}; - {ok, RequestedVersion} -> - {false, false}; - {ok, NewVersion} -> - {run_roster_get_hook(LUser, LServer), NewVersion} - end; - {true, false} when RequestedVersion /= undefined -> - RosterItems = run_roster_get_hook(LUser, LServer), - case roster_hash(RosterItems) of - RequestedVersion -> - {false, false}; - New -> - {RosterItems, New} - end; - _ -> - {run_roster_get_hook(LUser, LServer), false} - end, + case {mod_roster_opt:versioning(LServer), + mod_roster_opt:store_current_id(LServer)} of + {true, true} when RequestedVersion /= undefined -> + case read_roster_version(LUser, LServer) of + error -> + RosterVersion = write_roster_version(LUser, LServer), + {run_roster_get_hook(LUser, LServer), RosterVersion}; + {ok, RequestedVersion} -> + {false, false}; + {ok, NewVersion} -> + {run_roster_get_hook(LUser, LServer), NewVersion} + end; + {true, false} when RequestedVersion /= undefined -> + RosterItems = run_roster_get_hook(LUser, LServer), + case roster_hash(RosterItems) of + RequestedVersion -> + {false, false}; + New -> + {RosterItems, New} + end; + _ -> + {run_roster_get_hook(LUser, LServer), false} + end, % Store that MIX annotation is enabled (for roster pushes) set_mix_annotation_enabled(From, MixEnabled), % Only include element when MIX annotation is enabled Items = case ItemsToSend of - false -> false; - FullItems -> process_items_mix(FullItems, MixEnabled) - end, + false -> false; + FullItems -> process_items_mix(FullItems, MixEnabled) + end, xmpp:make_iq_result( IQ, case {Items, VersionToSend} of - {false, false} -> - undefined; - {Items, false} -> - #roster_query{items = Items}; - {Items, Version} -> - #roster_query{items = Items, - ver = Version} + {false, false} -> + undefined; + {Items, false} -> + #roster_query{items = Items}; + {Items, Version} -> + #roster_query{ + items = Items, + ver = Version + } end). + -spec run_roster_get_hook(binary(), binary()) -> [#roster_item{}]. run_roster_get_hook(LUser, LServer) -> ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]). + -spec get_filtered_roster(binary(), binary()) -> [#roster{}]. get_filtered_roster(LUser, LServer) -> lists:filter( - fun (#roster{subscription = none, ask = in}) -> false; - (_) -> true - end, - get_roster(LUser, LServer)). + fun(#roster{subscription = none, ask = in}) -> false; + (_) -> true + end, + get_roster(LUser, LServer)). + -spec get_user_roster_items([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. get_user_roster_items(Acc, {LUser, LServer}) -> lists:map(fun encode_item/1, get_filtered_roster(LUser, LServer)) ++ Acc. + -spec get_roster(binary(), binary()) -> [#roster{}]. get_roster(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), R = case use_cache(Mod, LServer, roster) of - true -> - ets_cache:lookup( - ?ROSTER_CACHE, {LUser, LServer}, - fun() -> Mod:get_roster(LUser, LServer) end); - false -> - Mod:get_roster(LUser, LServer) - end, + true -> + ets_cache:lookup( + ?ROSTER_CACHE, + {LUser, LServer}, + fun() -> Mod:get_roster(LUser, LServer) end); + false -> + Mod:get_roster(LUser, LServer) + end, case R of - {ok, Items} -> Items; - error -> [] + {ok, Items} -> Items; + error -> [] end. + -spec get_roster_item(binary(), binary(), ljid()) -> #roster{}. get_roster_item(LUser, LServer, LJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:get_roster_item(LUser, LServer, LJID) of - {ok, Item} -> - Item; - error -> - LBJID = jid:remove_resource(LJID), - #roster{usj = {LUser, LServer, LBJID}, - us = {LUser, LServer}, jid = LBJID} + {ok, Item} -> + Item; + error -> + LBJID = jid:remove_resource(LJID), + #roster{ + usj = {LUser, LServer, LBJID}, + us = {LUser, LServer}, + jid = LBJID + } end. + -spec get_subscription_and_groups(binary(), binary(), ljid()) -> - {subscription(), ask(), [binary()]}. + {subscription(), ask(), [binary()]}. get_subscription_and_groups(LUser, LServer, LJID) -> LBJID = jid:remove_resource(LJID), Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer, roster) of - true -> - ets_cache:lookup( - ?ROSTER_ITEM_CACHE, {LUser, LServer, LBJID}, - fun() -> - Items = get_roster(LUser, LServer), - case lists:keyfind(LBJID, #roster.jid, Items) of - #roster{subscription = Sub, - ask = Ask, - groups = Groups} -> - {ok, {Sub, Ask, Groups}}; - false -> - error - end - end); - false -> - case Mod:read_subscription_and_groups(LUser, LServer, LBJID) of - {ok, {Sub, Groups}} -> - %% Backward compatibility for third-party backends - {ok, {Sub, none, Groups}}; - Other -> - Other - end - end, + true -> + ets_cache:lookup( + ?ROSTER_ITEM_CACHE, + {LUser, LServer, LBJID}, + fun() -> + Items = get_roster(LUser, LServer), + case lists:keyfind(LBJID, #roster.jid, Items) of + #roster{ + subscription = Sub, + ask = Ask, + groups = Groups + } -> + {ok, {Sub, Ask, Groups}}; + false -> + error + end + end); + false -> + case Mod:read_subscription_and_groups(LUser, LServer, LBJID) of + {ok, {Sub, Groups}} -> + %% Backward compatibility for third-party backends + {ok, {Sub, none, Groups}}; + Other -> + Other + end + end, case Res of - {ok, SubAndGroups} -> - SubAndGroups; - error -> - {none, none, []} + {ok, SubAndGroups} -> + SubAndGroups; + error -> + {none, none, []} end. + -spec set_roster(#roster{}) -> {atomic | aborted, any()}. set_roster(#roster{us = {LUser, LServer}, jid = LJID} = Item) -> transaction( - LUser, LServer, [LJID], + LUser, + LServer, + [LJID], fun() -> - update_roster_t(LUser, LServer, LJID, Item) + update_roster_t(LUser, LServer, LJID, Item) end). + -spec del_roster(binary(), binary(), ljid()) -> {atomic | aborted, any()}. del_roster(LUser, LServer, LJID) -> transaction( - LUser, LServer, [LJID], + LUser, + LServer, + [LJID], fun() -> - del_roster_t(LUser, LServer, LJID) + del_roster_t(LUser, LServer, LJID) end). + -spec encode_item(#roster{}) -> roster_item(). encode_item(Item) -> - #roster_item{jid = jid:make(Item#roster.jid), - name = Item#roster.name, - subscription = Item#roster.subscription, - ask = case ask_to_pending(Item#roster.ask) of - out -> subscribe; - both -> subscribe; - _ -> undefined - end, - groups = Item#roster.groups}. + #roster_item{ + jid = jid:make(Item#roster.jid), + name = Item#roster.name, + subscription = Item#roster.subscription, + ask = case ask_to_pending(Item#roster.ask) of + out -> subscribe; + both -> subscribe; + _ -> undefined + end, + groups = Item#roster.groups + }. + -spec decode_item(roster_item(), #roster{}, boolean()) -> #roster{}. decode_item(#roster_item{subscription = remove} = Item, R, _) -> - R#roster{jid = jid:tolower(Item#roster_item.jid), - name = <<"">>, - subscription = remove, - ask = none, - groups = [], - askmessage = <<"">>, - xs = []}; + R#roster{ + jid = jid:tolower(Item#roster_item.jid), + name = <<"">>, + subscription = remove, + ask = none, + groups = [], + askmessage = <<"">>, + xs = [] + }; decode_item(Item, R, Managed) -> - R#roster{jid = jid:tolower(Item#roster_item.jid), - name = Item#roster_item.name, - subscription = case Item#roster_item.subscription of - Sub when Managed -> Sub; - _ -> R#roster.subscription - end, - groups = Item#roster_item.groups}. + R#roster{ + jid = jid:tolower(Item#roster_item.jid), + name = Item#roster_item.name, + subscription = case Item#roster_item.subscription of + Sub when Managed -> Sub; + _ -> R#roster.subscription + end, + groups = Item#roster_item.groups + }. + -spec process_iq_set(iq()) -> iq(). -process_iq_set(#iq{from = _From, to = To, lang = Lang, - sub_els = [#roster_query{items = [QueryItem]}]} = IQ) -> +process_iq_set(#iq{ + from = _From, + to = To, + lang = Lang, + sub_els = [#roster_query{items = [QueryItem]}] + } = IQ) -> case set_item_and_notify_clients(To, QueryItem, false) of - ok -> - xmpp:make_iq_result(IQ); - {error, _} -> - Txt = ?T("Database failure"), - Err = xmpp:err_internal_server_error(Txt, Lang), - xmpp:make_error(IQ, Err) + ok -> + xmpp:make_iq_result(IQ); + {error, _} -> + Txt = ?T("Database failure"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) end. + -spec set_item_and_notify_clients(jid(), #roster_item{}, boolean()) -> ok | {error, any()}. -set_item_and_notify_clients(To, #roster_item{jid = PeerJID} = RosterItem, - OverrideSubscription) -> +set_item_and_notify_clients(To, + #roster_item{jid = PeerJID} = RosterItem, + OverrideSubscription) -> #jid{luser = LUser, lserver = LServer} = To, PeerLJID = jid:tolower(PeerJID), - F = fun () -> - Item1 = get_roster_item(LUser, LServer, PeerLJID), - Item2 = decode_item(RosterItem, Item1, OverrideSubscription), - Item3 = ejabberd_hooks:run_fold(roster_process_item, - LServer, Item2, - [LServer]), - case Item3#roster.subscription of - remove -> del_roster_t(LUser, LServer, PeerLJID); - _ -> update_roster_t(LUser, LServer, PeerLJID, Item3) - end, - case mod_roster_opt:store_current_id(LServer) of - true -> write_roster_version_t(LUser, LServer); - false -> ok - end, - {Item1, Item3} - end, + F = fun() -> + Item1 = get_roster_item(LUser, LServer, PeerLJID), + Item2 = decode_item(RosterItem, Item1, OverrideSubscription), + Item3 = ejabberd_hooks:run_fold(roster_process_item, + LServer, + Item2, + [LServer]), + case Item3#roster.subscription of + remove -> del_roster_t(LUser, LServer, PeerLJID); + _ -> update_roster_t(LUser, LServer, PeerLJID, Item3) + end, + case mod_roster_opt:store_current_id(LServer) of + true -> write_roster_version_t(LUser, LServer); + false -> ok + end, + {Item1, Item3} + end, case transaction(LUser, LServer, [PeerLJID], F) of - {atomic, {OldItem, NewItem}} -> - push_item(To, encode_item(OldItem), encode_item(NewItem)), - case NewItem#roster.subscription of - remove -> - send_unsubscribing_presence(To, OldItem); - _ -> - ok - end; - {aborted, Reason} -> - {error, Reason} + {atomic, {OldItem, NewItem}} -> + push_item(To, encode_item(OldItem), encode_item(NewItem)), + case NewItem#roster.subscription of + remove -> + send_unsubscribing_presence(To, OldItem); + _ -> + ok + end; + {aborted, Reason} -> + {error, Reason} end. + -spec push_item(jid(), #roster_item{}, #roster_item{}) -> ok. push_item(To, OldItem, NewItem) -> #jid{luser = LUser, lserver = LServer} = To, Ver = case mod_roster_opt:versioning(LServer) of - true -> roster_version(LServer, LUser); - false -> undefined - end, + true -> roster_version(LServer, LUser); + false -> undefined + end, lists:foreach( fun(Resource) -> - To1 = jid:replace_resource(To, Resource), - push_item(To1, OldItem, NewItem, Ver) - end, ejabberd_sm:get_user_resources(LUser, LServer)). + To1 = jid:replace_resource(To, Resource), + push_item(To1, OldItem, NewItem, Ver) + end, + ejabberd_sm:get_user_resources(LUser, LServer)). + -spec push_item(jid(), #roster_item{}, #roster_item{}, undefined | binary()) -> ok. push_item(To, OldItem, NewItem, Ver) -> route_presence_change(To, OldItem, NewItem), [Item] = process_items_mix([NewItem], To), - IQ = #iq{type = set, to = To, - from = jid:remove_resource(To), - id = <<"push", (p1_rand:get_string())/binary>>, - sub_els = [#roster_query{ver = Ver, - items = [Item]}]}, + IQ = #iq{ + type = set, + to = To, + from = jid:remove_resource(To), + id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [#roster_query{ + ver = Ver, + items = [Item] + }] + }, ejabberd_router:route(IQ). + -spec route_presence_change(jid(), #roster_item{}, #roster_item{}) -> ok. route_presence_change(From, OldItem, NewItem) -> OldSub = OldItem#roster_item.subscription, @@ -503,148 +611,186 @@ route_presence_change(From, OldItem, NewItem) -> To = NewItem#roster_item.jid, NewIsFrom = NewSub == both orelse NewSub == from, OldIsFrom = OldSub == both orelse OldSub == from, - if NewIsFrom andalso not OldIsFrom -> - case ejabberd_sm:get_session_pid( - From#jid.luser, From#jid.lserver, From#jid.lresource) of - none -> - ok; - Pid -> - ejabberd_c2s:resend_presence(Pid, To) - end; - OldIsFrom andalso not NewIsFrom -> - PU = #presence{from = From, to = To, type = unavailable}, - case ejabberd_hooks:run_fold( - privacy_check_packet, allow, - [From, PU, out]) of - deny -> - ok; - allow -> - ejabberd_router:route(PU) - end; - true -> - ok + if + NewIsFrom andalso not OldIsFrom -> + case ejabberd_sm:get_session_pid( + From#jid.luser, From#jid.lserver, From#jid.lresource) of + none -> + ok; + Pid -> + ejabberd_c2s:resend_presence(Pid, To) + end; + OldIsFrom andalso not NewIsFrom -> + PU = #presence{from = From, to = To, type = unavailable}, + case ejabberd_hooks:run_fold( + privacy_check_packet, + allow, + [From, PU, out]) of + deny -> + ok; + allow -> + ejabberd_router:route(PU) + end; + true -> + ok end. + -spec ask_to_pending(ask()) -> none | in | out | both. ask_to_pending(subscribe) -> out; ask_to_pending(unsubscribe) -> none; ask_to_pending(Ask) -> Ask. + -spec roster_subscribe_t(binary(), binary(), ljid(), #roster{}) -> any(). roster_subscribe_t(LUser, LServer, LJID, Item) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:roster_subscribe(LUser, LServer, LJID, Item). + -spec transaction(binary(), binary(), [ljid()], fun(() -> T)) -> {atomic, T} | {aborted, any()}. transaction(LUser, LServer, LJIDs, F) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:transaction(LServer, F) of - {atomic, _} = Result -> - delete_cache(Mod, LUser, LServer, LJIDs), - Result; - Err -> - Err + {atomic, _} = Result -> + delete_cache(Mod, LUser, LServer, LJIDs), + Result; + Err -> + Err end. + -spec in_subscription(boolean(), presence()) -> boolean(). -in_subscription(_, #presence{from = JID, to = To, - sub_els = SubEls, - type = Type, status = Status}) -> +in_subscription(_, + #presence{ + from = JID, + to = To, + sub_els = SubEls, + type = Type, + status = Status + }) -> #jid{user = User, server = Server} = To, - Reason = if Type == subscribe -> xmpp:get_text(Status); - true -> <<"">> - end, - process_subscription(in, User, Server, JID, Type, - Reason, SubEls). + Reason = if + Type == subscribe -> xmpp:get_text(Status); + true -> <<"">> + end, + process_subscription(in, + User, + Server, + JID, + Type, + Reason, + SubEls). + -spec out_subscription(presence()) -> boolean(). out_subscription(#presence{from = From, to = JID, type = Type}) -> #jid{user = User, server = Server} = From, process_subscription(out, User, Server, JID, Type, <<"">>, []). --spec process_subscription(in | out, binary(), binary(), jid(), - subscribe | subscribed | unsubscribe | unsubscribed, - binary(), [fxml:xmlel()]) -> boolean(). -process_subscription(Direction, User, Server, JID1, - Type, Reason, SubEls) -> + +-spec process_subscription(in | out, + binary(), + binary(), + jid(), + subscribe | subscribed | unsubscribe | unsubscribed, + binary(), + [fxml:xmlel()]) -> boolean(). +process_subscription(Direction, + User, + Server, + JID1, + Type, + Reason, + SubEls) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LJID = jid:tolower(jid:remove_resource(JID1)), - F = fun () -> - Item = get_roster_item(LUser, LServer, LJID), - NewState = case Direction of - out -> - out_state_change(Item#roster.subscription, - Item#roster.ask, Type); - in -> - in_state_change(Item#roster.subscription, - Item#roster.ask, Type) - end, - AutoReply = case Direction of - out -> none; - in -> - in_auto_reply(Item#roster.subscription, - Item#roster.ask, Type) - end, - AskMessage = case NewState of - {_, both} -> Reason; - {_, in} -> Reason; - _ -> <<"">> - end, - case NewState of - none -> - {none, AutoReply}; - {none, none} when Item#roster.subscription == none, - Item#roster.ask == in -> - del_roster_t(LUser, LServer, LJID), {none, AutoReply}; - {Subscription, Pending} -> - NewItem = Item#roster{subscription = Subscription, - ask = Pending, - name = get_nick_subels(SubEls, Item#roster.name), - xs = SubEls, - askmessage = AskMessage}, - roster_subscribe_t(LUser, LServer, LJID, NewItem), - case mod_roster_opt:store_current_id(LServer) of - true -> write_roster_version_t(LUser, LServer); - false -> ok - end, - {{push, Item, NewItem}, AutoReply} - end - end, + F = fun() -> + Item = get_roster_item(LUser, LServer, LJID), + NewState = case Direction of + out -> + out_state_change(Item#roster.subscription, + Item#roster.ask, + Type); + in -> + in_state_change(Item#roster.subscription, + Item#roster.ask, + Type) + end, + AutoReply = case Direction of + out -> none; + in -> + in_auto_reply(Item#roster.subscription, + Item#roster.ask, + Type) + end, + AskMessage = case NewState of + {_, both} -> Reason; + {_, in} -> Reason; + _ -> <<"">> + end, + case NewState of + none -> + {none, AutoReply}; + {none, none} when Item#roster.subscription == none, + Item#roster.ask == in -> + del_roster_t(LUser, LServer, LJID), {none, AutoReply}; + {Subscription, Pending} -> + NewItem = Item#roster{ + subscription = Subscription, + ask = Pending, + name = get_nick_subels(SubEls, Item#roster.name), + xs = SubEls, + askmessage = AskMessage + }, + roster_subscribe_t(LUser, LServer, LJID, NewItem), + case mod_roster_opt:store_current_id(LServer) of + true -> write_roster_version_t(LUser, LServer); + false -> ok + end, + {{push, Item, NewItem}, AutoReply} + end + end, case transaction(LUser, LServer, [LJID], F) of - {atomic, {Push, AutoReply}} -> - case AutoReply of - none -> ok; - _ -> - ejabberd_router:route( - #presence{type = AutoReply, - from = jid:make(User, Server), - to = JID1}) - end, - case Push of - {push, OldItem, NewItem} -> - if NewItem#roster.subscription == none, - NewItem#roster.ask == in -> - ok; - true -> - push_item(jid:make(User, Server), - encode_item(OldItem), - encode_item(NewItem)) - end, - true; - none -> - false - end; - _ -> - false + {atomic, {Push, AutoReply}} -> + case AutoReply of + none -> ok; + _ -> + ejabberd_router:route( + #presence{ + type = AutoReply, + from = jid:make(User, Server), + to = JID1 + }) + end, + case Push of + {push, OldItem, NewItem} -> + if + NewItem#roster.subscription == none, + NewItem#roster.ask == in -> + ok; + true -> + push_item(jid:make(User, Server), + encode_item(OldItem), + encode_item(NewItem)) + end, + true; + none -> + false + end; + _ -> + false end. + get_nick_subels(SubEls, Default) -> case xmpp:get_subtag(#presence{sub_els = SubEls}, #nick{}) of {nick, N} -> N; _ -> Default end. + %% in_state_change(Subscription, Pending, Type) -> NewState %% NewState = none | {NewSubscription, NewPending} -ifdef(ROSTER_GATEWAY_WORKAROUND). @@ -661,6 +807,7 @@ get_nick_subels(SubEls, Default) -> -endif. + in_state_change(none, none, subscribe) -> {none, in}; in_state_change(none, none, subscribed) -> ?NNSD; in_state_change(none, none, unsubscribe) -> none; @@ -726,12 +873,13 @@ in_state_change(both, _, unsubscribe) -> {to, none}; in_state_change(both, _, unsubscribed) -> {from, none}. + out_state_change(none, none, subscribe) -> {none, out}; out_state_change(none, none, subscribed) -> none; out_state_change(none, none, unsubscribe) -> none; out_state_change(none, none, unsubscribed) -> none; out_state_change(none, out, subscribe) -> - {none, out}; %% We need to resend query (RFC3921, section 9.2) + {none, out}; %% We need to resend query (RFC3921, section 9.2) out_state_change(none, out, subscribed) -> none; out_state_change(none, out, unsubscribe) -> {none, none}; @@ -798,6 +946,7 @@ out_state_change(both, _, unsubscribe) -> out_state_change(both, _, unsubscribed) -> {to, none}. + in_auto_reply(from, none, subscribe) -> subscribed; in_auto_reply(from, out, subscribe) -> subscribed; in_auto_reply(both, none, subscribe) -> subscribed; @@ -809,6 +958,7 @@ in_auto_reply(from, out, unsubscribe) -> unsubscribed; in_auto_reply(both, none, unsubscribe) -> unsubscribed; in_auto_reply(_, _, _) -> none. + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), @@ -817,7 +967,8 @@ remove_user(User, Server) -> send_unsubscription_to_rosteritems(LUser, LServer, Items), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), - delete_cache(Mod, LUser, LServer, [Item#roster.jid || Item <- Items]). + delete_cache(Mod, LUser, LServer, [ Item#roster.jid || Item <- Items ]). + %% For each contact with Subscription: %% Both or From, send a "unsubscribed" presence stanza; @@ -825,177 +976,210 @@ remove_user(User, Server) -> -spec send_unsubscription_to_rosteritems(binary(), binary(), [#roster{}]) -> ok. send_unsubscription_to_rosteritems(LUser, LServer, RosterItems) -> From = jid:make({LUser, LServer, <<"">>}), - lists:foreach(fun (RosterItem) -> - send_unsubscribing_presence(From, RosterItem) - end, - RosterItems). + lists:foreach(fun(RosterItem) -> + send_unsubscribing_presence(From, RosterItem) + end, + RosterItems). + -spec send_unsubscribing_presence(jid(), #roster{}) -> ok. send_unsubscribing_presence(From, Item) -> IsTo = case Item#roster.subscription of - both -> true; - to -> true; - _ -> false - end, + both -> true; + to -> true; + _ -> false + end, IsFrom = case Item#roster.subscription of - both -> true; - from -> true; - _ -> false - end, - if IsTo -> - ejabberd_router:route( - #presence{type = unsubscribe, - from = jid:remove_resource(From), - to = jid:make(Item#roster.jid)}); - true -> ok + both -> true; + from -> true; + _ -> false + end, + if + IsTo -> + ejabberd_router:route( + #presence{ + type = unsubscribe, + from = jid:remove_resource(From), + to = jid:make(Item#roster.jid) + }); + true -> ok end, - if IsFrom -> - ejabberd_router:route( - #presence{type = unsubscribed, - from = jid:remove_resource(From), - to = jid:make(Item#roster.jid)}); - true -> ok + if + IsFrom -> + ejabberd_router:route( + #presence{ + type = unsubscribed, + from = jid:remove_resource(From), + to = jid:make(Item#roster.jid) + }); + true -> ok end. + %%%=================================================================== %%% MIX %%%=================================================================== + -spec remove_mix_channel([#roster_item{}]) -> [#roster_item{}]. remove_mix_channel(Items) -> lists:map( - fun(Item) -> - Item#roster_item{mix_channel = undefined} - end, Items). + fun(Item) -> + Item#roster_item{mix_channel = undefined} + end, + Items). + -spec process_items_mix([#roster_item{}], boolean() | jid()) -> [#roster_item{}]. process_items_mix(Items, true) -> Items; process_items_mix(Items, false) -> remove_mix_channel(Items); process_items_mix(Items, JID) -> process_items_mix(Items, is_mix_annotation_enabled(JID)). + -spec is_mix_annotation_enabled(jid()) -> boolean(). is_mix_annotation_enabled(#jid{luser = User, lserver = Host, lresource = Res}) -> case ejabberd_sm:get_user_info(User, Host, Res) of - offline -> false; - Info -> - case lists:keyfind(?SM_MIX_ANNOTATE, 1, Info) of - {_, true} -> true; - _ -> false - end + offline -> false; + Info -> + case lists:keyfind(?SM_MIX_ANNOTATE, 1, Info) of + {_, true} -> true; + _ -> false + end end. + -spec set_mix_annotation_enabled(jid(), boolean()) -> ok | {error, any()}. set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R} = JID, false) -> case is_mix_annotation_enabled(JID) of - true -> - ?DEBUG("Disabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]), - case ejabberd_sm:del_user_info(U, Host, R, ?SM_MIX_ANNOTATE) of - ok -> ok; - {error, Reason} = Err -> - ?ERROR_MSG("Failed to disable roster MIX annotation for ~ts@~ts/~ts: ~p", - [U, Host, R, Reason]), - Err - end; - false -> ok + true -> + ?DEBUG("Disabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]), + case ejabberd_sm:del_user_info(U, Host, R, ?SM_MIX_ANNOTATE) of + ok -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to disable roster MIX annotation for ~ts@~ts/~ts: ~p", + [U, Host, R, Reason]), + Err + end; + false -> ok end; -set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R}, true)-> +set_mix_annotation_enabled(#jid{luser = U, lserver = Host, lresource = R}, true) -> ?DEBUG("Enabling roster MIX annotation for ~ts@~ts/~ts", [U, Host, R]), case ejabberd_sm:set_user_info(U, Host, R, ?SM_MIX_ANNOTATE, true) of - ok -> ok; - {error, Reason} = Err -> - ?ERROR_MSG("Failed to enable roster MIX annotation for ~ts@~ts/~ts: ~p", - [U, Host, R, Reason]), - Err + ok -> ok; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to enable roster MIX annotation for ~ts@~ts/~ts: ~p", + [U, Host, R, Reason]), + Err end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + -spec set_items(binary(), binary(), roster_query()) -> {atomic, ok} | {aborted, any()}. set_items(User, Server, #roster_query{items = Items}) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - LJIDs = [jid:tolower(Item#roster_item.jid) || Item <- Items], - F = fun () -> - lists:foreach( - fun(Item) -> - process_item_set_t(LUser, LServer, Item) - end, Items) - end, + LJIDs = [ jid:tolower(Item#roster_item.jid) || Item <- Items ], + F = fun() -> + lists:foreach( + fun(Item) -> + process_item_set_t(LUser, LServer, Item) + end, + Items) + end, transaction(LUser, LServer, LJIDs, F). + -spec update_roster_t(binary(), binary(), ljid(), #roster{}) -> any(). update_roster_t(LUser, LServer, LJID, Item) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:update_roster(LUser, LServer, LJID, Item). + -spec del_roster_t(binary(), binary(), ljid()) -> any(). del_roster_t(LUser, LServer, LJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:del_roster(LUser, LServer, LJID). + -spec process_item_set_t(binary(), binary(), roster_item()) -> any(). process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) -> JID = {JID1#jid.user, JID1#jid.server, <<>>}, LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>}, - Item = #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = JID}, + Item = #roster{ + usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = JID + }, Item2 = decode_item(QueryItem, Item, _Managed = true), case Item2#roster.subscription of - remove -> del_roster_t(LUser, LServer, LJID); - _ -> update_roster_t(LUser, LServer, LJID, Item2) + remove -> del_roster_t(LUser, LServer, LJID); + _ -> update_roster_t(LUser, LServer, LJID, Item2) end; process_item_set_t(_LUser, _LServer, _) -> ok. + -spec c2s_self_presence({presence(), c2s_state()}) -> {presence(), c2s_state()}. c2s_self_presence({_, #{pres_last := _}} = Acc) -> Acc; c2s_self_presence({#presence{type = available} = Pkt, State}) -> Prio = get_priority_from_presence(Pkt), - if Prio >= 0 -> - State1 = resend_pending_subscriptions(State), - {Pkt, State1}; - true -> - {Pkt, State} + if + Prio >= 0 -> + State1 = resend_pending_subscriptions(State), + {Pkt, State1}; + true -> + {Pkt, State} end; c2s_self_presence(Acc) -> Acc. + -spec resend_pending_subscriptions(c2s_state()) -> c2s_state(). resend_pending_subscriptions(#{jid := JID} = State) -> BareJID = jid:remove_resource(JID), Result = get_roster(JID#jid.luser, JID#jid.lserver), lists:foldl( fun(#roster{ask = Ask} = R, AccState) when Ask == in; Ask == both -> - Message = R#roster.askmessage, - Status = if is_binary(Message) -> (Message); - true -> <<"">> - end, - Sub = #presence{from = jid:make(R#roster.jid), - to = BareJID, - type = subscribe, - sub_els = R#roster.xs, - status = xmpp:mk_text(Status)}, - ejabberd_c2s:send(AccState, Sub); - (_, AccState) -> - AccState - end, State, Result). + Message = R#roster.askmessage, + Status = if + is_binary(Message) -> (Message); + true -> <<"">> + end, + Sub = #presence{ + from = jid:make(R#roster.jid), + to = BareJID, + type = subscribe, + sub_els = R#roster.xs, + status = xmpp:mk_text(Status) + }, + ejabberd_c2s:send(AccState, Sub); + (_, AccState) -> + AccState + end, + State, + Result). + -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of - undefined -> 0; - _ -> Prio + undefined -> 0; + _ -> Prio end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) - -> {subscription(), ask(), [binary()]}. +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) -> + {subscription(), ask(), [binary()]}. get_jid_info(_, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LJID = jid:tolower(JID), get_subscription_and_groups(LUser, LServer, LJID). + %% Check if `From` is subscriberd to `To`s presence %% note 1: partial subscriptions are also considered, i.e. %% `To` has already sent a subscription request to `From` @@ -1003,28 +1187,33 @@ get_jid_info(_, User, Server, JID) -> %% note 3: `To` MUST be a local user, `From` can be any user -spec is_subscribed(jid(), jid()) -> boolean(). is_subscribed(#jid{luser = LUser, lserver = LServer}, - #jid{luser = LUser, lserver = LServer}) -> + #jid{luser = LUser, lserver = LServer}) -> true; is_subscribed(From, #jid{luser = LUser, lserver = LServer}) -> {Sub, Ask, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, - {none, none, []}, - [LUser, LServer, From]), - (Sub /= none) orelse (Ask == subscribe) - orelse (Ask == out) orelse (Ask == both). + roster_get_jid_info, + LServer, + {none, none, []}, + [LUser, LServer, From]), + (Sub /= none) orelse (Ask == subscribe) orelse + (Ask == out) orelse (Ask == both). + process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> LServer = ejabberd_config:get_myname(), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% @format-begin + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"roster">>, <<"Roster">>}]. + webadmin_page_hostuser(_, Host, Username, #request{path = [<<"roster">> | RPath]} = R) -> Head = ?H1GL(<<"Roster">>, <<"modules/#mod_roster">>, <<"mod_roster">>), %% Execute twice: first to perform the action, the second to get new roster @@ -1042,10 +1231,10 @@ webadmin_page_hostuser(_, Host, Username, #request{path = [<<"roster">> | RPath] webadmin_page_hostuser(Acc, _, _, _) -> Acc. + make_webadmin_roster_table(Host, Username, R, RPath) -> Contacts = - case make_command_raw_value(get_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}]) - of + case make_command_raw_value(get_roster, R, [{<<"user">>, Username}, {<<"host">>, Host}]) of Cs when is_list(Cs) -> Cs; _ -> @@ -1056,77 +1245,81 @@ make_webadmin_roster_table(Host, Username, R, RPath) -> [<<"jid">>, <<"nick">>, <<"subscription">>, <<"pending">>, <<"groups">>, <<"">>], Rows = lists:map(fun({Jid, Nick, Subscriptions, Pending, Groups}) -> - {JidSplit, ProblematicBin} = - try jid:decode(Jid) of - #jid{} = J -> - {jid:split(J), <<"">>} - catch - _:{bad_jid, _} -> - ?INFO_MSG("Error parsing contact of ~s@~s that is invalid JID: ~s", - [Username, Host, Jid]), - {{<<"000--error-parsing-jid">>, <<"localhost">>, <<"">>}, - <<", Error parsing JID: ", Jid/binary>>} - end, - {make_command(echo, - R, - [{<<"sentence">>, jid:encode(JidSplit)}], - [{only, raw_and_value}, - {result_links, [{sentence, user, Level, <<"">>}]}]), - ?C(<>), - ?C(Subscriptions), - ?C(Pending), - ?C(Groups), - make_command(delete_rosteritem, - R, - [{<<"localuser">>, Username}, - {<<"localhost">>, Host}, - {<<"user">>, element(1, JidSplit)}, - {<<"host">>, element(2, JidSplit)}], - [{only, button}, - {style, danger}, - {input_name_append, - [Username, - Host, - element(1, JidSplit), - element(2, JidSplit)]}])} + {JidSplit, ProblematicBin} = + try jid:decode(Jid) of + #jid{} = J -> + {jid:split(J), <<"">>} + catch + _:{bad_jid, _} -> + ?INFO_MSG("Error parsing contact of ~s@~s that is invalid JID: ~s", + [Username, Host, Jid]), + {{<<"000--error-parsing-jid">>, <<"localhost">>, <<"">>}, + <<", Error parsing JID: ", Jid/binary>>} + end, + {make_command(echo, + R, + [{<<"sentence">>, jid:encode(JidSplit)}], + [{only, raw_and_value}, + {result_links, [{sentence, user, Level, <<"">>}]}]), + ?C(<>), + ?C(Subscriptions), + ?C(Pending), + ?C(Groups), + make_command(delete_rosteritem, + R, + [{<<"localuser">>, Username}, + {<<"localhost">>, Host}, + {<<"user">>, element(1, JidSplit)}, + {<<"host">>, element(2, JidSplit)}], + [{only, button}, + {style, danger}, + {input_name_append, + [Username, + Host, + element(1, JidSplit), + element(2, JidSplit)]}])} end, lists:keysort(1, Contacts)), Table = make_table(20, RPath, Columns, Rows), ?XE(<<"blockquote">>, [Table]). + webadmin_user(Acc, User, Server, R) -> - Acc - ++ [make_command(get_roster_count, - R, - [{<<"user">>, User}, {<<"host">>, Server}], - [{result_links, - [{value, arg_host, 4, <<"user/", User/binary, "/roster/">>}]}])]. + Acc ++ + [make_command(get_roster_count, + R, + [{<<"user">>, User}, {<<"host">>, Server}], + [{result_links, + [{value, arg_host, 4, <<"user/", User/binary, "/roster/">>}]}])]. %%% @format-end + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec has_duplicated_groups([binary()]) -> boolean(). has_duplicated_groups(Groups) -> - GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]), + GroupsPrep = lists:usort([ jid:resourceprep(G) || G <- Groups ]), not (length(GroupsPrep) == length(Groups)). + -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> CacheOpts = cache_opts(Opts), case use_cache(Mod, Host, roster_version) of - true -> - ets_cache:new(?ROSTER_VERSION_CACHE, CacheOpts); - false -> - ets_cache:delete(?ROSTER_VERSION_CACHE) + true -> + ets_cache:new(?ROSTER_VERSION_CACHE, CacheOpts); + false -> + ets_cache:delete(?ROSTER_VERSION_CACHE) end, case use_cache(Mod, Host, roster) of - true -> - ets_cache:new(?ROSTER_CACHE, CacheOpts), - ets_cache:new(?ROSTER_ITEM_CACHE, CacheOpts); - false -> - ets_cache:delete(?ROSTER_CACHE), - ets_cache:delete(?ROSTER_ITEM_CACHE) + true -> + ets_cache:new(?ROSTER_CACHE, CacheOpts), + ets_cache:new(?ROSTER_ITEM_CACHE, CacheOpts); + false -> + ets_cache:delete(?ROSTER_CACHE), + ets_cache:delete(?ROSTER_ITEM_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_roster_opt:cache_size(Opts), @@ -1134,76 +1327,87 @@ cache_opts(Opts) -> LifeTime = mod_roster_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary(), roster | roster_version) -> boolean(). use_cache(Mod, Host, Table) -> case erlang:function_exported(Mod, use_cache, 2) of - true -> Mod:use_cache(Host, Table); - false -> mod_roster_opt:use_cache(Host) + true -> Mod:use_cache(Host, Table); + false -> mod_roster_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + -spec delete_cache(module(), binary(), binary(), [ljid()]) -> ok. delete_cache(Mod, LUser, LServer, LJIDs) -> case use_cache(Mod, LServer, roster_version) of - true -> - ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)); - false -> - ok + true -> + ets_cache:delete(?ROSTER_VERSION_CACHE, + {LUser, LServer}, + cache_nodes(Mod, LServer)); + false -> + ok end, case use_cache(Mod, LServer, roster) of - true -> - Nodes = cache_nodes(Mod, LServer), - ets_cache:delete(?ROSTER_CACHE, {LUser, LServer}, Nodes), - lists:foreach( - fun(LJID) -> - ets_cache:delete( - ?ROSTER_ITEM_CACHE, - {LUser, LServer, jid:remove_resource(LJID)}, - Nodes) - end, LJIDs); - false -> - ok + true -> + Nodes = cache_nodes(Mod, LServer), + ets_cache:delete(?ROSTER_CACHE, {LUser, LServer}, Nodes), + lists:foreach( + fun(LJID) -> + ets_cache:delete( + ?ROSTER_ITEM_CACHE, + {LUser, LServer, jid:remove_resource(LJID)}, + Nodes) + end, + LJIDs); + false -> + ok end. + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + import_info() -> [{<<"roster_version">>, 2}, {<<"rostergroups">>, 3}, {<<"rosterusers">>, 10}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), ets:new(rostergroups_tmp, [private, named_table, bag]), Mod:init(LServer, []), ok. + import_stop(_LServer, _DBType) -> ets:delete(rostergroups_tmp), ok. + row_length() -> case ejabberd_sql:use_new_schema() of true -> 10; false -> 9 end. + import(LServer, {sql, _}, _DBType, <<"rostergroups">>, [LUser, SJID, Group]) -> LJID = jid:tolower(jid:decode(SJID)), ets:insert(rostergroups_tmp, {{LUser, LServer, LJID}, Group}), ok; import(LServer, {sql, _}, DBType, <<"rosterusers">>, Row) -> I = mod_roster_sql:raw_to_record(LServer, lists:sublist(Row, row_length())), - Groups = [G || {_, G} <- ets:lookup(rostergroups_tmp, I#roster.usj)], + Groups = [ G || {_, G} <- ets:lookup(rostergroups_tmp, I#roster.usj) ], RosterItem = I#roster{groups = Groups}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, <<"rosterusers">>, RosterItem); @@ -1211,6 +1415,7 @@ import(LServer, {sql, _}, DBType, <<"roster_version">>, [LUser, Ver]) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, <<"roster_version">>, [LUser, Ver]). + mod_opt_type(access) -> econf:acl(); mod_opt_type(store_current_id) -> @@ -1228,6 +1433,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{access, all}, {store_current_id, false}, @@ -1238,8 +1444,10 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module implements roster management as " "defined in https://tools.ietf.org/html/rfc6121#section-2" "[RFC6121 Section 2]. The module also adds support for " @@ -1247,7 +1455,8 @@ mod_doc() -> "[XEP-0237: Roster Versioning]."), opts => [{access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("This option can be configured to specify " "rules to restrict roster management. " @@ -1255,14 +1464,18 @@ mod_doc() -> "user name, that user cannot modify their personal " "roster, i.e. they cannot add/remove/modify contacts " "or send presence subscriptions. " - "The default value is 'all', i.e. no restrictions.")}}, + "The default value is 'all', i.e. no restrictions.") + }}, {versioning, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Enables/disables Roster Versioning. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {store_current_id, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("If this option is set to 'true', the current " "roster version number is stored on the database. " @@ -1274,29 +1487,41 @@ mod_doc() -> "set to 'true'. The default value is 'false'. " "IMPORTANT: if you use _`mod_shared_roster`_ or " " _`mod_shared_roster_ldap`_, you must set the value " - "of the option to 'false'.")}}, + "of the option to 'false'.") + }}, {db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}], example => ["modules:", " mod_roster:", " versioning: true", - " store_current_id: false"]}. + " store_current_id: false"] + }. diff --git a/src/mod_roster_mnesia.erl b/src/mod_roster_mnesia.erl index d8d4bd1c9..461a85713 100644 --- a/src/mod_roster_mnesia.erl +++ b/src/mod_roster_mnesia.erl @@ -27,101 +27,129 @@ -behaviour(mod_roster). %% API --export([init/2, read_roster_version/2, write_roster_version/4, - get_roster/2, get_roster_item/3, roster_subscribe/4, - remove_user/2, update_roster/4, del_roster/3, transaction/2, - read_subscription_and_groups/3, import/3, create_roster/1, - process_rosteritems/5, - use_cache/2]). +-export([init/2, + read_roster_version/2, + write_roster_version/4, + get_roster/2, + get_roster_item/3, + roster_subscribe/4, + remove_user/2, + update_roster/4, + del_roster/3, + transaction/2, + read_subscription_and_groups/3, + import/3, + create_roster/1, + process_rosteritems/5, + use_cache/2]). -export([need_transform/1, transform/1]). -include("mod_roster.hrl"). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, roster, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, roster)}, - {index, [us]}]), - ejabberd_mnesia:create(?MODULE, roster_version, - [{disc_only_copies, [node()]}, - {attributes, - record_info(fields, roster_version)}]). + ejabberd_mnesia:create(?MODULE, + roster, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, roster)}, + {index, [us]}]), + ejabberd_mnesia:create(?MODULE, + roster_version, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, roster_version)}]). + use_cache(Host, Table) -> case mnesia:table_info(Table, storage_type) of - disc_only_copies -> - mod_roster_opt:use_cache(Host); - _ -> - false + disc_only_copies -> + mod_roster_opt:use_cache(Host); + _ -> + false end. + read_roster_version(LUser, LServer) -> US = {LUser, LServer}, case mnesia:dirty_read(roster_version, US) of - [#roster_version{version = V}] -> {ok, V}; - [] -> error + [#roster_version{version = V}] -> {ok, V}; + [] -> error end. + write_roster_version(LUser, LServer, InTransaction, Ver) -> US = {LUser, LServer}, - if InTransaction -> - mnesia:write(#roster_version{us = US, version = Ver}); - true -> - mnesia:dirty_write(#roster_version{us = US, version = Ver}) + if + InTransaction -> + mnesia:write(#roster_version{us = US, version = Ver}); + true -> + mnesia:dirty_write(#roster_version{us = US, version = Ver}) end. + get_roster(LUser, LServer) -> {ok, mnesia:dirty_index_read(roster, {LUser, LServer}, #roster.us)}. + get_roster_item(LUser, LServer, LJID) -> case mnesia:read({roster, {LUser, LServer, LJID}}) of - [I] -> {ok, I}; - [] -> error + [I] -> {ok, I}; + [] -> error end. + roster_subscribe(_LUser, _LServer, _LJID, Item) -> mnesia:write(Item). + remove_user(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> - lists:foreach( - fun (R) -> mnesia:delete_object(R) end, - mnesia:index_read(roster, US, #roster.us)) - end, + F = fun() -> + lists:foreach( + fun(R) -> mnesia:delete_object(R) end, + mnesia:index_read(roster, US, #roster.us)) + end, mnesia:transaction(F). + update_roster(_LUser, _LServer, _LJID, Item) -> mnesia:write(Item). + del_roster(LUser, LServer, LJID) -> mnesia:delete({roster, {LUser, LServer, LJID}}). + read_subscription_and_groups(LUser, LServer, LJID) -> case mnesia:dirty_read(roster, {LUser, LServer, LJID}) of - [#roster{subscription = Subscription, ask = Ask, groups = Groups}] -> - {ok, {Subscription, Ask, Groups}}; - _ -> - error + [#roster{subscription = Subscription, ask = Ask, groups = Groups}] -> + {ok, {Subscription, Ask, Groups}}; + _ -> + error end. + transaction(_LServer, F) -> mnesia:transaction(F). + create_roster(RItem) -> mnesia:dirty_write(RItem). + import(_LServer, <<"rosterusers">>, #roster{} = R) -> mnesia:dirty_write(R); import(LServer, <<"roster_version">>, [LUser, Ver]) -> RV = #roster_version{us = {LUser, LServer}, version = Ver}, mnesia:dirty_write(RV). + need_transform({roster, {U, S, _}, _, _, _, _, _, _, _, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'roster' will be converted to binary", []), @@ -133,32 +161,45 @@ need_transform({roster_version, {U, S}, Ver}) need_transform(_) -> false. -transform(#roster{usj = {U, S, {LU, LS, LR}}, - us = {U1, S1}, - jid = {U2, S2, R2}, - name = Name, - groups = Gs, - askmessage = Ask, - xs = Xs} = R) -> - R#roster{usj = {iolist_to_binary(U), iolist_to_binary(S), - {iolist_to_binary(LU), - iolist_to_binary(LS), - iolist_to_binary(LR)}}, - us = {iolist_to_binary(U1), iolist_to_binary(S1)}, - jid = {iolist_to_binary(U2), - iolist_to_binary(S2), - iolist_to_binary(R2)}, - name = iolist_to_binary(Name), - groups = [iolist_to_binary(G) || G <- Gs], - askmessage = try iolist_to_binary(Ask) - catch _:_ -> <<"">> end, - xs = [fxml:to_xmlel(X) || X <- Xs]}; + +transform(#roster{ + usj = {U, S, {LU, LS, LR}}, + us = {U1, S1}, + jid = {U2, S2, R2}, + name = Name, + groups = Gs, + askmessage = Ask, + xs = Xs + } = R) -> + R#roster{ + usj = {iolist_to_binary(U), + iolist_to_binary(S), + {iolist_to_binary(LU), + iolist_to_binary(LS), + iolist_to_binary(LR)}}, + us = {iolist_to_binary(U1), iolist_to_binary(S1)}, + jid = {iolist_to_binary(U2), + iolist_to_binary(S2), + iolist_to_binary(R2)}, + name = iolist_to_binary(Name), + groups = [ iolist_to_binary(G) || G <- Gs ], + askmessage = try + iolist_to_binary(Ask) + catch + _:_ -> <<"">> + end, + xs = [ fxml:to_xmlel(X) || X <- Xs ] + }; transform(#roster_version{us = {U, S}, version = Ver} = R) -> - R#roster_version{us = {iolist_to_binary(U), iolist_to_binary(S)}, - version = iolist_to_binary(Ver)}. + R#roster_version{ + us = {iolist_to_binary(U), iolist_to_binary(S)}, + version = iolist_to_binary(Ver) + }. + %%%=================================================================== + process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> Action = case ActionS of "list" -> list; @@ -169,37 +210,35 @@ process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> (Sub, Subs) -> [Sub | Subs] end, [], - [list_to_atom(S) || S <- string:tokens(SubsS, ":")] - ), + [ list_to_atom(S) || S <- string:tokens(SubsS, ":") ]), Asks = lists:foldl( fun(any, _) -> [none, out, in]; (Ask, Asks) -> [Ask | Asks] end, [], - [list_to_atom(S) || S <- string:tokens(AsksS, ":")] - ), + [ list_to_atom(S) || S <- string:tokens(AsksS, ":") ]), Users = lists:foldl( fun("any", _) -> ["*", "*@*"]; (U, Us) -> [U | Us] end, [], - [S || S <- string:tokens(UsersS, ":")] - ), + [ S || S <- string:tokens(UsersS, ":") ]), Contacts = lists:foldl( fun("any", _) -> ["*", "*@*"]; (U, Us) -> [U | Us] end, [], - [S || S <- string:tokens(ContactsS, ":")] - ), + [ S || S <- string:tokens(ContactsS, ":") ]), rosteritem_purge({Action, Subs, Asks, Users, Contacts}). + rosteritem_purge(Options) -> Num_rosteritems = mnesia:table_info(roster, size), io:format("There are ~p roster items in total.~n", [Num_rosteritems]), Key = mnesia:dirty_first(roster), rip(Key, Options, {0, Num_rosteritems, 0, 0}, []). + rip('$end_of_table', _Options, Counters, Res) -> print_progress_line(Counters), Res; @@ -207,16 +246,17 @@ rip(Key, Options, {Pr, NT, NV, ND}, Res) -> Key_next = mnesia:dirty_next(roster, Key), {Action, _, _, _, _} = Options, {ND2, Res2} = case decide_rip(Key, Options) of - true -> - Jids = apply_action(Action, Key), - {ND+1, [Jids | Res]}; - false -> - {ND, Res} - end, - NV2 = NV+1, + true -> + Jids = apply_action(Action, Key), + {ND + 1, [Jids | Res]}; + false -> + {ND, Res} + end, + NV2 = NV + 1, Pr2 = print_progress_line({Pr, NT, NV2, ND2}), rip(Key_next, Options, {Pr2, NT, NV2, ND2}, Res2). + apply_action(list, Key) -> {User, Server, JID} = Key, {RUser, RServer, _} = JID, @@ -229,10 +269,11 @@ apply_action(delete, Key) -> mnesia:dirty_delete(roster, Key), R. + print_progress_line({_Pr, 0, _NV, _ND}) -> ok; print_progress_line({Pr, NT, NV, ND}) -> - Pr2 = trunc((NV/NT)*100), + Pr2 = trunc((NV / NT) * 100), case Pr == Pr2 of true -> ok; @@ -241,17 +282,19 @@ print_progress_line({Pr, NT, NV, ND}) -> end, Pr2. + decide_rip(Key, {_Action, Subs, Asks, User, Contact}) -> case catch mnesia:dirty_read(roster, Key) of [RI] -> - lists:member(RI#roster.subscription, Subs) - andalso lists:member(RI#roster.ask, Asks) - andalso decide_rip_jid(RI#roster.us, User) - andalso decide_rip_jid(RI#roster.jid, Contact); + lists:member(RI#roster.subscription, Subs) andalso + lists:member(RI#roster.ask, Asks) andalso + decide_rip_jid(RI#roster.us, User) andalso + decide_rip_jid(RI#roster.jid, Contact); _ -> false end. + %% Returns true if the server of the JID is included in the servers decide_rip_jid({UName, UServer, _UResource}, Match_list) -> decide_rip_jid({UName, UServer}, Match_list); @@ -268,12 +311,13 @@ decide_rip_jid({UName, UServer}, Match_list) -> <<>> -> false; _ -> - Is_server - andalso is_glob_match(UName, MName) + Is_server andalso + is_glob_match(UName, MName) end end, Match_list). + %% Copied from ejabberd-2.0.0/src/acl.erl is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of @@ -287,6 +331,8 @@ is_regexp_match(String, RegExp) -> [RegExp, ErrDesc]), false end. + + is_glob_match(String, <<"!", Glob/binary>>) -> not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); is_glob_match(String, Glob) -> diff --git a/src/mod_roster_opt.erl b/src/mod_roster_opt.erl index 4275bf4e2..05b7dcea5 100644 --- a/src/mod_roster_opt.erl +++ b/src/mod_roster_opt.erl @@ -12,51 +12,58 @@ -export([use_cache/1]). -export([versioning/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_roster, access). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_roster, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_roster, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_roster, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_roster, db_type). + -spec store_current_id(gen_mod:opts() | global | binary()) -> boolean(). store_current_id(Opts) when is_map(Opts) -> gen_mod:get_opt(store_current_id, Opts); store_current_id(Host) -> gen_mod:get_module_opt(Host, mod_roster, store_current_id). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_roster, use_cache). + -spec versioning(gen_mod:opts() | global | binary()) -> boolean(). versioning(Opts) when is_map(Opts) -> gen_mod:get_opt(versioning, Opts); versioning(Host) -> gen_mod:get_module_opt(Host, mod_roster, versioning). - diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index 44d507e5e..d8ea30b3d 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -24,23 +24,33 @@ -module(mod_roster_sql). - -behaviour(mod_roster). %% API --export([init/2, read_roster_version/2, write_roster_version/4, - get_roster/2, get_roster_item/3, roster_subscribe/4, - read_subscription_and_groups/3, remove_user/2, - update_roster/4, del_roster/3, transaction/2, - process_rosteritems/5, - import/3, export/1, raw_to_record/2]). +-export([init/2, + read_roster_version/2, + write_roster_version/4, + get_roster/2, + get_roster_item/3, + roster_subscribe/4, + read_subscription_and_groups/3, + remove_user/2, + update_roster/4, + del_roster/3, + transaction/2, + process_rosteritems/5, + import/3, + export/1, + raw_to_record/2]). -export([sql_schemas/0]). -include("mod_roster.hrl"). -include("ejabberd_sql_pt.hrl"). -include("logger.hrl"). + -include_lib("xmpp/include/jid.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -48,134 +58,158 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"rosterusers">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"nick">>, type = text}, - #sql_column{name = <<"subscription">>, type = {char, 1}}, - #sql_column{name = <<"ask">>, type = {char, 1}}, - #sql_column{name = <<"askmessage">>, type = text}, - #sql_column{name = <<"server">>, type = {char, 1}}, - #sql_column{name = <<"subscribe">>, type = text}, - #sql_column{name = <<"type">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, - <<"jid">>], - unique = true}, - #sql_index{ - columns = [<<"server_host">>, <<"jid">>]}]}, - #sql_table{ - name = <<"rostergroups">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"grp">>, type = text}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>, - <<"jid">>]}]}, - #sql_table{ - name = <<"roster_version">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"version">>, type = text}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"rosterusers">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"nick">>, type = text}, + #sql_column{name = <<"subscription">>, type = {char, 1}}, + #sql_column{name = <<"ask">>, type = {char, 1}}, + #sql_column{name = <<"askmessage">>, type = text}, + #sql_column{name = <<"server">>, type = {char, 1}}, + #sql_column{name = <<"subscribe">>, type = text}, + #sql_column{name = <<"type">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"username">>, + <<"jid">>], + unique = true + }, + #sql_index{ + columns = [<<"server_host">>, <<"jid">>] + }] + }, + #sql_table{ + name = <<"rostergroups">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"grp">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"username">>, + <<"jid">>] + }] + }, + #sql_table{ + name = <<"roster_version">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"version">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }] + }]. + read_roster_version(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(version)s from roster_version" - " where username = %(LUser)s and %(LServer)H")) of - {selected, [{Version}]} -> {ok, Version}; - {selected, []} -> error; - _ -> {error, db_failure} + LServer, + ?SQL("select @(version)s from roster_version" + " where username = %(LUser)s and %(LServer)H")) of + {selected, [{Version}]} -> {ok, Version}; + {selected, []} -> error; + _ -> {error, db_failure} end. + write_roster_version(LUser, LServer, InTransaction, Ver) -> - if InTransaction -> - set_roster_version(LUser, LServer, Ver); - true -> - transaction( - LServer, - fun () -> set_roster_version(LUser, LServer, Ver) end) + if + InTransaction -> + set_roster_version(LUser, LServer, Ver); + true -> + transaction( + LServer, + fun() -> set_roster_version(LUser, LServer, Ver) end) end. + get_roster(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s, " - "@(ask)s, @(askmessage)s, @(server)s, @(subscribe)s, " - "@(type)s from rosterusers " + LServer, + ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s, " + "@(ask)s, @(askmessage)s, @(server)s, @(subscribe)s, " + "@(type)s from rosterusers " "where username=%(LUser)s and %(LServer)H")) of {selected, Items} when is_list(Items) -> JIDGroups = case get_roster_jid_groups(LServer, LUser) of {selected, JGrps} when is_list(JGrps) -> JGrps; _ -> - [] + [] end, GroupsDict = lists:foldl(fun({J, G}, Acc) -> Gs = maps:get(J, Acc, []), maps:put(J, [G | Gs], Acc) end, - maps:new(), JIDGroups), - {ok, lists:flatmap( - fun(I) -> - case raw_to_record(LServer, I) of - %% Bad JID in database: - error -> []; - R -> - SJID = jid:encode(R#roster.jid), + maps:new(), + JIDGroups), + {ok, lists:flatmap( + fun(I) -> + case raw_to_record(LServer, I) of + %% Bad JID in database: + error -> []; + R -> + SJID = jid:encode(R#roster.jid), Groups = maps:get(SJID, GroupsDict, []), - [R#roster{groups = Groups}] - end - end, Items)}; + [R#roster{groups = Groups}] + end + end, + Items)}; _ -> - error + error end. + roster_subscribe(_LUser, _LServer, _LJID, Item) -> ItemVals = record_to_row(Item), roster_subscribe(ItemVals). + transaction(LServer, F) -> ejabberd_sql:sql_transaction(LServer, F). + get_roster_item(LUser, LServer, LJID) -> SJID = jid:encode(LJID), case get_roster_by_jid(LServer, LUser, SJID) of - {selected, [I]} -> + {selected, [I]} -> case raw_to_record(LServer, I) of - error -> - error; - R -> - Groups = case get_roster_groups(LServer, LUser, SJID) of - {selected, JGrps} when is_list(JGrps) -> - [JGrp || {JGrp} <- JGrps]; - _ -> [] - end, - {ok, R#roster{groups = Groups}} - end; - {selected, []} -> - error + error -> + error; + R -> + Groups = case get_roster_groups(LServer, LUser, SJID) of + {selected, JGrps} when is_list(JGrps) -> + [ JGrp || {JGrp} <- JGrps ]; + _ -> [] + end, + {ok, R#roster{groups = Groups}} + end; + {selected, []} -> + error end. + remove_user(LUser, LServer) -> transaction( LServer, - fun () -> + fun() -> ejabberd_sql:sql_query_t( ?SQL("delete from rosterusers" " where username=%(LUser)s and %(LServer)H")), @@ -185,6 +219,7 @@ remove_user(LUser, LServer) -> end), ok. + update_roster(LUser, LServer, LJID, Item) -> SJID = jid:encode(LJID), ItemVals = record_to_row(Item), @@ -197,14 +232,15 @@ update_roster(LUser, LServer, LJID, Item) -> fun(ItemGroup) -> ejabberd_sql:sql_query_t( ?SQL_INSERT( - "rostergroups", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "jid=%(SJID)s", - "grp=%(ItemGroup)s"])) + "rostergroups", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "grp=%(ItemGroup)s"])) end, ItemGroups). + del_roster(LUser, LServer, LJID) -> SJID = jid:encode(LJID), ejabberd_sql:sql_query_t( @@ -214,22 +250,24 @@ del_roster(LUser, LServer, LJID) -> ?SQL("delete from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + read_subscription_and_groups(LUser, LServer, LJID) -> SJID = jid:encode(LJID), case get_subscription(LServer, LUser, SJID) of - {selected, [{SSubscription, SAsk}]} -> - Subscription = decode_subscription(LUser, LServer, SSubscription), - Ask = decode_ask(LUser, LServer, SAsk), - Groups = case get_rostergroup_by_jid(LServer, LUser, SJID) of - {selected, JGrps} when is_list(JGrps) -> - [JGrp || {JGrp} <- JGrps]; - _ -> [] - end, - {ok, {Subscription, Ask, Groups}}; - _ -> - error + {selected, [{SSubscription, SAsk}]} -> + Subscription = decode_subscription(LUser, LServer, SSubscription), + Ask = decode_ask(LUser, LServer, SAsk), + Groups = case get_rostergroup_by_jid(LServer, LUser, SJID) of + {selected, JGrps} when is_list(JGrps) -> + [ JGrp || {JGrp} <- JGrps ]; + _ -> [] + end, + {ok, {Subscription, Ask, Groups}}; + _ -> + error end. + export(_Server) -> [{roster, fun(Host, #roster{usj = {_LUser, LServer, _LJID}} = R) @@ -237,7 +275,7 @@ export(_Server) -> ItemVals = record_to_row(R), ItemGroups = R#roster.groups, update_roster_sql(ItemVals, ItemGroups); - (_Host, _R) -> + (_Host, _R) -> [] end}, {roster_version, @@ -246,26 +284,29 @@ export(_Server) -> [?SQL("delete from roster_version" " where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( - "roster_version", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "version=%(Ver)s"])]; + "roster_version", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "version=%(Ver)s"])]; (_Host, _R) -> [] end}]. + import(_, _, _) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== set_roster_version(LUser, LServer, Version) -> ?SQL_UPSERT_T( - "roster_version", - ["!username=%(LUser)s", - "!server_host=%(LServer)s", - "version=%(Version)s"]). + "roster_version", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "version=%(Version)s"]). + get_roster_jid_groups(LServer, LUser) -> ejabberd_sql:sql_query( @@ -273,24 +314,27 @@ get_roster_jid_groups(LServer, LUser) -> ?SQL("select @(jid)s, @(grp)s from rostergroups where " "username=%(LUser)s and %(LServer)H")). + get_roster_groups(LServer, LUser, SJID) -> ejabberd_sql:sql_query_t( ?SQL("select @(grp)s from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + roster_subscribe({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}) -> ?SQL_UPSERT_T( - "rosterusers", - ["!username=%(LUser)s", - "!server_host=%(LServer)s", - "!jid=%(SJID)s", - "nick=%(Name)s", - "subscription=%(SSubscription)s", - "ask=%(SAsk)s", - "askmessage=%(AskMessage)s", - "server='N'", - "subscribe=''", - "type='item'"]). + "rosterusers", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!jid=%(SJID)s", + "nick=%(Name)s", + "subscription=%(SSubscription)s", + "ask=%(SAsk)s", + "askmessage=%(AskMessage)s", + "server='N'", + "subscribe=''", + "type='item'"]). + get_roster_by_jid(LServer, LUser, SJID) -> ejabberd_sql:sql_query_t( @@ -299,161 +343,187 @@ get_roster_by_jid(LServer, LUser, SJID) -> " @(type)s from rosterusers" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + get_rostergroup_by_jid(LServer, LUser, SJID) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(grp)s from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + get_subscription(LServer, LUser, SJID) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(subscription)s, @(ask)s from rosterusers " "where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). + update_roster_sql({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}, - ItemGroups) -> + ItemGroups) -> [?SQL("delete from rosterusers where" " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;"), ?SQL_INSERT( - "rosterusers", + "rosterusers", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "nick=%(Name)s", + "subscription=%(SSubscription)s", + "ask=%(SAsk)s", + "askmessage=%(AskMessage)s", + "server='N'", + "subscribe=''", + "type='item'"]), + ?SQL("delete from rostergroups where" + " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;")] ++ + [ ?SQL_INSERT( + "rostergroups", ["username=%(LUser)s", "server_host=%(LServer)s", "jid=%(SJID)s", - "nick=%(Name)s", - "subscription=%(SSubscription)s", - "ask=%(SAsk)s", - "askmessage=%(AskMessage)s", - "server='N'", - "subscribe=''", - "type='item'"]), - ?SQL("delete from rostergroups where" - " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;")] - ++ - [?SQL_INSERT( - "rostergroups", - ["username=%(LUser)s", - "server_host=%(LServer)s", - "jid=%(SJID)s", - "grp=%(ItemGroup)s"]) - || ItemGroup <- ItemGroups]. + "grp=%(ItemGroup)s"]) + || ItemGroup <- ItemGroups ]. + raw_to_record(LServer, - [User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, - SServer, SSubscribe, SType]) -> + [User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + SServer, SSubscribe, SType]) -> raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, SServer, SSubscribe, SType}); raw_to_record(LServer, - {User, SJID, Nick, SSubscription, SAsk, SAskMessage, - SServer, SSubscribe, SType}) -> + {User, SJID, Nick, SSubscription, SAsk, SAskMessage, + SServer, SSubscribe, SType}) -> raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, SServer, SSubscribe, SType}); raw_to_record(LServer, - {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType}) -> + {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}) -> try jid:decode(SJID) of - JID -> - LJID = jid:tolower(JID), - Subscription = decode_subscription(User, LServer, SSubscription), - Ask = decode_ask(User, LServer, SAsk), - #roster{usj = {User, LServer, LJID}, - us = {User, LServer}, jid = LJID, name = Nick, - subscription = Subscription, ask = Ask, - askmessage = SAskMessage} - catch _:{bad_jid, _} -> - ?ERROR_MSG("~ts", [format_row_error(User, LServer, {jid, SJID})]), - error + JID -> + LJID = jid:tolower(JID), + Subscription = decode_subscription(User, LServer, SSubscription), + Ask = decode_ask(User, LServer, SAsk), + #roster{ + usj = {User, LServer, LJID}, + us = {User, LServer}, + jid = LJID, + name = Nick, + subscription = Subscription, + ask = Ask, + askmessage = SAskMessage + } + catch + _:{bad_jid, _} -> + ?ERROR_MSG("~ts", [format_row_error(User, LServer, {jid, SJID})]), + error end. -record_to_row( - #roster{us = {LUser, LServer}, - jid = JID, name = Name, subscription = Subscription, - ask = Ask, askmessage = AskMessage}) -> + +record_to_row(#roster{ + us = {LUser, LServer}, + jid = JID, + name = Name, + subscription = Subscription, + ask = Ask, + askmessage = AskMessage + }) -> SJID = jid:encode(jid:tolower(JID)), SSubscription = case Subscription of - both -> <<"B">>; - to -> <<"T">>; - from -> <<"F">>; - none -> <<"N">> - end, + both -> <<"B">>; + to -> <<"T">>; + from -> <<"F">>; + none -> <<"N">> + end, SAsk = case Ask of - subscribe -> <<"S">>; - unsubscribe -> <<"U">>; - both -> <<"B">>; - out -> <<"O">>; - in -> <<"I">>; - none -> <<"N">> - end, + subscribe -> <<"S">>; + unsubscribe -> <<"U">>; + both -> <<"B">>; + out -> <<"O">>; + in -> <<"I">>; + none -> <<"N">> + end, {LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}. + decode_subscription(User, Server, S) -> case S of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - <<"N">> -> none; - <<"">> -> none; - _ -> - ?ERROR_MSG("~ts", [format_row_error(User, Server, {subscription, S})]), - none + <<"B">> -> both; + <<"T">> -> to; + <<"F">> -> from; + <<"N">> -> none; + <<"">> -> none; + _ -> + ?ERROR_MSG("~ts", [format_row_error(User, Server, {subscription, S})]), + none end. + decode_ask(User, Server, A) -> case A of - <<"S">> -> subscribe; - <<"U">> -> unsubscribe; - <<"B">> -> both; - <<"O">> -> out; - <<"I">> -> in; - <<"N">> -> none; - <<"">> -> none; - _ -> - ?ERROR_MSG("~ts", [format_row_error(User, Server, {ask, A})]), - none + <<"S">> -> subscribe; + <<"U">> -> unsubscribe; + <<"B">> -> both; + <<"O">> -> out; + <<"I">> -> in; + <<"N">> -> none; + <<"">> -> none; + _ -> + ?ERROR_MSG("~ts", [format_row_error(User, Server, {ask, A})]), + none end. + format_row_error(User, Server, Why) -> [case Why of - {jid, JID} -> ["Malformed 'jid' field with value '", JID, "'"]; - {subscription, Sub} -> ["Malformed 'subscription' field with value '", Sub, "'"]; - {ask, Ask} -> ["Malformed 'ask' field with value '", Ask, "'"] + {jid, JID} -> ["Malformed 'jid' field with value '", JID, "'"]; + {subscription, Sub} -> ["Malformed 'subscription' field with value '", Sub, "'"]; + {ask, Ask} -> ["Malformed 'ask' field with value '", Ask, "'"] end, - " detected for ", User, "@", Server, " in table 'rosterusers'"]. + " detected for ", + User, + "@", + Server, + " in table 'rosterusers'"]. + process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> - process_rosteritems_sql(ActionS, list_to_atom(SubsS), list_to_atom(AsksS), - list_to_binary(UsersS), list_to_binary(ContactsS)). + process_rosteritems_sql(ActionS, + list_to_atom(SubsS), + list_to_atom(AsksS), + list_to_binary(UsersS), + list_to_binary(ContactsS)). + process_rosteritems_sql(ActionS, Subscription, Ask, SLocalJID, SJID) -> [LUser, LServer] = binary:split(SLocalJID, <<"@">>), SSubscription = case Subscription of - any -> <<"_">>; - both -> <<"B">>; - to -> <<"T">>; - from -> <<"F">>; - none -> <<"N">> - end, + any -> <<"_">>; + both -> <<"B">>; + to -> <<"T">>; + from -> <<"F">>; + none -> <<"N">> + end, SAsk = case Ask of - any -> <<"_">>; - subscribe -> <<"S">>; - unsubscribe -> <<"U">>; - both -> <<"B">>; - out -> <<"O">>; - in -> <<"I">>; - none -> <<"N">> - end, + any -> <<"_">>; + subscribe -> <<"S">>; + unsubscribe -> <<"U">>; + both -> <<"B">>; + out -> <<"O">>; + in -> <<"I">>; + none -> <<"N">> + end, {selected, List} = ejabberd_sql:sql_query( - LServer, - ?SQL("select @(username)s, @(jid)s from rosterusers " - "where username LIKE %(LUser)s" - " and %(LServer)H" - " and jid LIKE %(SJID)s" - " and subscription LIKE %(SSubscription)s" - " and ask LIKE %(SAsk)s")), + LServer, + ?SQL("select @(username)s, @(jid)s from rosterusers " + "where username LIKE %(LUser)s" + " and %(LServer)H" + " and jid LIKE %(SJID)s" + " and subscription LIKE %(SSubscription)s" + " and ask LIKE %(SAsk)s")), case ActionS of - "delete" -> [mod_roster:del_roster(User, LServer, jid:tolower(jid:decode(Contact))) || {User, Contact} <- List]; - "list" -> ok + "delete" -> [ mod_roster:del_roster(User, LServer, jid:tolower(jid:decode(Contact))) || {User, Contact} <- List ]; + "list" -> ok end, List. diff --git a/src/mod_s2s_bidi.erl b/src/mod_s2s_bidi.erl index 7b9556028..a81cee615 100644 --- a/src/mod_s2s_bidi.erl +++ b/src/mod_s2s_bidi.erl @@ -27,119 +27,146 @@ -export([start/2, stop/1, reload/3, depends/2, mod_options/1]). -export([mod_doc/0]). %% Hooks --export([s2s_in_packet/2, s2s_out_packet/2, - s2s_in_features/2, s2s_in_auth_result/3, s2s_out_unauthenticated_features/2, s2s_in_handle_info/2]). +-export([s2s_in_packet/2, + s2s_out_packet/2, + s2s_in_features/2, + s2s_in_auth_result/3, + s2s_out_unauthenticated_features/2, + s2s_in_handle_info/2]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("translate.hrl"). + %%%=================================================================== %%% API %%%=================================================================== start(_Host, _Opts) -> {ok, [{hook, s2s_in_pre_auth_features, s2s_in_features, 50}, - {hook, s2s_in_post_auth_features, s2s_in_features, 50}, - {hook, s2s_in_unauthenticated_packet, s2s_in_packet, 50}, - {hook, s2s_in_authenticated_packet, s2s_in_packet, 50}, - {hook, s2s_in_handle_info, s2s_in_handle_info, 50}, - {hook, s2s_in_auth_result, s2s_in_auth_result, 50}, - {hook, s2s_out_unauthenticated_features, s2s_out_unauthenticated_features, 50}, - {hook, s2s_out_packet, s2s_out_packet, 50}]}. + {hook, s2s_in_post_auth_features, s2s_in_features, 50}, + {hook, s2s_in_unauthenticated_packet, s2s_in_packet, 50}, + {hook, s2s_in_authenticated_packet, s2s_in_packet, 50}, + {hook, s2s_in_handle_info, s2s_in_handle_info, 50}, + {hook, s2s_in_auth_result, s2s_in_auth_result, 50}, + {hook, s2s_out_unauthenticated_features, s2s_out_unauthenticated_features, 50}, + {hook, s2s_out_packet, s2s_out_packet, 50}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + mod_options(_Host) -> []. + mod_doc() -> - #{desc => - [?T("The module adds support for " - "https://xmpp.org/extensions/xep-0288.html" - "[XEP-0288: Bidirectional Server-to-Server Connections] that allows using " - "single s2s connection to communicate in both directions.")], + #{ + desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0288.html" + "[XEP-0288: Bidirectional Server-to-Server Connections] that allows using " + "single s2s connection to communicate in both directions.")], note => "added in 24.10", opts => [], example => - ["modules:", - " mod_s2s_bidi: {}"]}. + ["modules:", + " mod_s2s_bidi: {}"] + }. + s2s_in_features(Acc, _) -> - [#s2s_bidi_feature{}|Acc]. + [#s2s_bidi_feature{} | Acc]. + s2s_in_packet(State, #s2s_bidi{}) -> {stop, State#{bidi_enabled => true}}; s2s_in_packet(State, _) -> State. + s2s_out_unauthenticated_features(#{db_verify := _} = State, _) -> State; s2s_out_unauthenticated_features(State, #stream_features{} = Pkt) -> try xmpp:try_subtag(Pkt, #s2s_bidi{}) of - #s2s_bidi{} -> - ejabberd_s2s_out:send(State#{bidi_enabled => true}, #s2s_bidi{}); - _ -> - State - catch _:{xmpp_codec, _Why} -> - State + #s2s_bidi{} -> + ejabberd_s2s_out:send(State#{bidi_enabled => true}, #s2s_bidi{}); + _ -> + State + catch + _:{xmpp_codec, _Why} -> + State end; s2s_out_unauthenticated_features(State, _Pkt) -> State. + s2s_out_packet(#{bidi_enabled := true, ip := {IP, _}} = State, Pkt0) - when ?is_stanza(Pkt0) -> + when ?is_stanza(Pkt0) -> To = xmpp:get_to(Pkt0), case check_from_to(State, xmpp:get_from(Pkt0), To) of - ok -> - Pkt = xmpp:put_meta(Pkt0, ip, IP), - LServer = ejabberd_router:host_of_route(To#jid.lserver), - State1 = ejabberd_hooks:run_fold(s2s_in_authenticated_packet, - LServer, State, [Pkt]), - {Pkt1, State2} = ejabberd_hooks:run_fold(s2s_receive_packet, LServer, - {Pkt, State1}, []), - case Pkt1 of - drop -> ok; - _ -> ejabberd_router:route(Pkt1) - end, - {stop, State2}; - {error, Err} -> - {stop, ejabberd_s2s_out:send(State, Err)} + ok -> + Pkt = xmpp:put_meta(Pkt0, ip, IP), + LServer = ejabberd_router:host_of_route(To#jid.lserver), + State1 = ejabberd_hooks:run_fold(s2s_in_authenticated_packet, + LServer, + State, + [Pkt]), + {Pkt1, State2} = ejabberd_hooks:run_fold(s2s_receive_packet, + LServer, + {Pkt, State1}, + []), + case Pkt1 of + drop -> ok; + _ -> ejabberd_router:route(Pkt1) + end, + {stop, State2}; + {error, Err} -> + {stop, ejabberd_s2s_out:send(State, Err)} end; s2s_out_packet(#{db_verify := _} = State, #stream_features{}) -> State; s2s_out_packet(State, #stream_features{} = Pkt) -> try xmpp:try_subtag(Pkt, #s2s_bidi_feature{}) of - #s2s_bidi_feature{} -> - ejabberd_s2s_out:send(State#{bidi_enabled => true}, #s2s_bidi{}) - catch _:{xmpp_codec, _Why} -> - State + #s2s_bidi_feature{} -> + ejabberd_s2s_out:send(State#{bidi_enabled => true}, #s2s_bidi{}) + catch + _:{xmpp_codec, _Why} -> + State end; s2s_out_packet(State, _Pkt) -> State. + s2s_in_handle_info(State, {route, Pkt}) when ?is_stanza(Pkt) -> {stop, ejabberd_s2s_in:send(State, Pkt)}; s2s_in_handle_info(State, _Info) -> State. -check_from_to(#{remote_server := RServer}, #jid{lserver = FromServer}, - #jid{lserver = ToServer}) -> + +check_from_to(#{remote_server := RServer}, + #jid{lserver = FromServer}, + #jid{lserver = ToServer}) -> if - RServer /= FromServer -> {error, xmpp:serr_invalid_from()}; - true -> - case ejabberd_router:is_my_route(ToServer) of - false -> {error, xmpp:serr_host_unknown()}; - _ -> ok - end + RServer /= FromServer -> {error, xmpp:serr_invalid_from()}; + true -> + case ejabberd_router:is_my_route(ToServer) of + false -> {error, xmpp:serr_host_unknown()}; + _ -> ok + end end. + s2s_in_auth_result(#{server := LServer, bidi_enabled := true} = State, true, RServer) -> ejabberd_s2s:register_connection({LServer, RServer}), State; diff --git a/src/mod_s2s_dialback.erl b/src/mod_s2s_dialback.erl index f6128b573..e0ea3e752 100644 --- a/src/mod_s2s_dialback.erl +++ b/src/mod_s2s_dialback.erl @@ -28,15 +28,22 @@ -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% Hooks --export([s2s_out_auth_result/2, s2s_out_downgraded/2, - s2s_in_packet/2, s2s_out_packet/2, s2s_in_recv/3, - s2s_in_features/2, s2s_out_init/2, s2s_out_closed/2, - s2s_out_tls_verify/2]). +-export([s2s_out_auth_result/2, + s2s_out_downgraded/2, + s2s_in_packet/2, + s2s_out_packet/2, + s2s_in_recv/3, + s2s_in_features/2, + s2s_out_init/2, + s2s_out_closed/2, + s2s_out_tls_verify/2]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("translate.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -53,27 +60,35 @@ start(_Host, _Opts) -> {hook, s2s_out_auth_result, s2s_out_auth_result, 50}, {hook, s2s_out_tls_verify, s2s_out_tls_verify, 50}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + mod_opt_type(access) -> econf:acl(). + mod_options(_Host) -> [{access, all}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("The module adds support for " "https://xmpp.org/extensions/xep-0220.html" "[XEP-0220: Server Dialback] to provide server identity " - "verification based on DNS."), "", + "verification based on DNS."), + "", ?T("WARNING: DNS-based verification is vulnerable to " "https://en.wikipedia.org/wiki/DNS_spoofing" "[DNS cache poisoning], so modern servers rely on " @@ -86,11 +101,13 @@ mod_doc() -> "not because it's compromised).")], opts => [{access, - #{value => ?T("AccessName"), + #{ + value => ?T("AccessName"), desc => ?T("An access rule that can be used to restrict " "dialback for some servers. The default value " - "is 'all'.")}}], + "is 'all'.") + }}], example => ["modules:", " mod_s2s_dialback:", @@ -98,137 +115,182 @@ mod_doc() -> " allow:", " server: legacy.domain.tld", " server: invalid-cert.example.org", - " deny: all"]}. + " deny: all"] + }. + s2s_in_features(Acc, _) -> - [#db_feature{errors = true}|Acc]. + [#db_feature{errors = true} | Acc]. + s2s_out_init({ok, State}, Opts) -> case proplists:get_value(db_verify, Opts) of - {StreamID, Key, Pid} -> - %% This is an outbound s2s connection created at step 1. - %% The purpose of this connection is to verify dialback key ONLY. - %% The connection is not registered in s2s table and thus is not - %% seen by anyone. - %% The connection will be closed immediately after receiving the - %% verification response (at step 3) - {ok, State#{db_verify => {StreamID, Key, Pid}}}; - undefined -> - {ok, State#{db_enabled => true}} + {StreamID, Key, Pid} -> + %% This is an outbound s2s connection created at step 1. + %% The purpose of this connection is to verify dialback key ONLY. + %% The connection is not registered in s2s table and thus is not + %% seen by anyone. + %% The connection will be closed immediately after receiving the + %% verification response (at step 3) + {ok, State#{db_verify => {StreamID, Key, Pid}}}; + undefined -> + {ok, State#{db_enabled => true}} end; s2s_out_init(Acc, _Opts) -> Acc. -s2s_out_closed(#{server := LServer, - remote_server := RServer, - lang := Lang, - db_verify := {StreamID, _Key, _Pid}} = State, Reason) -> + +s2s_out_closed(#{ + server := LServer, + remote_server := RServer, + lang := Lang, + db_verify := {StreamID, _Key, _Pid} + } = State, + Reason) -> %% Outbound s2s verificating connection (created at step 1) is %% closed suddenly without receiving the response. %% Building a response on our own - Response = #db_verify{from = RServer, to = LServer, - id = StreamID, type = error, - sub_els = [mk_error(Reason, Lang)]}, + Response = #db_verify{ + from = RServer, + to = LServer, + id = StreamID, + type = error, + sub_els = [mk_error(Reason, Lang)] + }, s2s_out_packet(State, Response); s2s_out_closed(State, _Reason) -> State. + s2s_out_auth_result(#{db_verify := _} = State, _) -> %% The temporary outbound s2s connect (intended for verification) %% has passed authentication state (either successfully or not, no matter) %% and at this point we can send verification request as described %% in section 2.1.2, step 2 {stop, send_verify_request(State)}; -s2s_out_auth_result(#{db_enabled := true, - socket := Socket, ip := IP, - server := LServer, - remote_server := RServer} = State, {false, _}) -> +s2s_out_auth_result(#{ + db_enabled := true, + socket := Socket, + ip := IP, + server := LServer, + remote_server := RServer + } = State, + {false, _}) -> %% SASL authentication has failed, retrying with dialback %% Sending dialback request, section 2.1.1, step 1 ?INFO_MSG("(~ts) Retrying with s2s dialback authentication: ~ts -> ~ts (~ts)", - [xmpp_socket:pp(Socket), LServer, RServer, - ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + [xmpp_socket:pp(Socket), + LServer, + RServer, + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), State1 = maps:remove(stop_reason, State#{on_route => queue}), {stop, send_db_request(State1)}; s2s_out_auth_result(State, _) -> State. + s2s_out_downgraded(#{db_verify := _} = State, _) -> %% The verifying outbound s2s connection detected non-RFC compliant %% server, send verification request immediately without auth phase, %% section 2.1.2, step 2 {stop, send_verify_request(State)}; -s2s_out_downgraded(#{db_enabled := true, - socket := Socket, ip := IP, - server := LServer, - remote_server := RServer} = State, _) -> +s2s_out_downgraded(#{ + db_enabled := true, + socket := Socket, + ip := IP, + server := LServer, + remote_server := RServer + } = State, + _) -> %% non-RFC compliant server detected, send dialback request instantly, %% section 2.1.1, step 1 ?INFO_MSG("(~ts) Trying s2s dialback authentication with " - "non-RFC compliant server: ~ts -> ~ts (~ts)", - [xmpp_socket:pp(Socket), LServer, RServer, - ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), + "non-RFC compliant server: ~ts -> ~ts (~ts)", + [xmpp_socket:pp(Socket), + LServer, + RServer, + ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), {stop, send_db_request(State)}; s2s_out_downgraded(State, _) -> State. + s2s_in_packet(#{stream_id := StreamID, lang := Lang} = State, - #db_result{from = From, to = To, key = Key, type = undefined}) -> + #db_result{from = From, to = To, key = Key, type = undefined}) -> %% Received dialback request, section 2.2.1, step 1 try - ok = check_from_to(From, To), - %% We're creating a temporary outbound s2s connection to - %% send verification request and to receive verification response - {ok, Pid} = ejabberd_s2s_out:start( - To, From, [{db_verify, {StreamID, Key, self()}}]), - ejabberd_s2s_out:connect(Pid), - {stop, State} - catch _:{badmatch, {error, Reason}} -> - {stop, - send_db_result(State, - #db_verify{from = From, to = To, type = error, - sub_els = [mk_error(Reason, Lang)]})} + ok = check_from_to(From, To), + %% We're creating a temporary outbound s2s connection to + %% send verification request and to receive verification response + {ok, Pid} = ejabberd_s2s_out:start( + To, From, [{db_verify, {StreamID, Key, self()}}]), + ejabberd_s2s_out:connect(Pid), + {stop, State} + catch + _:{badmatch, {error, Reason}} -> + {stop, + send_db_result(State, + #db_verify{ + from = From, + to = To, + type = error, + sub_els = [mk_error(Reason, Lang)] + })} end; -s2s_in_packet(State, #db_verify{to = To, from = From, key = Key, - id = StreamID, type = undefined}) -> +s2s_in_packet(State, + #db_verify{ + to = To, + from = From, + key = Key, + id = StreamID, + type = undefined + }) -> %% Received verification request, section 2.2.2, step 2 Type = case make_key(To, From, StreamID) of - Key -> valid; - _ -> invalid - end, + Key -> valid; + _ -> invalid + end, Response = #db_verify{from = To, to = From, id = StreamID, type = Type}, {stop, ejabberd_s2s_in:send(State, Response)}; s2s_in_packet(State, Pkt) when is_record(Pkt, db_result); - is_record(Pkt, db_verify) -> + is_record(Pkt, db_verify) -> ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), State; s2s_in_packet(State, _) -> State. + s2s_in_recv(#{lang := Lang} = State, El, {error, Why}) -> case xmpp:get_name(El) of - Tag when Tag == <<"db:result">>; - Tag == <<"db:verify">> -> - case xmpp:get_type(El) of - T when T /= <<"valid">>, - T /= <<"invalid">>, - T /= <<"error">> -> - Err = xmpp:make_error(El, mk_error({codec_error, Why}, Lang)), - {stop, ejabberd_s2s_in:send(State, Err)}; - _ -> - State - end; - _ -> - State + Tag when Tag == <<"db:result">>; + Tag == <<"db:verify">> -> + case xmpp:get_type(El) of + T when T /= <<"valid">>, + T /= <<"invalid">>, + T /= <<"error">> -> + Err = xmpp:make_error(El, mk_error({codec_error, Why}, Lang)), + {stop, ejabberd_s2s_in:send(State, Err)}; + _ -> + State + end; + _ -> + State end; s2s_in_recv(State, _El, _Pkt) -> State. -s2s_out_packet(#{server := LServer, - remote_server := RServer, - db_verify := {StreamID, _Key, Pid}} = State, - #db_verify{from = RServer, to = LServer, - id = StreamID, type = Type} = Response) + +s2s_out_packet(#{ + server := LServer, + remote_server := RServer, + db_verify := {StreamID, _Key, Pid} + } = State, + #db_verify{ + from = RServer, + to = LServer, + id = StreamID, + type = Type + } = Response) when Type /= undefined -> %% Received verification response, section 2.1.2, step 3 %% This is a response for the request sent at step 2 @@ -238,36 +300,41 @@ s2s_out_packet(#{server := LServer, ejabberd_s2s_out:stop_async(self()), State; s2s_out_packet(#{server := LServer, remote_server := RServer} = State, - #db_result{to = LServer, from = RServer, - type = Type} = Result) when Type /= undefined -> + #db_result{ + to = LServer, + from = RServer, + type = Type + } = Result) when Type /= undefined -> %% Received dialback response, section 2.1.1, step 4 %% This is a response to the request sent at step 1 State1 = maps:remove(db_enabled, State), case Type of - valid -> - State2 = ejabberd_s2s_out:handle_auth_success(<<"dialback">>, State1), - ejabberd_s2s_out:establish(State2); - _ -> - Reason = str:format("Peer responded with error: ~s", - [format_error(Result)]), - ejabberd_s2s_out:handle_auth_failure( - <<"dialback">>, {auth, Reason}, State1) + valid -> + State2 = ejabberd_s2s_out:handle_auth_success(<<"dialback">>, State1), + ejabberd_s2s_out:establish(State2); + _ -> + Reason = str:format("Peer responded with error: ~s", + [format_error(Result)]), + ejabberd_s2s_out:handle_auth_failure( + <<"dialback">>, {auth, Reason}, State1) end; s2s_out_packet(State, Pkt) when is_record(Pkt, db_result); - is_record(Pkt, db_verify) -> + is_record(Pkt, db_verify) -> ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), State; s2s_out_packet(State, _) -> State. + -spec s2s_out_tls_verify(boolean(), ejabberd_s2s_out:state()) -> boolean(). s2s_out_tls_verify(_, #{server_host := ServerHost, remote_server := RServer}) -> Access = mod_s2s_dialback_opt:access(ServerHost), case acl:match_rule(ServerHost, Access, jid:make(RServer)) of - allow -> false; - deny -> true + allow -> false; + deny -> true end. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -275,57 +342,79 @@ s2s_out_tls_verify(_, #{server_host := ServerHost, remote_server := RServer}) -> make_key(From, To, StreamID) -> Secret = ejabberd_config:get_shared_key(), str:to_hexlist( - misc:crypto_hmac(sha256, str:to_hexlist(crypto:hash(sha256, Secret)), - [To, " ", From, " ", StreamID])). + misc:crypto_hmac(sha256, + str:to_hexlist(crypto:hash(sha256, Secret)), + [To, " ", From, " ", StreamID])). + -spec send_verify_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). -send_verify_request(#{server := LServer, - remote_server := RServer, - db_verify := {StreamID, Key, _Pid}} = State) -> - Request = #db_verify{from = LServer, to = RServer, - key = Key, id = StreamID}, +send_verify_request(#{ + server := LServer, + remote_server := RServer, + db_verify := {StreamID, Key, _Pid} + } = State) -> + Request = #db_verify{ + from = LServer, + to = RServer, + key = Key, + id = StreamID + }, ejabberd_s2s_out:send(State, Request). + -spec send_db_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). -send_db_request(#{server := LServer, - remote_server := RServer, - stream_remote_id := StreamID} = State) -> +send_db_request(#{ + server := LServer, + remote_server := RServer, + stream_remote_id := StreamID + } = State) -> Key = make_key(LServer, RServer, StreamID), - ejabberd_s2s_out:send(State, #db_result{from = LServer, - to = RServer, - key = Key}). + ejabberd_s2s_out:send(State, + #db_result{ + from = LServer, + to = RServer, + key = Key + }). + -spec send_db_result(ejabberd_s2s_in:state(), db_verify()) -> ejabberd_s2s_in:state(). -send_db_result(State, #db_verify{from = From, to = To, - type = Type, sub_els = Els}) -> +send_db_result(State, + #db_verify{ + from = From, + to = To, + type = Type, + sub_els = Els + }) -> %% Sending dialback response, section 2.2.1, step 4 %% This is a response to the request received at step 1 Response = #db_result{from = To, to = From, type = Type, sub_els = Els}, State1 = ejabberd_s2s_in:send(State, Response), case Type of - valid -> - State2 = ejabberd_s2s_in:handle_auth_success( - From, <<"dialback">>, undefined, State1), - ejabberd_s2s_in:establish(State2); - _ -> - Reason = str:format("Verification failed: ~s", - [format_error(Response)]), - ejabberd_s2s_in:handle_auth_failure( - From, <<"dialback">>, Reason, State1) + valid -> + State2 = ejabberd_s2s_in:handle_auth_success( + From, <<"dialback">>, undefined, State1), + ejabberd_s2s_in:establish(State2); + _ -> + Reason = str:format("Verification failed: ~s", + [format_error(Response)]), + ejabberd_s2s_in:handle_auth_failure( + From, <<"dialback">>, Reason, State1) end. + -spec check_from_to(binary(), binary()) -> ok | {error, forbidden | host_unknown}. check_from_to(From, To) -> case ejabberd_router:is_my_route(To) of - false -> {error, host_unknown}; - true -> - LServer = ejabberd_router:host_of_route(To), - case ejabberd_s2s:allow_host(LServer, From) of - true -> ok; - false -> {error, forbidden} - end + false -> {error, host_unknown}; + true -> + LServer = ejabberd_router:host_of_route(To), + case ejabberd_s2s:allow_host(LServer, From) of + true -> ok; + false -> {error, forbidden} + end end. + -spec mk_error(term(), binary()) -> stanza_error(). mk_error(forbidden, Lang) -> xmpp:err_forbidden(?T("Access denied by service policy"), Lang); @@ -339,15 +428,16 @@ mk_error({_Class, _Reason} = Why, Lang) -> mk_error(_, _) -> xmpp:err_internal_server_error(). + -spec format_error(db_result()) -> binary(). format_error(#db_result{type = invalid}) -> <<"invalid dialback key">>; format_error(#db_result{type = error} = Result) -> case xmpp:get_error(Result) of - #stanza_error{} = Err -> - xmpp:format_stanza_error(Err); - undefined -> - <<"unrecognized error">> + #stanza_error{} = Err -> + xmpp:format_stanza_error(Err); + undefined -> + <<"unrecognized error">> end; format_error(_) -> <<"unexpected dialback result">>. diff --git a/src/mod_s2s_dialback_opt.erl b/src/mod_s2s_dialback_opt.erl index 6f91c4dd1..cc27e0d6a 100644 --- a/src/mod_s2s_dialback_opt.erl +++ b/src/mod_s2s_dialback_opt.erl @@ -5,9 +5,9 @@ -export([access/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_s2s_dialback, access). - diff --git a/src/mod_scram_upgrade.erl b/src/mod_scram_upgrade.erl index 37af47b46..75525d8a9 100644 --- a/src/mod_scram_upgrade.erl +++ b/src/mod_scram_upgrade.erl @@ -27,107 +27,137 @@ -export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). -export([mod_doc/0]). %% Hooks --export([c2s_inline_features/3, c2s_handle_sasl2_inline/1, - c2s_handle_sasl2_task_next/4, c2s_handle_sasl2_task_data/3]). +-export([c2s_inline_features/3, + c2s_handle_sasl2_inline/1, + c2s_handle_sasl2_task_next/4, + c2s_handle_sasl2_task_data/3]). -include_lib("xmpp/include/xmpp.hrl"). -include_lib("xmpp/include/scram.hrl"). + -include("logger.hrl"). -include("translate.hrl"). + %%%=================================================================== %%% API %%%=================================================================== start(_Host, _Opts) -> {ok, [{hook, c2s_inline_features, c2s_inline_features, 50}, - {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10}, - {hook, c2s_handle_sasl2_task_next, c2s_handle_sasl2_task_next, 10}, - {hook, c2s_handle_sasl2_task_data, c2s_handle_sasl2_task_data, 10}]}. + {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 10}, + {hook, c2s_handle_sasl2_task_next, c2s_handle_sasl2_task_next, 10}, + {hook, c2s_handle_sasl2_task_data, c2s_handle_sasl2_task_data, 10}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + mod_opt_type(offered_upgrades) -> econf:list(econf:enum([sha256, sha512])). + mod_options(_Host) -> [{offered_upgrades, [sha256, sha512]}]. + mod_doc() -> - #{desc => - [?T("The module adds support for " - "https://xmpp.org/extensions/xep-0480.html" - "[XEP-0480: SASL Upgrade Tasks] that allows users to upgrade " - "passwords to more secure representation.")], + #{ + desc => + [?T("The module adds support for " + "https://xmpp.org/extensions/xep-0480.html" + "[XEP-0480: SASL Upgrade Tasks] that allows users to upgrade " + "passwords to more secure representation.")], note => "added in 24.10", opts => [{offered_upgrades, - #{value => "list(sha256, sha512)", - desc => ?T("List with upgrade types that should be offered")}}], + #{ + value => "list(sha256, sha512)", + desc => ?T("List with upgrade types that should be offered") + }}], example => - ["modules:", - " mod_scram_upgrade:", - " offered_upgrades:", - " - sha256", - " - sha512"]}. + ["modules:", + " mod_scram_upgrade:", + " offered_upgrades:", + " - sha256", + " - sha512"] + }. + c2s_inline_features({Sasl, Bind, Extra}, Host, State) -> KnowTypes = case State of - #{sasl2_password_fun := Fun} -> - case Fun(<<>>) of - {Pass, _} -> lists:filtermap( - fun(#scram{hash = sha256}) -> {true, sha256}; - (#scram{hash = sha512}) -> {true, sha512}; - (_) -> false - end, Pass); - _ -> [] - end; - _ -> [] - end, + #{sasl2_password_fun := Fun} -> + case Fun(<<>>) of + {Pass, _} -> + lists:filtermap( + fun(#scram{hash = sha256}) -> {true, sha256}; + (#scram{hash = sha512}) -> {true, sha512}; + (_) -> false + end, + Pass); + _ -> [] + end; + _ -> [] + end, Methods = lists:filtermap( - fun(sha256) -> {true, #sasl_upgrade{cdata = <<"UPGR-SCRAM-SHA-256">>}}; - (sha512) -> {true, #sasl_upgrade{cdata = <<"UPGR-SCRAM-SHA-512">>}} - end, mod_scram_upgrade_opt:offered_upgrades(Host) -- KnowTypes), + fun(sha256) -> {true, #sasl_upgrade{cdata = <<"UPGR-SCRAM-SHA-256">>}}; + (sha512) -> {true, #sasl_upgrade{cdata = <<"UPGR-SCRAM-SHA-512">>}} + end, + mod_scram_upgrade_opt:offered_upgrades(Host) -- KnowTypes), {Sasl, Bind, Methods ++ Extra}. + c2s_handle_sasl2_inline({State, Els, _Results} = Acc) -> case lists:keyfind(sasl_upgrade, 1, Els) of - false -> - Acc; - #sasl_upgrade{cdata = Type} -> - {stop, {State, {continue, [Type]}, []}} + false -> + Acc; + #sasl_upgrade{cdata = Type} -> + {stop, {State, {continue, [Type]}, []}} end. + c2s_handle_sasl2_task_next({_, State}, Task, _Els, _InlineEls) -> Algo = case Task of - <<"UPGR-SCRAM-SHA-256">> -> sha256; - <<"UPGR-SCRAM-SHA-512">> -> sha512 - end, + <<"UPGR-SCRAM-SHA-256">> -> sha256; + <<"UPGR-SCRAM-SHA-512">> -> sha512 + end, Salt = p1_rand:bytes(16), {task_data, [#scram_upgrade_salt{cdata = Salt, iterations = 4096}], - State#{scram_upgrade => {Algo, Salt, 4096}}}. + State#{scram_upgrade => {Algo, Salt, 4096}}}. -c2s_handle_sasl2_task_data({_, #{user := User, server := Server, - scram_upgrade := {Algo, Salt, Iter}} = State}, - Els, InlineEls) -> + +c2s_handle_sasl2_task_data({_, + #{ + user := User, + server := Server, + scram_upgrade := {Algo, Salt, Iter} + } = State}, + Els, + InlineEls) -> case xmpp:get_subtag(#sasl2_task_data{sub_els = Els}, #scram_upgrade_hash{}) of - #scram_upgrade_hash{data = SaltedPassword} -> - StoredKey = scram:stored_key(Algo, scram:client_key(Algo, SaltedPassword)), - ServerKey = scram:server_key(Algo, SaltedPassword), - ejabberd_auth:set_password_instance(User, Server, - #scram{hash = Algo, iterationcount = Iter, - salt = base64:encode(Salt), - serverkey = base64:encode(ServerKey), - storedkey = base64:encode(StoredKey)}), - State2 = maps:remove(scram_upgrade, State), - InlineEls2 = lists:keydelete(sasl_upgrade, 1, InlineEls), - {State3, NewEls, Results} = ejabberd_c2s:handle_sasl2_inline(InlineEls2, State2), - {success, NewEls, Results, State3}; - _ -> - {abort, State} + #scram_upgrade_hash{data = SaltedPassword} -> + StoredKey = scram:stored_key(Algo, scram:client_key(Algo, SaltedPassword)), + ServerKey = scram:server_key(Algo, SaltedPassword), + ejabberd_auth:set_password_instance(User, + Server, + #scram{ + hash = Algo, + iterationcount = Iter, + salt = base64:encode(Salt), + serverkey = base64:encode(ServerKey), + storedkey = base64:encode(StoredKey) + }), + State2 = maps:remove(scram_upgrade, State), + InlineEls2 = lists:keydelete(sasl_upgrade, 1, InlineEls), + {State3, NewEls, Results} = ejabberd_c2s:handle_sasl2_inline(InlineEls2, State2), + {success, NewEls, Results, State3}; + _ -> + {abort, State} end. diff --git a/src/mod_scram_upgrade_opt.erl b/src/mod_scram_upgrade_opt.erl index abd6bad4b..f023dae85 100644 --- a/src/mod_scram_upgrade_opt.erl +++ b/src/mod_scram_upgrade_opt.erl @@ -5,9 +5,9 @@ -export([offered_upgrades/1]). + -spec offered_upgrades(gen_mod:opts() | global | binary()) -> ['sha256' | 'sha512']. offered_upgrades(Opts) when is_map(Opts) -> gen_mod:get_opt(offered_upgrades, Opts); offered_upgrades(Host) -> gen_mod:get_module_opt(Host, mod_scram_upgrade, offered_upgrades). - diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl index 533ff517f..3f1e70afc 100644 --- a/src/mod_service_log.erl +++ b/src/mod_service_log.erl @@ -29,55 +29,76 @@ -behaviour(gen_mod). --export([start/2, stop/1, log_user_send/1, mod_options/1, - log_user_receive/1, mod_opt_type/1, depends/2, mod_doc/0]). +-export([start/2, + stop/1, + log_user_send/1, + mod_options/1, + log_user_receive/1, + mod_opt_type/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + start(_Host, _Opts) -> {ok, [{hook, user_send_packet, log_user_send, 50}, {hook, user_receive_packet, log_user_receive, 50}]}. + stop(_Host) -> ok. + depends(_Host, _Opts) -> []. + -spec log_user_send({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. log_user_send({Packet, C2SState}) -> From = xmpp:get_from(Packet), log_packet(Packet, From#jid.lserver), {Packet, C2SState}. + -spec log_user_receive({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. log_user_receive({Packet, C2SState}) -> To = xmpp:get_to(Packet), log_packet(Packet, To#jid.lserver), {Packet, C2SState}. + -spec log_packet(stanza(), binary()) -> ok. log_packet(Packet, Host) -> Loggers = mod_service_log_opt:loggers(Host), - ForwardedMsg = #message{from = jid:make(Host), - id = p1_rand:get_string(), - sub_els = [#forwarded{ - sub_els = [Packet]}]}, + ForwardedMsg = #message{ + from = jid:make(Host), + id = p1_rand:get_string(), + sub_els = [#forwarded{ + sub_els = [Packet] + }] + }, lists:foreach( fun(Logger) -> - ejabberd_router:route(xmpp:set_to(ForwardedMsg, jid:make(Logger))) - end, Loggers). + ejabberd_router:route(xmpp:set_to(ForwardedMsg, jid:make(Logger))) + end, + Loggers). + mod_opt_type(loggers) -> econf:list(econf:domain()). + mod_options(_) -> [{loggers, []}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module forwards copies of all stanzas " "to remote XMPP servers or components. " "Every stanza is encapsulated into " @@ -86,13 +107,16 @@ mod_doc() -> "[XEP-0297: Stanza Forwarding]."), opts => [{loggers, - #{value => "[Domain, ...]", + #{ + value => "[Domain, ...]", desc => ?T("A list of servers or connected components " - "to which stanzas will be forwarded.")}}], + "to which stanzas will be forwarded.") + }}], example => ["modules:", " mod_service_log:", " loggers:", " - xmpp-server.tld", - " - component.domain.tld"]}. + " - component.domain.tld"] + }. diff --git a/src/mod_service_log_opt.erl b/src/mod_service_log_opt.erl index 34eae49a6..e0faab537 100644 --- a/src/mod_service_log_opt.erl +++ b/src/mod_service_log_opt.erl @@ -5,9 +5,9 @@ -export([loggers/1]). + -spec loggers(gen_mod:opts() | global | binary()) -> [binary()]. loggers(Opts) when is_map(Opts) -> gen_mod:get_opt(loggers, Opts); loggers(Host) -> gen_mod:get_module_opt(Host, mod_service_log, loggers). - diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 1c9e6f88f..9f0c0835e 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -29,17 +29,39 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, export/1, - import_info/0, webadmin_menu/3, webadmin_page/3, - get_user_roster/2, - get_jid_info/4, import/5, process_item/2, import_start/2, - in_subscription/2, out_subscription/1, c2s_self_presence/1, - unset_presence/4, register_user/2, remove_user/2, - list_groups/1, create_group/2, create_group/3, - delete_group/2, get_group_opts/2, set_group_opts/3, - get_group_users/2, get_group_explicit_users/2, - is_user_in_group/3, add_user_to_group/3, opts_to_binary/1, - remove_user_from_group/3, mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]). +-export([start/2, + stop/1, + reload/3, + export/1, + import_info/0, + webadmin_menu/3, + webadmin_page/3, + get_user_roster/2, + get_jid_info/4, + import/5, + process_item/2, + import_start/2, + in_subscription/2, + out_subscription/1, + c2s_self_presence/1, + unset_presence/4, + register_user/2, + remove_user/2, + list_groups/1, + create_group/2, create_group/3, + delete_group/2, + get_group_opts/2, + set_group_opts/3, + get_group_users/2, + get_group_explicit_users/2, + is_user_in_group/3, + add_user_to_group/3, + opts_to_binary/1, + remove_user_from_group/3, + mod_opt_type/1, + mod_options/1, + mod_doc/0, + depends/2]). -import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/2, make_table/4]). @@ -58,6 +80,8 @@ -include("translate.hrl"). -type group_options() :: [{atom(), any()}]. + + -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback list_groups(binary()) -> [binary()]. @@ -69,7 +93,7 @@ -callback get_user_groups({binary(), binary()}, binary()) -> [binary()]. -callback get_group_explicit_users(binary(), binary()) -> [{binary(), binary()}]. -callback get_user_displayed_groups(binary(), binary(), group_options()) -> - [{binary(), group_options()}]. + [{binary(), group_options()}]. -callback is_user_in_group({binary(), binary()}, binary(), binary()) -> boolean(). -callback add_user_to_group(binary(), {binary(), binary()}, binary()) -> any(). -callback remove_user_from_group(binary(), {binary(), binary()}, binary()) -> {atomic, any()}. @@ -78,10 +102,11 @@ -optional_callbacks([use_cache/1, cache_nodes/1]). --define(GROUP_OPTS_CACHE, shared_roster_group_opts_cache). --define(USER_GROUPS_CACHE, shared_roster_user_groups_cache). +-define(GROUP_OPTS_CACHE, shared_roster_group_opts_cache). +-define(USER_GROUPS_CACHE, shared_roster_user_groups_cache). -define(GROUP_EXPLICIT_USERS_CACHE, shared_roster_group_explicit_cache). --define(SPECIAL_GROUPS_CACHE, shared_roster_special_groups_cache). +-define(SPECIAL_GROUPS_CACHE, shared_roster_special_groups_cache). + start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), @@ -99,40 +124,45 @@ start(Host, Opts) -> {hook, register_user, register_user, 50}, {hook, remove_user, remove_user, 50}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if - NewMod /= OldMod -> - NewMod:init(Host, NewOpts); - true -> - ok + NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok end, init_cache(NewMod, Host, NewOpts), ok. + depends(_Host, _Opts) -> []. + -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> NumHosts = length(ejabberd_option:hosts()), ets_cache:new(?SPECIAL_GROUPS_CACHE, [{max_size, NumHosts * 4}]), case use_cache(Mod, Host) of true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?GROUP_OPTS_CACHE, CacheOpts), - ets_cache:new(?USER_GROUPS_CACHE, CacheOpts), - ets_cache:new(?GROUP_EXPLICIT_USERS_CACHE, CacheOpts); + CacheOpts = cache_opts(Opts), + ets_cache:new(?GROUP_OPTS_CACHE, CacheOpts), + ets_cache:new(?USER_GROUPS_CACHE, CacheOpts), + ets_cache:new(?GROUP_EXPLICIT_USERS_CACHE, CacheOpts); false -> - ets_cache:delete(?GROUP_OPTS_CACHE), - ets_cache:delete(?USER_GROUPS_CACHE), - ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE) + ets_cache:delete(?GROUP_OPTS_CACHE), + ets_cache:delete(?USER_GROUPS_CACHE), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_shared_roster_opt:cache_size(Opts), @@ -140,6 +170,7 @@ cache_opts(Opts) -> LifeTime = mod_shared_roster_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of @@ -147,6 +178,7 @@ use_cache(Mod, Host) -> false -> mod_shared_roster_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of @@ -154,64 +186,76 @@ cache_nodes(Mod, Host) -> false -> ejabberd_cluster:get_nodes() end. + -spec get_user_roster([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. get_user_roster(Items, {_, S} = US) -> {DisplayedGroups, Cache} = get_user_displayed_groups(US), SRUsers = lists:foldl( - fun(Group, Acc1) -> - GroupLabel = get_group_label_cached(S, Group, Cache), - lists:foldl( - fun(User, Acc2) -> - if User == US -> Acc2; - true -> - dict:append(User, GroupLabel, Acc2) - end - end, - Acc1, get_group_users_cached(S, Group, Cache)) - end, - dict:new(), DisplayedGroups), + fun(Group, Acc1) -> + GroupLabel = get_group_label_cached(S, Group, Cache), + lists:foldl( + fun(User, Acc2) -> + if + User == US -> Acc2; + true -> + dict:append(User, GroupLabel, Acc2) + end + end, + Acc1, + get_group_users_cached(S, Group, Cache)) + end, + dict:new(), + DisplayedGroups), {NewItems1, SRUsersRest} = lists:mapfoldl( - fun(Item = #roster_item{jid = #jid{luser = User1, lserver = Server1}}, SRUsers1) -> - US1 = {User1, Server1}, - case dict:find(US1, SRUsers1) of - {ok, GroupLabels} -> - {Item#roster_item{subscription = both, - groups = Item#roster_item.groups ++ GroupLabels, - ask = undefined}, - dict:erase(US1, SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), - SRItems = [#roster_item{jid = jid:make(U1, S1), - name = get_rosteritem_name(U1, S1), - subscription = both, ask = undefined, - groups = GroupLabels} - || {{U1, S1}, GroupLabels} <- dict:to_list(SRUsersRest)], + fun(Item = #roster_item{jid = #jid{luser = User1, lserver = Server1}}, SRUsers1) -> + US1 = {User1, Server1}, + case dict:find(US1, SRUsers1) of + {ok, GroupLabels} -> + {Item#roster_item{ + subscription = both, + groups = Item#roster_item.groups ++ GroupLabels, + ask = undefined + }, + dict:erase(US1, SRUsers1)}; + error -> + {Item, SRUsers1} + end + end, + SRUsers, + Items), + SRItems = [ #roster_item{ + jid = jid:make(U1, S1), + name = get_rosteritem_name(U1, S1), + subscription = both, + ask = undefined, + groups = GroupLabels + } + || {{U1, S1}, GroupLabels} <- dict:to_list(SRUsersRest) ], SRItems ++ NewItems1. + get_rosteritem_name(U, S) -> case gen_mod:is_loaded(S, mod_vcard) of true -> - SubEls = mod_vcard:get_vcard(U, S), - get_rosteritem_name_vcard(SubEls); + SubEls = mod_vcard:get_vcard(U, S), + get_rosteritem_name_vcard(SubEls); false -> <<"">> end. + -spec get_rosteritem_name_vcard([xmlel()]) -> binary(). -get_rosteritem_name_vcard([Vcard|_]) -> +get_rosteritem_name_vcard([Vcard | _]) -> case fxml:get_path_s(Vcard, - [{elem, <<"NICKNAME">>}, cdata]) - of - <<"">> -> - fxml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); - Nickname -> Nickname + [{elem, <<"NICKNAME">>}, cdata]) of + <<"">> -> + fxml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); + Nickname -> Nickname end; get_rosteritem_name_vcard(_) -> <<"">>. + %% This function rewrites the roster entries when moving or renaming %% them in the user contact list. -spec process_item(#roster{}, binary()) -> #roster{}. @@ -221,60 +265,93 @@ process_item(RosterItem, Host) -> NameTo = RosterItem#roster.name, USTo = {UserTo, ServerTo}, {DisplayedGroups, Cache} = get_user_displayed_groups(USFrom), - CommonGroups = lists:filter(fun (Group) -> - is_user_in_group(USTo, Group, Host) - end, - DisplayedGroups), + CommonGroups = lists:filter(fun(Group) -> + is_user_in_group(USTo, Group, Host) + end, + DisplayedGroups), case CommonGroups of - [] -> RosterItem; - %% Roster item cannot be removed: We simply reset the original groups: - _ when RosterItem#roster.subscription == remove -> - GroupLabels = lists:map(fun (Group) -> - get_group_label_cached(Host, Group, Cache) - end, - CommonGroups), - RosterItem#roster{subscription = both, ask = none, - groups = GroupLabels}; - %% Both users have at least a common shared group, - %% So each user can see the other - _ -> - case lists:subtract(RosterItem#roster.groups, - CommonGroups) - of - %% If it doesn't, then remove this user from any - %% existing roster groups. - [] -> - Pres = #presence{from = jid:make(UserTo, ServerTo), - to = jid:make(UserFrom, ServerFrom), - type = unsubscribe}, - mod_roster:out_subscription(Pres), - mod_roster:in_subscription(false, Pres), - RosterItem#roster{subscription = both, ask = none}; - %% If so, it means the user wants to add that contact - %% to his personal roster - PersonalGroups -> - set_new_rosteritems(UserFrom, ServerFrom, UserTo, - ServerTo, ResourceTo, NameTo, - PersonalGroups) - end + [] -> RosterItem; + %% Roster item cannot be removed: We simply reset the original groups: + _ when RosterItem#roster.subscription == remove -> + GroupLabels = lists:map(fun(Group) -> + get_group_label_cached(Host, Group, Cache) + end, + CommonGroups), + RosterItem#roster{ + subscription = both, + ask = none, + groups = GroupLabels + }; + %% Both users have at least a common shared group, + %% So each user can see the other + _ -> + case lists:subtract(RosterItem#roster.groups, + CommonGroups) of + %% If it doesn't, then remove this user from any + %% existing roster groups. + [] -> + Pres = #presence{ + from = jid:make(UserTo, ServerTo), + to = jid:make(UserFrom, ServerFrom), + type = unsubscribe + }, + mod_roster:out_subscription(Pres), + mod_roster:in_subscription(false, Pres), + RosterItem#roster{subscription = both, ask = none}; + %% If so, it means the user wants to add that contact + %% to his personal roster + PersonalGroups -> + set_new_rosteritems(UserFrom, + ServerFrom, + UserTo, + ServerTo, + ResourceTo, + NameTo, + PersonalGroups) + end end. -build_roster_record(User1, Server1, User2, Server2, - Name2, Groups) -> - USR2 = {User2, Server2, <<"">>}, - #roster{usj = {User1, Server1, USR2}, - us = {User1, Server1}, jid = USR2, name = Name2, - subscription = both, ask = none, groups = Groups}. -set_new_rosteritems(UserFrom, ServerFrom, UserTo, - ServerTo, ResourceTo, NameTo, GroupsFrom) -> - RIFrom = build_roster_record(UserFrom, ServerFrom, - UserTo, ServerTo, NameTo, GroupsFrom), +build_roster_record(User1, + Server1, + User2, + Server2, + Name2, + Groups) -> + USR2 = {User2, Server2, <<"">>}, + #roster{ + usj = {User1, Server1, USR2}, + us = {User1, Server1}, + jid = USR2, + name = Name2, + subscription = both, + ask = none, + groups = Groups + }. + + +set_new_rosteritems(UserFrom, + ServerFrom, + UserTo, + ServerTo, + ResourceTo, + NameTo, + GroupsFrom) -> + RIFrom = build_roster_record(UserFrom, + ServerFrom, + UserTo, + ServerTo, + NameTo, + GroupsFrom), set_item(UserFrom, ServerFrom, ResourceTo, RIFrom), JIDTo = jid:make(UserTo, ServerTo), JIDFrom = jid:make(UserFrom, ServerFrom), - RITo = build_roster_record(UserTo, ServerTo, UserFrom, - ServerFrom, UserFrom, []), + RITo = build_roster_record(UserTo, + ServerTo, + UserFrom, + ServerFrom, + UserFrom, + []), set_item(UserTo, ServerTo, <<"">>, RITo), mod_roster:out_subscription( #presence{from = JIDFrom, to = JIDTo, type = subscribe}), @@ -294,18 +371,26 @@ set_new_rosteritems(UserFrom, ServerFrom, UserTo, false, #presence{to = JIDTo, from = JIDFrom, type = subscribed}), RIFrom. + set_item(User, Server, Resource, Item) -> - ResIQ = #iq{from = jid:make(User, Server, Resource), - to = jid:make(Server), - type = set, id = <<"push", (p1_rand:get_string())/binary>>, - sub_els = [#roster_query{ - items = [mod_roster:encode_item(Item)]}]}, + ResIQ = #iq{ + from = jid:make(User, Server, Resource), + to = jid:make(Server), + type = set, + id = <<"push", (p1_rand:get_string())/binary>>, + sub_els = [#roster_query{ + items = [mod_roster:encode_item(Item)] + }] + }, ejabberd_router:route(ResIQ). --spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) - -> {subscription(), ask(), [binary()]}. -get_jid_info({Subscription, Ask, Groups}, User, Server, - JID) -> + +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) -> + {subscription(), ask(), [binary()]}. +get_jid_info({Subscription, Ask, Groups}, + User, + Server, + JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, @@ -313,237 +398,271 @@ get_jid_info({Subscription, Ask, Groups}, User, Server, US1 = {U1, S1}, {DisplayedGroups, Cache} = get_user_displayed_groups(US), SRUsers = lists:foldl( - fun(Group, Acc1) -> - GroupLabel = get_group_label_cached(LServer, Group, Cache), %++ - lists:foldl( - fun(User1, Acc2) -> - dict:append(User1, GroupLabel, Acc2) - end, Acc1, get_group_users_cached(LServer, Group, Cache)) - end, - dict:new(), DisplayedGroups), + fun(Group, Acc1) -> + GroupLabel = get_group_label_cached(LServer, Group, Cache), %++ + lists:foldl( + fun(User1, Acc2) -> + dict:append(User1, GroupLabel, Acc2) + end, + Acc1, + get_group_users_cached(LServer, Group, Cache)) + end, + dict:new(), + DisplayedGroups), case dict:find(US1, SRUsers) of - {ok, GroupLabels} -> - NewGroups = if Groups == [] -> GroupLabels; - true -> Groups - end, - {both, none, NewGroups}; - error -> {Subscription, Ask, Groups} + {ok, GroupLabels} -> + NewGroups = if + Groups == [] -> GroupLabels; + true -> Groups + end, + {both, none, NewGroups}; + error -> {Subscription, Ask, Groups} end. + -spec in_subscription(boolean(), presence()) -> boolean(). in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). + -spec out_subscription(presence()) -> boolean(). out_subscription(#presence{from = From, to = To, type = unsubscribed} = Pres) -> #jid{user = User, server = Server} = From, mod_roster:out_subscription(Pres#presence{type = unsubscribe}), - mod_roster:in_subscription(false, xmpp:set_from_to( - Pres#presence{type = unsubscribe}, - To, From)), + mod_roster:in_subscription(false, + xmpp:set_from_to( + Pres#presence{type = unsubscribe}, + To, + From)), process_subscription(out, User, Server, To, unsubscribed, false); out_subscription(#presence{from = From, to = To, type = Type}) -> #jid{user = User, server = Server} = From, process_subscription(out, User, Server, To, Type, false). -process_subscription(Direction, User, Server, JID, - _Type, Acc) -> + +process_subscription(Direction, + User, + Server, + JID, + _Type, + Acc) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = - jid:tolower(jid:remove_resource(JID)), + jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, {DisplayedGroups, _} = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), + SRUsers = lists:usort(lists:flatmap(fun(Group) -> + get_group_users(LServer, Group) + end, + DisplayedGroups)), case lists:member(US1, SRUsers) of - true -> - case Direction of - in -> {stop, false}; - out -> stop - end; - false -> Acc + true -> + case Direction of + in -> {stop, false}; + out -> stop + end; + false -> Acc end. + list_groups(Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), Mod:list_groups(Host). + groups_with_opts(Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), Mod:groups_with_opts(Host). + create_group(Host, Group) -> create_group(Host, Group, []). + create_group(Host, Group, Opts) -> case jid:nameprep(Group) of - error -> - {error, invalid_group_name}; - LGroup -> - case jid:nameprep(Host) of - error -> - {error, invalid_group_host}; - LHost -> - create_group2(LHost, LGroup, Opts) - end + error -> + {error, invalid_group_name}; + LGroup -> + case jid:nameprep(Host) of + error -> + {error, invalid_group_host}; + LHost -> + create_group2(LHost, LGroup, Opts) + end end. + create_group2(Host, Group, Opts) -> Mod = gen_mod:db_mod(Host, ?MODULE), case proplists:get_value(all_users, Opts, false) orelse - proplists:get_value(online_users, Opts, false) of - true -> - update_wildcard_cache(Host, Group, Opts); - _ -> - ok + proplists:get_value(online_users, Opts, false) of + true -> + update_wildcard_cache(Host, Group, Opts); + _ -> + ok end, case use_cache(Mod, Host) of - true -> - ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), - ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); - _ -> - ok + true -> + ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), + ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); + _ -> + ok end, Mod:create_group(Host, Group, Opts). + delete_group(Host, Group) -> Mod = gen_mod:db_mod(Host, ?MODULE), update_wildcard_cache(Host, Group, []), case use_cache(Mod, Host) of - true -> - ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), - ets_cache:clear(?USER_GROUPS_CACHE, cache_nodes(Mod, Host)), - ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); - _ -> - ok + true -> + ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), + ets_cache:clear(?USER_GROUPS_CACHE, cache_nodes(Mod, Host)), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); + _ -> + ok end, Mod:delete_group(Host, Group). + get_groups_opts_cached(Host1, Group1, Cache) -> {Host, Group} = split_grouphost(Host1, Group1), Key = {Group, Host}, case Cache of - #{Key := Opts} -> - {Opts, Cache}; - _ -> - Opts = get_group_opts_int(Host, Group), - {Opts, Cache#{Key => Opts}} + #{Key := Opts} -> + {Opts, Cache}; + _ -> + Opts = get_group_opts_int(Host, Group), + {Opts, Cache#{Key => Opts}} end. + get_group_opts(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), get_group_opts_int(Host, Group). + get_group_opts_int(Host, Group) -> Mod = gen_mod:db_mod(Host, ?MODULE), Res = case use_cache(Mod, Host) of - true -> - ets_cache:lookup( - ?GROUP_OPTS_CACHE, {Host, Group}, - fun() -> - case Mod:get_group_opts(Host, Group) of - error -> error; - V -> {cache, V} - end - end); - false -> - Mod:get_group_opts(Host, Group) - end, + true -> + ets_cache:lookup( + ?GROUP_OPTS_CACHE, + {Host, Group}, + fun() -> + case Mod:get_group_opts(Host, Group) of + error -> error; + V -> {cache, V} + end + end); + false -> + Mod:get_group_opts(Host, Group) + end, case Res of {ok, Opts} -> Opts; error -> error end. + set_group_opts(Host, Group, Opts) -> Mod = gen_mod:db_mod(Host, ?MODULE), update_wildcard_cache(Host, Group, Opts), case use_cache(Mod, Host) of - true -> - ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), - ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); - _ -> - ok + true -> + ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), + ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); + _ -> + ok end, Mod:set_group_opts(Host, Group, Opts). + get_user_groups(US) -> Host = element(2, US), Mod = gen_mod:db_mod(Host, ?MODULE), UG = case use_cache(Mod, Host) of - true -> - ets_cache:lookup( - ?USER_GROUPS_CACHE, {Host, US}, - fun() -> - {cache, Mod:get_user_groups(US, Host)} - end); - false -> - Mod:get_user_groups(US, Host) - end, + true -> + ets_cache:lookup( + ?USER_GROUPS_CACHE, + {Host, US}, + fun() -> + {cache, Mod:get_user_groups(US, Host)} + end); + false -> + Mod:get_user_groups(US, Host) + end, UG ++ get_groups_with_wildcards(Host, both). + get_group_opt_cached(Host, Group, Opt, Default, Cache) -> case get_groups_opts_cached(Host, Group, Cache) of - {error, _} -> Default; - {Opts, _} -> - proplists:get_value(Opt, Opts, Default) + {error, _} -> Default; + {Opts, _} -> + proplists:get_value(Opt, Opts, Default) end. --spec get_group_opt(Host::binary(), Group::binary(), displayed_groups | label, Default) -> - OptValue::any() | Default. + +-spec get_group_opt(Host :: binary(), Group :: binary(), displayed_groups | label, Default) -> + OptValue :: any() | Default. get_group_opt(Host, Group, Opt, Default) -> case get_group_opts(Host, Group) of - error -> Default; - Opts -> - proplists:get_value(Opt, Opts, Default) + error -> Default; + Opts -> + proplists:get_value(Opt, Opts, Default) end. + get_online_users(Host) -> - lists:usort([{U, S} - || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host)]). + lists:usort([ {U, S} + || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host) ]). + get_group_users_cached(Host1, Group1, Cache) -> {Host, Group} = split_grouphost(Host1, Group1), {Opts, _} = get_groups_opts_cached(Host, Group, Cache), get_group_users(Host, Group, Opts). + get_group_users(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), get_group_users(Host, Group, get_group_opts(Host, Group)). + get_group_users(Host, Group, GroupOpts) -> case proplists:get_value(all_users, GroupOpts, false) of - true -> ejabberd_auth:get_users(Host); - false -> [] - end - ++ - case proplists:get_value(online_users, GroupOpts, false) - of - true -> get_online_users(Host); - false -> [] - end - ++ get_group_explicit_users(Host, Group). + true -> ejabberd_auth:get_users(Host); + false -> [] + end ++ + case proplists:get_value(online_users, GroupOpts, false) of + true -> get_online_users(Host); + false -> [] + end ++ + get_group_explicit_users(Host, Group). + get_group_explicit_users(Host, Group) -> Mod = gen_mod:db_mod(Host, ?MODULE), case use_cache(Mod, Host) of - true -> - ets_cache:lookup( - ?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, - fun() -> - {cache, Mod:get_group_explicit_users(Host, Group)} - end); - false -> - Mod:get_group_explicit_users(Host, Group) + true -> + ets_cache:lookup( + ?GROUP_EXPLICIT_USERS_CACHE, + {Host, Group}, + fun() -> + {cache, Mod:get_group_explicit_users(Host, Group)} + end); + false -> + Mod:get_group_explicit_users(Host, Group) end. + get_group_label_cached(Host, Group, Cache) -> get_group_opt_cached(Host, Group, label, Group, Cache). + -spec update_wildcard_cache(binary(), binary(), list()) -> ok. update_wildcard_cache(Host, Group, NewOpts) -> Mod = gen_mod:db_mod(Host, ?MODULE), @@ -556,70 +675,81 @@ update_wildcard_cache(Host, Group, NewOpts) -> BothUpdated = lists:member(Group, Both) /= (IsOnline orelse IsAll), if - OnlineUpdated -> - NewOnline = case IsOnline of - true -> [Group | Online]; - _ -> Online -- [Group] - end, - ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, online}, - {ok, NewOnline}, fun() -> ok end, cache_nodes(Mod, Host)); - true -> ok + OnlineUpdated -> + NewOnline = case IsOnline of + true -> [Group | Online]; + _ -> Online -- [Group] + end, + ets_cache:update(?SPECIAL_GROUPS_CACHE, + {Host, online}, + {ok, NewOnline}, + fun() -> ok end, + cache_nodes(Mod, Host)); + true -> ok end, if - BothUpdated -> - NewBoth = case IsOnline orelse IsAll of - true -> [Group | Both]; - _ -> Both -- [Group] - end, - ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, both}, - {ok, NewBoth}, fun() -> ok end, cache_nodes(Mod, Host)); - true -> ok + BothUpdated -> + NewBoth = case IsOnline orelse IsAll of + true -> [Group | Both]; + _ -> Both -- [Group] + end, + ets_cache:update(?SPECIAL_GROUPS_CACHE, + {Host, both}, + {ok, NewBoth}, + fun() -> ok end, + cache_nodes(Mod, Host)); + true -> ok end, ok. + -spec get_groups_with_wildcards(binary(), online | both) -> list(binary()). get_groups_with_wildcards(Host, Type) -> Res = - ets_cache:lookup( - ?SPECIAL_GROUPS_CACHE, {Host, Type}, - fun() -> - Res = lists:filtermap( - fun({Group, Opts}) -> - case proplists:get_value(online_users, Opts, false) orelse - (Type == both andalso proplists:get_value(all_users, Opts, false)) of - true -> {true, Group}; - false -> false - end - end, - groups_with_opts(Host)), - {cache, {ok, Res}} - end), + ets_cache:lookup( + ?SPECIAL_GROUPS_CACHE, + {Host, Type}, + fun() -> + Res = lists:filtermap( + fun({Group, Opts}) -> + case proplists:get_value(online_users, Opts, false) orelse + (Type == both andalso proplists:get_value(all_users, Opts, false)) of + true -> {true, Group}; + false -> false + end + end, + groups_with_opts(Host)), + {cache, {ok, Res}} + end), case Res of - {ok, List} -> List; - _ -> [] + {ok, List} -> List; + _ -> [] end. + %% Given two lists of groupnames and their options, %% return the list of displayed groups to the second list displayed_groups(GroupsOpts, SelectedGroupsOpts) -> DisplayedGroups = lists:usort(lists:flatmap( - fun - ({_Group, Opts}) -> - [G || G <- proplists:get_value(displayed_groups, Opts, []), - not lists:member(disabled, Opts)] - end, SelectedGroupsOpts)), - [G || G <- DisplayedGroups, not lists:member(disabled, proplists:get_value(G, GroupsOpts, []))]. + fun({_Group, Opts}) -> + [ G || G <- proplists:get_value(displayed_groups, Opts, []), + not lists:member(disabled, Opts) ] + end, + SelectedGroupsOpts)), + [ G || G <- DisplayedGroups, not lists:member(disabled, proplists:get_value(G, GroupsOpts, [])) ]. + %% Given a list of group names with options, %% for those that have @all@ in memberlist, %% get the list of groups displayed get_special_displayed_groups(GroupsOpts) -> - Groups = lists:filter(fun ({_Group, Opts}) -> - proplists:get_value(all_users, Opts, false) - end, - GroupsOpts), + Groups = lists:filter(fun({_Group, Opts}) -> + proplists:get_value(all_users, Opts, false) + end, + GroupsOpts), displayed_groups(GroupsOpts, Groups). + %% Given a username and server, and a list of group names with options, %% for the list of groups of that server that user is member %% get the list of groups displayed @@ -628,267 +758,350 @@ get_user_displayed_groups(LUser, LServer, GroupsOpts) -> Groups = Mod:get_user_displayed_groups(LUser, LServer, GroupsOpts), displayed_groups(GroupsOpts, Groups). + %% @doc Get the list of groups that are displayed to this user get_user_displayed_groups(US) -> Host = element(2, US), {Groups, Cache} = + lists:foldl( + fun(Group, {Groups, Cache}) -> + case get_groups_opts_cached(Host, Group, Cache) of + {error, Cache2} -> + {Groups, Cache2}; + {Opts, Cache3} -> + case lists:member(disabled, Opts) of + false -> + {proplists:get_value(displayed_groups, Opts, []) ++ Groups, Cache3}; + _ -> + {Groups, Cache3} + end + end + end, + {[], #{}}, + get_user_groups(US)), lists:foldl( - fun(Group, {Groups, Cache}) -> - case get_groups_opts_cached(Host, Group, Cache) of - {error, Cache2} -> - {Groups, Cache2}; - {Opts, Cache3} -> - case lists:member(disabled, Opts) of - false -> - {proplists:get_value(displayed_groups, Opts, []) ++ Groups, Cache3}; - _ -> - {Groups, Cache3} - end - end - end, {[], #{}}, get_user_groups(US)), - lists:foldl( - fun(Group, {Groups0, Cache0}) -> - case get_groups_opts_cached(Host, Group, Cache0) of - {error, Cache1} -> - {Groups0, Cache1}; - {Opts, Cache2} -> - case lists:member(disabled, Opts) of - false -> - {[Group|Groups0], Cache2}; - _ -> - {Groups0, Cache2} - end - end - end, {[], Cache}, lists:usort(Groups)). + fun(Group, {Groups0, Cache0}) -> + case get_groups_opts_cached(Host, Group, Cache0) of + {error, Cache1} -> + {Groups0, Cache1}; + {Opts, Cache2} -> + case lists:member(disabled, Opts) of + false -> + {[Group | Groups0], Cache2}; + _ -> + {Groups0, Cache2} + end + end + end, + {[], Cache}, + lists:usort(Groups)). + is_user_in_group(US, Group, Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), case Mod:is_user_in_group(US, Group, Host) of - false -> - lists:member(US, get_group_users(Host, Group)); - true -> - true + false -> + lists:member(US, get_group_users(Host, Group)); + true -> + true end. --spec add_user_to_group(Host::binary(), {User::binary(), Server::binary()}, - Group::binary()) -> {atomic, ok} | error. + +-spec add_user_to_group(Host :: binary(), + {User :: binary(), Server :: binary()}, + Group :: binary()) -> {atomic, ok} | error. add_user_to_group(Host, US, Group) -> {_LUser, LServer} = US, case lists:member(LServer, ejabberd_config:get_option(hosts)) of - true -> add_user_to_group2(Host, US, Group); - false -> - ?INFO_MSG("Attempted adding to shared roster user of inexistent vhost ~ts", [LServer]), - error + true -> add_user_to_group2(Host, US, Group); + false -> + ?INFO_MSG("Attempted adding to shared roster user of inexistent vhost ~ts", [LServer]), + error end. + + add_user_to_group2(Host, US, Group) -> {LUser, LServer} = US, case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of - match -> - GroupOpts = get_group_opts(Host, Group), - MoreGroupOpts = case LUser of - <<"@all@">> -> [{all_users, true}]; - <<"@online@">> -> [{online_users, true}]; - _ -> [] - end, - set_group_opts(Host, Group, - GroupOpts ++ MoreGroupOpts); - nomatch -> - DisplayedToGroups = displayed_to_groups(Group, Host), - DisplayedGroups = get_displayed_groups(Group, LServer), - push_user_to_displayed(LUser, LServer, Group, Host, both, DisplayedToGroups), - push_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), - Mod = gen_mod:db_mod(Host, ?MODULE), - Mod:add_user_to_group(Host, US, Group), - case use_cache(Mod, Host) of - true -> - ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), - ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); - false -> - ok - end + match -> + GroupOpts = get_group_opts(Host, Group), + MoreGroupOpts = case LUser of + <<"@all@">> -> [{all_users, true}]; + <<"@online@">> -> [{online_users, true}]; + _ -> [] + end, + set_group_opts(Host, + Group, + GroupOpts ++ MoreGroupOpts); + nomatch -> + DisplayedToGroups = displayed_to_groups(Group, Host), + DisplayedGroups = get_displayed_groups(Group, LServer), + push_user_to_displayed(LUser, LServer, Group, Host, both, DisplayedToGroups), + push_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:add_user_to_group(Host, US, Group), + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); + false -> + ok + end end. + get_displayed_groups(Group, LServer) -> get_group_opt(LServer, Group, displayed_groups, []). + push_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) -> - [push_members_to_user(LUser, LServer, DGroup, Host, - Subscription) - || DGroup <- DisplayedGroups]. + [ push_members_to_user(LUser, + LServer, + DGroup, + Host, + Subscription) + || DGroup <- DisplayedGroups ]. + remove_user_from_group(Host, US, Group) -> {LUser, LServer} = US, case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of - match -> - GroupOpts = get_group_opts(Host, Group), - NewGroupOpts = case LUser of - <<"@all@">> -> - lists:filter(fun (X) -> X /= {all_users, true} - end, - GroupOpts); - <<"@online@">> -> - lists:filter(fun (X) -> X /= {online_users, true} - end, - GroupOpts) - end, - set_group_opts(Host, Group, NewGroupOpts); - nomatch -> - Mod = gen_mod:db_mod(Host, ?MODULE), - Result = Mod:remove_user_from_group(Host, US, Group), - case use_cache(Mod, Host) of - true -> - ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), - ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); - false -> - ok - end, - DisplayedToGroups = displayed_to_groups(Group, Host), - DisplayedGroups = get_displayed_groups(Group, LServer), - push_user_to_displayed(LUser, LServer, Group, Host, remove, DisplayedToGroups), - push_displayed_to_user(LUser, LServer, Host, remove, DisplayedGroups), - Result + match -> + GroupOpts = get_group_opts(Host, Group), + NewGroupOpts = case LUser of + <<"@all@">> -> + lists:filter(fun(X) -> X /= {all_users, true} + end, + GroupOpts); + <<"@online@">> -> + lists:filter(fun(X) -> X /= {online_users, true} + end, + GroupOpts) + end, + set_group_opts(Host, Group, NewGroupOpts); + nomatch -> + Mod = gen_mod:db_mod(Host, ?MODULE), + Result = Mod:remove_user_from_group(Host, US, Group), + case use_cache(Mod, Host) of + true -> + ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), + ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); + false -> + ok + end, + DisplayedToGroups = displayed_to_groups(Group, Host), + DisplayedGroups = get_displayed_groups(Group, LServer), + push_user_to_displayed(LUser, LServer, Group, Host, remove, DisplayedToGroups), + push_displayed_to_user(LUser, LServer, Host, remove, DisplayedGroups), + Result end. -push_members_to_user(LUser, LServer, Group, Host, - Subscription) -> + +push_members_to_user(LUser, + LServer, + Group, + Host, + Subscription) -> GroupOpts = get_group_opts(LServer, Group), - GroupLabel = proplists:get_value(label, GroupOpts, Group), %++ + GroupLabel = proplists:get_value(label, GroupOpts, Group), %++ Members = get_group_users(Host, Group), - lists:foreach(fun ({U, S}) -> - N = get_rosteritem_name(U, S), - push_roster_item(LUser, LServer, U, S, N, GroupLabel, - Subscription) - end, - Members). + lists:foreach(fun({U, S}) -> + N = get_rosteritem_name(U, S), + push_roster_item(LUser, + LServer, + U, + S, + N, + GroupLabel, + Subscription) + end, + Members). + -spec register_user(binary(), binary()) -> ok. register_user(User, Server) -> Groups = get_user_groups({User, Server}), - [push_user_to_displayed(User, Server, Group, Server, - both, displayed_to_groups(Group, Server)) - || Group <- Groups], + [ push_user_to_displayed(User, + Server, + Group, + Server, + both, + displayed_to_groups(Group, Server)) + || Group <- Groups ], ok. + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> push_user_to_members(User, Server, remove). + push_user_to_members(User, Server, Subscription) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), RosterName = get_rosteritem_name(LUser, LServer), GroupsOpts = groups_with_opts(LServer), SpecialGroups = - get_special_displayed_groups(GroupsOpts), - UserGroups = get_user_displayed_groups(LUser, LServer, - GroupsOpts), - lists:foreach(fun (Group) -> - remove_user_from_group(LServer, {LUser, LServer}, - Group), - GroupOpts = proplists:get_value(Group, GroupsOpts, - []), - GroupLabel = proplists:get_value(label, GroupOpts, - Group), - lists:foreach(fun ({U, S}) -> - push_roster_item(U, S, LUser, - LServer, - RosterName, - GroupLabel, - Subscription) - end, - get_group_users(LServer, Group, - GroupOpts)) - end, - lists:usort(SpecialGroups ++ UserGroups)). + get_special_displayed_groups(GroupsOpts), + UserGroups = get_user_displayed_groups(LUser, + LServer, + GroupsOpts), + lists:foreach(fun(Group) -> + remove_user_from_group(LServer, + {LUser, LServer}, + Group), + GroupOpts = proplists:get_value(Group, + GroupsOpts, + []), + GroupLabel = proplists:get_value(label, + GroupOpts, + Group), + lists:foreach(fun({U, S}) -> + push_roster_item(U, + S, + LUser, + LServer, + RosterName, + GroupLabel, + Subscription) + end, + get_group_users(LServer, + Group, + GroupOpts)) + end, + lists:usort(SpecialGroups ++ UserGroups)). + push_user_to_displayed(LUser, LServer, Group, Host, Subscription, DisplayedToGroupsOpts) -> - GroupLabel = get_group_opt(Host, Group, label, Group), %++ - [push_user_to_group(LUser, LServer, GroupD, Host, - GroupLabel, Subscription) - || GroupD <- DisplayedToGroupsOpts]. + GroupLabel = get_group_opt(Host, Group, label, Group), %++ + [ push_user_to_group(LUser, + LServer, + GroupD, + Host, + GroupLabel, + Subscription) + || GroupD <- DisplayedToGroupsOpts ]. -push_user_to_group(LUser, LServer, Group, Host, - GroupLabel, Subscription) -> + +push_user_to_group(LUser, + LServer, + Group, + Host, + GroupLabel, + Subscription) -> RosterName = get_rosteritem_name(LUser, LServer), - lists:foreach(fun ({U, S}) - when (U == LUser) and (S == LServer) -> - ok; - ({U, S}) -> - case lists:member(S, ejabberd_option:hosts()) of - true -> - push_roster_item(U, S, LUser, LServer, RosterName, GroupLabel, - Subscription); - _ -> - ok - end - end, - get_group_users(Host, Group)). + lists:foreach(fun({U, S}) + when (U == LUser) and (S == LServer) -> + ok; + ({U, S}) -> + case lists:member(S, ejabberd_option:hosts()) of + true -> + push_roster_item(U, + S, + LUser, + LServer, + RosterName, + GroupLabel, + Subscription); + _ -> + ok + end + end, + get_group_users(Host, Group)). + %% Get list of groups to which this group is displayed displayed_to_groups(GroupName, LServer) -> GroupsOpts = groups_with_opts(LServer), - Gs = lists:filter(fun ({_Group, Opts}) -> - lists:member(GroupName, - proplists:get_value(displayed_groups, - Opts, [])) - end, - GroupsOpts), - [Name || {Name, _} <- Gs]. + Gs = lists:filter(fun({_Group, Opts}) -> + lists:member(GroupName, + proplists:get_value(displayed_groups, + Opts, + [])) + end, + GroupsOpts), + [ Name || {Name, _} <- Gs ]. + push_item(User, Server, Item) -> mod_roster:push_item(jid:make(User, Server), - Item#roster_item{subscription = none}, - Item). + Item#roster_item{subscription = none}, + Item). -push_roster_item(User, Server, ContactU, ContactS, ContactN, - GroupLabel, Subscription) -> - Item = #roster_item{jid = jid:make(ContactU, ContactS), - name = ContactN, subscription = Subscription, ask = undefined, - groups = [GroupLabel]}, + +push_roster_item(User, + Server, + ContactU, + ContactS, + ContactN, + GroupLabel, + Subscription) -> + Item = #roster_item{ + jid = jid:make(ContactU, ContactS), + name = ContactN, + subscription = Subscription, + ask = undefined, + groups = [GroupLabel] + }, push_item(User, Server, Item). --spec c2s_self_presence({presence(), ejabberd_c2s:state()}) - -> {presence(), ejabberd_c2s:state()}. + +-spec c2s_self_presence({presence(), ejabberd_c2s:state()}) -> + {presence(), ejabberd_c2s:state()}. c2s_self_presence(Acc) -> Acc. + -spec unset_presence(binary(), binary(), binary(), binary()) -> ok. 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), + LServer), ?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p " - "(~p resources)", - [LUser, LServer, LResource, Status, length(Resources)]), + "(~p resources)", + [LUser, LServer, LResource, Status, length(Resources)]), case length(Resources) of - 0 -> - lists:foreach( - fun(OG) -> - DisplayedToGroups = displayed_to_groups(OG, LServer), - push_user_to_displayed(LUser, LServer, OG, - LServer, remove, DisplayedToGroups), - push_displayed_to_user(LUser, LServer, - LServer, remove, DisplayedToGroups) - end, get_groups_with_wildcards(LServer, online)); - _ -> ok + 0 -> + lists:foreach( + fun(OG) -> + DisplayedToGroups = displayed_to_groups(OG, LServer), + push_user_to_displayed(LUser, + LServer, + OG, + LServer, + remove, + DisplayedToGroups), + push_displayed_to_user(LUser, + LServer, + LServer, + remove, + DisplayedToGroups) + end, + get_groups_with_wildcards(LServer, online)); + _ -> ok end. + %%--------------------- %% Web Admin: Page Frontend %%--------------------- %% @format-begin + webadmin_menu(Acc, _Host, Lang) -> [{<<"shared-roster">>, translate:translate(Lang, ?T("Shared Roster Groups"))} | Acc]. + webadmin_page(_, Host, - #request{us = _US, - path = [<<"shared-roster">> | RPath], - lang = Lang} = + #request{ + us = _US, + path = [<<"shared-roster">> | RPath], + lang = Lang + } = R) -> PageTitle = translate:translate(Lang, ?T("Shared Roster Groups")), Head = ?H1GL(PageTitle, <<"modules/#mod_shared_roster">>, <<"mod_shared_roster">>), @@ -903,6 +1116,7 @@ webadmin_page(_, webadmin_page(Acc, _, _) -> Acc. + check_group_exists(Host, [<<"group">>, Id | _]) -> case get_group_opts(Host, Id) of error -> @@ -913,10 +1127,12 @@ check_group_exists(Host, [<<"group">>, Id | _]) -> check_group_exists(_, _) -> true. + %%--------------------- %% Web Admin: Page Backend %%--------------------- + webadmin_page_backend(Host, [<<"group">>, Id, <<"info">> | RPath], R, _Lang, Level) -> Breadcrumb = make_breadcrumb({group_section, @@ -1056,7 +1272,7 @@ webadmin_page_backend(Host, [<<"group">>, Id | _RPath], _R, _Lang, Level) -> {<<"members/">>, <<"Members">>}, {<<"displayed/">>, <<"Displayed Groups">>}, {<<"delete/">>, <<"Delete">>}], - Get = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems])], + Get = [?XE(<<"ul">>, [ ?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems ])], Breadcrumb ++ Get; webadmin_page_backend(Host, RPath, R, _Lang, Level) -> Breadcrumb = make_breadcrumb({groups, <<"Groups of ", Host/binary>>}), @@ -1070,10 +1286,12 @@ webadmin_page_backend(Host, RPath, R, _Lang, Level) -> ?XE(<<"blockquote">>, [RV2])], Breadcrumb ++ Get ++ Set. + %%--------------------- %% Web Admin: Table Generation %%--------------------- + make_webadmin_srg_table(Host, R, Level, RPath) -> Groups = case make_command_raw_value(srg_list, R, [{<<"host">>, Host}]) of @@ -1092,222 +1310,230 @@ make_webadmin_srg_table(Host, R, Level, RPath) -> {<<"displayed">>, right}, <<"">>], Rows = - [{make_command(echo3, - R, - [{<<"first">>, Id}, {<<"second">>, Host}, {<<"sentence">>, Id}], - [{only, value}, {result_links, [{sentence, shared_roster, Level, <<"">>}]}]), - make_command(echo3, - R, - [{<<"first">>, Id}, - {<<"second">>, Host}, - {<<"sentence">>, - iolist_to_binary(proplists:get_value(<<"label">>, - make_command_raw_value(srg_get_info, - R, - [{<<"group">>, - Id}, - {<<"host">>, - Host}]), - ""))}], - [{only, value}, - {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), - make_command(echo3, - R, - [{<<"first">>, Id}, - {<<"second">>, Host}, - {<<"sentence">>, - iolist_to_binary(proplists:get_value(<<"description">>, - make_command_raw_value(srg_get_info, - R, - [{<<"group">>, - Id}, - {<<"host">>, - Host}]), - ""))}], - [{only, value}, - {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), - make_command(echo3, - R, - [{<<"first">>, Id}, - {<<"second">>, Host}, - {<<"sentence">>, - iolist_to_binary(proplists:get_value(<<"all_users">>, - make_command_raw_value(srg_get_info, - R, - [{<<"group">>, - Id}, - {<<"host">>, - Host}]), - ""))}], - [{only, value}, - {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), - make_command(echo3, - R, - [{<<"first">>, Id}, - {<<"second">>, Host}, - {<<"sentence">>, - iolist_to_binary(proplists:get_value(<<"online_users">>, - make_command_raw_value(srg_get_info, - R, - [{<<"group">>, - Id}, - {<<"host">>, - Host}]), - ""))}], - [{only, value}, - {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), - make_command(echo3, - R, - [{<<"first">>, Id}, - {<<"second">>, Host}, - {<<"sentence">>, - integer_to_binary(length(make_command_raw_value(srg_get_members, - R, - [{<<"group">>, Id}, - {<<"host">>, Host}])))}], - [{only, value}, - {result_links, [{sentence, shared_roster, Level, <<"members">>}]}]), - make_command(echo3, - R, - [{<<"first">>, Id}, - {<<"second">>, Host}, - {<<"sentence">>, - integer_to_binary(length(make_command_raw_value(srg_get_displayed, - R, - [{<<"group">>, Id}, - {<<"host">>, Host}])))}], - [{only, value}, - {result_links, [{sentence, shared_roster, Level, <<"displayed">>}]}]), - make_command(srg_delete, - R, - [{<<"group">>, Id}, {<<"host">>, Host}], - [{only, button}, {style, danger}, {input_name_append, [Id, Host]}])} - || Id <- Groups], + [ {make_command(echo3, + R, + [{<<"first">>, Id}, {<<"second">>, Host}, {<<"sentence">>, Id}], + [{only, value}, {result_links, [{sentence, shared_roster, Level, <<"">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"label">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"description">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"all_users">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + iolist_to_binary(proplists:get_value(<<"online_users">>, + make_command_raw_value(srg_get_info, + R, + [{<<"group">>, + Id}, + {<<"host">>, + Host}]), + ""))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + integer_to_binary(length(make_command_raw_value(srg_get_members, + R, + [{<<"group">>, Id}, + {<<"host">>, Host}])))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"members">>}]}]), + make_command(echo3, + R, + [{<<"first">>, Id}, + {<<"second">>, Host}, + {<<"sentence">>, + integer_to_binary(length(make_command_raw_value(srg_get_displayed, + R, + [{<<"group">>, Id}, + {<<"host">>, Host}])))}], + [{only, value}, + {result_links, [{sentence, shared_roster, Level, <<"displayed">>}]}]), + make_command(srg_delete, + R, + [{<<"group">>, Id}, {<<"host">>, Host}], + [{only, button}, {style, danger}, {input_name_append, [Id, Host]}])} + || Id <- Groups ], make_table(20, RPath, Columns, Rows). + make_webadmin_members_table(Host, Id, R) -> Members = - case make_command_raw_value(srg_get_members, R, [{<<"host">>, Host}, {<<"group">>, Id}]) - of + case make_command_raw_value(srg_get_members, R, [{<<"host">>, Host}, {<<"group">>, Id}]) of Ms when is_list(Ms) -> Ms; _ -> [] end, make_table([<<"member">>, <<"">>], - [{make_command(echo, - R, - [{<<"sentence">>, Jid}], - [{only, value}, {result_links, [{sentence, user, 6, <<"">>}]}]), - make_command(srg_user_del, - R, - [{<<"user">>, - element(1, - jid:split( - jid:decode(Jid)))}, - {<<"host">>, - element(2, - jid:split( - jid:decode(Jid)))}, - {<<"group">>, Id}, - {<<"grouphost">>, Host}], - [{only, button}, - {style, danger}, - {input_name_append, - [element(1, + [ {make_command(echo, + R, + [{<<"sentence">>, Jid}], + [{only, value}, {result_links, [{sentence, user, 6, <<"">>}]}]), + make_command(srg_user_del, + R, + [{<<"user">>, + element(1, jid:split( - jid:decode(Jid))), + jid:decode(Jid)))}, + {<<"host">>, element(2, jid:split( - jid:decode(Jid))), - Id, - Host]}])} - || Jid <- Members]). + jid:decode(Jid)))}, + {<<"group">>, Id}, + {<<"grouphost">>, Host}], + [{only, button}, + {style, danger}, + {input_name_append, + [element(1, + jid:split( + jid:decode(Jid))), + element(2, + jid:split( + jid:decode(Jid))), + Id, + Host]}])} + || Jid <- Members ]). + make_webadmin_displayed_table(Host, Id, R) -> Displayed = - case make_command_raw_value(srg_get_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}]) - of + case make_command_raw_value(srg_get_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}]) of Ms when is_list(Ms) -> Ms; _ -> [] end, make_table([<<"group">>, <<"">>], - [{make_command(echo3, - R, - [{<<"first">>, ThisId}, - {<<"second">>, Host}, - {<<"sentence">>, ThisId}], - [{only, value}, - {result_links, [{sentence, shared_roster, 6, <<"">>}]}]), - make_command(srg_del_displayed, - R, - [{<<"group">>, Id}, {<<"host">>, Host}, {<<"del">>, ThisId}], - [{only, button}, - {style, danger}, - {input_name_append, [Id, Host, ThisId]}])} - || ThisId <- Displayed]). + [ {make_command(echo3, + R, + [{<<"first">>, ThisId}, + {<<"second">>, Host}, + {<<"sentence">>, ThisId}], + [{only, value}, + {result_links, [{sentence, shared_roster, 6, <<"">>}]}]), + make_command(srg_del_displayed, + R, + [{<<"group">>, Id}, {<<"host">>, Host}, {<<"del">>, ThisId}], + [{only, button}, + {style, danger}, + {input_name_append, [Id, Host, ThisId]}])} + || ThisId <- Displayed ]). + make_breadcrumb({groups, Service}) -> make_breadcrumb([Service]); make_breadcrumb({group, Level, Service, Name}) -> make_breadcrumb([{Level, Service}, separator, Name]); make_breadcrumb({group_section, Level, Service, Name, Section, RPath}) -> - make_breadcrumb([{Level, Service}, separator, {Level - 2, Name}, separator, Section - | RPath]); + make_breadcrumb([{Level, Service}, separator, {Level - 2, Name}, separator, Section | RPath]); make_breadcrumb(Elements) -> - lists:map(fun ({xmlel, _, _, _} = Xmlel) -> + lists:map(fun({xmlel, _, _, _} = Xmlel) -> Xmlel; - (<<"sort">>) -> + (<<"sort">>) -> ?C(<<" +">>); - (<<"page">>) -> + (<<"page">>) -> ?C(<<" #">>); - (separator) -> + (separator) -> ?C(<<" > ">>); - (Bin) when is_binary(Bin) -> + (Bin) when is_binary(Bin) -> ?C(Bin); - ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> + ({Level, Bin}) when is_integer(Level) and is_binary(Bin) -> ?AC(binary:copy(<<"../">>, Level), Bin) end, Elements). %% @format-end + split_grouphost(Host, Group) -> case str:tokens(Group, <<"@">>) of - [GroupName, HostName] -> {HostName, GroupName}; - [_] -> {Host, Group} + [GroupName, HostName] -> {HostName, GroupName}; + [_] -> {Host, Group} end. + opts_to_binary(Opts) -> lists:map( fun({label, Label}) -> {label, iolist_to_binary(Label)}; - ({name, Label}) -> % For SQL backwards compat with ejabberd 20.03 and older + ({name, Label}) -> % For SQL backwards compat with ejabberd 20.03 and older {label, iolist_to_binary(Label)}; ({description, Desc}) -> {description, iolist_to_binary(Desc)}; ({displayed_groups, Gs}) -> - {displayed_groups, [iolist_to_binary(G) || G <- Gs]}; + {displayed_groups, [ iolist_to_binary(G) || G <- Gs ]}; (Opt) -> Opt - end, Opts). + end, + Opts). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + import_info() -> [{<<"sr_group">>, 3}, {<<"sr_user">>, 3}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). + import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). + mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> @@ -1319,6 +1545,7 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, @@ -1326,96 +1553,114 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => - [?T("This module enables you to create shared roster groups: " - "groups of accounts that can see members from (other) groups " - "in their rosters."), "", - ?T("The big advantages of this feature are that end users do not " - "need to manually add all users to their rosters, and that they " - "cannot permanently delete users from the shared roster groups. " - "A shared roster group can have members from any XMPP server, " - "but the presence will only be available from and to members of " - "the same virtual host where the group is created. It still " - "allows the users to have / add their own contacts, as it does " - "not replace the standard roster. Instead, the shared roster " - "contacts are merged to the relevant users at retrieval time. " - "The standard user rosters thus stay unmodified."), "", - ?T("Shared roster groups can be edited via the Web Admin, " - "and some API commands called 'srg_', for example _`srg_add`_ API. " - "Each group has a unique name and those parameters:"), "", - ?T("- Label: Used in the rosters where this group is displayed."),"", - ?T("- Description: of the group, which has no effect."), "", - ?T("- Members: A list of JIDs of group members, entered one per " - "line in the Web Admin. The special member directive '@all@' " - "represents all the registered users in the virtual host; " - "which is only recommended for a small server with just a few " - "hundred users. The special member directive '@online@' " - "represents the online users in the virtual host. With those " - "two directives, the actual list of members in those shared " - "rosters is generated dynamically at retrieval time."), "", - ?T("- Displayed: A list of groups that will be in the " - "rosters of this group's members. A group of other vhost can " - "be identified with 'groupid@vhost'."), "", - ?T("This module depends on _`mod_roster`_. " - "If not enabled, roster queries will return 503 errors.")], + #{ + desc => + [?T("This module enables you to create shared roster groups: " + "groups of accounts that can see members from (other) groups " + "in their rosters."), + "", + ?T("The big advantages of this feature are that end users do not " + "need to manually add all users to their rosters, and that they " + "cannot permanently delete users from the shared roster groups. " + "A shared roster group can have members from any XMPP server, " + "but the presence will only be available from and to members of " + "the same virtual host where the group is created. It still " + "allows the users to have / add their own contacts, as it does " + "not replace the standard roster. Instead, the shared roster " + "contacts are merged to the relevant users at retrieval time. " + "The standard user rosters thus stay unmodified."), + "", + ?T("Shared roster groups can be edited via the Web Admin, " + "and some API commands called 'srg_', for example _`srg_add`_ API. " + "Each group has a unique name and those parameters:"), + "", + ?T("- Label: Used in the rosters where this group is displayed."), + "", + ?T("- Description: of the group, which has no effect."), + "", + ?T("- Members: A list of JIDs of group members, entered one per " + "line in the Web Admin. The special member directive '@all@' " + "represents all the registered users in the virtual host; " + "which is only recommended for a small server with just a few " + "hundred users. The special member directive '@online@' " + "represents the online users in the virtual host. With those " + "two directives, the actual list of members in those shared " + "rosters is generated dynamically at retrieval time."), + "", + ?T("- Displayed: A list of groups that will be in the " + "rosters of this group's members. A group of other vhost can " + "be identified with 'groupid@vhost'."), + "", + ?T("This module depends on _`mod_roster`_. " + "If not enabled, roster queries will return 503 errors.")], opts => [{db_type, - #{value => "mnesia | sql", + #{ + value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, " - "but applied to this module only.")}}, + "but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}], example => - [{?T("Take the case of a computer club that wants all its members " - "seeing each other in their rosters. To achieve this, they " - "need to create a shared roster group similar to this one:"), - ["Name: club_members", - "Label: Club Members", - "Description: Members from the computer club", - "Members: member1@example.org, member2@example.org, member3@example.org", - "Displayed Groups: club_members"]}, - {?T("In another case we have a company which has three divisions: " - "Management, Marketing and Sales. All group members should see " - "all other members in their rosters. Additionally, all managers " - "should have all marketing and sales people in their roster. " - "Simultaneously, all marketeers and the whole sales team " - "should see all managers. This scenario can be achieved by " - "creating shared roster groups as shown in the following lists:"), - ["First list:", - "Name: management", - "Label: Management", - "Description: Management", - "Members: manager1@example.org, manager2@example.org", - "Displayed: management, marketing, sales", - "", - "Second list:", - "Name: marketing", - "Label: Marketing", - "Description: Marketing", - "Members: marketeer1@example.org, marketeer2@example.org, marketeer3@example.org", - "Displayed: management, marketing", - "", - "Third list:", - "Name: sales", - "Label: Sales", - "Description: Sales", - "Members: salesman1@example.org, salesman2@example.org, salesman3@example.org", - "Displayed: management, sales" - ]} - ]}. + [{?T("Take the case of a computer club that wants all its members " + "seeing each other in their rosters. To achieve this, they " + "need to create a shared roster group similar to this one:"), + ["Name: club_members", + "Label: Club Members", + "Description: Members from the computer club", + "Members: member1@example.org, member2@example.org, member3@example.org", + "Displayed Groups: club_members"]}, + {?T("In another case we have a company which has three divisions: " + "Management, Marketing and Sales. All group members should see " + "all other members in their rosters. Additionally, all managers " + "should have all marketing and sales people in their roster. " + "Simultaneously, all marketeers and the whole sales team " + "should see all managers. This scenario can be achieved by " + "creating shared roster groups as shown in the following lists:"), + ["First list:", + "Name: management", + "Label: Management", + "Description: Management", + "Members: manager1@example.org, manager2@example.org", + "Displayed: management, marketing, sales", + "", + "Second list:", + "Name: marketing", + "Label: Marketing", + "Description: Marketing", + "Members: marketeer1@example.org, marketeer2@example.org, marketeer3@example.org", + "Displayed: management, marketing", + "", + "Third list:", + "Name: sales", + "Label: Sales", + "Description: Sales", + "Members: salesman1@example.org, salesman2@example.org, salesman3@example.org", + "Displayed: management, sales"]}] + }. diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index 509334be1..53e631e2d 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -34,77 +34,96 @@ -export([start/2, stop/1, reload/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -export([get_user_roster/2, - get_jid_info/4, process_item/2, in_subscription/2, - out_subscription/1, mod_opt_type/1, mod_options/1, - depends/2, mod_doc/0]). + get_jid_info/4, + process_item/2, + in_subscription/2, + out_subscription/1, + mod_opt_type/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_roster.hrl"). -include("eldap.hrl"). -include("translate.hrl"). --define(USER_CACHE, shared_roster_ldap_user_cache). --define(GROUP_CACHE, shared_roster_ldap_group_cache). --define(DISPLAYED_CACHE, shared_roster_ldap_displayed_cache). --define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds +-define(USER_CACHE, shared_roster_ldap_user_cache). +-define(GROUP_CACHE, shared_roster_ldap_group_cache). +-define(DISPLAYED_CACHE, shared_roster_ldap_displayed_cache). +-define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds --record(state, - {host = <<"">> :: binary(), - eldap_id = <<"">> :: binary(), - servers = [] :: [binary()], - backups = [] :: [binary()], - port = ?LDAP_PORT :: inet:port_number(), - tls_options = [] :: list(), - dn = <<"">> :: binary(), - base = <<"">> :: binary(), - password = <<"">> :: binary(), - uid = <<"">> :: binary(), - deref_aliases = never :: never | searching | - finding | always, - group_attr = <<"">> :: binary(), - group_desc = <<"">> :: binary(), - user_desc = <<"">> :: binary(), - user_uid = <<"">> :: binary(), - uid_format = <<"">> :: binary(), - uid_format_re :: undefined | misc:re_mp(), - filter = <<"">> :: binary(), - ufilter = <<"">> :: binary(), - rfilter = <<"">> :: binary(), - gfilter = <<"">> :: binary(), - user_jid_attr = <<"">> :: binary(), - auth_check = true :: boolean()}). +-record(state, { + host = <<"">> :: binary(), + eldap_id = <<"">> :: binary(), + servers = [] :: [binary()], + backups = [] :: [binary()], + port = ?LDAP_PORT :: inet:port_number(), + tls_options = [] :: list(), + dn = <<"">> :: binary(), + base = <<"">> :: binary(), + password = <<"">> :: binary(), + uid = <<"">> :: binary(), + deref_aliases = never :: never | + searching | + finding | + always, + group_attr = <<"">> :: binary(), + group_desc = <<"">> :: binary(), + user_desc = <<"">> :: binary(), + user_uid = <<"">> :: binary(), + uid_format = <<"">> :: binary(), + uid_format_re :: undefined | misc:re_mp(), + filter = <<"">> :: binary(), + ufilter = <<"">> :: binary(), + rfilter = <<"">> :: binary(), + gfilter = <<"">> :: binary(), + user_jid_attr = <<"">> :: binary(), + auth_check = true :: boolean() + }). -record(group_info, {desc, members}). + %%==================================================================== %% API %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + reload(Host, NewOpts, _OldOpts) -> case init_cache(Host, NewOpts) of - true -> - ets_cache:setopts(?USER_CACHE, cache_opts(Host, NewOpts)), - ets_cache:setopts(?GROUP_CACHE, cache_opts(Host, NewOpts)), - ets_cache:setopts(?DISPLAYED_CACHE, cache_opts(Host, NewOpts)); - false -> - ok + true -> + ets_cache:setopts(?USER_CACHE, cache_opts(Host, NewOpts)), + ets_cache:setopts(?GROUP_CACHE, cache_opts(Host, NewOpts)), + ets_cache:setopts(?DISPLAYED_CACHE, cache_opts(Host, NewOpts)); + false -> + ok end, Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {set_state, parse_options(Host, NewOpts)}). + depends(_Host, _Opts) -> [{mod_roster, hard}]. + %%-------------------------------------------------------------------- %% Hooks %%-------------------------------------------------------------------- @@ -112,24 +131,32 @@ depends(_Host, _Opts) -> get_user_roster(Items, US) -> SRUsers = get_user_to_groups_map(US, true), {NewItems1, SRUsersRest} = lists:mapfoldl( - fun(Item = #roster_item{jid = #jid{luser = U1, lserver = S1}}, SRUsers1) -> - US1 = {U1, S1}, - case dict:find(US1, SRUsers1) of - {ok, GroupNames} -> - {Item#roster_item{subscription = both, - groups = Item#roster_item.groups ++ GroupNames}, - dict:erase(US1, SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), - SRItems = [#roster_item{jid = jid:make(U1, S1), - name = get_user_name(U1, S1), subscription = both, - ask = undefined, groups = GroupNames} - || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], + fun(Item = #roster_item{jid = #jid{luser = U1, lserver = S1}}, SRUsers1) -> + US1 = {U1, S1}, + case dict:find(US1, SRUsers1) of + {ok, GroupNames} -> + {Item#roster_item{ + subscription = both, + groups = Item#roster_item.groups ++ GroupNames + }, + dict:erase(US1, SRUsers1)}; + error -> + {Item, SRUsers1} + end + end, + SRUsers, + Items), + SRItems = [ #roster_item{ + jid = jid:make(U1, S1), + name = get_user_name(U1, S1), + subscription = both, + ask = undefined, + groups = GroupNames + } + || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest) ], SRItems ++ NewItems1. + %% This function in use to rewrite the roster entries when moving or renaming %% them in the user contact list. -spec process_item(#roster{}, binary()) -> #roster{}. @@ -139,19 +166,25 @@ process_item(RosterItem, _Host) -> USTo = {User, Server}, Map = get_user_to_groups_map(USFrom, false), case dict:find(USTo, Map) of - error -> RosterItem; - {ok, []} -> RosterItem; - {ok, GroupNames} - when RosterItem#roster.subscription == remove -> - RosterItem#roster{subscription = both, ask = none, - groups = GroupNames}; - _ -> RosterItem#roster{subscription = both, ask = none} + error -> RosterItem; + {ok, []} -> RosterItem; + {ok, GroupNames} + when RosterItem#roster.subscription == remove -> + RosterItem#roster{ + subscription = both, + ask = none, + groups = GroupNames + }; + _ -> RosterItem#roster{subscription = both, ask = none} end. --spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) - -> {subscription(), ask(), [binary()]}. -get_jid_info({Subscription, Ask, Groups}, User, Server, - JID) -> + +-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) -> + {subscription(), ask(), [binary()]}. +get_jid_info({Subscription, Ask, Groups}, + User, + Server, + JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, @@ -159,236 +192,298 @@ get_jid_info({Subscription, Ask, Groups}, User, Server, US1 = {U1, S1}, SRUsers = get_user_to_groups_map(US, false), case dict:find(US1, SRUsers) of - {ok, GroupNames} -> - NewGroups = if Groups == [] -> GroupNames; - true -> Groups - end, - {both, none, NewGroups}; - error -> {Subscription, Ask, Groups} + {ok, GroupNames} -> + NewGroups = if + Groups == [] -> GroupNames; + true -> Groups + end, + {both, none, NewGroups}; + error -> {Subscription, Ask, Groups} end. + -spec in_subscription(boolean(), presence()) -> boolean(). in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). + -spec out_subscription(presence()) -> boolean(). out_subscription(#presence{from = From, to = JID, type = Type}) -> #jid{user = User, server = Server} = From, process_subscription(out, User, Server, JID, Type, false). -process_subscription(Direction, User, Server, JID, - _Type, Acc) -> + +process_subscription(Direction, + User, + Server, + JID, + _Type, + Acc) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = - jid:tolower(jid:remove_resource(JID)), + jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), + SRUsers = lists:usort(lists:flatmap(fun(Group) -> + get_group_users(LServer, Group) + end, + DisplayedGroups)), case lists:member(US1, SRUsers) of - true -> - case Direction of - in -> {stop, false}; - out -> stop - end; - false -> Acc + true -> + case Direction of + in -> {stop, false}; + out -> stop + end; + false -> Acc end. + %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), State = parse_options(Host, Opts), init_cache(Host, Opts), - ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster, 70), - ejabberd_hooks:add(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), - ejabberd_hooks:add(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), - ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, - get_jid_info, 70), - ejabberd_hooks:add(roster_process_item, Host, ?MODULE, - process_item, 50), + ejabberd_hooks:add(roster_get, + Host, + ?MODULE, + get_user_roster, + 70), + ejabberd_hooks:add(roster_in_subscription, + Host, + ?MODULE, + in_subscription, + 30), + ejabberd_hooks:add(roster_out_subscription, + Host, + ?MODULE, + out_subscription, + 30), + ejabberd_hooks:add(roster_get_jid_info, + Host, + ?MODULE, + get_jid_info, + 70), + ejabberd_hooks:add(roster_process_item, + Host, + ?MODULE, + process_item, + 50), eldap_pool:start_link(State#state.eldap_id, - State#state.servers, State#state.backups, - State#state.port, State#state.dn, - State#state.password, State#state.tls_options), + State#state.servers, + State#state.backups, + State#state.port, + State#state.dn, + State#state.password, + State#state.tls_options), {ok, State}. + handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast({set_state, NewState}, _State) -> {noreply, NewState}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, State) -> Host = State#state.host, - ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster, 70), - ejabberd_hooks:delete(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), - ejabberd_hooks:delete(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), - ejabberd_hooks:delete(roster_get_jid_info, Host, - ?MODULE, get_jid_info, 70), - ejabberd_hooks:delete(roster_process_item, Host, - ?MODULE, process_item, 50). + ejabberd_hooks:delete(roster_get, + Host, + ?MODULE, + get_user_roster, + 70), + ejabberd_hooks:delete(roster_in_subscription, + Host, + ?MODULE, + in_subscription, + 30), + ejabberd_hooks:delete(roster_out_subscription, + Host, + ?MODULE, + out_subscription, + 30), + ejabberd_hooks:delete(roster_get_jid_info, + Host, + ?MODULE, + get_jid_info, + 70), + ejabberd_hooks:delete(roster_process_item, + Host, + ?MODULE, + process_item, + 50). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- + get_user_to_groups_map({_, Server} = US, SkipUS) -> DisplayedGroups = get_user_displayed_groups(US), - lists:foldl(fun (Group, Dict1) -> - GroupName = get_group_name(Server, Group), - lists:foldl(fun (Contact, Dict) -> - if SkipUS, Contact == US -> Dict; - true -> - dict:append(Contact, - GroupName, Dict) - end - end, - Dict1, get_group_users(Server, Group)) - end, - dict:new(), DisplayedGroups). + lists:foldl(fun(Group, Dict1) -> + GroupName = get_group_name(Server, Group), + lists:foldl(fun(Contact, Dict) -> + if + SkipUS, Contact == US -> Dict; + true -> + dict:append(Contact, + GroupName, + Dict) + end + end, + Dict1, + get_group_users(Server, Group)) + end, + dict:new(), + DisplayedGroups). + eldap_search(State, FilterParseArgs, AttributesList) -> case apply(eldap_filter, parse, FilterParseArgs) of - {ok, EldapFilter} -> - case eldap_pool:search(State#state.eldap_id, - [{base, State#state.base}, - {filter, EldapFilter}, - {timeout, ?LDAP_SEARCH_TIMEOUT}, - {deref_aliases, State#state.deref_aliases}, - {attributes, AttributesList}]) - of - #eldap_search_result{entries = Es} -> - %% A result with entries. Return their list. - Es; - _ -> - %% Something else. Pretend we got no results. - [] - end; - _ -> - %% Filter parsing failed. Pretend we got no results. - [] + {ok, EldapFilter} -> + case eldap_pool:search(State#state.eldap_id, + [{base, State#state.base}, + {filter, EldapFilter}, + {timeout, ?LDAP_SEARCH_TIMEOUT}, + {deref_aliases, State#state.deref_aliases}, + {attributes, AttributesList}]) of + #eldap_search_result{entries = Es} -> + %% A result with entries. Return their list. + Es; + _ -> + %% Something else. Pretend we got no results. + [] + end; + _ -> + %% Filter parsing failed. Pretend we got no results. + [] end. + get_user_displayed_groups({User, Host}) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), ets_cache:lookup(?DISPLAYED_CACHE, - {User, Host}, - fun () -> - search_user_displayed_groups(State, User) - end). + {User, Host}, + fun() -> + search_user_displayed_groups(State, User) + end). + search_user_displayed_groups(State, User) -> GroupAttr = State#state.group_attr, Entries = eldap_search(State, - [eldap_filter:do_sub(State#state.rfilter, - [{<<"%u">>, User}])], - [GroupAttr]), - Reply = lists:flatmap(fun (#eldap_entry{attributes = - Attrs}) -> - case Attrs of - [{GroupAttr, ValuesList}] -> ValuesList; - _ -> [] - end - end, - Entries), + [eldap_filter:do_sub(State#state.rfilter, + [{<<"%u">>, User}])], + [GroupAttr]), + Reply = lists:flatmap(fun(#eldap_entry{ + attributes = + Attrs + }) -> + case Attrs of + [{GroupAttr, ValuesList}] -> ValuesList; + _ -> [] + end + end, + Entries), lists:usort(Reply). + get_group_users(Host, Group) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), case ets_cache:lookup(?GROUP_CACHE, - {Group, Host}, - fun () -> search_group_info(State, Group) end) of + {Group, Host}, + fun() -> search_group_info(State, Group) end) of {ok, #group_info{members = Members}} - when Members /= undefined -> + when Members /= undefined -> Members; _ -> [] end. + get_group_name(Host, Group) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), case ets_cache:lookup(?GROUP_CACHE, - {Group, Host}, - fun () -> search_group_info(State, Group) end) - of - {ok, #group_info{desc = GroupName}} - when GroupName /= undefined -> - GroupName; - _ -> Group + {Group, Host}, + fun() -> search_group_info(State, Group) end) of + {ok, #group_info{desc = GroupName}} + when GroupName /= undefined -> + GroupName; + _ -> Group end. + get_user_name(User, Host) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), case ets_cache:lookup(?USER_CACHE, - {User, Host}, - fun () -> search_user_name(State, User) end) - of - {ok, UserName} -> UserName; - error -> User + {User, Host}, + fun() -> search_user_name(State, User) end) of + {ok, UserName} -> UserName; + error -> User end. + search_group_info(State, Group) -> Extractor = case State#state.uid_format_re of - undefined -> - fun (UID) -> - catch eldap_utils:get_user_part(UID, - State#state.uid_format) - end; - _ -> - fun (UID) -> - catch get_user_part_re(UID, - State#state.uid_format_re) - end - end, + undefined -> + fun(UID) -> + catch eldap_utils:get_user_part(UID, + State#state.uid_format) + end; + _ -> + fun(UID) -> + catch get_user_part_re(UID, + State#state.uid_format_re) + end + end, AuthChecker = case State#state.auth_check of - true -> fun ejabberd_auth:user_exists/2; - _ -> fun (_U, _S) -> true end - end, + true -> fun ejabberd_auth:user_exists/2; + _ -> fun(_U, _S) -> true end + end, case eldap_search(State, - [eldap_filter:do_sub(State#state.gfilter, - [{<<"%g">>, Group}])], - [State#state.group_attr, State#state.group_desc, - State#state.uid]) - of + [eldap_filter:do_sub(State#state.gfilter, + [{<<"%g">>, Group}])], + [State#state.group_attr, + State#state.group_desc, + State#state.uid]) of [] -> error; - LDAPEntries -> - {GroupDesc, MembersLists} = lists:foldl(fun(Entry, Acc) -> - extract_members(State, Extractor, AuthChecker, Entry, Acc) - end, - {Group, []}, LDAPEntries), - {ok, #group_info{desc = GroupDesc, members = lists:usort(lists:flatten(MembersLists))}} + LDAPEntries -> + {GroupDesc, MembersLists} = lists:foldl(fun(Entry, Acc) -> + extract_members(State, Extractor, AuthChecker, Entry, Acc) + end, + {Group, []}, + LDAPEntries), + {ok, #group_info{desc = GroupDesc, members = lists:usort(lists:flatten(MembersLists))}} end. + get_member_jid(#state{user_jid_attr = <<>>}, UID, Host) -> {jid:nodeprep(UID), Host}; get_member_jid(#state{user_jid_attr = UserJIDAttr, user_uid = UIDAttr} = State, - UID, Host) -> + UID, + Host) -> Entries = eldap_search(State, [eldap_filter:do_sub(<<"(", UIDAttr/binary, "=%u)">>, [{<<"%u">>, UID}])], @@ -400,10 +495,11 @@ get_member_jid(#state{user_jid_attr = UserJIDAttr, user_uid = UIDAttr} = State, catch error:{bad_jid, _} -> {error, Host} end; - _ -> - {error, error} + _ -> + {error, error} end. + extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {DescAcc, JIDsAcc}) -> Host = State#state.host, case {eldap_utils:get_ldap_attr(State#state.group_attr, Attrs), @@ -412,61 +508,64 @@ extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {ID, Desc, {value, {GroupMemberAttr, Members}}} when ID /= <<"">>, GroupMemberAttr == State#state.uid -> JIDs = lists:foldl( - fun({ok, UID}, L) -> - {MemberUID, MemberHost} = get_member_jid(State, UID, Host), - case MemberUID of - error -> - L; - _ -> - case AuthChecker(MemberUID, MemberHost) of - true -> - [{MemberUID, MemberHost} | L]; - _ -> - L - end - end; - (_, L) -> L - end, [], lists:map(Extractor, Members)), + fun({ok, UID}, L) -> + {MemberUID, MemberHost} = get_member_jid(State, UID, Host), + case MemberUID of + error -> + L; + _ -> + case AuthChecker(MemberUID, MemberHost) of + true -> + [{MemberUID, MemberHost} | L]; + _ -> + L + end + end; + (_, L) -> L + end, + [], + lists:map(Extractor, Members)), {Desc, [JIDs | JIDsAcc]}; _ -> {DescAcc, JIDsAcc} end. + search_user_name(State, User) -> case eldap_search(State, - [eldap_filter:do_sub(State#state.ufilter, - [{<<"%u">>, User}])], - [State#state.user_desc, State#state.user_uid]) - of - [#eldap_entry{attributes = Attrs} | _] -> - case {eldap_utils:get_ldap_attr(State#state.user_uid, - Attrs), - eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} - of - {UID, Desc} when UID /= <<"">> -> {ok, Desc}; - _ -> error - end; - [] -> error + [eldap_filter:do_sub(State#state.ufilter, + [{<<"%u">>, User}])], + [State#state.user_desc, State#state.user_uid]) of + [#eldap_entry{attributes = Attrs} | _] -> + case {eldap_utils:get_ldap_attr(State#state.user_uid, + Attrs), + eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} of + {UID, Desc} when UID /= <<"">> -> {ok, Desc}; + _ -> error + end; + [] -> error end. + %% Getting User ID part by regex pattern get_user_part_re(String, Pattern) -> case catch re:run(String, Pattern) of - {match, Captured} -> - {First, Len} = lists:nth(2, Captured), - Result = str:sub_string(String, First + 1, First + Len), - {ok, Result}; - _ -> {error, badmatch} + {match, Captured} -> + {First, Len} = lists:nth(2, Captured), + Result = str:sub_string(String, First + 1, First + Len), + {ok, Result}; + _ -> {error, badmatch} end. + parse_options(Host, Opts) -> Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), Cfg = ?eldap_config(mod_shared_roster_ldap_opt, Opts), GroupAttr = mod_shared_roster_ldap_opt:ldap_groupattr(Opts), GroupDesc = case mod_shared_roster_ldap_opt:ldap_groupdesc(Opts) of - undefined -> GroupAttr; - GD -> GD - end, + undefined -> GroupAttr; + GD -> GD + end, UserDesc = mod_shared_roster_ldap_opt:ldap_userdesc(Opts), UserUID = mod_shared_roster_ldap_opt:ldap_useruid(Opts), UIDAttr = mod_shared_roster_ldap_opt:ldap_memberattr(Opts), @@ -479,76 +578,88 @@ parse_options(Host, Opts) -> ConfigGroupFilter = mod_shared_roster_ldap_opt:ldap_gfilter(Opts), RosterFilter = mod_shared_roster_ldap_opt:ldap_rfilter(Opts), SubFilter = <<"(&(", UIDAttr/binary, "=", - UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, + UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, UserSubFilter = case ConfigUserFilter of - <<"">> -> - eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); - UString -> UString - end, + <<"">> -> + eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); + UString -> UString + end, GroupSubFilter = case ConfigGroupFilter of - <<"">> -> - eldap_filter:do_sub(SubFilter, - [{<<"%u">>, <<"*">>}]); - GString -> GString - end, + <<"">> -> + eldap_filter:do_sub(SubFilter, + [{<<"%u">>, <<"*">>}]); + GString -> GString + end, Filter = case ConfigFilter of - <<"">> -> SubFilter; - _ -> - <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> - end, + <<"">> -> SubFilter; + _ -> + <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> + end, UserFilter = case ConfigFilter of - <<"">> -> UserSubFilter; - _ -> - <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> - end, + <<"">> -> UserSubFilter; + _ -> + <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> + end, GroupFilter = case ConfigFilter of - <<"">> -> GroupSubFilter; - _ -> - <<"(&", GroupSubFilter/binary, ConfigFilter/binary, - ")">> - end, - #state{host = Host, eldap_id = Eldap_ID, - servers = Cfg#eldap_config.servers, - backups = Cfg#eldap_config.backups, - port = Cfg#eldap_config.port, - tls_options = Cfg#eldap_config.tls_options, - dn = Cfg#eldap_config.dn, - password = Cfg#eldap_config.password, - base = Cfg#eldap_config.base, - deref_aliases = Cfg#eldap_config.deref_aliases, - uid = UIDAttr, - user_jid_attr = JIDAttr, - group_attr = GroupAttr, group_desc = GroupDesc, - user_desc = UserDesc, user_uid = UserUID, - uid_format = UIDAttrFormat, - uid_format_re = UIDAttrFormatRe, filter = Filter, - ufilter = UserFilter, rfilter = RosterFilter, - gfilter = GroupFilter, auth_check = AuthCheck}. + <<"">> -> GroupSubFilter; + _ -> + <<"(&", GroupSubFilter/binary, ConfigFilter/binary, + ")">> + end, + #state{ + host = Host, + eldap_id = Eldap_ID, + servers = Cfg#eldap_config.servers, + backups = Cfg#eldap_config.backups, + port = Cfg#eldap_config.port, + tls_options = Cfg#eldap_config.tls_options, + dn = Cfg#eldap_config.dn, + password = Cfg#eldap_config.password, + base = Cfg#eldap_config.base, + deref_aliases = Cfg#eldap_config.deref_aliases, + uid = UIDAttr, + user_jid_attr = JIDAttr, + group_attr = GroupAttr, + group_desc = GroupDesc, + user_desc = UserDesc, + user_uid = UserUID, + uid_format = UIDAttrFormat, + uid_format_re = UIDAttrFormatRe, + filter = Filter, + ufilter = UserFilter, + rfilter = RosterFilter, + gfilter = GroupFilter, + auth_check = AuthCheck + }. + init_cache(Host, Opts) -> UseCache = use_cache(Host, Opts), case UseCache of - true -> - CacheOpts = cache_opts(Host, Opts), - ets_cache:new(?USER_CACHE, CacheOpts), - ets_cache:new(?GROUP_CACHE, CacheOpts), - ets_cache:new(?DISPLAYED_CACHE, CacheOpts); - false -> - ets_cache:delete(?USER_CACHE), - ets_cache:delete(?GROUP_CACHE), - ets_cache:delete(?DISPLAYED_CACHE) + true -> + CacheOpts = cache_opts(Host, Opts), + ets_cache:new(?USER_CACHE, CacheOpts), + ets_cache:new(?GROUP_CACHE, CacheOpts), + ets_cache:new(?DISPLAYED_CACHE, CacheOpts); + false -> + ets_cache:delete(?USER_CACHE), + ets_cache:delete(?GROUP_CACHE), + ets_cache:delete(?DISPLAYED_CACHE) end, UseCache. + use_cache(_Host, Opts) -> mod_shared_roster_ldap_opt:use_cache(Opts). + cache_opts(_Host, Opts) -> MaxSize = mod_shared_roster_ldap_opt:cache_size(Opts), CacheMissed = mod_shared_roster_ldap_opt:cache_missed(Opts), LifeTime = mod_shared_roster_ldap_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + mod_opt_type(ldap_auth_check) -> econf:bool(); mod_opt_type(ldap_gfilter) -> @@ -615,8 +726,9 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + -spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} | - {atom(), any()}]. + {atom(), any()}]. mod_options(Host) -> [{ldap_auth_check, true}, {ldap_gfilter, <<"">>}, @@ -649,144 +761,180 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module lets the server administrator automatically " - "populate users' rosters (contact lists) with entries based on " - "users and groups defined in an LDAP-based directory."), "", + "populate users' rosters (contact lists) with entries based on " + "users and groups defined in an LDAP-based directory."), + "", ?T("NOTE: 'mod_shared_roster_ldap' depends on 'mod_roster' being " - "enabled. Roster queries will return '503' errors if " - "'mod_roster' is not enabled."), "", + "enabled. Roster queries will return '503' errors if " + "'mod_roster' is not enabled."), + "", ?T("The module accepts many configuration options. Some of them, " - "if unspecified, default to the values specified for the top " - "level of configuration. This lets you avoid specifying, for " - "example, the bind password in multiple places."), "", + "if unspecified, default to the values specified for the top " + "level of configuration. This lets you avoid specifying, for " + "example, the bind password in multiple places."), + "", ?T("- Filters: 'ldap_rfilter', 'ldap_ufilter', 'ldap_gfilter', " - "'ldap_filter'. These options specify LDAP filters used to " - "query for shared roster information. All of them are run " - "against the ldap_base."), + "'ldap_filter'. These options specify LDAP filters used to " + "query for shared roster information. All of them are run " + "against the ldap_base."), ?T("- Attributes: 'ldap_groupattr', 'ldap_groupdesc', " - "'ldap_memberattr', 'ldap_userdesc', 'ldap_useruid'. These " - "options specify the names of the attributes which hold " - "interesting data in the entries returned by running filters " - "specified with the filter options."), + "'ldap_memberattr', 'ldap_userdesc', 'ldap_useruid'. These " + "options specify the names of the attributes which hold " + "interesting data in the entries returned by running filters " + "specified with the filter options."), ?T("- Control parameters: 'ldap_auth_check', " - "'ldap_group_cache_validity', 'ldap_memberattr_format', " - "'ldap_memberattr_format_re', 'ldap_user_cache_validity'. " - "These parameters control the behaviour of the module."), + "'ldap_group_cache_validity', 'ldap_memberattr_format', " + "'ldap_memberattr_format_re', 'ldap_user_cache_validity'. " + "These parameters control the behaviour of the module."), ?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 _`ldap.md#ldap-connection|LDAP Connection`_ " - "section for more information about them."), "", + "connection parameters, all of which default to the top-level " + "parameter of the same name, if unspecified. " + "See _`ldap.md#ldap-connection|LDAP Connection`_ " + "section for more information about them."), + "", ?T("Check also the _`ldap.md#ldap-examples|Configuration examples`_ " - "section to get details about " - "retrieving the roster, " - "and configuration examples including Flat DIT and Deep DIT.")], + "section to get details about " + "retrieving the roster, " + "and configuration examples including Flat DIT and Deep DIT.")], opts => [ - %% Filters: + %% Filters: {ldap_rfilter, - #{desc => + #{ + desc => ?T("So called \"Roster Filter\". Used to find names of " - "all \"shared roster\" groups. See also the " - "'ldap_groupattr' parameter. If unspecified, defaults to " - "the top-level parameter of the same name. You must " - "specify it in some place in the configuration, there is " - "no default.")}}, + "all \"shared roster\" groups. See also the " + "'ldap_groupattr' parameter. If unspecified, defaults to " + "the top-level parameter of the same name. You must " + "specify it in some place in the configuration, there is " + "no default.") + }}, {ldap_gfilter, - #{desc => + #{ + desc => ?T("\"Group Filter\", used when retrieving human-readable " - "name (a.k.a. \"Display Name\") and the members of a " - "group. See also the parameters 'ldap_groupattr', " - "'ldap_groupdesc' and 'ldap_memberattr'. If unspecified, " - "defaults to the top-level parameter of the same name. " - "If that one also is unspecified, then the filter is " - "constructed exactly like \"User Filter\".")}}, + "name (a.k.a. \"Display Name\") and the members of a " + "group. See also the parameters 'ldap_groupattr', " + "'ldap_groupdesc' and 'ldap_memberattr'. If unspecified, " + "defaults to the top-level parameter of the same name. " + "If that one also is unspecified, then the filter is " + "constructed exactly like \"User Filter\".") + }}, {ldap_ufilter, - #{desc => + #{ + desc => ?T("\"User Filter\", used for retrieving the human-readable " - "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 " - "_`ldap.md#filters|Filters`_ section.")}}, + "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 " + "_`ldap.md#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 " - "_`ldap.md#filters|Filters`_ section.")}}, - %% Attributes: + #{ + desc => + ?T("Additional filter which is AND-ed together " + "with \"User Filter\" and \"Group Filter\". " + "For more information check the LDAP " + "_`ldap.md#filters|Filters`_ section.") + }}, + %% Attributes: {ldap_groupattr, - #{desc => - ?T("The name of the attribute that holds the group name, and " - "that is used to differentiate between them. Retrieved " - "from results of the \"Roster Filter\" " - "and \"Group Filter\". Defaults to 'cn'.")}}, + #{ + desc => + ?T("The name of the attribute that holds the group name, and " + "that is used to differentiate between them. Retrieved " + "from results of the \"Roster Filter\" " + "and \"Group Filter\". Defaults to 'cn'.") + }}, {ldap_groupdesc, - #{desc => - ?T("The name of the attribute which holds the human-readable " - "group name in the objects you use to represent groups. " - "Retrieved from results of the \"Group Filter\". " - "Defaults to whatever 'ldap_groupattr' is set.")}}, + #{ + desc => + ?T("The name of the attribute which holds the human-readable " + "group name in the objects you use to represent groups. " + "Retrieved from results of the \"Group Filter\". " + "Defaults to whatever 'ldap_groupattr' is set.") + }}, {ldap_memberattr, - #{desc => - ?T("The name of the attribute which holds the IDs of the " - "members of a group. Retrieved from results of the " - "\"Group Filter\". Defaults to 'memberUid'. The name of " - "the attribute differs depending on the objectClass you " - "use for your group objects, for example: " - "'posixGroup' -> 'memberUid'; 'groupOfNames' -> 'member'; " - "'groupOfUniqueNames' -> 'uniqueMember'.")}}, + #{ + desc => + ?T("The name of the attribute which holds the IDs of the " + "members of a group. Retrieved from results of the " + "\"Group Filter\". Defaults to 'memberUid'. The name of " + "the attribute differs depending on the objectClass you " + "use for your group objects, for example: " + "'posixGroup' -> 'memberUid'; 'groupOfNames' -> 'member'; " + "'groupOfUniqueNames' -> 'uniqueMember'.") + }}, {ldap_userdesc, - #{desc => - ?T("The name of the attribute which holds the human-readable " - "user name. Retrieved from results of the " - "\"User Filter\". Defaults to 'cn'.")}}, + #{ + desc => + ?T("The name of the attribute which holds the human-readable " + "user name. Retrieved from results of the " + "\"User Filter\". Defaults to 'cn'.") + }}, {ldap_useruid, - #{desc => - ?T("The name of the attribute which holds the ID of a roster " - "item. Value of this attribute in the roster item objects " - "needs to match the ID retrieved from the " - "'ldap_memberattr' attribute of a group object. " - "Retrieved from results of the \"User Filter\". " - "Defaults to 'cn'.")}}, + #{ + desc => + ?T("The name of the attribute which holds the ID of a roster " + "item. Value of this attribute in the roster item objects " + "needs to match the ID retrieved from the " + "'ldap_memberattr' attribute of a group object. " + "Retrieved from results of the \"User Filter\". " + "Defaults to 'cn'.") + }}, {ldap_userjidattr, - #{desc => - ?T("The name of the attribute which is used to map user id " - "to XMPP jid. If not specified (and that is default value " - "of this option), user jid will be created from user id and " - " this module host.")}}, - %% Control parameters: + #{ + desc => + ?T("The name of the attribute which is used to map user id " + "to XMPP jid. If not specified (and that is default value " + "of this option), user jid will be created from user id and " + " this module host.") + }}, + %% Control parameters: {ldap_memberattr_format, - #{desc => - ?T("A globbing format for extracting user ID from the value " - "of the attribute named by 'ldap_memberattr'. Defaults " - "to '%u', which means that the whole value is the member " - "ID. If you change it to something different, you may " - "also need to specify the User and Group Filters " - "manually; see section Filters.")}}, + #{ + desc => + ?T("A globbing format for extracting user ID from the value " + "of the attribute named by 'ldap_memberattr'. Defaults " + "to '%u', which means that the whole value is the member " + "ID. If you change it to something different, you may " + "also need to specify the User and Group Filters " + "manually; see section Filters.") + }}, {ldap_memberattr_format_re, - #{desc => - ?T("A regex for extracting user ID from the value of the " - "attribute named by 'ldap_memberattr'. Check the LDAP " - "_`ldap.md#control-parameters|Control Parameters`_ section.")}}, - {ldap_auth_check, - #{value => "true | false", + #{ desc => - ?T("Whether the module should check (via the ejabberd " - "authentication subsystem) for existence of each user in " - "the shared LDAP roster. Set to 'false' if you want to " - "disable the check. Default value is 'true'.")}}] ++ - [{Opt, - #{desc => - {?T("Same as top-level _`~s`_ option, but " - "applied to this module only."), [Opt]}}} - || Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases, - ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, - ldap_servers, ldap_tls_certfile, ldap_tls_cacertfile, - ldap_tls_depth, ldap_tls_verify, use_cache, cache_size, - cache_missed, cache_life_time]]}. + ?T("A regex for extracting user ID from the value of the " + "attribute named by 'ldap_memberattr'. Check the LDAP " + "_`ldap.md#control-parameters|Control Parameters`_ section.") + }}, + {ldap_auth_check, + #{ + value => "true | false", + desc => + ?T("Whether the module should check (via the ejabberd " + "authentication subsystem) for existence of each user in " + "the shared LDAP roster. Set to 'false' if you want to " + "disable the check. Default value is 'true'.") + }}] ++ + [ {Opt, + #{ + desc => + {?T("Same as top-level _`~s`_ option, but " + "applied to this module only."), + [Opt]} + }} + || Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases, + ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, + ldap_servers, ldap_tls_certfile, ldap_tls_cacertfile, + ldap_tls_depth, ldap_tls_verify, use_cache, cache_size, + cache_missed, cache_life_time] ] + }. diff --git a/src/mod_shared_roster_ldap_opt.erl b/src/mod_shared_roster_ldap_opt.erl index d4657222e..98311223b 100644 --- a/src/mod_shared_roster_ldap_opt.erl +++ b/src/mod_shared_roster_ldap_opt.erl @@ -34,183 +34,212 @@ -export([ldap_useruid/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_size). + -spec ldap_auth_check(gen_mod:opts() | global | binary()) -> boolean(). ldap_auth_check(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_auth_check, Opts); ldap_auth_check(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_auth_check). + -spec ldap_backups(gen_mod:opts() | global | binary()) -> [binary()]. ldap_backups(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_backups, Opts); ldap_backups(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_backups). + -spec ldap_base(gen_mod:opts() | global | binary()) -> binary(). ldap_base(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_base, Opts); ldap_base(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_base). + -spec ldap_deref_aliases(gen_mod:opts() | global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. ldap_deref_aliases(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_deref_aliases, Opts); ldap_deref_aliases(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_deref_aliases). + -spec ldap_encrypt(gen_mod:opts() | global | binary()) -> 'none' | 'starttls' | 'tls'. ldap_encrypt(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_encrypt, Opts); ldap_encrypt(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_encrypt). + -spec ldap_filter(gen_mod:opts() | global | binary()) -> binary(). ldap_filter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_filter, Opts); ldap_filter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_filter). + -spec ldap_gfilter(gen_mod:opts() | global | binary()) -> binary(). ldap_gfilter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_gfilter, Opts); ldap_gfilter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_gfilter). + -spec ldap_groupattr(gen_mod:opts() | global | binary()) -> binary(). ldap_groupattr(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_groupattr, Opts); ldap_groupattr(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_groupattr). + -spec ldap_groupdesc(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). ldap_groupdesc(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_groupdesc, Opts); ldap_groupdesc(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_groupdesc). + -spec ldap_memberattr(gen_mod:opts() | global | binary()) -> binary(). ldap_memberattr(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_memberattr, Opts); ldap_memberattr(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr). + -spec ldap_memberattr_format(gen_mod:opts() | global | binary()) -> binary(). ldap_memberattr_format(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_memberattr_format, Opts); ldap_memberattr_format(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr_format). + -spec ldap_memberattr_format_re(gen_mod:opts() | global | binary()) -> 'undefined' | misc:re_mp(). ldap_memberattr_format_re(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_memberattr_format_re, Opts); ldap_memberattr_format_re(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr_format_re). + -spec ldap_password(gen_mod:opts() | global | binary()) -> binary(). ldap_password(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_password, Opts); ldap_password(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_password). + -spec ldap_port(gen_mod:opts() | global | binary()) -> 1..1114111. ldap_port(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_port, Opts); ldap_port(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_port). + -spec ldap_rfilter(gen_mod:opts() | global | binary()) -> binary(). ldap_rfilter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_rfilter, Opts); ldap_rfilter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_rfilter). + -spec ldap_rootdn(gen_mod:opts() | global | binary()) -> binary(). ldap_rootdn(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_rootdn, Opts); ldap_rootdn(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_rootdn). + -spec ldap_servers(gen_mod:opts() | global | binary()) -> [binary()]. ldap_servers(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_servers, Opts); ldap_servers(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_servers). + -spec ldap_tls_cacertfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_cacertfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_cacertfile, Opts); ldap_tls_cacertfile(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_cacertfile). + -spec ldap_tls_certfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_certfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_certfile, Opts); ldap_tls_certfile(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_certfile). + -spec ldap_tls_depth(gen_mod:opts() | global | binary()) -> non_neg_integer(). ldap_tls_depth(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_depth, Opts); ldap_tls_depth(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_depth). + -spec ldap_tls_verify(gen_mod:opts() | global | binary()) -> 'false' | 'hard' | 'soft'. ldap_tls_verify(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_verify, Opts); ldap_tls_verify(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_verify). + -spec ldap_ufilter(gen_mod:opts() | global | binary()) -> binary(). ldap_ufilter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_ufilter, Opts); ldap_ufilter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_ufilter). --spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. ldap_uids(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_uids, Opts); ldap_uids(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_uids). + -spec ldap_userdesc(gen_mod:opts() | global | binary()) -> binary(). ldap_userdesc(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_userdesc, Opts); ldap_userdesc(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_userdesc). + -spec ldap_userjidattr(gen_mod:opts() | global | binary()) -> binary(). ldap_userjidattr(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_userjidattr, Opts); ldap_userjidattr(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_userjidattr). + -spec ldap_useruid(gen_mod:opts() | global | binary()) -> binary(). ldap_useruid(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_useruid, Opts); ldap_useruid(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_useruid). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, use_cache). - diff --git a/src/mod_shared_roster_mnesia.erl b/src/mod_shared_roster_mnesia.erl index 028584459..c8ecde610 100644 --- a/src/mod_shared_roster_mnesia.erl +++ b/src/mod_shared_roster_mnesia.erl @@ -27,127 +27,165 @@ -behaviour(mod_shared_roster). %% API --export([init/2, list_groups/1, groups_with_opts/1, create_group/3, - delete_group/2, get_group_opts/2, set_group_opts/3, - get_user_groups/2, get_group_explicit_users/2, - get_user_displayed_groups/3, is_user_in_group/3, - add_user_to_group/3, remove_user_from_group/3, import/3]). +-export([init/2, + list_groups/1, + groups_with_opts/1, + create_group/3, + delete_group/2, + get_group_opts/2, + set_group_opts/3, + get_user_groups/2, + get_group_explicit_users/2, + get_user_displayed_groups/3, + is_user_in_group/3, + add_user_to_group/3, + remove_user_from_group/3, + import/3]). -export([need_transform/1, transform/1, use_cache/1]). -include("mod_roster.hrl"). -include("mod_shared_roster.hrl"). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, sr_group, - [{disc_copies, [node()]}, - {attributes, record_info(fields, sr_group)}]), - ejabberd_mnesia:create(?MODULE, sr_user, - [{disc_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, sr_user)}, - {index, [group_host]}]). + ejabberd_mnesia:create(?MODULE, + sr_group, + [{disc_copies, [node()]}, + {attributes, record_info(fields, sr_group)}]), + ejabberd_mnesia:create(?MODULE, + sr_user, + [{disc_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, sr_user)}, + {index, [group_host]}]). + list_groups(Host) -> mnesia:dirty_select(sr_group, - [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, - [{'==', '$2', Host}], ['$1']}]). + [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, + [{'==', '$2', Host}], + ['$1']}]). + -spec use_cache(binary()) -> boolean(). use_cache(_Host) -> false. + groups_with_opts(Host) -> Gs = mnesia:dirty_select(sr_group, - [{#sr_group{group_host = {'$1', Host}, opts = '$2', - _ = '_'}, - [], [['$1', '$2']]}]), - lists:map(fun ([G, O]) -> {G, O} end, Gs). + [{#sr_group{ + group_host = {'$1', Host}, + opts = '$2', + _ = '_' + }, + [], + [['$1', '$2']]}]), + lists:map(fun([G, O]) -> {G, O} end, Gs). + create_group(Host, Group, Opts) -> R = #sr_group{group_host = {Group, Host}, opts = Opts}, - F = fun () -> mnesia:write(R) end, + F = fun() -> mnesia:write(R) end, mnesia:transaction(F). + delete_group(Host, Group) -> GroupHost = {Group, Host}, - F = fun () -> - mnesia:delete({sr_group, GroupHost}), - Users = mnesia:index_read(sr_user, GroupHost, - #sr_user.group_host), - lists:foreach(fun (UserEntry) -> - mnesia:delete_object(UserEntry) - end, - Users) - end, + F = fun() -> + mnesia:delete({sr_group, GroupHost}), + Users = mnesia:index_read(sr_user, + GroupHost, + #sr_user.group_host), + lists:foreach(fun(UserEntry) -> + mnesia:delete_object(UserEntry) + end, + Users) + end, mnesia:transaction(F). + get_group_opts(Host, Group) -> case catch mnesia:dirty_read(sr_group, {Group, Host}) of - [#sr_group{opts = Opts}] -> {ok, Opts}; - _ -> error + [#sr_group{opts = Opts}] -> {ok, Opts}; + _ -> error end. + set_group_opts(Host, Group, Opts) -> R = #sr_group{group_host = {Group, Host}, opts = Opts}, - F = fun () -> mnesia:write(R) end, + F = fun() -> mnesia:write(R) end, mnesia:transaction(F). + get_user_groups(US, Host) -> case catch mnesia:dirty_read(sr_user, US) of - Rs when is_list(Rs) -> - [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; - _ -> - [] + Rs when is_list(Rs) -> + [ Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host ]; + _ -> + [] end. + get_group_explicit_users(Host, Group) -> Read = (catch mnesia:dirty_index_read(sr_user, - {Group, Host}, #sr_user.group_host)), + {Group, Host}, + #sr_user.group_host)), case Read of - Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs]; - _ -> [] + Rs when is_list(Rs) -> [ R#sr_user.us || R <- Rs ]; + _ -> [] end. + get_user_displayed_groups(LUser, LServer, GroupsOpts) -> case catch mnesia:dirty_read(sr_user, {LUser, LServer}) of - Rs when is_list(Rs) -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || #sr_user{group_host = {Group, H}} <- Rs, - H == LServer]; - _ -> - [] + Rs when is_list(Rs) -> + [ {Group, proplists:get_value(Group, GroupsOpts, [])} + || #sr_user{group_host = {Group, H}} <- Rs, + H == LServer ]; + _ -> + [] end. + is_user_in_group(US, Group, Host) -> case mnesia:dirty_match_object( - #sr_user{us = US, group_host = {Group, Host}}) of - [] -> false; - _ -> true + #sr_user{us = US, group_host = {Group, Host}}) of + [] -> false; + _ -> true end. + add_user_to_group(Host, US, Group) -> R = #sr_user{us = US, group_host = {Group, Host}}, - F = fun () -> mnesia:write(R) end, + F = fun() -> mnesia:write(R) end, mnesia:transaction(F). + remove_user_from_group(Host, US, Group) -> R = #sr_user{us = US, group_host = {Group, Host}}, - F = fun () -> mnesia:delete_object(R) end, + F = fun() -> mnesia:delete_object(R) end, mnesia:transaction(F). + import(LServer, <<"sr_group">>, [Group, SOpts, _TimeStamp]) -> - G = #sr_group{group_host = {Group, LServer}, - opts = ejabberd_sql:decode_term(SOpts)}, + G = #sr_group{ + group_host = {Group, LServer}, + opts = ejabberd_sql:decode_term(SOpts) + }, mnesia:dirty_write(G); import(LServer, <<"sr_user">>, [SJID, Group, _TimeStamp]) -> #jid{luser = U, lserver = S} = jid:decode(SJID), User = #sr_user{us = {U, S}, group_host = {Group, LServer}}, mnesia:dirty_write(User). + need_transform({sr_group, {G, H}, _}) when is_list(G) orelse is_list(H) -> ?INFO_MSG("Mnesia table 'sr_group' will be converted to binary", []), @@ -162,19 +200,24 @@ need_transform({sr_group, {_, _}, [{name, _} | _]}) -> need_transform(_) -> false. + transform(#sr_group{group_host = {G, _H}, opts = Opts} = R) - when is_binary(G) -> + when is_binary(G) -> Opts2 = case proplists:get_value(name, Opts, false) of - false -> Opts; - Name -> [{label, Name} | proplists:delete(name, Opts)] - end, + false -> Opts; + Name -> [{label, Name} | proplists:delete(name, Opts)] + end, R#sr_group{opts = Opts2}; transform(#sr_group{group_host = {G, H}, opts = Opts} = R) -> - R#sr_group{group_host = {iolist_to_binary(G), iolist_to_binary(H)}, - opts = mod_shared_roster:opts_to_binary(Opts)}; + R#sr_group{ + group_host = {iolist_to_binary(G), iolist_to_binary(H)}, + opts = mod_shared_roster:opts_to_binary(Opts) + }; transform(#sr_user{us = {U, S}, group_host = {G, H}} = R) -> - R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)}, - group_host = {iolist_to_binary(G), iolist_to_binary(H)}}. + R#sr_user{ + us = {iolist_to_binary(U), iolist_to_binary(S)}, + group_host = {iolist_to_binary(G), iolist_to_binary(H)} + }. %%%=================================================================== %%% Internal functions diff --git a/src/mod_shared_roster_opt.erl b/src/mod_shared_roster_opt.erl index 825196e2c..86b2293d9 100644 --- a/src/mod_shared_roster_opt.erl +++ b/src/mod_shared_roster_opt.erl @@ -9,33 +9,37 @@ -export([db_type/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, db_type). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, use_cache). - diff --git a/src/mod_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl index 4d582a1bf..d70ccc4ba 100644 --- a/src/mod_shared_roster_sql.erl +++ b/src/mod_shared_roster_sql.erl @@ -24,23 +24,33 @@ -module(mod_shared_roster_sql). - -behaviour(mod_shared_roster). %% API --export([init/2, list_groups/1, groups_with_opts/1, create_group/3, - delete_group/2, get_group_opts/2, set_group_opts/3, - get_user_groups/2, get_group_explicit_users/2, - get_user_displayed_groups/3, is_user_in_group/3, - add_user_to_group/3, remove_user_from_group/3, import/3, - export/1]). +-export([init/2, + list_groups/1, + groups_with_opts/1, + create_group/3, + delete_group/2, + get_group_opts/2, + set_group_opts/3, + get_user_groups/2, + get_group_explicit_users/2, + get_user_displayed_groups/3, + is_user_in_group/3, + add_user_to_group/3, + remove_user_from_group/3, + import/3, + export/1]). -export([sql_schemas/0]). -include_lib("xmpp/include/jid.hrl"). + -include("mod_roster.hrl"). -include("mod_shared_roster.hrl"). -include("ejabberd_sql_pt.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -48,166 +58,193 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"sr_group">>, - columns = - [#sql_column{name = <<"name">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"opts">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"name">>], - unique = true}]}, - #sql_table{ - name = <<"sr_user">>, - columns = - [#sql_column{name = <<"jid">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"grp">>, type = text}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, - <<"jid">>, <<"grp">>], - unique = true}, - #sql_index{ - columns = [<<"server_host">>, <<"grp">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"sr_group">>, + columns = + [#sql_column{name = <<"name">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"opts">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"name">>], + unique = true + }] + }, + #sql_table{ + name = <<"sr_user">>, + columns = + [#sql_column{name = <<"jid">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"grp">>, type = text}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, + <<"jid">>, + <<"grp">>], + unique = true + }, + #sql_index{ + columns = [<<"server_host">>, <<"grp">>] + }] + }] + }]. + list_groups(Host) -> case ejabberd_sql:sql_query( - Host, + Host, ?SQL("select @(name)s from sr_group where %(Host)H")) of - {selected, Rs} -> [G || {G} <- Rs]; - _ -> [] + {selected, Rs} -> [ G || {G} <- Rs ]; + _ -> [] end. + groups_with_opts(Host) -> case ejabberd_sql:sql_query( Host, - ?SQL("select @(name)s, @(opts)s from sr_group where %(Host)H")) - of - {selected, Rs} -> - [{G, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(Opts))} - || {G, Opts} <- Rs]; - _ -> [] + ?SQL("select @(name)s, @(opts)s from sr_group where %(Host)H")) of + {selected, Rs} -> + [ {G, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(Opts))} + || {G, Opts} <- Rs ]; + _ -> [] end. + create_group(Host, Group, Opts) -> SOpts = misc:term_to_expr(Opts), - F = fun () -> - ?SQL_UPSERT_T( - "sr_group", - ["!name=%(Group)s", - "!server_host=%(Host)s", - "opts=%(SOpts)s"]) - end, + F = fun() -> + ?SQL_UPSERT_T( + "sr_group", + ["!name=%(Group)s", + "!server_host=%(Host)s", + "opts=%(SOpts)s"]) + end, ejabberd_sql:sql_transaction(Host, F). + delete_group(Host, Group) -> - F = fun () -> - ejabberd_sql:sql_query_t( + F = fun() -> + ejabberd_sql:sql_query_t( ?SQL("delete from sr_group where name=%(Group)s and %(Host)H")), - ejabberd_sql:sql_query_t( + ejabberd_sql:sql_query_t( ?SQL("delete from sr_user where grp=%(Group)s and %(Host)H")) - end, + end, case ejabberd_sql:sql_transaction(Host, F) of - {atomic,{updated,_}} -> {atomic, ok}; + {atomic, {updated, _}} -> {atomic, ok}; Res -> Res end. + get_group_opts(Host, Group) -> case catch ejabberd_sql:sql_query( - Host, - ?SQL("select @(opts)s from sr_group" + Host, + ?SQL("select @(opts)s from sr_group" " where name=%(Group)s and %(Host)H")) of - {selected, [{SOpts}]} -> + {selected, [{SOpts}]} -> {ok, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(SOpts))}; - _ -> error + _ -> error end. + set_group_opts(Host, Group, Opts) -> SOpts = misc:term_to_expr(Opts), - F = fun () -> - ?SQL_UPSERT_T( - "sr_group", - ["!name=%(Group)s", - "!server_host=%(Host)s", - "opts=%(SOpts)s"]) - end, + F = fun() -> + ?SQL_UPSERT_T( + "sr_group", + ["!name=%(Group)s", + "!server_host=%(Host)s", + "opts=%(SOpts)s"]) + end, ejabberd_sql:sql_transaction(Host, F). + get_user_groups(US, Host) -> SJID = make_jid_s(US), case catch ejabberd_sql:sql_query( - Host, - ?SQL("select @(grp)s from sr_user" + Host, + ?SQL("select @(grp)s from sr_user" " where jid=%(SJID)s and %(Host)H")) of - {selected, Rs} -> [G || {G} <- Rs]; - _ -> [] + {selected, Rs} -> [ G || {G} <- Rs ]; + _ -> [] end. + get_group_explicit_users(Host, Group) -> case catch ejabberd_sql:sql_query( - Host, - ?SQL("select @(jid)s from sr_user" + Host, + ?SQL("select @(jid)s from sr_user" " where grp=%(Group)s and %(Host)H")) of - {selected, Rs} -> - lists:map( - fun({JID}) -> - {U, S, _} = jid:tolower(jid:decode(JID)), - {U, S} - end, Rs); - _ -> - [] + {selected, Rs} -> + lists:map( + fun({JID}) -> + {U, S, _} = jid:tolower(jid:decode(JID)), + {U, S} + end, + Rs); + _ -> + [] end. + get_user_displayed_groups(LUser, LServer, GroupsOpts) -> SJID = make_jid_s(LUser, LServer), case catch ejabberd_sql:sql_query( - LServer, - ?SQL("select @(grp)s from sr_user" + LServer, + ?SQL("select @(grp)s from sr_user" " where jid=%(SJID)s and %(LServer)H")) of - {selected, Rs} -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || {Group} <- Rs]; - _ -> [] + {selected, Rs} -> + [ {Group, proplists:get_value(Group, GroupsOpts, [])} + || {Group} <- Rs ]; + _ -> [] end. + is_user_in_group(US, Group, Host) -> SJID = make_jid_s(US), case catch ejabberd_sql:sql_query( Host, ?SQL("select @(jid)s from sr_user where jid=%(SJID)s" " and %(Host)H and grp=%(Group)s")) of - {selected, []} -> false; - _ -> true + {selected, []} -> false; + _ -> true end. + add_user_to_group(Host, US, Group) -> SJID = make_jid_s(US), ejabberd_sql:sql_query( Host, ?SQL_INSERT( - "sr_user", - ["jid=%(SJID)s", - "server_host=%(Host)s", - "grp=%(Group)s"])). + "sr_user", + ["jid=%(SJID)s", + "server_host=%(Host)s", + "grp=%(Group)s"])). + remove_user_from_group(Host, US, Group) -> SJID = make_jid_s(US), - F = fun () -> - ejabberd_sql:sql_query_t( + F = fun() -> + ejabberd_sql:sql_query_t( ?SQL("delete from sr_user where jid=%(SJID)s and %(Host)H" " and grp=%(Group)s")), - ok - end, + ok + end, ejabberd_sql:sql_transaction(Host, F). + export(_Server) -> [{sr_group, fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) @@ -215,10 +252,10 @@ export(_Server) -> SOpts = misc:term_to_expr(Opts), [?SQL("delete from sr_group where name=%(Group)s and %(Host)H;"), ?SQL_INSERT( - "sr_group", - ["name=%(Group)s", - "server_host=%(Host)s", - "opts=%(SOpts)s"])]; + "sr_group", + ["name=%(Group)s", + "server_host=%(Host)s", + "opts=%(SOpts)s"])]; (_Host, _R) -> [] end}, @@ -229,21 +266,24 @@ export(_Server) -> [?SQL("select @(jid)s from sr_user where jid=%(SJID)s" " and %(Host)H and grp=%(Group)s;"), ?SQL_INSERT( - "sr_user", - ["jid=%(SJID)s", - "server_host=%(Host)s", - "grp=%(Group)s"])]; + "sr_user", + ["jid=%(SJID)s", + "server_host=%(Host)s", + "grp=%(Group)s"])]; (_Host, _R) -> [] end}]. + import(_, _, _) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== make_jid_s(U, S) -> jid:encode(jid:tolower(jid:make(U, S))). + make_jid_s({U, S}) -> make_jid_s(U, S). diff --git a/src/mod_sic.erl b/src/mod_sic.erl index f781c1690..e76fe4ecd 100644 --- a/src/mod_sic.erl +++ b/src/mod_sic.erl @@ -31,40 +31,64 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_local_iq/1, - process_sm_iq/1, mod_options/1, depends/2, mod_doc/0]). +-export([start/2, + stop/1, + reload/3, + process_local_iq/1, + process_sm_iq/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). + start(_Host, _Opts) -> {ok, [{iq_handler, ejabberd_local, ?NS_SIC_0, process_local_iq}, {iq_handler, ejabberd_sm, ?NS_SIC_0, process_sm_iq}, {iq_handler, ejabberd_local, ?NS_SIC_1, process_local_iq}, {iq_handler, ejabberd_sm, ?NS_SIC_1, process_sm_iq}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. -process_local_iq(#iq{from = #jid{user = User, server = Server, - resource = Resource}, - type = get} = IQ) -> + +process_local_iq(#iq{ + from = #jid{ + user = User, + server = Server, + resource = Resource + }, + type = get + } = IQ) -> get_ip({User, Server, Resource}, IQ); process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). -process_sm_iq(#iq{from = #jid{user = User, server = Server, - resource = Resource}, - to = #jid{user = User, server = Server}, - type = get} = IQ) -> + +process_sm_iq(#iq{ + from = #jid{ + user = User, + server = Server, + resource = Resource + }, + to = #jid{user = User, server = Server}, + type = get + } = IQ) -> get_ip({User, Server, Resource}, IQ); process_sm_iq(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), @@ -73,30 +97,36 @@ process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). + get_ip({User, Server, Resource}, #iq{lang = Lang, sub_els = [#sic{xmlns = NS}]} = IQ) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of - {IP, Port} when is_tuple(IP) -> - Result = case NS of - ?NS_SIC_0 -> #sic{ip = IP, xmlns = NS}; - ?NS_SIC_1 -> #sic{ip = IP, port = Port, xmlns = NS} - end, - xmpp:make_iq_result(IQ, Result); - _ -> - Txt = ?T("User session not found"), - xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + {IP, Port} when is_tuple(IP) -> + Result = case NS of + ?NS_SIC_0 -> #sic{ip = IP, xmlns = NS}; + ?NS_SIC_1 -> #sic{ip = IP, port = Port, xmlns = NS} + end, + xmpp:make_iq_result(IQ, Result); + _ -> + Txt = ?T("User session not found"), + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end. + mod_options(_Host) -> []. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0279.html" "[XEP-0279: Server IP Check]. This protocol enables " - "a client to discover its external IP address."), "", + "a client to discover its external IP address."), + "", ?T("WARNING: The protocol extension is deferred and seems " "like there are no clients supporting it, so using this " "module is not recommended and, furthermore, the module " - "might be removed in the future.")]}. + "might be removed in the future.")] + }. diff --git a/src/mod_sip.erl b/src/mod_sip.erl index aa98be2cc..8db0c83bb 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -31,32 +31,56 @@ -ifndef(SIP). -export([start/2, stop/1, depends/2, mod_options/1, mod_doc/0]). + + start(_, _) -> ?CRITICAL_MSG("ejabberd is not compiled with SIP support", []), {error, sip_not_compiled}. + + stop(_) -> ok. + + depends(_, _) -> []. + + mod_options(_) -> []. + + mod_doc() -> #{desc => [?T("SIP support has not been enabled.")]}. + + -else. -behaviour(gen_mod). -behaviour(esip). %% API --export([start/2, stop/1, reload/3, - make_response/2, is_my_host/1, at_my_host/1]). +-export([start/2, + stop/1, + reload/3, + make_response/2, + is_my_host/1, + at_my_host/1]). --export([data_in/2, data_out/2, message_in/2, - message_out/2, request/2, request/3, response/2, - locate/1, mod_opt_type/1, mod_options/1, depends/2, +-export([data_in/2, + data_out/2, + message_in/2, + message_out/2, + request/2, request/3, + response/2, + locate/1, + mod_opt_type/1, + mod_options/1, + depends/2, mod_doc/0]). -include_lib("esip/include/esip.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -68,39 +92,65 @@ start(_Host, _Opts) -> software, <<"ejabberd ", (ejabberd_option:version())/binary>>), esip:set_config_value(module, ?MODULE), Spec = {mod_sip_registrar, {mod_sip_registrar, start_link, []}, - transient, 2000, worker, [mod_sip_registrar]}, + transient, + 2000, + worker, + [mod_sip_registrar]}, TmpSupSpec = {mod_sip_proxy_sup, - {ejabberd_tmp_sup, start_link, - [mod_sip_proxy_sup, mod_sip_proxy]}, - permanent, infinity, supervisor, [ejabberd_tmp_sup]}, + {ejabberd_tmp_sup, start_link, + [mod_sip_proxy_sup, mod_sip_proxy]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, supervisor:start_child(ejabberd_gen_mod_sup, Spec), supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec), ok. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. -data_in(Data, #sip_socket{type = Transport, - addr = {MyIP, MyPort}, - peer = {PeerIP, PeerPort}}) -> - ?DEBUG( - "SIP [~p/in] ~ts:~p -> ~ts:~p:~n~ts", - [Transport, inet_parse:ntoa(PeerIP), PeerPort, - inet_parse:ntoa(MyIP), MyPort, Data]). -data_out(Data, #sip_socket{type = Transport, - addr = {MyIP, MyPort}, - peer = {PeerIP, PeerPort}}) -> +data_in(Data, + #sip_socket{ + type = Transport, + addr = {MyIP, MyPort}, + peer = {PeerIP, PeerPort} + }) -> ?DEBUG( - "SIP [~p/out] ~ts:~p -> ~ts:~p:~n~ts", - [Transport, inet_parse:ntoa(MyIP), MyPort, - inet_parse:ntoa(PeerIP), PeerPort, Data]). + "SIP [~p/in] ~ts:~p -> ~ts:~p:~n~ts", + [Transport, + inet_parse:ntoa(PeerIP), + PeerPort, + inet_parse:ntoa(MyIP), + MyPort, + Data]). + + +data_out(Data, + #sip_socket{ + type = Transport, + addr = {MyIP, MyPort}, + peer = {PeerIP, PeerPort} + }) -> + ?DEBUG( + "SIP [~p/out] ~ts:~p -> ~ts:~p:~n~ts", + [Transport, + inet_parse:ntoa(MyIP), + MyPort, + inet_parse:ntoa(PeerIP), + PeerPort, + Data]). + message_in(#sip{type = request, method = M} = Req, SIPSock) when M /= <<"ACK">>, M /= <<"CANCEL">> -> @@ -115,27 +165,32 @@ message_in(ping, SIPSock) -> message_in(_, _) -> ok. + message_out(_, _) -> ok. + response(_Resp, _SIPSock) -> ok. + request(#sip{method = <<"ACK">>} = Req, SIPSock) -> case action(Req, SIPSock) of - {relay, LServer} -> - mod_sip_proxy:route(Req, LServer, [{authenticated, true}]); - {proxy_auth, LServer} -> - mod_sip_proxy:route(Req, LServer, [{authenticated, false}]); - _ -> - ok + {relay, LServer} -> + mod_sip_proxy:route(Req, LServer, [{authenticated, true}]); + {proxy_auth, LServer} -> + mod_sip_proxy:route(Req, LServer, [{authenticated, false}]); + _ -> + ok end; request(_Req, _SIPSock) -> ok. + request(Req, SIPSock, TrID) -> request(Req, SIPSock, TrID, action(Req, SIPSock)). + request(Req, SIPSock, TrID, Action) -> case Action of to_me -> @@ -145,85 +200,107 @@ request(Req, SIPSock, TrID, Action) -> loop -> make_response(Req, #sip{status = 483, type = response}); {unsupported, Require} -> - make_response(Req, #sip{status = 420, - type = response, - hdrs = [{'unsupported', - Require}]}); + make_response(Req, + #sip{ + status = 420, + type = response, + hdrs = [{'unsupported', + Require}] + }); {relay, LServer} -> case mod_sip_proxy:start(LServer, []) of {ok, Pid} -> mod_sip_proxy:route(Req, SIPSock, TrID, Pid), {mod_sip_proxy, route, [Pid]}; Err -> - ?WARNING_MSG("Failed to proxy request ~p: ~p", [Req, Err]), + ?WARNING_MSG("Failed to proxy request ~p: ~p", [Req, Err]), Err end; {proxy_auth, LServer} -> make_response( Req, - #sip{status = 407, - type = response, - hdrs = [{'proxy-authenticate', - make_auth_hdr(LServer)}]}); + #sip{ + status = 407, + type = response, + hdrs = [{'proxy-authenticate', + make_auth_hdr(LServer)}] + }); {auth, LServer} -> make_response( Req, - #sip{status = 401, - type = response, - hdrs = [{'www-authenticate', - make_auth_hdr(LServer)}]}); + #sip{ + status = 401, + type = response, + hdrs = [{'www-authenticate', + make_auth_hdr(LServer)}] + }); deny -> - make_response(Req, #sip{status = 403, - type = response}); + make_response(Req, + #sip{ + status = 403, + type = response + }); not_found -> - make_response(Req, #sip{status = 480, - type = response}) + make_response(Req, + #sip{ + status = 480, + type = response + }) end. + locate(_SIPMsg) -> ok. + find(#uri{user = User, host = Host}) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), - if LUser == <<"">> -> - to_me; - true -> - case mod_sip_registrar:find_sockets(LUser, LServer) of - [] -> - not_found; - [_|_] -> - {relay, LServer} - end + if + LUser == <<"">> -> + to_me; + true -> + case mod_sip_registrar:find_sockets(LUser, LServer) of + [] -> + not_found; + [_ | _] -> + {relay, LServer} + end end. + %%%=================================================================== %%% Internal functions %%%=================================================================== -action(#sip{method = <<"REGISTER">>, type = request, hdrs = Hdrs, - uri = #uri{user = <<"">>} = URI} = Req, SIPSock) -> +action(#sip{ + method = <<"REGISTER">>, + type = request, + hdrs = Hdrs, + uri = #uri{user = <<"">>} = URI + } = Req, + SIPSock) -> case at_my_host(URI) of - true -> - Require = esip:get_hdrs('require', Hdrs) -- supported(), - case Require of - [_|_] -> - {unsupported, Require}; - _ -> - {_, ToURI, _} = esip:get_hdr('to', Hdrs), - case at_my_host(ToURI) of - true -> - case check_auth(Req, 'authorization', SIPSock) of - true -> - register; - false -> - {auth, jid:nameprep(ToURI#uri.host)} - end; - false -> - deny - end - end; - false -> - deny + true -> + Require = esip:get_hdrs('require', Hdrs) -- supported(), + case Require of + [_ | _] -> + {unsupported, Require}; + _ -> + {_, ToURI, _} = esip:get_hdr('to', Hdrs), + case at_my_host(ToURI) of + true -> + case check_auth(Req, 'authorization', SIPSock) of + true -> + register; + false -> + {auth, jid:nameprep(ToURI#uri.host)} + end; + false -> + deny + end + end; + false -> + deny end; action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) -> case esip:get_hdr('max-forwards', Hdrs) of @@ -232,38 +309,39 @@ action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) -> 0 -> loop; _ -> - Require = esip:get_hdrs('proxy-require', Hdrs) -- supported(), + Require = esip:get_hdrs('proxy-require', Hdrs) -- supported(), case Require of - [_|_] -> + [_ | _] -> {unsupported, Require}; _ -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), {_, FromURI, _} = esip:get_hdr('from', Hdrs), - case at_my_host(FromURI) of - true -> - case check_auth(Req, 'proxy-authorization', SIPSock) of + case at_my_host(FromURI) of + true -> + case check_auth(Req, 'proxy-authorization', SIPSock) of true -> - case at_my_host(ToURI) of - true -> - find(ToURI); - false -> - LServer = jid:nameprep(FromURI#uri.host), - {relay, LServer} - end; + case at_my_host(ToURI) of + true -> + find(ToURI); + false -> + LServer = jid:nameprep(FromURI#uri.host), + {relay, LServer} + end; false -> {proxy_auth, FromURI#uri.host} end; - false -> - case at_my_host(ToURI) of - true -> - find(ToURI); - false -> - deny - end + false -> + case at_my_host(ToURI) of + true -> + find(ToURI); + false -> + deny + end end end end. + check_auth(#sip{method = <<"CANCEL">>}, _, _SIPSock) -> true; check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) -> @@ -280,54 +358,73 @@ check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) - fun({_, Params}) -> Username = esip:get_param(<<"username">>, Params), Realm = esip:get_param(<<"realm">>, Params), - (LUser == esip:unquote(Username)) - and (LServer == esip:unquote(Realm)) - end, esip:get_hdrs(AuthHdr, Hdrs)) of - [Auth|_] -> - case ejabberd_auth:get_password_s(LUser, LServer) of - <<"">> -> - false; - Password when is_binary(Password) -> - esip:check_auth(Auth, Method, Body, Password); - _ScramedPassword -> - ?ERROR_MSG("Unable to authenticate ~ts@~ts against SCRAM'ed " - "password", [LUser, LServer]), - false - end; + (LUser == esip:unquote(Username)) and + (LServer == esip:unquote(Realm)) + end, + esip:get_hdrs(AuthHdr, Hdrs)) of + [Auth | _] -> + case ejabberd_auth:get_password_s(LUser, LServer) of + <<"">> -> + false; + Password when is_binary(Password) -> + esip:check_auth(Auth, Method, Body, Password); + _ScramedPassword -> + ?ERROR_MSG("Unable to authenticate ~ts@~ts against SCRAM'ed " + "password", + [LUser, LServer]), + false + end; [] -> false end. + allow() -> [<<"OPTIONS">>, <<"REGISTER">>]. + supported() -> [<<"path">>, <<"outbound">>]. + process(#sip{method = <<"OPTIONS">>} = Req, _) -> - make_response(Req, #sip{type = response, status = 200, - hdrs = [{'allow', allow()}, - {'supported', supported()}]}); + make_response(Req, + #sip{ + type = response, + status = 200, + hdrs = [{'allow', allow()}, + {'supported', supported()}] + }); process(#sip{method = <<"REGISTER">>} = Req, _) -> make_response(Req, #sip{type = response, status = 400}); process(Req, _) -> - make_response(Req, #sip{type = response, status = 405, - hdrs = [{'allow', allow()}]}). + make_response(Req, + #sip{ + type = response, + status = 405, + hdrs = [{'allow', allow()}] + }). + make_auth_hdr(LServer) -> - {<<"Digest">>, [{<<"realm">>, esip:quote(LServer)}, - {<<"qop">>, esip:quote(<<"auth">>)}, - {<<"nonce">>, esip:quote(esip:make_hexstr(20))}]}. + {<<"Digest">>, + [{<<"realm">>, esip:quote(LServer)}, + {<<"qop">>, esip:quote(<<"auth">>)}, + {<<"nonce">>, esip:quote(esip:make_hexstr(20))}]}. + make_response(Req, Resp) -> esip:make_response(Req, Resp, esip:make_tag()). + at_my_host(#uri{host = Host}) -> is_my_host(jid:nameprep(Host)). + is_my_host(LServer) -> gen_mod:is_loaded(LServer, ?MODULE). + mod_opt_type(always_record_route) -> econf:bool(); mod_opt_type(flow_timeout_tcp) -> @@ -343,9 +440,11 @@ mod_opt_type(via) -> fun(L) when is_list(L) -> (econf:and_then( econf:options( - #{type => econf:enum([tcp, tls, udp]), + #{ + type => econf:enum([tcp, tls, udp]), host => econf:domain(), - port => econf:port()}, + port => econf:port() + }, [{required, [type, host]}]), fun(Opts) -> Type = proplists:get_value(type, Opts), @@ -358,17 +457,21 @@ mod_opt_type(via) -> econf:url([tls, tcp, udp]), fun(URI) -> {ok, Type, _UserInfo, Host, Port, _, _} = - misc:uri_parse(URI), + misc:uri_parse(URI), {list_to_atom(Type), {unicode:characters_to_binary(Host), Port}} end))(U) - end, [unique]). + end, + [unique]). + -spec mod_options(binary()) -> [{via, [{tcp | tls | udp, {binary(), 1..65535}}]} | - {atom(), term()}]. + {atom(), term()}]. mod_options(Host) -> - Route = #uri{scheme = <<"sip">>, - host = Host, - params = [{<<"lr">>, <<>>}]}, + Route = #uri{ + scheme = <<"sip">>, + host = Host, + params = [{<<"lr">>, <<>>}] + }, [{always_record_route, true}, {flow_timeout_tcp, timer:seconds(120)}, {flow_timeout_udp, timer:seconds(29)}, @@ -376,10 +479,13 @@ mod_options(Host) -> {routes, [Route]}, {via, []}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module adds SIP proxy/registrar support " - "for the corresponding virtual host."), "", + "for the corresponding virtual host."), + "", ?T("NOTE: It is not enough to just load this module. " "You should also configure listeners and DNS records " "properly. For details see the section about the " @@ -387,42 +493,53 @@ mod_doc() -> "in the ejabberd Documentation.")], opts => [{always_record_route, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Always insert \"Record-Route\" header into " "SIP messages. With this approach it is possible to bypass " "NATs/firewalls a bit more easily. " - "The default value is 'true'.")}}, + "The default value is 'true'.") + }}, {flow_timeout_tcp, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("The option sets a keep-alive timer for " "https://tools.ietf.org/html/rfc5626[SIP outbound] " - "TCP connections. The default value is '2' minutes.")}}, + "TCP connections. The default value is '2' minutes.") + }}, {flow_timeout_udp, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("The options sets a keep-alive timer for " "https://tools.ietf.org/html/rfc5626[SIP outbound] " - "UDP connections. The default value is '29' seconds.")}}, + "UDP connections. The default value is '29' seconds.") + }}, {record_route, - #{value => ?T("URI"), + #{ + value => ?T("URI"), desc => ?T("When the option 'always_record_route' is set to " "'true' or when https://tools.ietf.org/html/rfc5626" "[SIP outbound] is utilized, ejabberd inserts " "\"Record-Route\" header field with this 'URI' into " "a SIP message. The default is a SIP URI constructed " - "from the virtual host on which the module is loaded.")}}, + "from the virtual host on which the module is loaded.") + }}, {routes, - #{value => "[URI, ...]", + #{ + value => "[URI, ...]", desc => ?T("You can set a list of SIP URIs of routes pointing " "to this SIP proxy server. The default is a list containing " "a single SIP URI constructed from the virtual host " - "on which the module is loaded.")}}, + "on which the module is loaded.") + }}, {via, - #{value => "[URI, ...]", + #{ + value => "[URI, ...]", desc => ?T("A list to construct \"Via\" headers for " "inserting them into outgoing SIP messages. " @@ -434,7 +551,8 @@ mod_doc() -> "be a domain name or an IP address and \"port\" " "must be an internet port number. Note that all " "parts of the 'URI' are mandatory (e.g. you " - "cannot omit \"port\" or \"scheme\").")}}], + "cannot omit \"port\" or \"scheme\").") + }}], example => ["modules:", " mod_sip:", @@ -448,6 +566,8 @@ mod_doc() -> " via:", " - tls://sip-tls.example.com:5061", " - tcp://sip-tcp.example.com:5060", - " - udp://sip-udp.example.com:5060"]}. + " - udp://sip-udp.example.com:5060"] + }. + -endif. diff --git a/src/mod_sip_opt.erl b/src/mod_sip_opt.erl index e160d2e12..4c59a18f0 100644 --- a/src/mod_sip_opt.erl +++ b/src/mod_sip_opt.erl @@ -10,39 +10,44 @@ -export([routes/1]). -export([via/1]). + -spec always_record_route(gen_mod:opts() | global | binary()) -> boolean(). always_record_route(Opts) when is_map(Opts) -> gen_mod:get_opt(always_record_route, Opts); always_record_route(Host) -> gen_mod:get_module_opt(Host, mod_sip, always_record_route). + -spec flow_timeout_tcp(gen_mod:opts() | global | binary()) -> pos_integer(). flow_timeout_tcp(Opts) when is_map(Opts) -> gen_mod:get_opt(flow_timeout_tcp, Opts); flow_timeout_tcp(Host) -> gen_mod:get_module_opt(Host, mod_sip, flow_timeout_tcp). + -spec flow_timeout_udp(gen_mod:opts() | global | binary()) -> pos_integer(). flow_timeout_udp(Opts) when is_map(Opts) -> gen_mod:get_opt(flow_timeout_udp, Opts); flow_timeout_udp(Host) -> gen_mod:get_module_opt(Host, mod_sip, flow_timeout_udp). + -spec record_route(gen_mod:opts() | global | binary()) -> esip:uri(). record_route(Opts) when is_map(Opts) -> gen_mod:get_opt(record_route, Opts); record_route(Host) -> gen_mod:get_module_opt(Host, mod_sip, record_route). + -spec routes(gen_mod:opts() | global | binary()) -> [esip:uri()]. routes(Opts) when is_map(Opts) -> gen_mod:get_opt(routes, Opts); routes(Host) -> gen_mod:get_module_opt(Host, mod_sip, routes). --spec via(gen_mod:opts() | global | binary()) -> [{'tcp' | 'tls' | 'udp',{binary(),1..65535}}]. + +-spec via(gen_mod:opts() | global | binary()) -> [{'tcp' | 'tls' | 'udp', {binary(), 1..65535}}]. via(Opts) when is_map(Opts) -> gen_mod:get_opt(via, Opts); via(Host) -> gen_mod:get_module_opt(Host, mod_sip, via). - diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index 8c5d8348c..689179bfa 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -33,22 +33,30 @@ %% API -export([start/2, start_link/2, route/3, route/4]). --export([init/1, wait_for_request/2, - wait_for_response/2, handle_event/3, - handle_sync_event/4, handle_info/3, terminate/3, - code_change/4]). +-export([init/1, + wait_for_request/2, + wait_for_response/2, + handle_event/3, + handle_sync_event/4, + handle_info/3, + terminate/3, + code_change/4]). -include("logger.hrl"). + -include_lib("esip/include/esip.hrl"). --define(SIGN_LIFETIME, 300). %% in seconds. +-define(SIGN_LIFETIME, 300). %% in seconds. + +-record(state, { + host = <<"">> :: binary(), + opts = [] :: [{certfile, binary()}], + orig_trid, + responses = [] :: [#sip{}], + tr_ids = [] :: list(), + orig_req = #sip{} :: #sip{} + }). --record(state, {host = <<"">> :: binary(), - opts = [] :: [{certfile, binary()}], - orig_trid, - responses = [] :: [#sip{}], - tr_ids = [] :: list(), - orig_req = #sip{} :: #sip{}}). %%%=================================================================== %%% API @@ -56,49 +64,55 @@ start(LServer, Opts) -> supervisor:start_child(mod_sip_proxy_sup, [LServer, Opts]). + start_link(LServer, Opts) -> p1_fsm:start_link(?MODULE, [LServer, Opts], []). + route(SIPMsg, _SIPSock, TrID, Pid) -> p1_fsm:send_event(Pid, {SIPMsg, TrID}). + route(#sip{hdrs = Hdrs} = Req, LServer, Opts) -> case proplists:get_bool(authenticated, Opts) of - true -> - route_statelessly(Req, LServer, Opts); - false -> - ConfiguredRRoute = get_configured_record_route(LServer), - case esip:get_hdrs('route', Hdrs) of - [{_, URI, _}|_] -> - case cmp_uri(URI, ConfiguredRRoute) of - true -> - case is_signed_by_me(URI#uri.user, Hdrs) of - true -> - route_statelessly(Req, LServer, Opts); - false -> - error - end; - false -> - error - end; - [] -> - error - end + true -> + route_statelessly(Req, LServer, Opts); + false -> + ConfiguredRRoute = get_configured_record_route(LServer), + case esip:get_hdrs('route', Hdrs) of + [{_, URI, _} | _] -> + case cmp_uri(URI, ConfiguredRRoute) of + true -> + case is_signed_by_me(URI#uri.user, Hdrs) of + true -> + route_statelessly(Req, LServer, Opts); + false -> + error + end; + false -> + error + end; + [] -> + error + end end. + route_statelessly(Req, LServer, Opts) -> Req1 = prepare_request(LServer, Req), case connect(Req1, add_certfile(LServer, Opts)) of - {ok, SIPSocketsWithURIs} -> - lists:foreach( - fun({SIPSocket, _URI}) -> - Req2 = add_via(SIPSocket, LServer, Req1), - esip:send(SIPSocket, Req2) - end, SIPSocketsWithURIs); - _ -> - error + {ok, SIPSocketsWithURIs} -> + lists:foreach( + fun({SIPSocket, _URI}) -> + Req2 = add_via(SIPSocket, LServer, Req1), + esip:send(SIPSocket, Req2) + end, + SIPSocketsWithURIs); + _ -> + error end. + %%%=================================================================== %%% gen_fsm callbacks %%%=================================================================== @@ -106,217 +120,255 @@ init([Host, Opts]) -> Opts1 = add_certfile(Host, Opts), {ok, wait_for_request, #state{opts = Opts1, host = Host}}. + wait_for_request({#sip{type = request} = Req, TrID}, State) -> Opts = State#state.opts, Req1 = prepare_request(State#state.host, Req), case connect(Req1, Opts) of - {ok, SIPSocketsWithURIs} -> - NewState = - lists:foldl( - fun(_SIPSocketWithURI, {error, _} = Err) -> - Err; - ({SIPSocket, URI}, #state{tr_ids = TrIDs} = AccState) -> - Req2 = add_record_route_and_set_uri( - URI, State#state.host, Req1), - Req3 = add_via(SIPSocket, State#state.host, Req2), - case esip:request(SIPSocket, Req3, - {?MODULE, route, [self()]}) of - {ok, ClientTrID} -> - NewTrIDs = [ClientTrID|TrIDs], - AccState#state{tr_ids = NewTrIDs}; - Err -> - cancel_pending_transactions(AccState), - Err - end - end, State, SIPSocketsWithURIs), - case NewState of - {error, _} = Err -> - {Status, Reason} = esip:error_status(Err), - esip:reply(TrID, mod_sip:make_response( - Req, #sip{type = response, - status = Status, - reason = Reason})), - {stop, normal, State}; - _ -> - {next_state, wait_for_response, - NewState#state{orig_req = Req, orig_trid = TrID}} - end; - {error, notfound} -> - esip:reply(TrID, mod_sip:make_response( - Req, #sip{type = response, - status = 480, - reason = esip:reason(480)})), - {stop, normal, State}; - Err -> - {Status, Reason} = esip:error_status(Err), - esip:reply(TrID, mod_sip:make_response( - Req, #sip{type = response, - status = Status, - reason = Reason})), - {stop, normal, State} + {ok, SIPSocketsWithURIs} -> + NewState = + lists:foldl( + fun(_SIPSocketWithURI, {error, _} = Err) -> + Err; + ({SIPSocket, URI}, #state{tr_ids = TrIDs} = AccState) -> + Req2 = add_record_route_and_set_uri( + URI, State#state.host, Req1), + Req3 = add_via(SIPSocket, State#state.host, Req2), + case esip:request(SIPSocket, + Req3, + {?MODULE, route, [self()]}) of + {ok, ClientTrID} -> + NewTrIDs = [ClientTrID | TrIDs], + AccState#state{tr_ids = NewTrIDs}; + Err -> + cancel_pending_transactions(AccState), + Err + end + end, + State, + SIPSocketsWithURIs), + case NewState of + {error, _} = Err -> + {Status, Reason} = esip:error_status(Err), + esip:reply(TrID, + mod_sip:make_response( + Req, + #sip{ + type = response, + status = Status, + reason = Reason + })), + {stop, normal, State}; + _ -> + {next_state, wait_for_response, + NewState#state{orig_req = Req, orig_trid = TrID}} + end; + {error, notfound} -> + esip:reply(TrID, + mod_sip:make_response( + Req, + #sip{ + type = response, + status = 480, + reason = esip:reason(480) + })), + {stop, normal, State}; + Err -> + {Status, Reason} = esip:error_status(Err), + esip:reply(TrID, + mod_sip:make_response( + Req, + #sip{ + type = response, + status = Status, + reason = Reason + })), + {stop, normal, State} end; wait_for_request(_Event, State) -> {next_state, wait_for_request, State}. + wait_for_response({#sip{method = <<"CANCEL">>, type = request}, _TrID}, State) -> cancel_pending_transactions(State), {next_state, wait_for_response, State}; wait_for_response({Resp, TrID}, - #state{orig_req = #sip{method = Method} = Req} = State) -> + #state{orig_req = #sip{method = Method} = Req} = State) -> case Resp of - {error, timeout} when Method /= <<"INVITE">> -> - %% Absorb useless 408. See RFC4320 - choose_best_response(State), - esip:stop_transaction(State#state.orig_trid), - {stop, normal, State}; - {error, _} -> - {Status, Reason} = esip:error_status(Resp), - State1 = mark_transaction_as_complete(TrID, State), - SIPResp = mod_sip:make_response(Req, - #sip{type = response, - status = Status, - reason = Reason}), - State2 = collect_response(SIPResp, State1), - case State2#state.tr_ids of - [] -> - choose_best_response(State2), - {stop, normal, State2}; - _ -> - {next_state, wait_for_response, State2} - end; + {error, timeout} when Method /= <<"INVITE">> -> + %% Absorb useless 408. See RFC4320 + choose_best_response(State), + esip:stop_transaction(State#state.orig_trid), + {stop, normal, State}; + {error, _} -> + {Status, Reason} = esip:error_status(Resp), + State1 = mark_transaction_as_complete(TrID, State), + SIPResp = mod_sip:make_response(Req, + #sip{ + type = response, + status = Status, + reason = Reason + }), + State2 = collect_response(SIPResp, State1), + case State2#state.tr_ids of + [] -> + choose_best_response(State2), + {stop, normal, State2}; + _ -> + {next_state, wait_for_response, State2} + end; #sip{status = 100} -> {next_state, wait_for_response, State}; #sip{status = Status} -> - {[_|Vias], NewHdrs} = esip:split_hdrs('via', Resp#sip.hdrs), - NewResp = case Vias of - [] -> - Resp#sip{hdrs = NewHdrs}; - _ -> - Resp#sip{hdrs = [{'via', Vias}|NewHdrs]} - end, - if Status < 300 -> - esip:reply(State#state.orig_trid, NewResp); - true -> - ok - end, - State1 = if Status >= 200 -> - mark_transaction_as_complete(TrID, State); - true -> - State - end, - State2 = if Status >= 300 -> - collect_response(NewResp, State1); - true -> - State1 - end, - if Status >= 600 -> - cancel_pending_transactions(State2); - true -> - ok - end, - case State2#state.tr_ids of - [] -> - choose_best_response(State2), - {stop, normal, State2}; - _ -> - {next_state, wait_for_response, State2} - end + {[_ | Vias], NewHdrs} = esip:split_hdrs('via', Resp#sip.hdrs), + NewResp = case Vias of + [] -> + Resp#sip{hdrs = NewHdrs}; + _ -> + Resp#sip{hdrs = [{'via', Vias} | NewHdrs]} + end, + if + Status < 300 -> + esip:reply(State#state.orig_trid, NewResp); + true -> + ok + end, + State1 = if + Status >= 200 -> + mark_transaction_as_complete(TrID, State); + true -> + State + end, + State2 = if + Status >= 300 -> + collect_response(NewResp, State1); + true -> + State1 + end, + if + Status >= 600 -> + cancel_pending_transactions(State2); + true -> + ok + end, + case State2#state.tr_ids of + [] -> + choose_best_response(State2), + {stop, normal, State2}; + _ -> + {next_state, wait_for_response, State2} + end end; wait_for_response(_Event, State) -> {next_state, wait_for_response, State}. + handle_event(_Event, StateName, State) -> {next_state, StateName, State}. + handle_sync_event(_Event, _From, StateName, State) -> Reply = ok, {reply, Reply, StateName, State}. + handle_info(_Info, StateName, State) -> {next_state, StateName, State}. + terminate(_Reason, _StateName, _State) -> ok. + code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== connect(#sip{hdrs = Hdrs} = Req, Opts) -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), case mod_sip:at_my_host(ToURI) of - true -> - LUser = jid:nodeprep(ToURI#uri.user), - LServer = jid:nameprep(ToURI#uri.host), - case mod_sip_registrar:find_sockets(LUser, LServer) of - [_|_] = SIPSocks -> - {ok, SIPSocks}; - [] -> - {error, notfound} - end; - false -> - case esip:connect(Req, Opts) of - {ok, SIPSock} -> - {ok, [{SIPSock, Req#sip.uri}]}; - {error, _} = Err -> - Err - end + true -> + LUser = jid:nodeprep(ToURI#uri.user), + LServer = jid:nameprep(ToURI#uri.host), + case mod_sip_registrar:find_sockets(LUser, LServer) of + [_ | _] = SIPSocks -> + {ok, SIPSocks}; + [] -> + {error, notfound} + end; + false -> + case esip:connect(Req, Opts) of + {ok, SIPSock} -> + {ok, [{SIPSock, Req#sip.uri}]}; + {error, _} = Err -> + Err + end end. + cancel_pending_transactions(State) -> lists:foreach(fun esip:cancel/1, State#state.tr_ids). + add_certfile(LServer, Opts) -> case ejabberd_pkix:get_certfile(LServer) of - {ok, CertFile} -> - [{certfile, CertFile}|Opts]; - error -> - Opts + {ok, CertFile} -> + [{certfile, CertFile} | Opts]; + error -> + Opts end. + add_via(#sip_socket{type = Transport}, LServer, #sip{hdrs = Hdrs} = Req) -> ConfiguredVias = get_configured_vias(LServer), {ViaHost, ViaPort} = proplists:get_value( - Transport, ConfiguredVias, {LServer, undefined}), + Transport, ConfiguredVias, {LServer, undefined}), ViaTransport = case Transport of - tls -> <<"TLS">>; - tcp -> <<"TCP">>; - udp -> <<"UDP">> - end, - Via = #via{transport = ViaTransport, - host = ViaHost, - port = ViaPort, - params = [{<<"branch">>, esip:make_branch()}]}, - Req#sip{hdrs = [{'via', [Via]}|Hdrs]}. + tls -> <<"TLS">>; + tcp -> <<"TCP">>; + udp -> <<"UDP">> + end, + Via = #via{ + transport = ViaTransport, + host = ViaHost, + port = ViaPort, + params = [{<<"branch">>, esip:make_branch()}] + }, + Req#sip{hdrs = [{'via', [Via]} | Hdrs]}. + add_record_route_and_set_uri(URI, LServer, #sip{hdrs = Hdrs} = Req) -> case is_request_within_dialog(Req) of - false -> - case need_record_route(LServer) of - true -> - RR_URI = get_configured_record_route(LServer), - TS = (integer_to_binary(erlang:system_time(second))), - Sign = make_sign(TS, Hdrs), - User = <>, - NewRR_URI = RR_URI#uri{user = User}, - Hdrs1 = [{'record-route', [{<<>>, NewRR_URI, []}]}|Hdrs], - Req#sip{uri = URI, hdrs = Hdrs1}; - false -> - Req - end; - true -> - Req + false -> + case need_record_route(LServer) of + true -> + RR_URI = get_configured_record_route(LServer), + TS = (integer_to_binary(erlang:system_time(second))), + Sign = make_sign(TS, Hdrs), + User = <>, + NewRR_URI = RR_URI#uri{user = User}, + Hdrs1 = [{'record-route', [{<<>>, NewRR_URI, []}]} | Hdrs], + Req#sip{uri = URI, hdrs = Hdrs1}; + false -> + Req + end; + true -> + Req end. + is_request_within_dialog(#sip{hdrs = Hdrs}) -> {_, _, Params} = esip:get_hdr('to', Hdrs), esip:has_param(<<"tag">>, Params). + need_record_route(LServer) -> mod_sip_opt:always_record_route(LServer). + make_sign(TS, Hdrs) -> {_, #uri{user = FUser, host = FServer}, FParams} = esip:get_hdr('from', Hdrs), {_, #uri{user = TUser, host = TServer}, _} = esip:get_hdr('to', Hdrs), @@ -328,99 +380,117 @@ make_sign(TS, Hdrs) -> CallID = esip:get_hdr('call-id', Hdrs), SharedKey = ejabberd_config:get_shared_key(), str:sha([SharedKey, LFUser, LFServer, LTUser, LTServer, - FromTag, CallID, TS]). + FromTag, CallID, TS]). + is_signed_by_me(TS_Sign, Hdrs) -> try - [TSBin, Sign] = str:tokens(TS_Sign, <<"-">>), - TS = (binary_to_integer(TSBin)), - NowTS = erlang:system_time(second), - true = (NowTS - TS) =< ?SIGN_LIFETIME, - Sign == make_sign(TSBin, Hdrs) - catch _:_ -> - false + [TSBin, Sign] = str:tokens(TS_Sign, <<"-">>), + TS = (binary_to_integer(TSBin)), + NowTS = erlang:system_time(second), + true = (NowTS - TS) =< ?SIGN_LIFETIME, + Sign == make_sign(TSBin, Hdrs) + catch + _:_ -> + false end. + get_configured_vias(LServer) -> mod_sip_opt:via(LServer). + get_configured_record_route(LServer) -> mod_sip_opt:record_route(LServer). + get_configured_routes(LServer) -> mod_sip_opt:routes(LServer). + mark_transaction_as_complete(TrID, State) -> NewTrIDs = lists:delete(TrID, State#state.tr_ids), State#state{tr_ids = NewTrIDs}. + collect_response(Resp, #state{responses = Resps} = State) -> - State#state{responses = [Resp|Resps]}. + State#state{responses = [Resp | Resps]}. + choose_best_response(#state{responses = Responses} = State) -> SortedResponses = lists:keysort(#sip.status, Responses), case lists:filter( - fun(#sip{status = Status}) -> - Status >= 600 - end, SortedResponses) of - [Resp|_] -> - esip:reply(State#state.orig_trid, Resp); - [] -> - case SortedResponses of - [Resp|_] -> - esip:reply(State#state.orig_trid, Resp); - [] -> - ok - end + fun(#sip{status = Status}) -> + Status >= 600 + end, + SortedResponses) of + [Resp | _] -> + esip:reply(State#state.orig_trid, Resp); + [] -> + case SortedResponses of + [Resp | _] -> + esip:reply(State#state.orig_trid, Resp); + [] -> + ok + end end. + %% Just compare host part only. cmp_uri(#uri{host = H1}, #uri{host = H2}) -> jid:nameprep(H1) == jid:nameprep(H2). + is_my_route(URI, URIs) -> lists:any(fun(U) -> cmp_uri(URI, U) end, URIs). + prepare_request(LServer, #sip{hdrs = Hdrs} = Req) -> ConfiguredRRoute = get_configured_record_route(LServer), ConfiguredRoutes = get_configured_routes(LServer), Hdrs1 = lists:flatmap( - fun({Hdr, HdrList}) when Hdr == 'route'; - Hdr == 'record-route' -> - case lists:filter( - fun({_, URI, _}) -> - not cmp_uri(URI, ConfiguredRRoute) - and not is_my_route(URI, ConfiguredRoutes) - end, HdrList) of - [] -> - []; - HdrList1 -> - [{Hdr, HdrList1}] - end; - (Hdr) -> - [Hdr] - end, Hdrs), + fun({Hdr, HdrList}) when Hdr == 'route'; + Hdr == 'record-route' -> + case lists:filter( + fun({_, URI, _}) -> + not cmp_uri(URI, ConfiguredRRoute) and + not is_my_route(URI, ConfiguredRoutes) + end, + HdrList) of + [] -> + []; + HdrList1 -> + [{Hdr, HdrList1}] + end; + (Hdr) -> + [Hdr] + end, + Hdrs), MF = esip:get_hdr('max-forwards', Hdrs1), - Hdrs2 = esip:set_hdr('max-forwards', MF-1, Hdrs1), + Hdrs2 = esip:set_hdr('max-forwards', MF - 1, Hdrs1), Hdrs3 = lists:filter( fun({'proxy-authorization', {_, Params}}) -> Realm = esip:unquote(esip:get_param(<<"realm">>, Params)), - not mod_sip:is_my_host(jid:nameprep(Realm)); + not mod_sip:is_my_host(jid:nameprep(Realm)); (_) -> true - end, Hdrs2), + end, + Hdrs2), Req#sip{hdrs = Hdrs3}. + safe_nodeprep(S) -> case jid:nodeprep(S) of - error -> S; - S1 -> S1 + error -> S; + S1 -> S1 end. + safe_nameprep(S) -> case jid:nameprep(S) of - error -> S; - S1 -> S1 + error -> S; + S1 -> S1 end. + -endif. diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl index ade4c0be0..594e681b2 100644 --- a/src/mod_sip_registrar.erl +++ b/src/mod_sip_registrar.erl @@ -37,34 +37,43 @@ -export([start_link/0, request/2, find_sockets/2, ping/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). + -include_lib("esip/include/esip.hrl"). --define(CALL_TIMEOUT, timer:seconds(30)). +-define(CALL_TIMEOUT, timer:seconds(30)). -define(DEFAULT_EXPIRES, 3600). --record(sip_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - socket = #sip_socket{} :: #sip_socket{}, - call_id = <<"">> :: binary(), - cseq = 0 :: non_neg_integer(), - timestamp = erlang:timestamp() :: erlang:timestamp(), - contact :: {binary(), #uri{}, [{binary(), binary()}]}, - flow_tref :: reference() | undefined, - reg_tref = make_ref() :: reference(), - conn_mref = make_ref() :: reference(), - expires = 0 :: non_neg_integer()}). +-record(sip_session, { + us = {<<"">>, <<"">>} :: {binary(), binary()}, + socket = #sip_socket{} :: #sip_socket{}, + call_id = <<"">> :: binary(), + cseq = 0 :: non_neg_integer(), + timestamp = erlang:timestamp() :: erlang:timestamp(), + contact :: {binary(), #uri{}, [{binary(), binary()}]}, + flow_tref :: reference() | undefined, + reg_tref = make_ref() :: reference(), + conn_mref = make_ref() :: reference(), + expires = 0 :: non_neg_integer() + }). -record(state, {}). + %%%=================================================================== %%% API %%%=================================================================== start_link() -> ?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []). + request(#sip{hdrs = Hdrs} = Req, SIPSock) -> {_, #uri{user = U, host = S}, _} = esip:get_hdr('to', Hdrs), LUser = jid:nodeprep(U), @@ -79,116 +88,156 @@ request(#sip{hdrs = Hdrs} = Req, SIPSock) -> case esip:get_hdrs('contact', Hdrs) of [<<"*">>] when Expires == 0 -> case unregister_session(US, CallID, CSeq) of - {ok, ContactsWithExpires} -> - ?INFO_MSG("Unregister SIP session for user ~ts@~ts from ~ts", - [LUser, LServer, inet_parse:ntoa(PeerIP)]), - Cs = prepare_contacts_to_send(ContactsWithExpires), - mod_sip:make_response( - Req, - #sip{type = response, - status = 200, - hdrs = [{'contact', Cs}]}); - {error, Why} -> - {Status, Reason} = make_status(Why), - mod_sip:make_response( - Req, #sip{type = response, - status = Status, - reason = Reason}) - end; - [{_, _URI, _Params}|_] = Contacts -> - ContactsWithExpires = make_contacts_with_expires(Contacts, Expires), - ContactsHaveManyRegID = contacts_have_many_reg_id(Contacts), - Expires1 = lists:max([E || {_, E} <- ContactsWithExpires]), - MinExpires = min_expires(), - if Expires1 > 0, Expires1 < MinExpires -> - mod_sip:make_response( - Req, #sip{type = response, - status = 423, - hdrs = [{'min-expires', MinExpires}]}); - ContactsHaveManyRegID -> - mod_sip:make_response( - Req, #sip{type = response, status = 400, - reason = <<"Multiple 'reg-id' parameter">>}); - true -> - case register_session(US, SIPSock, CallID, CSeq, - IsOutboundSupported, - ContactsWithExpires) of - {ok, Res} -> - ?INFO_MSG("~ts SIP session for user ~ts@~ts from ~ts", - [Res, LUser, LServer, - inet_parse:ntoa(PeerIP)]), - Cs = prepare_contacts_to_send(ContactsWithExpires), - Require = case need_ob_hdrs( - Contacts, IsOutboundSupported) of - true -> [{'require', [<<"outbound">>]}, - {'flow-timer', - get_flow_timeout(LServer, SIPSock)}]; - false -> [] - end, - mod_sip:make_response( - Req, - #sip{type = response, - status = 200, - hdrs = [{'contact', Cs}|Require]}); - {error, Why} -> - {Status, Reason} = make_status(Why), - mod_sip:make_response( - Req, #sip{type = response, - status = Status, - reason = Reason}) - end + {ok, ContactsWithExpires} -> + ?INFO_MSG("Unregister SIP session for user ~ts@~ts from ~ts", + [LUser, LServer, inet_parse:ntoa(PeerIP)]), + Cs = prepare_contacts_to_send(ContactsWithExpires), + mod_sip:make_response( + Req, + #sip{ + type = response, + status = 200, + hdrs = [{'contact', Cs}] + }); + {error, Why} -> + {Status, Reason} = make_status(Why), + mod_sip:make_response( + Req, + #sip{ + type = response, + status = Status, + reason = Reason + }) + end; + [{_, _URI, _Params} | _] = Contacts -> + ContactsWithExpires = make_contacts_with_expires(Contacts, Expires), + ContactsHaveManyRegID = contacts_have_many_reg_id(Contacts), + Expires1 = lists:max([ E || {_, E} <- ContactsWithExpires ]), + MinExpires = min_expires(), + if + Expires1 > 0, Expires1 < MinExpires -> + mod_sip:make_response( + Req, + #sip{ + type = response, + status = 423, + hdrs = [{'min-expires', MinExpires}] + }); + ContactsHaveManyRegID -> + mod_sip:make_response( + Req, + #sip{ + type = response, + status = 400, + reason = <<"Multiple 'reg-id' parameter">> + }); + true -> + case register_session(US, + SIPSock, + CallID, + CSeq, + IsOutboundSupported, + ContactsWithExpires) of + {ok, Res} -> + ?INFO_MSG("~ts SIP session for user ~ts@~ts from ~ts", + [Res, + LUser, + LServer, + inet_parse:ntoa(PeerIP)]), + Cs = prepare_contacts_to_send(ContactsWithExpires), + Require = case need_ob_hdrs( + Contacts, IsOutboundSupported) of + true -> + [{'require', [<<"outbound">>]}, + {'flow-timer', + get_flow_timeout(LServer, SIPSock)}]; + false -> [] + end, + mod_sip:make_response( + Req, + #sip{ + type = response, + status = 200, + hdrs = [{'contact', Cs} | Require] + }); + {error, Why} -> + {Status, Reason} = make_status(Why), + mod_sip:make_response( + Req, + #sip{ + type = response, + status = Status, + reason = Reason + }) + end + end; + [] -> + case mnesia:dirty_read(sip_session, US) of + [_ | _] = Sessions -> + ContactsWithExpires = + lists:map( + fun(#sip_session{contact = Contact, expires = Es}) -> + {Contact, Es} + end, + Sessions), + Cs = prepare_contacts_to_send(ContactsWithExpires), + mod_sip:make_response( + Req, + #sip{ + type = response, + status = 200, + hdrs = [{'contact', Cs}] + }); + [] -> + {Status, Reason} = make_status(notfound), + mod_sip:make_response( + Req, + #sip{ + type = response, + status = Status, + reason = Reason + }) end; - [] -> - case mnesia:dirty_read(sip_session, US) of - [_|_] = Sessions -> - ContactsWithExpires = - lists:map( - fun(#sip_session{contact = Contact, expires = Es}) -> - {Contact, Es} - end, Sessions), - Cs = prepare_contacts_to_send(ContactsWithExpires), - mod_sip:make_response( - Req, #sip{type = response, status = 200, - hdrs = [{'contact', Cs}]}); - [] -> - {Status, Reason} = make_status(notfound), - mod_sip:make_response( - Req, #sip{type = response, - status = Status, - reason = Reason}) - end; _ -> mod_sip:make_response(Req, #sip{type = response, status = 400}) end. + find_sockets(U, S) -> case mnesia:dirty_read(sip_session, {U, S}) of - [_|_] = Sessions -> - lists:map( - fun(#sip_session{contact = {_, URI, _}, - socket = Socket}) -> - {Socket, URI} - end, Sessions); - [] -> - [] + [_ | _] = Sessions -> + lists:map( + fun(#sip_session{ + contact = {_, URI, _}, + socket = Socket + }) -> + {Socket, URI} + end, + Sessions); + [] -> + [] end. + ping(SIPSocket) -> call({ping, SIPSocket}). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> process_flag(trap_exit, true), update_table(), - ejabberd_mnesia:create(?MODULE, sip_session, - [{ram_copies, [node()]}, - {type, bag}, - {attributes, record_info(fields, sip_session)}, - {index, [conn_mref,socket]}]), + ejabberd_mnesia:create(?MODULE, + sip_session, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, sip_session)}, + {index, [conn_mref, socket]}]), {ok, #state{}}. + handle_call({write, Sessions, Supported}, _From, State) -> Res = write_session(Sessions, Supported), {reply, Res, State}; @@ -202,10 +251,12 @@ handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info({write, Sessions, Supported}, State) -> write_session(Sessions, Supported), {noreply, State}; @@ -217,145 +268,178 @@ handle_info({timeout, TRef, US}, State) -> {noreply, State}; handle_info({'DOWN', MRef, process, _Pid, _Reason}, State) -> case mnesia:dirty_index_read(sip_session, MRef, #sip_session.conn_mref) of - [Session] -> - mnesia:dirty_delete_object(Session); - _ -> - ok + [Session] -> + mnesia:dirty_delete_object(Session); + _ -> + ok end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== -register_session(US, SIPSocket, CallID, CSeq, IsOutboundSupported, - ContactsWithExpires) -> +register_session(US, + SIPSocket, + CallID, + CSeq, + IsOutboundSupported, + ContactsWithExpires) -> Sessions = lists:map( - fun({Contact, Expires}) -> - #sip_session{us = US, - socket = SIPSocket, - call_id = CallID, - cseq = CSeq, - timestamp = erlang:timestamp(), - contact = Contact, - expires = Expires} - end, ContactsWithExpires), + fun({Contact, Expires}) -> + #sip_session{ + us = US, + socket = SIPSocket, + call_id = CallID, + cseq = CSeq, + timestamp = erlang:timestamp(), + contact = Contact, + expires = Expires + } + end, + ContactsWithExpires), Msg = {write, Sessions, IsOutboundSupported}, call(Msg). + unregister_session(US, CallID, CSeq) -> Msg = {delete, US, CallID, CSeq}, call(Msg). -write_session([#sip_session{us = {U, S} = US}|_] = NewSessions, - IsOutboundSupported) -> + +write_session([#sip_session{us = {U, S} = US} | _] = NewSessions, + IsOutboundSupported) -> PrevSessions = mnesia:dirty_read(sip_session, US), Res = lists:foldl( - fun(_, {error, _} = Err) -> - Err; - (#sip_session{call_id = CallID, - expires = Expires, - cseq = CSeq} = Session, {Add, Del}) -> - case find_session(Session, PrevSessions, - IsOutboundSupported) of - {ok, normal, #sip_session{call_id = CallID, - cseq = PrevCSeq}} - when PrevCSeq > CSeq -> - {error, cseq_out_of_order}; - {ok, _Type, PrevSession} when Expires == 0 -> - {Add, [PrevSession|Del]}; - {ok, _Type, PrevSession} -> - {[Session|Add], [PrevSession|Del]}; - {error, notfound} when Expires == 0 -> - {error, notfound}; - {error, notfound} -> - {[Session|Add], Del} - end - end, {[], []}, NewSessions), + fun(_, {error, _} = Err) -> + Err; + (#sip_session{ + call_id = CallID, + expires = Expires, + cseq = CSeq + } = Session, + {Add, Del}) -> + case find_session(Session, + PrevSessions, + IsOutboundSupported) of + {ok, normal, + #sip_session{ + call_id = CallID, + cseq = PrevCSeq + }} + when PrevCSeq > CSeq -> + {error, cseq_out_of_order}; + {ok, _Type, PrevSession} when Expires == 0 -> + {Add, [PrevSession | Del]}; + {ok, _Type, PrevSession} -> + {[Session | Add], [PrevSession | Del]}; + {error, notfound} when Expires == 0 -> + {error, notfound}; + {error, notfound} -> + {[Session | Add], Del} + end + end, + {[], []}, + NewSessions), MaxSessions = ejabberd_sm:get_max_user_sessions(U, S), case Res of - {error, Why} -> - {error, Why}; - {AddSessions, DelSessions} -> - MaxSessions = ejabberd_sm:get_max_user_sessions(U, S), - AllSessions = AddSessions ++ PrevSessions -- DelSessions, - if length(AllSessions) > MaxSessions -> - {error, too_many_sessions}; - true -> - lists:foreach(fun delete_session/1, DelSessions), - lists:foreach( - fun(Session) -> - NewSession = set_monitor_and_timer( - Session, IsOutboundSupported), - mnesia:dirty_write(NewSession) - end, AddSessions), - case {AllSessions, AddSessions} of - {[], _} -> - {ok, unregister}; - {_, []} -> - {ok, unregister}; - _ -> - {ok, register} - end - end + {error, Why} -> + {error, Why}; + {AddSessions, DelSessions} -> + MaxSessions = ejabberd_sm:get_max_user_sessions(U, S), + AllSessions = AddSessions ++ PrevSessions -- DelSessions, + if + length(AllSessions) > MaxSessions -> + {error, too_many_sessions}; + true -> + lists:foreach(fun delete_session/1, DelSessions), + lists:foreach( + fun(Session) -> + NewSession = set_monitor_and_timer( + Session, IsOutboundSupported), + mnesia:dirty_write(NewSession) + end, + AddSessions), + case {AllSessions, AddSessions} of + {[], _} -> + {ok, unregister}; + {_, []} -> + {ok, unregister}; + _ -> + {ok, register} + end + end end. + delete_session(US, CallID, CSeq) -> case mnesia:dirty_read(sip_session, US) of - [_|_] = Sessions -> - case lists:all( - fun(S) when S#sip_session.call_id == CallID, - S#sip_session.cseq > CSeq -> - false; - (_) -> - true - end, Sessions) of - true -> - ContactsWithExpires = - lists:map( - fun(#sip_session{contact = Contact} = Session) -> - delete_session(Session), - {Contact, 0} - end, Sessions), - {ok, ContactsWithExpires}; - false -> - {error, cseq_out_of_order} - end; - [] -> - {error, notfound} + [_ | _] = Sessions -> + case lists:all( + fun(S) when S#sip_session.call_id == CallID, + S#sip_session.cseq > CSeq -> + false; + (_) -> + true + end, + Sessions) of + true -> + ContactsWithExpires = + lists:map( + fun(#sip_session{contact = Contact} = Session) -> + delete_session(Session), + {Contact, 0} + end, + Sessions), + {ok, ContactsWithExpires}; + false -> + {error, cseq_out_of_order} + end; + [] -> + {error, notfound} end. + delete_expired_session(US, TRef) -> case mnesia:dirty_read(sip_session, US) of - [_|_] = Sessions -> - lists:foreach( - fun(#sip_session{reg_tref = T1, - flow_tref = T2} = Session) - when T1 == TRef; T2 == TRef -> - if T2 /= undefined -> - close_socket(Session); - true -> - ok - end, - delete_session(Session); - (_) -> - ok - end, Sessions); - [] -> - ok + [_ | _] = Sessions -> + lists:foreach( + fun(#sip_session{ + reg_tref = T1, + flow_tref = T2 + } = Session) + when T1 == TRef; T2 == TRef -> + if + T2 /= undefined -> + close_socket(Session); + true -> + ok + end, + delete_session(Session); + (_) -> + ok + end, + Sessions); + [] -> + ok end. + min_expires() -> 60. + to_integer(Bin, Min, Max) -> case catch (binary_to_integer(Bin)) of N when N >= Min, N =< Max -> @@ -364,97 +448,114 @@ to_integer(Bin, Min, Max) -> error end. + call(Msg) -> case catch ?GEN_SERVER:call(?MODULE, Msg, ?CALL_TIMEOUT) of - {'EXIT', {timeout, _}} -> - {error, timeout}; - {'EXIT', Why} -> - {error, Why}; - Reply -> - Reply + {'EXIT', {timeout, _}} -> + {error, timeout}; + {'EXIT', Why} -> + {error, Why}; + Reply -> + Reply end. + make_contacts_with_expires(Contacts, Expires) -> lists:map( fun({Name, URI, Params}) -> - E1 = case to_integer(esip:get_param(<<"expires">>, Params), - 0, (1 bsl 32)-1) of - {ok, E} -> E; - _ -> Expires - end, - Params1 = lists:keydelete(<<"expires">>, 1, Params), - {{Name, URI, Params1}, E1} - end, Contacts). + E1 = case to_integer(esip:get_param(<<"expires">>, Params), + 0, + (1 bsl 32) - 1) of + {ok, E} -> E; + _ -> Expires + end, + Params1 = lists:keydelete(<<"expires">>, 1, Params), + {{Name, URI, Params1}, E1} + end, + Contacts). + prepare_contacts_to_send(ContactsWithExpires) -> lists:map( fun({{Name, URI, Params}, Expires}) -> - Params1 = esip:set_param(<<"expires">>, - list_to_binary( - integer_to_list(Expires)), - Params), - {Name, URI, Params1} - end, ContactsWithExpires). + Params1 = esip:set_param(<<"expires">>, + list_to_binary( + integer_to_list(Expires)), + Params), + {Name, URI, Params1} + end, + ContactsWithExpires). + contacts_have_many_reg_id(Contacts) -> Sum = lists:foldl( - fun({_Name, _URI, Params}, Acc) -> - case get_ob_params(Params) of - error -> - Acc; - {_, _} -> - Acc + 1 - end - end, 0, Contacts), - if Sum > 1 -> - true; - true -> - false + fun({_Name, _URI, Params}, Acc) -> + case get_ob_params(Params) of + error -> + Acc; + {_, _} -> + Acc + 1 + end + end, + 0, + Contacts), + if + Sum > 1 -> + true; + true -> + false end. -find_session(#sip_session{contact = {_, URI, Params}}, Sessions, - IsOutboundSupported) -> - if IsOutboundSupported -> - case get_ob_params(Params) of - {InstanceID, RegID} -> - find_session_by_ob({InstanceID, RegID}, Sessions); - error -> - find_session_by_uri(URI, Sessions) - end; - true -> - find_session_by_uri(URI, Sessions) + +find_session(#sip_session{contact = {_, URI, Params}}, + Sessions, + IsOutboundSupported) -> + if + IsOutboundSupported -> + case get_ob_params(Params) of + {InstanceID, RegID} -> + find_session_by_ob({InstanceID, RegID}, Sessions); + error -> + find_session_by_uri(URI, Sessions) + end; + true -> + find_session_by_uri(URI, Sessions) end. + find_session_by_ob({InstanceID, RegID}, - [#sip_session{contact = {_, _, Params}} = Session|Sessions]) -> + [#sip_session{contact = {_, _, Params}} = Session | Sessions]) -> case get_ob_params(Params) of - {InstanceID, RegID} -> - {ok, flow, Session}; - _ -> - find_session_by_ob({InstanceID, RegID}, Sessions) + {InstanceID, RegID} -> + {ok, flow, Session}; + _ -> + find_session_by_ob({InstanceID, RegID}, Sessions) end; find_session_by_ob(_, []) -> {error, notfound}. + find_session_by_uri(URI1, - [#sip_session{contact = {_, URI2, _}} = Session|Sessions]) -> + [#sip_session{contact = {_, URI2, _}} = Session | Sessions]) -> case cmp_uri(URI1, URI2) of - true -> - {ok, normal, Session}; - false -> - find_session_by_uri(URI1, Sessions) + true -> + {ok, normal, Session}; + false -> + find_session_by_uri(URI1, Sessions) end; find_session_by_uri(_, []) -> {error, notfound}. + %% TODO: this is *totally* wrong. %% Rewrite this using URI comparison rules cmp_uri(#uri{user = U, host = H, port = P}, - #uri{user = U, host = H, port = P}) -> + #uri{user = U, host = H, port = P}) -> true; cmp_uri(_, _) -> false. + make_status(notfound) -> {404, esip:reason(404)}; make_status(cseq_out_of_order) -> @@ -466,112 +567,141 @@ make_status(too_many_sessions) -> make_status(_) -> {500, esip:reason(500)}. + get_ob_params(Params) -> case esip:get_param(<<"+sip.instance">>, Params) of - <<>> -> - error; - InstanceID -> - case to_integer(esip:get_param(<<"reg-id">>, Params), - 0, (1 bsl 32)-1) of - {ok, RegID} -> - {InstanceID, RegID}; - error -> - error - end + <<>> -> + error; + InstanceID -> + case to_integer(esip:get_param(<<"reg-id">>, Params), + 0, + (1 bsl 32) - 1) of + {ok, RegID} -> + {InstanceID, RegID}; + error -> + error + end end. + need_ob_hdrs(_Contacts, _IsOutboundSupported = false) -> false; need_ob_hdrs(Contacts, _IsOutboundSupported = true) -> lists:any( fun({_Name, _URI, Params}) -> - case get_ob_params(Params) of - error -> false; - {_, _} -> true - end - end, Contacts). + case get_ob_params(Params) of + error -> false; + {_, _} -> true + end + end, + Contacts). + get_flow_timeout(LServer, #sip_socket{type = Type}) -> case Type of - udp -> - mod_sip_opt:flow_timeout_udp(LServer) div 1000; - _ -> - mod_sip_opt:flow_timeout_tcp(LServer) div 1000 + udp -> + mod_sip_opt:flow_timeout_udp(LServer) div 1000; + _ -> + mod_sip_opt:flow_timeout_tcp(LServer) div 1000 end. + update_table() -> Fields = record_info(fields, sip_session), case catch mnesia:table_info(sip_session, attributes) of - Fields -> - ok; - [_|_] -> - mnesia:delete_table(sip_session); - {'EXIT', _} -> - ok + Fields -> + ok; + [_ | _] -> + mnesia:delete_table(sip_session); + {'EXIT', _} -> + ok end. -set_monitor_and_timer(#sip_session{socket = #sip_socket{type = Type, - pid = Pid} = SIPSock, - conn_mref = MRef, - expires = Expires, - us = {_, LServer}, - contact = {_, _, Params}} = Session, - IsOutboundSupported) -> + +set_monitor_and_timer(#sip_session{ + socket = #sip_socket{ + type = Type, + pid = Pid + } = SIPSock, + conn_mref = MRef, + expires = Expires, + us = {_, LServer}, + contact = {_, _, Params} + } = Session, + IsOutboundSupported) -> RegTRef = set_timer(Session, Expires), Session1 = Session#sip_session{reg_tref = RegTRef}, - if IsOutboundSupported -> - case get_ob_params(Params) of - error -> - Session1; - {_, _} -> - FlowTimeout = get_flow_timeout(LServer, SIPSock), - FlowTRef = set_timer(Session1, FlowTimeout), - NewMRef = if Type == udp -> MRef; - true -> erlang:monitor(process, Pid) - end, - Session1#sip_session{conn_mref = NewMRef, - flow_tref = FlowTRef} - end; - true -> - Session1 + if + IsOutboundSupported -> + case get_ob_params(Params) of + error -> + Session1; + {_, _} -> + FlowTimeout = get_flow_timeout(LServer, SIPSock), + FlowTRef = set_timer(Session1, FlowTimeout), + NewMRef = if + Type == udp -> MRef; + true -> erlang:monitor(process, Pid) + end, + Session1#sip_session{ + conn_mref = NewMRef, + flow_tref = FlowTRef + } + end; + true -> + Session1 end. + set_timer(#sip_session{us = US}, Timeout) -> erlang:start_timer(Timeout * 1000, self(), US). + close_socket(#sip_session{socket = SIPSocket}) -> - if SIPSocket#sip_socket.type /= udp -> - esip_socket:close(SIPSocket); - true -> - ok + if + SIPSocket#sip_socket.type /= udp -> + esip_socket:close(SIPSocket); + true -> + ok end. -delete_session(#sip_session{reg_tref = RegTRef, - flow_tref = FlowTRef, - conn_mref = MRef} = Session) -> + +delete_session(#sip_session{ + reg_tref = RegTRef, + flow_tref = FlowTRef, + conn_mref = MRef + } = Session) -> misc:cancel_timer(RegTRef), misc:cancel_timer(FlowTRef), catch erlang:demonitor(MRef, [flush]), mnesia:dirty_delete_object(Session). + process_ping(SIPSocket) -> - ErrResponse = if SIPSocket#sip_socket.type == udp -> pang; - true -> drop - end, + ErrResponse = if + SIPSocket#sip_socket.type == udp -> pang; + true -> drop + end, Sessions = mnesia:dirty_index_read( - sip_session, SIPSocket, #sip_session.socket), + sip_session, SIPSocket, #sip_session.socket), lists:foldl( - fun(#sip_session{flow_tref = TRef, - us = {_, LServer}} = Session, _) - when TRef /= undefined -> - erlang:cancel_timer(TRef), - mnesia:dirty_delete_object(Session), - Timeout = get_flow_timeout(LServer, SIPSocket), - NewTRef = set_timer(Session, Timeout), - mnesia:dirty_write(Session#sip_session{flow_tref = NewTRef}), - pong; - (_, Acc) -> - Acc - end, ErrResponse, Sessions). + fun(#sip_session{ + flow_tref = TRef, + us = {_, LServer} + } = Session, + _) + when TRef /= undefined -> + erlang:cancel_timer(TRef), + mnesia:dirty_delete_object(Session), + Timeout = get_flow_timeout(LServer, SIPSocket), + NewTRef = set_timer(Session, Timeout), + mnesia:dirty_write(Session#sip_session{flow_tref = NewTRef}), + pong; + (_, Acc) -> + Acc + end, + ErrResponse, + Sessions). + -endif. diff --git a/src/mod_stats.erl b/src/mod_stats.erl index 184407e8c..2d93d5baa 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -31,226 +31,270 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_iq/1, - mod_options/1, depends/2, mod_doc/0]). +-export([start/2, + stop/1, + reload/3, + process_iq/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). + start(_Host, _Opts) -> {ok, [{iq_handler, ejabberd_local, ?NS_STATS, process_iq}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); -process_iq(#iq{type = get, to = To, lang = Lang, - sub_els = [#stats{} = Stats]} = IQ) -> +process_iq(#iq{ + type = get, + to = To, + lang = Lang, + sub_els = [#stats{} = Stats] + } = IQ) -> Node = str:tokens(Stats#stats.node, <<"/">>), - Names = [Name || #stat{name = Name} <- Stats#stats.list], + Names = [ Name || #stat{name = Name} <- Stats#stats.list ], case get_local_stats(To#jid.server, Node, Names, Lang) of - {result, List} -> - xmpp:make_iq_result(IQ, Stats#stats{list = List}); - {error, Error} -> - xmpp:make_error(IQ, Error) + {result, List} -> + xmpp:make_iq_result(IQ, Stats#stats{list = List}); + {error, Error} -> + xmpp:make_error(IQ, Error) end. + -define(STAT(Name), #stat{name = Name}). + get_local_stats(_Server, [], [], _Lang) -> {result, - [?STAT(<<"users/online">>), ?STAT(<<"users/total">>), + [?STAT(<<"users/online">>), + ?STAT(<<"users/total">>), ?STAT(<<"users/all-hosts/online">>), ?STAT(<<"users/all-hosts/total">>)]}; get_local_stats(Server, [], Names, _Lang) -> {result, - lists:map(fun (Name) -> get_local_stat(Server, [], Name) - end, - Names)}; -get_local_stats(_Server, [<<"running nodes">>, _], - [], _Lang) -> + lists:map(fun(Name) -> get_local_stat(Server, [], Name) + end, + Names)}; +get_local_stats(_Server, + [<<"running nodes">>, _], + [], + _Lang) -> {result, - [?STAT(<<"time/uptime">>), ?STAT(<<"time/cputime">>), + [?STAT(<<"time/uptime">>), + ?STAT(<<"time/cputime">>), ?STAT(<<"users/online">>), ?STAT(<<"transactions/committed">>), ?STAT(<<"transactions/aborted">>), ?STAT(<<"transactions/restarted">>), ?STAT(<<"transactions/logged">>)]}; -get_local_stats(_Server, [<<"running nodes">>, ENode], - Names, Lang) -> +get_local_stats(_Server, + [<<"running nodes">>, ENode], + Names, + Lang) -> case search_running_node(ENode) of - false -> - Txt = ?T("No running node found"), - {error, xmpp:err_item_not_found(Txt, Lang)}; - Node -> - {result, - lists:map(fun (Name) -> get_node_stat(Node, Name) end, - Names)} + false -> + Txt = ?T("No running node found"), + {error, xmpp:err_item_not_found(Txt, Lang)}; + Node -> + {result, + lists:map(fun(Name) -> get_node_stat(Node, Name) end, + Names)} end; get_local_stats(_Server, _, _, Lang) -> Txt = ?T("No statistics found for this item"), {error, xmpp:err_feature_not_implemented(Txt, Lang)}. + -define(STATVAL(Val, Unit), #stat{name = Name, units = Unit, value = Val}). -define(STATERR(Code, Desc), - #stat{name = Name, - error = #stat_error{code = Code, reason = Desc}}). + #stat{ + name = Name, + error = #stat_error{code = Code, reason = Desc} + }). + get_local_stat(Server, [], Name) - when Name == <<"users/online">> -> + when Name == <<"users/online">> -> case catch ejabberd_sm:get_vh_session_list(Server) of - {'EXIT', _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - Users -> - ?STATVAL((integer_to_binary(length(Users))), - <<"users">>) + {'EXIT', _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + Users -> + ?STATVAL((integer_to_binary(length(Users))), + <<"users">>) end; get_local_stat(Server, [], Name) - when Name == <<"users/total">> -> - case catch - ejabberd_auth:count_users(Server) - of - {'EXIT', _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - NUsers -> - ?STATVAL((integer_to_binary(NUsers)), - <<"users">>) + when Name == <<"users/total">> -> + case catch ejabberd_auth:count_users(Server) of + {'EXIT', _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + NUsers -> + ?STATVAL((integer_to_binary(NUsers)), + <<"users">>) end; get_local_stat(_Server, [], Name) - when Name == <<"users/all-hosts/online">> -> + when Name == <<"users/all-hosts/online">> -> Users = ejabberd_sm:connected_users_number(), ?STATVAL((integer_to_binary(Users)), <<"users">>); get_local_stat(_Server, [], Name) - when Name == <<"users/all-hosts/total">> -> - NumUsers = lists:foldl(fun (Host, Total) -> - ejabberd_auth:count_users(Host) - + Total - end, - 0, ejabberd_option:hosts()), + when Name == <<"users/all-hosts/total">> -> + NumUsers = lists:foldl(fun(Host, Total) -> + ejabberd_auth:count_users(Host) + + Total + end, + 0, + ejabberd_option:hosts()), ?STATVAL((integer_to_binary(NumUsers)), - <<"users">>); + <<"users">>); get_local_stat(_Server, _, Name) -> ?STATERR(404, <<"Not Found">>). + get_node_stat(Node, Name) - when Name == <<"time/uptime">> -> - case catch ejabberd_cluster:call(Node, erlang, statistics, - [wall_clock]) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - CPUTime -> - ?STATVAL(str:format("~.3f", [element(1, CPUTime) / 1000]), - <<"seconds">>) + when Name == <<"time/uptime">> -> + case catch ejabberd_cluster:call(Node, + erlang, + statistics, + [wall_clock]) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + CPUTime -> + ?STATVAL(str:format("~.3f", [element(1, CPUTime) / 1000]), + <<"seconds">>) end; get_node_stat(Node, Name) - when Name == <<"time/cputime">> -> - case catch ejabberd_cluster:call(Node, erlang, statistics, [runtime]) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - RunTime -> - ?STATVAL(str:format("~.3f", [element(1, RunTime) / 1000]), - <<"seconds">>) + when Name == <<"time/cputime">> -> + case catch ejabberd_cluster:call(Node, erlang, statistics, [runtime]) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + RunTime -> + ?STATVAL(str:format("~.3f", [element(1, RunTime) / 1000]), + <<"seconds">>) end; get_node_stat(Node, Name) - when Name == <<"users/online">> -> - case catch ejabberd_cluster:call(Node, ejabberd_sm, - dirty_get_my_sessions_list, []) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - Users -> - ?STATVAL((integer_to_binary(length(Users))), - <<"users">>) + when Name == <<"users/online">> -> + case catch ejabberd_cluster:call(Node, + ejabberd_sm, + dirty_get_my_sessions_list, + []) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + Users -> + ?STATVAL((integer_to_binary(length(Users))), + <<"users">>) end; get_node_stat(Node, Name) - when Name == <<"transactions/committed">> -> - case catch ejabberd_cluster:call(Node, mnesia, system_info, - [transaction_commits]) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - Transactions -> - ?STATVAL((integer_to_binary(Transactions)), - <<"transactions">>) + when Name == <<"transactions/committed">> -> + case catch ejabberd_cluster:call(Node, + mnesia, + system_info, + [transaction_commits]) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + Transactions -> + ?STATVAL((integer_to_binary(Transactions)), + <<"transactions">>) end; get_node_stat(Node, Name) - when Name == <<"transactions/aborted">> -> - case catch ejabberd_cluster:call(Node, mnesia, system_info, - [transaction_failures]) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - Transactions -> - ?STATVAL((integer_to_binary(Transactions)), - <<"transactions">>) + when Name == <<"transactions/aborted">> -> + case catch ejabberd_cluster:call(Node, + mnesia, + system_info, + [transaction_failures]) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + Transactions -> + ?STATVAL((integer_to_binary(Transactions)), + <<"transactions">>) end; get_node_stat(Node, Name) - when Name == <<"transactions/restarted">> -> - case catch ejabberd_cluster:call(Node, mnesia, system_info, - [transaction_restarts]) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - Transactions -> - ?STATVAL((integer_to_binary(Transactions)), - <<"transactions">>) + when Name == <<"transactions/restarted">> -> + case catch ejabberd_cluster:call(Node, + mnesia, + system_info, + [transaction_restarts]) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + Transactions -> + ?STATVAL((integer_to_binary(Transactions)), + <<"transactions">>) end; get_node_stat(Node, Name) - when Name == <<"transactions/logged">> -> - case catch ejabberd_cluster:call(Node, mnesia, system_info, - [transaction_log_writes]) - of - {badrpc, _Reason} -> - ?STATERR(500, <<"Internal Server Error">>); - Transactions -> - ?STATVAL((integer_to_binary(Transactions)), - <<"transactions">>) + when Name == <<"transactions/logged">> -> + case catch ejabberd_cluster:call(Node, + mnesia, + system_info, + [transaction_log_writes]) of + {badrpc, _Reason} -> + ?STATERR(500, <<"Internal Server Error">>); + Transactions -> + ?STATVAL((integer_to_binary(Transactions)), + <<"transactions">>) end; get_node_stat(_, Name) -> ?STATERR(404, <<"Not Found">>). + search_running_node(SNode) -> search_running_node(SNode, - mnesia:system_info(running_db_nodes)). + mnesia:system_info(running_db_nodes)). + search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case iolist_to_binary(atom_to_list(Node)) of - SNode -> Node; - _ -> search_running_node(SNode, Nodes) + SNode -> Node; + _ -> search_running_node(SNode, Nodes) end. + mod_options(_Host) -> []. + mod_doc() -> - #{desc => + #{ + desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0039.html" "[XEP-0039: Statistics Gathering]. This protocol " "allows you to retrieve the following statistics " - "from your ejabberd server:"), "", + "from your ejabberd server:"), + "", ?T("- Total number of registered users on the current " - "virtual host (users/total)."), "", + "virtual host (users/total)."), + "", ?T("- Total number of registered users on all virtual " - "hosts (users/all-hosts/total)."), "", + "hosts (users/all-hosts/total)."), + "", ?T("- Total number of online users on the current " - "virtual host (users/online)."), "", + "virtual host (users/online)."), + "", ?T("- Total number of online users on all virtual " - "hosts (users/all-hosts/online)."), "", + "hosts (users/all-hosts/online)."), + "", ?T("NOTE: The protocol extension is deferred and seems " "like even a few clients that were supporting it " "are now abandoned. So using this module makes " - "very little sense.")]}. + "very little sense.")] + }. diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl index f3a641a7a..e8922738f 100644 --- a/src/mod_stream_mgmt.erl +++ b/src/mod_stream_mgmt.erl @@ -29,13 +29,22 @@ -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% hooks --export([c2s_stream_started/2, c2s_stream_features/2, - c2s_authenticated_packet/2, c2s_unauthenticated_packet/2, - c2s_unbinded_packet/2, c2s_closed/2, c2s_terminated/2, - c2s_handle_send/3, c2s_handle_info/2, c2s_handle_cast/2, - c2s_handle_call/3, c2s_handle_recv/3, c2s_inline_features/3, - c2s_handle_sasl2_inline/1, c2s_handle_sasl2_inline_post/3, - c2s_handle_bind2_inline/1]). +-export([c2s_stream_started/2, + c2s_stream_features/2, + c2s_authenticated_packet/2, + c2s_unauthenticated_packet/2, + c2s_unbinded_packet/2, + c2s_closed/2, + c2s_terminated/2, + c2s_handle_send/3, + c2s_handle_info/2, + c2s_handle_cast/2, + c2s_handle_call/3, + c2s_handle_recv/3, + c2s_inline_features/3, + c2s_handle_sasl2_inline/1, + c2s_handle_sasl2_inline_post/3, + c2s_handle_bind2_inline/1]). %% adjust pending session timeout / access queue -export([get_resume_timeout/1, set_resume_timeout/2, queue_find/2]). @@ -43,25 +52,32 @@ -export([has_resume_data/2, post_resume_tasks/1]). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). + -include_lib("p1_utils/include/p1_queue.hrl"). + -include("translate.hrl"). -define(STREAM_MGMT_CACHE, stream_mgmt_cache). -define(is_sm_packet(Pkt), - is_record(Pkt, sm_enable) or - is_record(Pkt, sm_resume) or - is_record(Pkt, sm_a) or - is_record(Pkt, sm_r)). + is_record(Pkt, sm_enable) or + is_record(Pkt, sm_resume) or + is_record(Pkt, sm_a) or + is_record(Pkt, sm_r)). -type state() :: ejabberd_c2s:state(). -type queue() :: p1_queue:queue({non_neg_integer(), erlang:timestamp(), xmpp_element() | xmlel()}). -type id() :: binary(). --type error_reason() :: session_not_found | session_timed_out | - session_is_dead | session_has_exited | - session_was_killed | session_copy_timed_out | - invalid_previd. +-type error_reason() :: session_not_found | + session_timed_out | + session_is_dead | + session_has_exited | + session_was_killed | + session_copy_timed_out | + invalid_previd. + %%%=================================================================== %%% API @@ -70,7 +86,7 @@ start(_Host, Opts) -> init_cache(Opts), {ok, [{hook, c2s_stream_started, c2s_stream_started, 50}, {hook, c2s_post_auth_features, c2s_stream_features, 50}, - {hook, c2s_inline_features, c2s_inline_features, 50}, + {hook, c2s_inline_features, c2s_inline_features, 50}, {hook, c2s_unauthenticated_packet, c2s_unauthenticated_packet, 50}, {hook, c2s_unbinded_packet, c2s_unbinded_packet, 50}, {hook, c2s_authenticated_packet, c2s_authenticated_packet, 50}, @@ -79,187 +95,220 @@ start(_Host, Opts) -> {hook, c2s_handle_info, c2s_handle_info, 50}, {hook, c2s_handle_cast, c2s_handle_cast, 50}, {hook, c2s_handle_call, c2s_handle_call, 50}, - {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 50}, - {hook, c2s_handle_sasl2_inline_post, c2s_handle_sasl2_inline_post, 50}, - {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, + {hook, c2s_handle_sasl2_inline, c2s_handle_sasl2_inline, 50}, + {hook, c2s_handle_sasl2_inline_post, c2s_handle_sasl2_inline_post, 50}, + {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50}, {hook, c2s_closed, c2s_closed, 50}, {hook, c2s_terminated, c2s_terminated, 50}]}. + stop(_Host) -> ok. + reload(_Host, NewOpts, _OldOpts) -> init_cache(NewOpts), ?WARNING_MSG("Module ~ts is reloaded, but new configuration will take " - "effect for newly created client connections only", [?MODULE]). + "effect for newly created client connections only", + [?MODULE]). + depends(_Host, _Opts) -> []. + c2s_stream_started(#{lserver := LServer} = State, _StreamStart) -> State1 = maps:remove(mgmt_options, State), ResumeTimeout = get_configured_resume_timeout(LServer), MaxResumeTimeout = get_max_resume_timeout(LServer, ResumeTimeout), - State1#{mgmt_state => inactive, - mgmt_queue_type => get_queue_type(LServer), - mgmt_max_queue => get_max_ack_queue(LServer), - mgmt_timeout => ResumeTimeout, - mgmt_max_timeout => MaxResumeTimeout, - mgmt_ack_timeout => get_ack_timeout(LServer), - mgmt_resend => get_resend_on_timeout(LServer), - mgmt_stanzas_in => 0, - mgmt_stanzas_out => 0, - mgmt_stanzas_req => 0}; + State1#{ + mgmt_state => inactive, + mgmt_queue_type => get_queue_type(LServer), + mgmt_max_queue => get_max_ack_queue(LServer), + mgmt_timeout => ResumeTimeout, + mgmt_max_timeout => MaxResumeTimeout, + mgmt_ack_timeout => get_ack_timeout(LServer), + mgmt_resend => get_resend_on_timeout(LServer), + mgmt_stanzas_in => 0, + mgmt_stanzas_out => 0, + mgmt_stanzas_req => 0 + }; c2s_stream_started(State, _StreamStart) -> State. + c2s_stream_features(Acc, Host) -> case gen_mod:is_loaded(Host, ?MODULE) of - true -> - [#feature_sm{xmlns = ?NS_STREAM_MGMT_2}, - #feature_sm{xmlns = ?NS_STREAM_MGMT_3}|Acc]; - false -> - Acc + true -> + [#feature_sm{xmlns = ?NS_STREAM_MGMT_2}, + #feature_sm{xmlns = ?NS_STREAM_MGMT_3} | Acc]; + false -> + Acc end. + c2s_inline_features({Sasl, Bind, Extra} = Acc, Host, _State) -> case gen_mod:is_loaded(Host, ?MODULE) of - true -> - {[#feature_sm{xmlns = ?NS_STREAM_MGMT_3} | Sasl], - [#bind2_feature{var = ?NS_STREAM_MGMT_3} | Bind], - Extra}; - false -> - Acc + true -> + {[#feature_sm{xmlns = ?NS_STREAM_MGMT_3} | Sasl], + [#bind2_feature{var = ?NS_STREAM_MGMT_3} | Bind], + Extra}; + false -> + Acc end. + c2s_handle_sasl2_inline({State, Els, Results} = Acc) -> case lists:keytake(sm_resume, 1, Els) of - {value, Resume, Rest} -> - case has_resume_data(State, Resume) of - {ok, NewState, Resumed} -> - Rest2 = lists:keydelete(bind2_bind, 1, Rest), - {NewState, Rest2, [Resumed | Results]}; - {error, ResumeError, _Reason} -> - {State, Els, [ResumeError | Results]} - end; - _ -> - Acc + {value, Resume, Rest} -> + case has_resume_data(State, Resume) of + {ok, NewState, Resumed} -> + Rest2 = lists:keydelete(bind2_bind, 1, Rest), + {NewState, Rest2, [Resumed | Results]}; + {error, ResumeError, _Reason} -> + {State, Els, [ResumeError | Results]} + end; + _ -> + Acc end. + c2s_handle_sasl2_inline_post(State, _Els, Results) -> case lists:keyfind(sm_resumed, 1, Results) of - false -> - State; - _ -> - post_resume_tasks(State) + false -> + State; + _ -> + post_resume_tasks(State) end. + c2s_handle_bind2_inline({State, Els, Results}) -> case lists:keyfind(sm_enable, 1, Els) of - #sm_enable{xmlns = XMLNS} = Pkt -> - {State2, Res} = handle_enable_int(State#{mgmt_xmlns => XMLNS}, Pkt), - {State2, Els, [Res | Results]}; - _ -> - {State, Els, Results} + #sm_enable{xmlns = XMLNS} = Pkt -> + {State2, Res} = handle_enable_int(State#{mgmt_xmlns => XMLNS}, Pkt), + {State2, Els, [Res | Results]}; + _ -> + {State, Els, Results} end. + c2s_unauthenticated_packet(#{lang := Lang} = State, Pkt) when ?is_sm_packet(Pkt) -> %% XEP-0198 says: "For client-to-server connections, the client MUST NOT %% attempt to enable stream management until after it has completed Resource %% Binding unless it is resuming a previous session". However, it also %% says: "Stream management errors SHOULD be considered recoverable", so we %% won't bail out. - Err = #sm_failed{reason = 'not-authorized', - text = xmpp:mk_text(?T("Unauthorized"), Lang), - xmlns = ?NS_STREAM_MGMT_3}, + Err = #sm_failed{ + reason = 'not-authorized', + text = xmpp:mk_text(?T("Unauthorized"), Lang), + xmlns = ?NS_STREAM_MGMT_3 + }, {stop, send(State, Err)}; c2s_unauthenticated_packet(State, _Pkt) -> State. + c2s_unbinded_packet(State, #sm_resume{} = Pkt) -> case handle_resume(State, Pkt) of - {ok, ResumedState} -> - {stop, ResumedState}; - {error, State1} -> - {stop, State1} + {ok, ResumedState} -> + {stop, ResumedState}; + {error, State1} -> + {stop, State1} end; c2s_unbinded_packet(State, Pkt) when ?is_sm_packet(Pkt) -> c2s_unauthenticated_packet(State, Pkt); c2s_unbinded_packet(State, _Pkt) -> State. + c2s_authenticated_packet(#{mgmt_state := MgmtState} = State, Pkt) when ?is_sm_packet(Pkt) -> - if MgmtState == pending; MgmtState == active -> - {stop, perform_stream_mgmt(Pkt, State)}; - true -> - {stop, negotiate_stream_mgmt(Pkt, State)} + if + MgmtState == pending; MgmtState == active -> + {stop, perform_stream_mgmt(Pkt, State)}; + true -> + {stop, negotiate_stream_mgmt(Pkt, State)} end; c2s_authenticated_packet(State, Pkt) -> update_num_stanzas_in(State, Pkt). -c2s_handle_recv(#{mgmt_state := MgmtState, - lang := Lang} = State, El, {error, Why}) -> + +c2s_handle_recv(#{ + mgmt_state := MgmtState, + lang := Lang + } = State, + El, + {error, Why}) -> Xmlns = xmpp:get_ns(El), IsStanza = xmpp:is_stanza(El), - if Xmlns == ?NS_STREAM_MGMT_2; Xmlns == ?NS_STREAM_MGMT_3 -> - Txt = xmpp:io_format_error(Why), - Err = #sm_failed{reason = 'bad-request', - text = xmpp:mk_text(Txt, Lang), - xmlns = Xmlns}, - send(State, Err); - IsStanza andalso (MgmtState == pending orelse MgmtState == active) -> - State1 = update_num_stanzas_in(State, El), - case xmpp:get_type(El) of - <<"result">> -> State1; - <<"error">> -> State1; - _ -> - State1#{mgmt_force_enqueue => true} - end; - true -> - State + if + Xmlns == ?NS_STREAM_MGMT_2; Xmlns == ?NS_STREAM_MGMT_3 -> + Txt = xmpp:io_format_error(Why), + Err = #sm_failed{ + reason = 'bad-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns + }, + send(State, Err); + IsStanza andalso (MgmtState == pending orelse MgmtState == active) -> + State1 = update_num_stanzas_in(State, El), + case xmpp:get_type(El) of + <<"result">> -> State1; + <<"error">> -> State1; + _ -> + State1#{mgmt_force_enqueue => true} + end; + true -> + State end; c2s_handle_recv(State, _, _) -> State. -c2s_handle_send(#{mgmt_state := MgmtState, mod := Mod, - lang := Lang} = State, Pkt, SendResult) + +c2s_handle_send(#{ + mgmt_state := MgmtState, + mod := Mod, + lang := Lang + } = State, + Pkt, + SendResult) when MgmtState == pending; MgmtState == active; MgmtState == resumed -> IsStanza = xmpp:is_stanza(Pkt), case Pkt of - _ when IsStanza -> - case need_to_enqueue(State, Pkt) of - {true, State1} -> - case mgmt_queue_add(State1, Pkt) of - #{mgmt_max_queue := exceeded} = State2 -> - State3 = State2#{mgmt_resend => false}, - Err = xmpp:serr_policy_violation( - ?T("Too many unacked stanzas"), Lang), - send(State3, Err); - State2 when SendResult == ok -> - send_rack(State2); - State2 -> - State2 - end; - {false, State1} -> - State1 - end; - #stream_error{} -> - case MgmtState of - resumed -> - State; - active -> - State; - pending -> - Mod:stop_async(self()), - {stop, State#{stop_reason => {stream, {out, Pkt}}}} - end; - _ -> - State + _ when IsStanza -> + case need_to_enqueue(State, Pkt) of + {true, State1} -> + case mgmt_queue_add(State1, Pkt) of + #{mgmt_max_queue := exceeded} = State2 -> + State3 = State2#{mgmt_resend => false}, + Err = xmpp:serr_policy_violation( + ?T("Too many unacked stanzas"), Lang), + send(State3, Err); + State2 when SendResult == ok -> + send_rack(State2); + State2 -> + State2 + end; + {false, State1} -> + State1 + end; + #stream_error{} -> + case MgmtState of + resumed -> + State; + active -> + State; + pending -> + Mod:stop_async(self()), + {stop, State#{stop_reason => {stream, {out, Pkt}}}} + end; + _ -> + State end; c2s_handle_send(State, _Pkt, _Result) -> State. + c2s_handle_cast(#{mgmt_state := active} = State, send_ping) -> {stop, send_rack(State)}; c2s_handle_cast(#{mgmt_state := pending} = State, send_ping) -> @@ -267,8 +316,10 @@ c2s_handle_cast(#{mgmt_state := pending} = State, send_ping) -> c2s_handle_cast(State, _Msg) -> State. + c2s_handle_call(#{mgmt_id := MgmtID, mgmt_queue := Queue, mod := Mod} = State, - {resume_session, MgmtID}, From) -> + {resume_session, MgmtID}, + From) -> State1 = State#{mgmt_queue => p1_queue:file_to_ram(Queue)}, Mod:reply(From, {resume, State1}), {stop, State#{mgmt_state => resumed, mgmt_queue => p1_queue:clear(Queue)}}; @@ -278,32 +329,40 @@ c2s_handle_call(#{mod := Mod} = State, {resume_session, _}, From) -> c2s_handle_call(State, _Call, _From) -> State. + c2s_handle_info(#{mgmt_ack_timer := TRef, jid := JID, mod := Mod} = State, - {timeout, TRef, ack_timeout}) -> + {timeout, TRef, ack_timeout}) -> ?DEBUG("Timed out waiting for stream management acknowledgement of ~ts", - [jid:encode(JID)]), + [jid:encode(JID)]), State1 = Mod:close(State), State2 = State1#{stop_reason => {socket, ack_timeout}}, {stop, transition_to_pending(State2, ack_timeout)}; -c2s_handle_info(#{mgmt_state := pending, lang := Lang, - mgmt_pending_timer := TRef, jid := JID, mod := Mod} = State, - {timeout, TRef, pending_timeout}) -> +c2s_handle_info(#{ + mgmt_state := pending, + lang := Lang, + mgmt_pending_timer := TRef, + jid := JID, + mod := Mod + } = State, + {timeout, TRef, pending_timeout}) -> ?DEBUG("Timed out waiting for resumption of stream for ~ts", - [jid:encode(JID)]), + [jid:encode(JID)]), Txt = ?T("Timed out waiting for stream resumption"), Err = xmpp:serr_connection_timeout(Txt, Lang), Mod:stop_async(self()), - {stop, State#{mgmt_state => timeout, - stop_reason => {stream, {out, Err}}}}; + {stop, State#{ + mgmt_state => timeout, + stop_reason => {stream, {out, Err}} + }}; c2s_handle_info(State, {_Ref, {resume, #{jid := JID} = OldState}}) -> %% This happens if the resume_session/1 request timed out; the new session %% now receives the late response. ?DEBUG("Received old session state for ~ts after failed resumption", - [jid:encode(JID)]), + [jid:encode(JID)]), route_unacked_stanzas(OldState#{mgmt_resend => false}), {stop, State}; c2s_handle_info(State, {timeout, _, Timeout}) when Timeout == ack_timeout; - Timeout == pending_timeout -> + Timeout == pending_timeout -> %% Late arrival of an already cancelled timer: we just ignore it. %% This might happen because misc:cancel_timer/1 doesn't guarantee %% timer cancellation in the case when p1_server is used. @@ -311,6 +370,7 @@ c2s_handle_info(State, {timeout, _, Timeout}) when Timeout == ack_timeout; c2s_handle_info(State, _) -> State. + c2s_closed(State, {stream, _}) -> State; c2s_closed(#{mgmt_state := active} = State, Reason) -> @@ -318,27 +378,34 @@ c2s_closed(#{mgmt_state := active} = State, Reason) -> c2s_closed(State, _Reason) -> State. + c2s_terminated(#{mgmt_state := resumed, sid := SID, jid := JID} = State, _Reason) -> ?DEBUG("Closing former stream of resumed session for ~ts", - [jid:encode(JID)]), + [jid:encode(JID)]), {U, S, R} = jid:tolower(JID), ejabberd_sm:close_session(SID, U, S, R), route_late_queue_after_resume(State), ejabberd_c2s:bounce_message_queue(SID, JID), {stop, State}; -c2s_terminated(#{mgmt_state := MgmtState, mgmt_stanzas_in := In, - mgmt_id := MgmtID, jid := JID} = State, _Reason) -> +c2s_terminated(#{ + mgmt_state := MgmtState, + mgmt_stanzas_in := In, + mgmt_id := MgmtID, + jid := JID + } = State, + _Reason) -> case MgmtState of - timeout -> - store_stanzas_in(jid:tolower(JID), MgmtID, In); - _ -> - ok + timeout -> + store_stanzas_in(jid:tolower(JID), MgmtID, In); + _ -> + ok end, route_unacked_stanzas(State), State; c2s_terminated(State, _Reason) -> State. + %%%=================================================================== %%% Adjust pending session timeout / access queue %%%=================================================================== @@ -346,6 +413,7 @@ c2s_terminated(State, _Reason) -> get_resume_timeout(#{mgmt_timeout := Timeout}) -> Timeout. + -spec set_resume_timeout(state(), non_neg_integer()) -> state(). set_resume_timeout(#{mgmt_timeout := Timeout} = State, Timeout) -> State; @@ -353,21 +421,23 @@ set_resume_timeout(State, Timeout) -> State1 = restart_pending_timer(State, Timeout), State1#{mgmt_timeout => Timeout}. --spec queue_find(fun((stanza()) -> boolean()), queue()) - -> stanza() | none. + +-spec queue_find(fun((stanza()) -> boolean()), queue()) -> + stanza() | none. queue_find(Pred, Queue) -> case p1_queue:out(Queue) of - {{value, {_, _, Pkt}}, Queue1} -> - case Pred(Pkt) of - true -> - Pkt; - false -> - queue_find(Pred, Queue1) - end; - {empty, _Queue1} -> - none + {{value, {_, _, Pkt}}, Queue1} -> + case Pred(Pkt) of + true -> + Pkt; + false -> + queue_find(Pred, Queue1) + end; + {empty, _Queue1} -> + none end. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -375,394 +445,493 @@ queue_find(Pred, Queue) -> negotiate_stream_mgmt(Pkt, #{lang := Lang} = State) -> Xmlns = xmpp:get_ns(Pkt), case Pkt of - #sm_enable{} -> - handle_enable(State#{mgmt_xmlns => Xmlns}, Pkt); - _ when is_record(Pkt, sm_a); - is_record(Pkt, sm_r); - is_record(Pkt, sm_resume) -> - Txt = ?T("Stream management is not enabled"), - Err = #sm_failed{reason = 'unexpected-request', - text = xmpp:mk_text(Txt, Lang), - xmlns = Xmlns}, - send(State, Err) + #sm_enable{} -> + handle_enable(State#{mgmt_xmlns => Xmlns}, Pkt); + _ when is_record(Pkt, sm_a); + is_record(Pkt, sm_r); + is_record(Pkt, sm_resume) -> + Txt = ?T("Stream management is not enabled"), + Err = #sm_failed{ + reason = 'unexpected-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns + }, + send(State, Err) end. + -spec perform_stream_mgmt(xmpp_element(), state()) -> state(). perform_stream_mgmt(Pkt, #{mgmt_xmlns := Xmlns, lang := Lang} = State) -> case xmpp:get_ns(Pkt) of - Xmlns -> - case Pkt of - #sm_r{} -> - handle_r(State); - #sm_a{} -> - handle_a(State, Pkt); - _ when is_record(Pkt, sm_enable); - is_record(Pkt, sm_resume) -> - Txt = ?T("Stream management is already enabled"), - send(State, #sm_failed{reason = 'unexpected-request', - text = xmpp:mk_text(Txt, Lang), - xmlns = Xmlns}) - end; - _ -> - Txt = ?T("Unsupported version"), - send(State, #sm_failed{reason = 'unexpected-request', - text = xmpp:mk_text(Txt, Lang), - xmlns = Xmlns}) + Xmlns -> + case Pkt of + #sm_r{} -> + handle_r(State); + #sm_a{} -> + handle_a(State, Pkt); + _ when is_record(Pkt, sm_enable); + is_record(Pkt, sm_resume) -> + Txt = ?T("Stream management is already enabled"), + send(State, + #sm_failed{ + reason = 'unexpected-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns + }) + end; + _ -> + Txt = ?T("Unsupported version"), + send(State, + #sm_failed{ + reason = 'unexpected-request', + text = xmpp:mk_text(Txt, Lang), + xmlns = Xmlns + }) end. + -spec handle_enable_int(state(), sm_enable()) -> {state(), sm_enabled()}. -handle_enable_int(#{mgmt_timeout := DefaultTimeout, - mgmt_queue_type := QueueType, - mgmt_max_timeout := MaxTimeout, - mgmt_xmlns := Xmlns, jid := JID} = State, - #sm_enable{resume = Resume, max = Max}) -> +handle_enable_int(#{ + mgmt_timeout := DefaultTimeout, + mgmt_queue_type := QueueType, + mgmt_max_timeout := MaxTimeout, + mgmt_xmlns := Xmlns, + jid := JID + } = State, + #sm_enable{resume = Resume, max = Max}) -> State1 = State#{mgmt_id => make_id()}, - Timeout = if Resume == false -> - 0; - Max /= undefined, Max > 0, Max*1000 =< MaxTimeout -> - Max*1000; - true -> - DefaultTimeout - end, - Res = if Timeout > 0 -> - ?DEBUG("Stream management with resumption enabled for ~ts", - [jid:encode(JID)]), - #sm_enabled{xmlns = Xmlns, - id = encode_id(State1), - resume = true, - max = Timeout div 1000}; - true -> - ?DEBUG("Stream management without resumption enabled for ~ts", - [jid:encode(JID)]), - #sm_enabled{xmlns = Xmlns} - end, - State2 = State1#{mgmt_state => active, - mgmt_queue => p1_queue:new(QueueType), - mgmt_timeout => Timeout}, + Timeout = if + Resume == false -> + 0; + Max /= undefined, Max > 0, Max * 1000 =< MaxTimeout -> + Max * 1000; + true -> + DefaultTimeout + end, + Res = if + Timeout > 0 -> + ?DEBUG("Stream management with resumption enabled for ~ts", + [jid:encode(JID)]), + #sm_enabled{ + xmlns = Xmlns, + id = encode_id(State1), + resume = true, + max = Timeout div 1000 + }; + true -> + ?DEBUG("Stream management without resumption enabled for ~ts", + [jid:encode(JID)]), + #sm_enabled{xmlns = Xmlns} + end, + State2 = State1#{ + mgmt_state => active, + mgmt_queue => p1_queue:new(QueueType), + mgmt_timeout => Timeout + }, {State2, Res}. + -spec handle_enable(state(), sm_enable()) -> state(). handle_enable(State, Enable) -> {State2, Res} = handle_enable_int(State, Enable), send(State2, Res). + -spec handle_r(state()) -> state(). handle_r(#{mgmt_xmlns := Xmlns, mgmt_stanzas_in := H} = State) -> Res = #sm_a{xmlns = Xmlns, h = H}, send(State, Res). + -spec handle_a(state(), sm_a()) -> state(). handle_a(State, #sm_a{h = H}) -> State1 = check_h_attribute(State, H), resend_rack(State1). + -spec handle_resume(state(), sm_resume()) -> {ok, state()} | {error, state()}. handle_resume(#{user := User, lserver := LServer} = State, - #sm_resume{} = Resume) -> + #sm_resume{} = Resume) -> case has_resume_data(State, Resume) of - {ok, ResumedState, ResumedEl} -> - State2 = send(ResumedState, ResumedEl), - {ok, post_resume_tasks(State2)}; - {error, El, Reason} -> - log_resumption_error(User, LServer, Reason), - {error, send(State, El)} + {ok, ResumedState, ResumedEl} -> + State2 = send(ResumedState, ResumedEl), + {ok, post_resume_tasks(State2)}; + {error, El, Reason} -> + log_resumption_error(User, LServer, Reason), + {error, send(State, El)} end. + -spec has_resume_data(state(), sm_resume()) -> - {ok, state(), sm_resumed()} | {error, sm_failed(), error_reason()}. + {ok, state(), sm_resumed()} | {error, sm_failed(), error_reason()}. has_resume_data(#{lang := Lang} = State, - #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) -> + #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) -> case inherit_session_state(State, PrevID) of - {ok, InheritedState} -> - State1 = check_h_attribute(InheritedState, H), - #{mgmt_xmlns := AttrXmlns, mgmt_stanzas_in := AttrH} = State1, - {ok, State1, #sm_resumed{xmlns = AttrXmlns, - h = AttrH, - previd = PrevID}}; - {error, Err, InH} -> - {error, #sm_failed{reason = 'item-not-found', - text = xmpp:mk_text(format_error(Err), Lang), - h = InH, xmlns = Xmlns}, Err}; - {error, Err} -> - {error, #sm_failed{reason = 'item-not-found', - text = xmpp:mk_text(format_error(Err), Lang), - xmlns = Xmlns}, Err} + {ok, InheritedState} -> + State1 = check_h_attribute(InheritedState, H), + #{mgmt_xmlns := AttrXmlns, mgmt_stanzas_in := AttrH} = State1, + {ok, State1, + #sm_resumed{ + xmlns = AttrXmlns, + h = AttrH, + previd = PrevID + }}; + {error, Err, InH} -> + {error, #sm_failed{ + reason = 'item-not-found', + text = xmpp:mk_text(format_error(Err), Lang), + h = InH, + xmlns = Xmlns + }, + Err}; + {error, Err} -> + {error, #sm_failed{ + reason = 'item-not-found', + text = xmpp:mk_text(format_error(Err), Lang), + xmlns = Xmlns + }, + Err} end. + -spec post_resume_tasks(state()) -> state(). -post_resume_tasks(#{lserver := LServer, socket := Socket, jid := JID, - mgmt_xmlns := AttrXmlns} = State) -> +post_resume_tasks(#{ + lserver := LServer, + socket := Socket, + jid := JID, + mgmt_xmlns := AttrXmlns + } = State) -> State3 = resend_unacked_stanzas(State), State4 = send(State3, #sm_r{xmlns = AttrXmlns}), State5 = ejabberd_hooks:run_fold(c2s_session_resumed, LServer, State4, []), ?INFO_MSG("(~ts) Resumed session for ~ts", - [xmpp_socket:pp(Socket), jid:encode(JID)]), + [xmpp_socket:pp(Socket), jid:encode(JID)]), State5. + -spec transition_to_pending(state(), _) -> state(). -transition_to_pending(#{mgmt_state := active, mod := Mod, - mgmt_timeout := 0} = State, _Reason) -> +transition_to_pending(#{ + mgmt_state := active, + mod := Mod, + mgmt_timeout := 0 + } = State, + _Reason) -> Mod:stop_async(self()), State; -transition_to_pending(#{mgmt_state := active, jid := JID, socket := Socket, - lserver := LServer, mgmt_timeout := Timeout} = State, - Reason) -> +transition_to_pending(#{ + mgmt_state := active, + jid := JID, + socket := Socket, + lserver := LServer, + mgmt_timeout := Timeout + } = State, + Reason) -> State1 = cancel_ack_timer(State), ?INFO_MSG("(~ts) Closing c2s connection for ~ts: ~ts; " - "waiting ~B seconds for stream resumption", - [xmpp_socket:pp(Socket), jid:encode(JID), - format_reason(State, Reason), Timeout div 1000]), + "waiting ~B seconds for stream resumption", + [xmpp_socket:pp(Socket), + jid:encode(JID), + format_reason(State, Reason), + Timeout div 1000]), TRef = erlang:start_timer(Timeout, self(), pending_timeout), State2 = State1#{mgmt_state => pending, mgmt_pending_timer => TRef}, ejabberd_hooks:run_fold(c2s_session_pending, LServer, State2, []); transition_to_pending(State, _Reason) -> State. + -spec check_h_attribute(state(), non_neg_integer()) -> state(). -check_h_attribute(#{mgmt_stanzas_out := NumStanzasOut, jid := JID, - lang := Lang} = State, H) +check_h_attribute(#{ + mgmt_stanzas_out := NumStanzasOut, + jid := JID, + lang := Lang + } = State, + H) when H > NumStanzasOut -> ?WARNING_MSG("~ts acknowledged ~B stanzas, but only ~B were sent", - [jid:encode(JID), H, NumStanzasOut]), + [jid:encode(JID), H, NumStanzasOut]), State1 = State#{mgmt_resend => false}, Err = xmpp:serr_undefined_condition( - ?T("Client acknowledged more stanzas than sent by server"), Lang), + ?T("Client acknowledged more stanzas than sent by server"), Lang), send(State1, Err); check_h_attribute(#{mgmt_stanzas_out := NumStanzasOut, jid := JID} = State, H) -> ?DEBUG("~ts acknowledged ~B of ~B stanzas", - [jid:encode(JID), H, NumStanzasOut]), + [jid:encode(JID), H, NumStanzasOut]), mgmt_queue_drop(State, H). + -spec update_num_stanzas_in(state(), xmpp_element() | xmlel()) -> state(). -update_num_stanzas_in(#{mgmt_state := MgmtState, - mgmt_stanzas_in := NumStanzasIn} = State, El) +update_num_stanzas_in(#{ + mgmt_state := MgmtState, + mgmt_stanzas_in := NumStanzasIn + } = State, + El) when MgmtState == active; MgmtState == pending -> NewNum = case {xmpp:is_stanza(El), NumStanzasIn} of - {true, 4294967295} -> - 0; - {true, Num} -> - Num + 1; - {false, Num} -> - Num - end, + {true, 4294967295} -> + 0; + {true, Num} -> + Num + 1; + {false, Num} -> + Num + end, State#{mgmt_stanzas_in => NewNum}; update_num_stanzas_in(State, _El) -> State. + -spec send_rack(state()) -> state(). send_rack(#{mgmt_ack_timer := _} = State) -> State; -send_rack(#{mgmt_xmlns := Xmlns, - mgmt_stanzas_out := NumStanzasOut} = State) -> +send_rack(#{ + mgmt_xmlns := Xmlns, + mgmt_stanzas_out := NumStanzasOut + } = State) -> State1 = State#{mgmt_stanzas_req => NumStanzasOut}, State2 = start_ack_timer(State1), send(State2, #sm_r{xmlns = Xmlns}). + -spec resend_rack(state()) -> state(). -resend_rack(#{mgmt_ack_timer := _, - mgmt_queue := Queue, - mgmt_stanzas_out := NumStanzasOut, - mgmt_stanzas_req := NumStanzasReq} = State) -> +resend_rack(#{ + mgmt_ack_timer := _, + mgmt_queue := Queue, + mgmt_stanzas_out := NumStanzasOut, + mgmt_stanzas_req := NumStanzasReq + } = State) -> State1 = cancel_ack_timer(State), case NumStanzasReq < NumStanzasOut andalso not p1_queue:is_empty(Queue) of - true -> send_rack(State1); - false -> State1 + true -> send_rack(State1); + false -> State1 end; resend_rack(State) -> State. + -spec mgmt_queue_add(state(), xmlel() | xmpp_element()) -> state(). -mgmt_queue_add(#{mgmt_stanzas_out := NumStanzasOut, - mgmt_queue := Queue} = State, Pkt) -> +mgmt_queue_add(#{ + mgmt_stanzas_out := NumStanzasOut, + mgmt_queue := Queue + } = State, + Pkt) -> NewNum = case NumStanzasOut of - 4294967295 -> 0; - Num -> Num + 1 - end, + 4294967295 -> 0; + Num -> Num + 1 + end, Queue1 = p1_queue:in({NewNum, erlang:timestamp(), Pkt}, Queue), State1 = State#{mgmt_queue => Queue1, mgmt_stanzas_out => NewNum}, check_queue_length(State1). + -spec mgmt_queue_drop(state(), non_neg_integer()) -> state(). mgmt_queue_drop(#{mgmt_queue := Queue} = State, NumHandled) -> NewQueue = p1_queue:dropwhile( - fun({N, _T, _E}) -> N =< NumHandled end, Queue), + fun({N, _T, _E}) -> N =< NumHandled end, Queue), State#{mgmt_queue => NewQueue}. + -spec check_queue_length(state()) -> state(). check_queue_length(#{mgmt_max_queue := Limit} = State) when Limit == infinity; Limit == exceeded -> State; check_queue_length(#{mgmt_queue := Queue, mgmt_max_queue := Limit} = State) -> case p1_queue:len(Queue) > Limit of - true -> - State#{mgmt_max_queue => exceeded}; - false -> - State + true -> + State#{mgmt_max_queue => exceeded}; + false -> + State end. + -spec route_late_queue_after_resume(state()) -> ok. route_late_queue_after_resume(#{mgmt_queue := Queue, jid := JID}) - when ?qlen(Queue) > 0 -> + when ?qlen(Queue) > 0 -> ?DEBUG("Re-routing ~B late queued packets to ~ts", - [p1_queue:len(Queue), jid:encode(JID)]), + [p1_queue:len(Queue), jid:encode(JID)]), p1_queue:foreach( - fun({_, _Time, Pkt}) -> - ejabberd_router:route(Pkt) - end, Queue); + fun({_, _Time, Pkt}) -> + ejabberd_router:route(Pkt) + end, + Queue); route_late_queue_after_resume(_State) -> ok. + -spec resend_unacked_stanzas(state()) -> state(). -resend_unacked_stanzas(#{mgmt_state := MgmtState, - mgmt_queue := Queue, - jid := JID} = State) +resend_unacked_stanzas(#{ + mgmt_state := MgmtState, + mgmt_queue := Queue, + jid := JID + } = State) when (MgmtState == active orelse - MgmtState == pending orelse - MgmtState == timeout) andalso ?qlen(Queue) > 0 -> + MgmtState == pending orelse + MgmtState == timeout) andalso ?qlen(Queue) > 0 -> ?DEBUG("Resending ~B unacknowledged stanza(s) to ~ts", - [p1_queue:len(Queue), jid:encode(JID)]), + [p1_queue:len(Queue), jid:encode(JID)]), p1_queue:foldl( fun({_, Time, Pkt}, AccState) -> - Pkt1 = add_resent_delay_info(AccState, Pkt, Time), - Pkt2 = if ?is_stanza(Pkt1) -> - xmpp:put_meta(Pkt1, mgmt_is_resent, true); - true -> - Pkt1 - end, - send(AccState, Pkt2) - end, State, Queue); + Pkt1 = add_resent_delay_info(AccState, Pkt, Time), + Pkt2 = if + ?is_stanza(Pkt1) -> + xmpp:put_meta(Pkt1, mgmt_is_resent, true); + true -> + Pkt1 + end, + send(AccState, Pkt2) + end, + State, + Queue); resend_unacked_stanzas(State) -> State. + -spec route_unacked_stanzas(state()) -> ok. -route_unacked_stanzas(#{mgmt_state := MgmtState, - mgmt_resend := MgmtResend, - lang := Lang, user := User, - jid := JID, lserver := LServer, - mgmt_queue := Queue, - resource := Resource} = State) +route_unacked_stanzas(#{ + mgmt_state := MgmtState, + mgmt_resend := MgmtResend, + lang := Lang, + user := User, + jid := JID, + lserver := LServer, + mgmt_queue := Queue, + resource := Resource + } = State) when (MgmtState == active orelse - MgmtState == pending orelse - MgmtState == timeout) andalso ?qlen(Queue) > 0 -> + MgmtState == pending orelse + MgmtState == timeout) andalso ?qlen(Queue) > 0 -> ResendOnTimeout = case MgmtResend of - Resend when is_boolean(Resend) -> - Resend; - if_offline -> - case ejabberd_sm:get_user_resources(User, LServer) of - [Resource] -> - %% Same resource opened new session - true; - [] -> true; - _ -> false - end - end, + Resend when is_boolean(Resend) -> + Resend; + if_offline -> + case ejabberd_sm:get_user_resources(User, LServer) of + [Resource] -> + %% Same resource opened new session + true; + [] -> true; + _ -> false + end + end, ?DEBUG("Re-routing ~B unacknowledged stanza(s) to ~ts", - [p1_queue:len(Queue), jid:encode(JID)]), + [p1_queue:len(Queue), jid:encode(JID)]), ModOfflineEnabled = gen_mod:is_loaded(LServer, mod_offline), p1_queue:foreach( fun({_, _Time, #presence{from = From}}) -> - ?DEBUG("Dropping presence stanza from ~ts", [jid:encode(From)]); - ({_, _Time, #iq{} = El}) -> - Txt = ?T("User session terminated"), - ejabberd_router:route_error( - El, xmpp:err_service_unavailable(Txt, Lang)); - ({_, _Time, #message{from = From, meta = #{carbon_copy := true}}}) -> - %% XEP-0280 says: "When a receiving server attempts to deliver a - %% forked message, and that message bounces with an error for - %% any reason, the receiving server MUST NOT forward that error - %% back to the original sender." Resending such a stanza could - %% easily lead to unexpected results as well. - ?DEBUG("Dropping forwarded message stanza from ~ts", - [jid:encode(From)]); - ({_, Time, #message{} = Msg}) -> - case {ModOfflineEnabled, ResendOnTimeout, - xmpp:get_meta(Msg, mam_archived, false)} of - Val when Val == {true, true, false}; - Val == {true, true, true}; - Val == {false, true, false} -> - NewEl = add_resent_delay_info(State, Msg, Time), - ejabberd_router:route(NewEl); - {_, _, true} -> - ?DEBUG("Dropping archived message stanza from ~s", - [jid:encode(xmpp:get_from(Msg))]); - _ -> - Txt = ?T("User session terminated"), - ejabberd_router:route_error( - Msg, xmpp:err_service_unavailable(Txt, Lang)) - end; - ({_, _Time, El}) -> - %% Raw element of type 'error' resulting from a validation error - %% We cannot pass it to the router, it will generate an error - ?DEBUG("Do not route raw element from ack queue: ~p", [El]) - end, Queue); + ?DEBUG("Dropping presence stanza from ~ts", [jid:encode(From)]); + ({_, _Time, #iq{} = El}) -> + Txt = ?T("User session terminated"), + ejabberd_router:route_error( + El, xmpp:err_service_unavailable(Txt, Lang)); + ({_, _Time, #message{from = From, meta = #{carbon_copy := true}}}) -> + %% XEP-0280 says: "When a receiving server attempts to deliver a + %% forked message, and that message bounces with an error for + %% any reason, the receiving server MUST NOT forward that error + %% back to the original sender." Resending such a stanza could + %% easily lead to unexpected results as well. + ?DEBUG("Dropping forwarded message stanza from ~ts", + [jid:encode(From)]); + ({_, Time, #message{} = Msg}) -> + case {ModOfflineEnabled, + ResendOnTimeout, + xmpp:get_meta(Msg, mam_archived, false)} of + Val when Val == {true, true, false}; + Val == {true, true, true}; + Val == {false, true, false} -> + NewEl = add_resent_delay_info(State, Msg, Time), + ejabberd_router:route(NewEl); + {_, _, true} -> + ?DEBUG("Dropping archived message stanza from ~s", + [jid:encode(xmpp:get_from(Msg))]); + _ -> + Txt = ?T("User session terminated"), + ejabberd_router:route_error( + Msg, xmpp:err_service_unavailable(Txt, Lang)) + end; + ({_, _Time, El}) -> + %% Raw element of type 'error' resulting from a validation error + %% We cannot pass it to the router, it will generate an error + ?DEBUG("Do not route raw element from ack queue: ~p", [El]) + end, + Queue); route_unacked_stanzas(_State) -> ok. + -spec inherit_session_state(state(), binary()) -> {ok, state()} | - {error, error_reason()} | - {error, error_reason(), non_neg_integer()}. -inherit_session_state(#{user := U, server := S, - mgmt_queue_type := QueueType} = State, PrevID) -> + {error, error_reason()} | + {error, error_reason(), non_neg_integer()}. +inherit_session_state(#{ + user := U, + server := S, + mgmt_queue_type := QueueType + } = State, + PrevID) -> case decode_id(PrevID) of - {ok, {R, MgmtID}} -> - case ejabberd_sm:get_session_sid(U, S, R) of - none -> - case pop_stanzas_in({U, S, R}, MgmtID) of - error -> - {error, session_not_found}; - {ok, H} -> - {error, session_timed_out, H} - end; - {_, OldPID} = OldSID -> - try resume_session(OldPID, MgmtID, State) of - {resume, #{mgmt_xmlns := Xmlns, - mgmt_queue := Queue, - mgmt_timeout := Timeout, - mgmt_stanzas_in := NumStanzasIn, - mgmt_stanzas_out := NumStanzasOut} = OldState} -> - State1 = ejabberd_c2s:copy_state(State, OldState), - Queue1 = case QueueType of - ram -> Queue; - _ -> p1_queue:ram_to_file(Queue) - end, - State2 = State1#{sid => ejabberd_sm:make_sid(), - mgmt_id => MgmtID, - mgmt_xmlns => Xmlns, - mgmt_queue => Queue1, - mgmt_timeout => Timeout, - mgmt_stanzas_in => NumStanzasIn, - mgmt_stanzas_out => NumStanzasOut, - mgmt_state => active}, - State3 = ejabberd_c2s:open_session(State2), - ejabberd_c2s:stop_async(OldPID), - {ok, State3}; - {error, Msg} -> - {error, Msg} - catch exit:{noproc, _} -> - {error, session_is_dead}; - exit:{normal, _} -> - {error, session_has_exited}; - exit:{shutdown, _} -> - {error, session_has_exited}; - exit:{killed, _} -> - {error, session_was_killed}; - exit:{timeout, _} -> - ejabberd_sm:close_session(OldSID, U, S, R), - ejabberd_c2s:stop_async(OldPID), - {error, session_copy_timed_out} - end - end; - error -> - {error, invalid_previd} + {ok, {R, MgmtID}} -> + case ejabberd_sm:get_session_sid(U, S, R) of + none -> + case pop_stanzas_in({U, S, R}, MgmtID) of + error -> + {error, session_not_found}; + {ok, H} -> + {error, session_timed_out, H} + end; + {_, OldPID} = OldSID -> + try resume_session(OldPID, MgmtID, State) of + {resume, #{ + mgmt_xmlns := Xmlns, + mgmt_queue := Queue, + mgmt_timeout := Timeout, + mgmt_stanzas_in := NumStanzasIn, + mgmt_stanzas_out := NumStanzasOut + } = OldState} -> + State1 = ejabberd_c2s:copy_state(State, OldState), + Queue1 = case QueueType of + ram -> Queue; + _ -> p1_queue:ram_to_file(Queue) + end, + State2 = State1#{ + sid => ejabberd_sm:make_sid(), + mgmt_id => MgmtID, + mgmt_xmlns => Xmlns, + mgmt_queue => Queue1, + mgmt_timeout => Timeout, + mgmt_stanzas_in => NumStanzasIn, + mgmt_stanzas_out => NumStanzasOut, + mgmt_state => active + }, + State3 = ejabberd_c2s:open_session(State2), + ejabberd_c2s:stop_async(OldPID), + {ok, State3}; + {error, Msg} -> + {error, Msg} + catch + exit:{noproc, _} -> + {error, session_is_dead}; + exit:{normal, _} -> + {error, session_has_exited}; + exit:{shutdown, _} -> + {error, session_has_exited}; + exit:{killed, _} -> + {error, session_was_killed}; + exit:{timeout, _} -> + ejabberd_sm:close_session(OldSID, U, S, R), + ejabberd_c2s:stop_async(OldPID), + {error, session_copy_timed_out} + end + end; + error -> + {error, invalid_previd} end. + -spec resume_session(pid(), id(), state()) -> {resume, state()} | - {error, error_reason()}. + {error, error_reason()}. resume_session(PID, MgmtID, _State) -> ejabberd_c2s:call(PID, {resume_session, MgmtID}, timer:seconds(15)). + -spec add_resent_delay_info(state(), stanza(), erlang:timestamp()) -> stanza(); - (state(), xmlel(), erlang:timestamp()) -> xmlel(). + (state(), xmlel(), erlang:timestamp()) -> xmlel(). add_resent_delay_info(#{lserver := LServer}, El, Time) when is_record(El, message); is_record(El, presence) -> misc:add_delay_info(El, jid:make(LServer), Time, <<"Resent">>); @@ -770,10 +939,12 @@ add_resent_delay_info(_State, El, _Time) -> %% TODO El. + -spec send(state(), xmpp_element()) -> state(). send(#{mod := Mod} = State, Pkt) -> Mod:send(State, Pkt). + -spec restart_pending_timer(state(), non_neg_integer()) -> state(). restart_pending_timer(#{mgmt_pending_timer := TRef} = State, NewTimeout) -> misc:cancel_timer(TRef), @@ -782,6 +953,7 @@ restart_pending_timer(#{mgmt_pending_timer := TRef} = State, NewTimeout) -> restart_pending_timer(State, _NewTimeout) -> State. + -spec start_ack_timer(state()) -> state(). start_ack_timer(#{mgmt_ack_timeout := infinity} = State) -> State; @@ -789,6 +961,7 @@ start_ack_timer(#{mgmt_ack_timeout := AckTimeout} = State) -> TRef = erlang:start_timer(AckTimeout, self(), ack_timeout), State#{mgmt_ack_timer => TRef}. + -spec cancel_ack_timer(state()) -> state(). cancel_ack_timer(#{mgmt_ack_timer := TRef} = State) -> misc:cancel_timer(TRef), @@ -796,6 +969,7 @@ cancel_ack_timer(#{mgmt_ack_timer := TRef} = State) -> cancel_ack_timer(State) -> State. + -spec need_to_enqueue(state(), xmlel() | stanza()) -> {boolean(), state()}. need_to_enqueue(State, Pkt) when ?is_stanza(Pkt) -> {not xmpp:get_meta(Pkt, mgmt_is_resent, false), State}; @@ -806,24 +980,28 @@ need_to_enqueue(#{mgmt_force_enqueue := true} = State, #xmlel{}) -> need_to_enqueue(State, _) -> {false, State}. + -spec make_id() -> id(). make_id() -> p1_rand:bytes(8). + -spec encode_id(state()) -> binary(). encode_id(#{mgmt_id := MgmtID, resource := Resource}) -> misc:term_to_base64({Resource, MgmtID}). + -spec decode_id(binary()) -> {ok, {binary(), id()}} | error. decode_id(Encoded) -> case misc:base64_to_term(Encoded) of - {term, {Resource, MgmtID}} when is_binary(Resource), - is_binary(MgmtID) -> - {ok, {Resource, MgmtID}}; - _ -> - error + {term, {Resource, MgmtID}} when is_binary(Resource), + is_binary(MgmtID) -> + {ok, {Resource, MgmtID}}; + _ -> + error end. + %%%=================================================================== %%% Formatters and Logging %%%=================================================================== @@ -843,6 +1021,7 @@ format_error(session_copy_timed_out) -> format_error(invalid_previd) -> ?T("Invalid 'previd' value"). + -spec format_reason(state(), term()) -> binary(). format_reason(_, ack_timeout) -> <<"Timed out waiting for stream acknowledgement">>; @@ -851,14 +1030,16 @@ format_reason(#{stop_reason := {socket, ack_timeout}} = State, _) -> format_reason(State, Reason) -> ejabberd_c2s:format_reason(State, Reason). + -spec log_resumption_error(binary(), binary(), error_reason()) -> ok. log_resumption_error(User, Server, Reason) when Reason == invalid_previd -> ?WARNING_MSG("Cannot resume session for ~ts@~ts: ~ts", - [User, Server, format_error(Reason)]); + [User, Server, format_error(Reason)]); log_resumption_error(User, Server, Reason) -> ?INFO_MSG("Cannot resume session for ~ts@~ts: ~ts", - [User, Server, format_error(Reason)]). + [User, Server, format_error(Reason)]). + %%%=================================================================== %%% Cache-like storage for last handled stanzas @@ -866,52 +1047,65 @@ log_resumption_error(User, Server, Reason) -> init_cache(Opts) -> ets_cache:new(?STREAM_MGMT_CACHE, cache_opts(Opts)). + cache_opts(Opts) -> [{max_size, mod_stream_mgmt_opt:cache_size(Opts)}, {life_time, mod_stream_mgmt_opt:cache_life_time(Opts)}, {type, ordered_set}]. + -spec store_stanzas_in(ljid(), id(), non_neg_integer()) -> boolean(). store_stanzas_in(LJID, MgmtID, Num) -> - ets_cache:insert(?STREAM_MGMT_CACHE, {LJID, MgmtID}, Num, - ejabberd_cluster:get_nodes()). + ets_cache:insert(?STREAM_MGMT_CACHE, + {LJID, MgmtID}, + Num, + ejabberd_cluster:get_nodes()). + -spec pop_stanzas_in(ljid(), id()) -> {ok, non_neg_integer()} | error. pop_stanzas_in(LJID, MgmtID) -> case ets_cache:lookup(?STREAM_MGMT_CACHE, {LJID, MgmtID}) of - {ok, Val} -> - ets_cache:match_delete(?STREAM_MGMT_CACHE, {LJID, '_'}, - ejabberd_cluster:get_nodes()), - {ok, Val}; - error -> - error + {ok, Val} -> + ets_cache:match_delete(?STREAM_MGMT_CACHE, + {LJID, '_'}, + ejabberd_cluster:get_nodes()), + {ok, Val}; + error -> + error end. + %%%=================================================================== %%% Configuration processing %%%=================================================================== get_max_ack_queue(Host) -> mod_stream_mgmt_opt:max_ack_queue(Host). + get_configured_resume_timeout(Host) -> mod_stream_mgmt_opt:resume_timeout(Host). + get_max_resume_timeout(Host, ResumeTimeout) -> case mod_stream_mgmt_opt:max_resume_timeout(Host) of - undefined -> ResumeTimeout; - Max when Max >= ResumeTimeout -> Max; - _ -> ResumeTimeout + undefined -> ResumeTimeout; + Max when Max >= ResumeTimeout -> Max; + _ -> ResumeTimeout end. + get_ack_timeout(Host) -> mod_stream_mgmt_opt:ack_timeout(Host). + get_resend_on_timeout(Host) -> mod_stream_mgmt_opt:resend_on_timeout(Host). + get_queue_type(Host) -> mod_stream_mgmt_opt:queue_type(Host). + mod_opt_type(max_ack_queue) -> econf:pos_int(infinity); mod_opt_type(resume_timeout) -> @@ -935,6 +1129,7 @@ mod_opt_type(cache_life_time) -> mod_opt_type(queue_type) -> econf:queue_type(). + mod_options(Host) -> [{max_ack_queue, 5000}, {resume_timeout, timer:seconds(300)}, @@ -945,8 +1140,10 @@ mod_options(Host) -> {resend_on_timeout, false}, {queue_type, ejabberd_option:queue_type(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0198.html" "[XEP-0198: Stream Management]. This protocol allows " @@ -955,7 +1152,8 @@ mod_doc() -> "and stream resumption."), opts => [{max_ack_queue, - #{value => ?T("Size"), + #{ + value => ?T("Size"), desc => ?T("This option specifies the maximum number of " "unacknowledged stanzas queued for possible " @@ -969,9 +1167,11 @@ mod_doc() -> "It should definitely be set higher that the size " "of the offline queue (for example at least 3 times " "the value of the max offline queue and never lower " - "than '1000'). The default value is '5000'.")}}, + "than '1000'). The default value is '5000'.") + }}, {resume_timeout, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("This option configures the (default) period of time " "until a session times out if the connection is lost. " @@ -979,9 +1179,11 @@ mod_doc() -> "session. Note that the client may request a different " "timeout value, see the 'max_resume_timeout' option. " "Setting it to '0' effectively disables session resumption. " - "The default value is '5' minutes.")}}, + "The default value is '5' minutes.") + }}, {max_resume_timeout, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("A client may specify the period of time until a session " "times out if the connection is lost. During this period " @@ -989,15 +1191,19 @@ mod_doc() -> "limits the period of time a client is permitted to request. " "It must be set to a timeout equal to or larger than the " "default 'resume_timeout'. By default, it is set to the " - "same value as the 'resume_timeout' option.")}}, + "same value as the 'resume_timeout' option.") + }}, {ack_timeout, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("A time to wait for stanza acknowledgments. " "Setting it to 'infinity' effectively disables the timeout. " - "The default value is '1' minute.")}}, + "The default value is '1' minute.") + }}, {resend_on_timeout, - #{value => "true | false | if_offline", + #{ + value => "true | false | if_offline", desc => ?T("If this option is set to 'true', any message stanzas " "that weren't acknowledged by the client will be resent " @@ -1010,18 +1216,26 @@ mod_doc() -> "As an alternative, the option may be set to 'if_offline'. " "In this case, unacknowledged messages are resent only if " "no other resource is online when the session times out. " - "Otherwise, error messages are generated.")}}, + "Otherwise, error messages are generated.") + }}, {queue_type, - #{value => "ram | file", + #{ + value => "ram | file", desc => - ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, " "but applied to this module only. " - "The default value is '48 hours'.")}}]}. + "The default value is '48 hours'.") + }}] + }. diff --git a/src/mod_stream_mgmt_opt.erl b/src/mod_stream_mgmt_opt.erl index 58d4fe1e7..1ab69fb80 100644 --- a/src/mod_stream_mgmt_opt.erl +++ b/src/mod_stream_mgmt_opt.erl @@ -12,51 +12,58 @@ -export([resend_on_timeout/1]). -export([resume_timeout/1]). + -spec ack_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). ack_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(ack_timeout, Opts); ack_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, ack_timeout). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, cache_life_time). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, cache_size). + -spec max_ack_queue(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_ack_queue(Opts) when is_map(Opts) -> gen_mod:get_opt(max_ack_queue, Opts); max_ack_queue(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, max_ack_queue). + -spec max_resume_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). max_resume_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(max_resume_timeout, Opts); max_resume_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, max_resume_timeout). + -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, queue_type). + -spec resend_on_timeout(gen_mod:opts() | global | binary()) -> 'false' | 'if_offline' | 'true'. resend_on_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(resend_on_timeout, Opts); resend_on_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, resend_on_timeout). + -spec resume_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). resume_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(resume_timeout, Opts); resume_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, resume_timeout). - diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl index 61942019e..7f5a15638 100644 --- a/src/mod_stun_disco.erl +++ b/src/mod_stun_disco.erl @@ -32,20 +32,20 @@ %% gen_mod callbacks. -export([start/2, - stop/1, - reload/3, - mod_opt_type/1, - mod_options/1, - depends/2]). + stop/1, + reload/3, + mod_opt_type/1, + mod_options/1, + depends/2]). -export([mod_doc/0]). %% gen_server callbacks. -export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). %% ejabberd_hooks callbacks. -export([disco_local_features/5, stun_get_password/3]). @@ -55,6 +55,7 @@ -include("logger.hrl"). -include("translate.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). -define(STUN_MODULE, ejabberd_stun). @@ -67,25 +68,26 @@ %% @indent-begin -record(request, - {host :: binary() | inet:ip_address() | undefined, - port :: 0..65535 | undefined, - transport :: udp | tcp | undefined, - type :: service_type(), - restricted :: true | undefined}). + {host :: binary() | inet:ip_address() | undefined, + port :: 0..65535 | undefined, + transport :: udp | tcp | undefined, + type :: service_type(), + restricted :: true | undefined}). -record(state, - {host :: binary(), - services :: [service()], - secret :: binary(), - ttl :: non_neg_integer()}). + {host :: binary(), + services :: [service()], + secret :: binary(), + ttl :: non_neg_integer()}). %% @indent-end %% @efmt:on -%% + %% -type request() :: #request{}. -type state() :: #state{}. + %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- @@ -94,19 +96,23 @@ start(Host, Opts) -> Proc = get_proc_name(Host), gen_mod:start_child(?MODULE, Host, Opts, Proc). + -spec stop(binary()) -> ok | {error, any()}. stop(Host) -> Proc = get_proc_name(Host), gen_mod:stop_child(Proc). + -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> cast(Host, {reload, NewOpts, OldOpts}). + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. + -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access) -> econf:acl(); @@ -119,40 +125,45 @@ mod_opt_type(secret) -> mod_opt_type(services) -> econf:list( econf:and_then( - econf:options( - #{host => econf:either(econf:ip(), econf:binary()), - port => econf:port(), - type => econf:enum([stun, turn, stuns, turns]), - transport => econf:enum([tcp, udp]), - restricted => econf:bool()}, - [{required, [host]}]), - fun(Opts) -> - DefPort = fun(stun) -> 3478; - (turn) -> 3478; - (stuns) -> 5349; - (turns) -> 5349 - end, - DefTrns = fun(stun) -> udp; - (turn) -> udp; - (stuns) -> tcp; - (turns) -> tcp - end, - DefRstr = fun(stun) -> false; - (turn) -> true; - (stuns) -> false; - (turns) -> true - end, - Host = proplists:get_value(host, Opts), - Type = proplists:get_value(type, Opts, stun), - Port = proplists:get_value(port, Opts, DefPort(Type)), - Trns = proplists:get_value(transport, Opts, DefTrns(Type)), - Rstr = proplists:get_value(restricted, Opts, DefRstr(Type)), - #service{host = Host, - port = Port, - type = Type, - transport = Trns, - restricted = Rstr} - end)). + econf:options( + #{ + host => econf:either(econf:ip(), econf:binary()), + port => econf:port(), + type => econf:enum([stun, turn, stuns, turns]), + transport => econf:enum([tcp, udp]), + restricted => econf:bool() + }, + [{required, [host]}]), + fun(Opts) -> + DefPort = fun(stun) -> 3478; + (turn) -> 3478; + (stuns) -> 5349; + (turns) -> 5349 + end, + DefTrns = fun(stun) -> udp; + (turn) -> udp; + (stuns) -> tcp; + (turns) -> tcp + end, + DefRstr = fun(stun) -> false; + (turn) -> true; + (stuns) -> false; + (turns) -> true + end, + Host = proplists:get_value(host, Opts), + Type = proplists:get_value(type, Opts, stun), + Port = proplists:get_value(port, Opts, DefPort(Type)), + Trns = proplists:get_value(transport, Opts, DefTrns(Type)), + Rstr = proplists:get_value(restricted, Opts, DefRstr(Type)), + #service{ + host = Host, + port = Port, + type = Type, + transport = Trns, + restricted = Rstr + } + end)). + -spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}]. mod_options(_Host) -> @@ -162,140 +173,164 @@ mod_options(_Host) -> {secret, undefined}, {services, []}]. + mod_doc() -> - #{desc => - ?T("This module allows XMPP clients to discover STUN/TURN services " - "and to obtain temporary credentials for using them as per " - "https://xmpp.org/extensions/xep-0215.html" - "[XEP-0215: External Service Discovery]."), + #{ + desc => + ?T("This module allows XMPP clients to discover STUN/TURN services " + "and to obtain temporary credentials for using them as per " + "https://xmpp.org/extensions/xep-0215.html" + "[XEP-0215: External Service Discovery]."), note => "added in 20.04", opts => - [{access, - #{value => ?T("AccessName"), - desc => - ?T("This option defines which access rule will be used to " - "control who is allowed to discover STUN/TURN services " - "and to request temporary credentials. The default value " - "is 'local'.")}}, - {credentials_lifetime, - #{value => "timeout()", - desc => - ?T("The lifetime of temporary credentials offered to " - "clients. If ejabberd's built-in TURN service is used, " - "TURN relays allocated using temporary credentials will " - "be terminated shortly after the credentials expired. The " - "default value is '12 hours'. Note that restarting the " - "ejabberd node invalidates any temporary credentials " - "offered before the restart unless a 'secret' is " - "specified (see below).")}}, - {offer_local_services, - #{value => "true | false", - desc => - ?T("This option specifies whether local STUN/TURN services " - "configured as ejabberd listeners should be announced " - "automatically. Note that this will not include " - "TLS-enabled services, which must be configured manually " - "using the 'services' option (see below). For " - "non-anonymous TURN services, temporary credentials will " - "be offered to the client. The default value is " - "'true'.")}}, - {secret, - #{value => ?T("Text"), - desc => - ?T("The secret used for generating temporary credentials. If " - "this option isn't specified, a secret will be " - "auto-generated. However, a secret must be specified " - "explicitly if non-anonymous TURN services running on " - "other ejabberd nodes and/or external TURN 'services' are " - "configured. Also note that auto-generated secrets are " - "lost when the node is restarted, which invalidates any " - "credentials offered before the restart. Therefore, it's " - "recommended to explicitly specify a secret if clients " - "cache retrieved credentials (for later use) across " - "service restarts.")}}, - {services, - #{value => "[Service, ...]", - example => - ["services:", - " -", - " host: 203.0.113.3", - " port: 3478", - " type: stun", - " transport: udp", - " restricted: false", - " -", - " host: 203.0.113.3", - " port: 3478", - " type: turn", - " transport: udp", - " restricted: true", - " -", - " host: 2001:db8::3", - " port: 3478", - " type: stun", - " transport: udp", - " restricted: false", - " -", - " host: 2001:db8::3", - " port: 3478", - " type: turn", - " transport: udp", - " restricted: true", - " -", - " host: server.example.com", - " port: 5349", - " type: turns", - " transport: tcp", - " restricted: true"], - desc => - ?T("The list of services offered to clients. This list can " - "include STUN/TURN services running on any ejabberd node " - "and/or external services. However, if any listed TURN " - "service not running on the local ejabberd node requires " - "authentication, a 'secret' must be specified explicitly, " - "and must be shared with that service. This will only " - "work with ejabberd's built-in STUN/TURN server and with " - "external servers that support the same " - "https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00" - "[REST API For Access To TURN Services]. Unless the " - "'offer_local_services' is set to 'false', the explicitly " - "listed services will be offered in addition to those " - "announced automatically.")}, - [{host, - #{value => ?T("Host"), - desc => - ?T("The hostname or IP address the STUN/TURN service is " - "listening on. For non-TLS services, it's recommended " - "to specify an IP address (to avoid additional DNS " - "lookup latency on the client side). For TLS services, " - "the hostname (or IP address) should match the " - "certificate. Specifying the 'host' option is " - "mandatory.")}}, - {port, - #{value => "1..65535", - desc => - ?T("The port number the STUN/TURN service is listening " - "on. The default port number is 3478 for non-TLS " - "services and 5349 for TLS services.")}}, - {type, - #{value => "stun | turn | stuns | turns", - desc => - ?T("The type of service. Must be 'stun' or 'turn' for " - "non-TLS services, 'stuns' or 'turns' for TLS services. " - "The default type is 'stun'.")}}, - {transport, - #{value => "tcp | udp", - desc => - ?T("The transport protocol supported by the service. The " - "default is 'udp' for non-TLS services and 'tcp' for " - "TLS services.")}}, - {restricted, - #{value => "true | false", - desc => - ?T("This option determines whether temporary credentials " - "for accessing the service are offered. The default is " - "'false' for STUN/STUNS services and 'true' for " - "TURN/TURNS services.")}}]}]}. + [{access, + #{ + value => ?T("AccessName"), + desc => + ?T("This option defines which access rule will be used to " + "control who is allowed to discover STUN/TURN services " + "and to request temporary credentials. The default value " + "is 'local'.") + }}, + {credentials_lifetime, + #{ + value => "timeout()", + desc => + ?T("The lifetime of temporary credentials offered to " + "clients. If ejabberd's built-in TURN service is used, " + "TURN relays allocated using temporary credentials will " + "be terminated shortly after the credentials expired. The " + "default value is '12 hours'. Note that restarting the " + "ejabberd node invalidates any temporary credentials " + "offered before the restart unless a 'secret' is " + "specified (see below).") + }}, + {offer_local_services, + #{ + value => "true | false", + desc => + ?T("This option specifies whether local STUN/TURN services " + "configured as ejabberd listeners should be announced " + "automatically. Note that this will not include " + "TLS-enabled services, which must be configured manually " + "using the 'services' option (see below). For " + "non-anonymous TURN services, temporary credentials will " + "be offered to the client. The default value is " + "'true'.") + }}, + {secret, + #{ + value => ?T("Text"), + desc => + ?T("The secret used for generating temporary credentials. If " + "this option isn't specified, a secret will be " + "auto-generated. However, a secret must be specified " + "explicitly if non-anonymous TURN services running on " + "other ejabberd nodes and/or external TURN 'services' are " + "configured. Also note that auto-generated secrets are " + "lost when the node is restarted, which invalidates any " + "credentials offered before the restart. Therefore, it's " + "recommended to explicitly specify a secret if clients " + "cache retrieved credentials (for later use) across " + "service restarts.") + }}, + {services, + #{ + value => "[Service, ...]", + example => + ["services:", + " -", + " host: 203.0.113.3", + " port: 3478", + " type: stun", + " transport: udp", + " restricted: false", + " -", + " host: 203.0.113.3", + " port: 3478", + " type: turn", + " transport: udp", + " restricted: true", + " -", + " host: 2001:db8::3", + " port: 3478", + " type: stun", + " transport: udp", + " restricted: false", + " -", + " host: 2001:db8::3", + " port: 3478", + " type: turn", + " transport: udp", + " restricted: true", + " -", + " host: server.example.com", + " port: 5349", + " type: turns", + " transport: tcp", + " restricted: true"], + desc => + ?T("The list of services offered to clients. This list can " + "include STUN/TURN services running on any ejabberd node " + "and/or external services. However, if any listed TURN " + "service not running on the local ejabberd node requires " + "authentication, a 'secret' must be specified explicitly, " + "and must be shared with that service. This will only " + "work with ejabberd's built-in STUN/TURN server and with " + "external servers that support the same " + "https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00" + "[REST API For Access To TURN Services]. Unless the " + "'offer_local_services' is set to 'false', the explicitly " + "listed services will be offered in addition to those " + "announced automatically.") + }, + [{host, + #{ + value => ?T("Host"), + desc => + ?T("The hostname or IP address the STUN/TURN service is " + "listening on. For non-TLS services, it's recommended " + "to specify an IP address (to avoid additional DNS " + "lookup latency on the client side). For TLS services, " + "the hostname (or IP address) should match the " + "certificate. Specifying the 'host' option is " + "mandatory.") + }}, + {port, + #{ + value => "1..65535", + desc => + ?T("The port number the STUN/TURN service is listening " + "on. The default port number is 3478 for non-TLS " + "services and 5349 for TLS services.") + }}, + {type, + #{ + value => "stun | turn | stuns | turns", + desc => + ?T("The type of service. Must be 'stun' or 'turn' for " + "non-TLS services, 'stuns' or 'turns' for TLS services. " + "The default type is 'stun'.") + }}, + {transport, + #{ + value => "tcp | udp", + desc => + ?T("The transport protocol supported by the service. The " + "default is 'udp' for non-TLS services and 'tcp' for " + "TLS services.") + }}, + {restricted, + #{ + value => "true | false", + desc => + ?T("This option determines whether temporary credentials " + "for accessing the service are offered. The default is " + "'false' for STUN/STUNS services and 'true' for " + "TURN/TURNS services.") + }}]}] + }. + %%-------------------------------------------------------------------- %% gen_server callbacks. @@ -310,35 +345,43 @@ init([Host, Opts]) -> register_hooks(Host), {ok, #state{host = Host, services = Services, secret = Secret, ttl = TTL}}. --spec handle_call(term(), {pid(), term()}, state()) - -> {reply, {turn_disco, [service()] | binary()}, state()} | - {noreply, state()}. -handle_call({get_services, JID, #request{host = ReqHost, - port = ReqPort, - type = ReqType, - transport = ReqTrns, - restricted = ReqRstr}}, _From, - #state{host = Host, - services = List0, - secret = Secret, - ttl = TTL} = State) -> + +-spec handle_call(term(), {pid(), term()}, state()) -> + {reply, {turn_disco, [service()] | binary()}, state()} | + {noreply, state()}. +handle_call({get_services, JID, + #request{ + host = ReqHost, + port = ReqPort, + type = ReqType, + transport = ReqTrns, + restricted = ReqRstr + }}, + _From, + #state{ + host = Host, + services = List0, + secret = Secret, + ttl = TTL + } = State) -> ?DEBUG("Getting STUN/TURN service list for ~ts", [jid:encode(JID)]), Hash = <<(hash(jid:encode(JID)))/binary, (hash(Host))/binary>>, List = lists:filtermap( - fun(#service{host = H, port = P, type = T, restricted = R}) - when (ReqHost /= undefined) and (H /= ReqHost); - (ReqPort /= undefined) and (P /= ReqPort); - (ReqType /= undefined) and (T /= ReqType); - (ReqTrns /= undefined) and (T /= ReqTrns); - (ReqRstr /= undefined) and (R /= ReqRstr) -> - false; - (#service{restricted = false}) -> - true; - (#service{restricted = true} = Service) -> - {true, add_credentials(Service, Hash, Secret, TTL)} - end, List0), + fun(#service{host = H, port = P, type = T, restricted = R}) + when (ReqHost /= undefined) and (H /= ReqHost); + (ReqPort /= undefined) and (P /= ReqPort); + (ReqType /= undefined) and (T /= ReqType); + (ReqTrns /= undefined) and (T /= ReqTrns); + (ReqRstr /= undefined) and (R /= ReqRstr) -> + false; + (#service{restricted = false}) -> + true; + (#service{restricted = true} = Service) -> + {true, add_credentials(Service, Hash, Secret, TTL)} + end, + List0), ?INFO_MSG("Offering STUN/TURN services to ~ts (~s)", - [jid:encode(JID), Hash]), + [jid:encode(JID), Hash]), {reply, {turn_disco, List}, State}; handle_call({get_password, Username}, _From, #state{secret = Secret} = State) -> ?DEBUG("Getting STUN/TURN password for ~ts", [Username]), @@ -348,6 +391,7 @@ handle_call(Request, From, State) -> ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), {noreply, State}. + -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({reload, NewOpts, _OldOpts}, #state{host = Host} = State) -> ?DEBUG("Reloading STUN/TURN discovery configuration for ~ts", [Host]), @@ -359,129 +403,167 @@ handle_cast(Request, State) -> ?ERROR_MSG("Got unexpected request: ~p", [Request]), {noreply, State}. + -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?ERROR_MSG("Got unexpected info: ~p", [Info]), {noreply, State}. + -spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. terminate(Reason, #state{host = Host}) -> ?DEBUG("Stopping STUN/TURN discovery process for ~ts: ~p", - [Host, Reason]), + [Host, Reason]), unregister_hooks(Host), unregister_iq_handlers(Host). + -spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, #state{host = Host} = State, _Extra) -> ?DEBUG("Updating STUN/TURN discovery process for ~ts", [Host]), {ok, State}. + %%-------------------------------------------------------------------- %% Register/unregister hooks. %%-------------------------------------------------------------------- -spec register_hooks(binary()) -> ok. register_hooks(Host) -> - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, - disco_local_features, 50), - ejabberd_hooks:add(stun_get_password, ?MODULE, - stun_get_password, 50). + ejabberd_hooks:add(disco_local_features, + Host, + ?MODULE, + disco_local_features, + 50), + ejabberd_hooks:add(stun_get_password, + ?MODULE, + stun_get_password, + 50). + -spec unregister_hooks(binary()) -> ok. unregister_hooks(Host) -> - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, - disco_local_features, 50), + ejabberd_hooks:delete(disco_local_features, + Host, + ?MODULE, + disco_local_features, + 50), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of - false -> - ejabberd_hooks:delete(stun_get_password, ?MODULE, - stun_get_password, 50); - true -> - ok + false -> + ejabberd_hooks:delete(stun_get_password, + ?MODULE, + stun_get_password, + 50); + true -> + ok end. + %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- --spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), - binary()) -> mod_disco:features_acc(). +-spec disco_local_features(mod_disco:features_acc(), + jid(), + jid(), + binary(), + binary()) -> mod_disco:features_acc(). disco_local_features(empty, From, To, Node, Lang) -> disco_local_features({result, []}, From, To, Node, Lang); -disco_local_features({result, OtherFeatures} = Acc, From, - #jid{lserver = LServer}, <<"">>, _Lang) -> +disco_local_features({result, OtherFeatures} = Acc, + From, + #jid{lserver = LServer}, + <<"">>, + _Lang) -> Access = mod_stun_disco_opt:access(LServer), case acl:match_rule(LServer, Access, From) of - allow -> - ?DEBUG("Announcing feature to ~ts", [jid:encode(From)]), - {result, [?NS_EXTDISCO_2 | OtherFeatures]}; - deny -> - ?DEBUG("Not announcing feature to ~ts", [jid:encode(From)]), - Acc + allow -> + ?DEBUG("Announcing feature to ~ts", [jid:encode(From)]), + {result, [?NS_EXTDISCO_2 | OtherFeatures]}; + deny -> + ?DEBUG("Not announcing feature to ~ts", [jid:encode(From)]), + Acc end; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. --spec stun_get_password(any(), binary(), binary()) - -> binary() | {stop, binary()}. + +-spec stun_get_password(any(), binary(), binary()) -> + binary() | {stop, binary()}. stun_get_password(<<>>, Username, _Realm) -> case binary:split(Username, <<$:>>) of - [Expiration, <<_UserHash:8/binary, HostHash:8/binary>>] -> - try binary_to_integer(Expiration) of - ExpireTime -> - case erlang:system_time(second) of - Now when Now < ExpireTime -> - ?DEBUG("Looking up password for: ~ts", [Username]), - {stop, get_password(Username, HostHash)}; - Now when Now >= ExpireTime -> - ?INFO_MSG("Credentials expired: ~ts", [Username]), - {stop, <<>>} - end - catch _:badarg -> - ?DEBUG("Non-numeric expiration field: ~ts", [Username]), - <<>> - end; - _ -> - ?DEBUG("Not an ephemeral username: ~ts", [Username]), - <<>> + [Expiration, <<_UserHash:8/binary, HostHash:8/binary>>] -> + try binary_to_integer(Expiration) of + ExpireTime -> + case erlang:system_time(second) of + Now when Now < ExpireTime -> + ?DEBUG("Looking up password for: ~ts", [Username]), + {stop, get_password(Username, HostHash)}; + Now when Now >= ExpireTime -> + ?INFO_MSG("Credentials expired: ~ts", [Username]), + {stop, <<>>} + end + catch + _:badarg -> + ?DEBUG("Non-numeric expiration field: ~ts", [Username]), + <<>> + end; + _ -> + ?DEBUG("Not an ephemeral username: ~ts", [Username]), + <<>> end; stun_get_password(Acc, _Username, _Realm) -> Acc. + %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_EXTDISCO_2, ?MODULE, process_iq). + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_EXTDISCO_2, + ?MODULE, + process_iq). + -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_EXTDISCO_2). + -spec process_iq(iq()) -> iq(). -process_iq(#iq{type = get, - sub_els = [#services{type = ReqType}]} = IQ) -> +process_iq(#iq{ + type = get, + sub_els = [#services{type = ReqType}] + } = IQ) -> Request = #request{type = ReqType}, process_iq_get(IQ, Request); -process_iq(#iq{type = get, - sub_els = [#credentials{ - services = [#service{ - host = ReqHost, - port = ReqPort, - type = ReqType, - transport = ReqTrns, - name = <<>>, - username = <<>>, - password = <<>>, - expires = undefined, - restricted = undefined, - action = undefined, - xdata = undefined}]}]} = IQ) -> +process_iq(#iq{ + type = get, + sub_els = [#credentials{ + services = [#service{ + host = ReqHost, + port = ReqPort, + type = ReqType, + transport = ReqTrns, + name = <<>>, + username = <<>>, + password = <<>>, + expires = undefined, + restricted = undefined, + action = undefined, + xdata = undefined + }] + }] + } = IQ) -> % Accepting the 'transport' request attribute is an ejabberd extension. - Request = #request{host = ReqHost, - port = ReqPort, - type = ReqType, - transport = ReqTrns, - restricted = true}, + Request = #request{ + host = ReqHost, + port = ReqPort, + type = ReqType, + transport = ReqTrns, + restricted = true + }, process_iq_get(IQ, Request); process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -490,166 +572,186 @@ process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). + -spec process_iq_get(iq(), request()) -> iq(). process_iq_get(#iq{from = From, to = #jid{lserver = Host}, lang = Lang} = IQ, - #request{restricted = Restricted} = Request) -> + #request{restricted = Restricted} = Request) -> Access = mod_stun_disco_opt:access(Host), case acl:match_rule(Host, Access, From) of allow -> - ?DEBUG("Performing external service discovery for ~ts", - [jid:encode(From)]), - case get_services(Host, From, Request) of - {ok, Services} when Restricted -> % A request. - xmpp:make_iq_result(IQ, #credentials{services = Services}); - {ok, Services} -> - xmpp:make_iq_result(IQ, #services{list = Services}); - {error, timeout} -> % Has been logged already. - Txt = ?T("Service list retrieval timed out"), - Err = xmpp:err_internal_server_error(Txt, Lang), - xmpp:make_error(IQ, Err) - end; - deny -> - ?DEBUG("Won't perform external service discovery for ~ts", - [jid:encode(From)]), - Txt = ?T("Access denied by service policy"), - xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) + ?DEBUG("Performing external service discovery for ~ts", + [jid:encode(From)]), + case get_services(Host, From, Request) of + {ok, Services} when Restricted -> % A request. + xmpp:make_iq_result(IQ, #credentials{services = Services}); + {ok, Services} -> + xmpp:make_iq_result(IQ, #services{list = Services}); + {error, timeout} -> % Has been logged already. + Txt = ?T("Service list retrieval timed out"), + Err = xmpp:err_internal_server_error(Txt, Lang), + xmpp:make_error(IQ, Err) + end; + deny -> + ?DEBUG("Won't perform external service discovery for ~ts", + [jid:encode(From)]), + Txt = ?T("Access denied by service policy"), + xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec get_configured_services(gen_mod:opts()) -> [service()]. get_configured_services(Opts) -> LocalServices = case mod_stun_disco_opt:offer_local_services(Opts) of - true -> - ?DEBUG("Discovering local services", []), - find_local_services(); - false -> - ?DEBUG("Won't discover local services", []), - [] - end, + true -> + ?DEBUG("Discovering local services", []), + find_local_services(); + false -> + ?DEBUG("Won't discover local services", []), + [] + end, dedup(LocalServices ++ mod_stun_disco_opt:services(Opts)). + -spec get_configured_secret(gen_mod:opts()) -> binary(). get_configured_secret(Opts) -> case mod_stun_disco_opt:secret(Opts) of - undefined -> - ?DEBUG("Auto-generating secret", []), - new_secret(); - Secret -> - ?DEBUG("Using configured secret", []), - Secret + undefined -> + ?DEBUG("Auto-generating secret", []), + new_secret(); + Secret -> + ?DEBUG("Using configured secret", []), + Secret end. + -spec get_configured_ttl(gen_mod:opts()) -> non_neg_integer(). get_configured_ttl(Opts) -> mod_stun_disco_opt:credentials_lifetime(Opts) div 1000. + -spec new_secret() -> binary(). new_secret() -> p1_rand:bytes(20). --spec add_credentials(service(), binary(), binary(), non_neg_integer()) - -> service(). + +-spec add_credentials(service(), binary(), binary(), non_neg_integer()) -> + service(). add_credentials(Service, Hash, Secret, TTL) -> ExpireAt = erlang:system_time(second) + TTL, Username = make_username(ExpireAt, Hash), Password = make_password(Username, Secret), ?DEBUG("Created ephemeral credentials: ~s | ~s", [Username, Password]), - Service#service{username = Username, - password = Password, - expires = seconds_to_timestamp(ExpireAt)}. + Service#service{ + username = Username, + password = Password, + expires = seconds_to_timestamp(ExpireAt) + }. + -spec make_username(non_neg_integer(), binary()) -> binary(). make_username(ExpireAt, Hash) -> <<(integer_to_binary(ExpireAt))/binary, $:, Hash/binary>>. + -spec make_password(binary(), binary()) -> binary(). make_password(Username, Secret) -> base64:encode(misc:crypto_hmac(sha, Secret, Username)). + -spec get_password(binary(), binary()) -> binary(). get_password(Username, HostHash) -> try call({hash, HostHash}, {get_password, Username}) of - {turn_disco, Password} -> - Password + {turn_disco, Password} -> + Password catch - exit:{timeout, _} -> - ?ERROR_MSG("Asking ~ts for password timed out", [HostHash]), - <<>>; - exit:{noproc, _} -> % Can be triggered by bogus Username. - ?DEBUG("Cannot retrieve password for ~ts", [Username]), - <<>> + exit:{timeout, _} -> + ?ERROR_MSG("Asking ~ts for password timed out", [HostHash]), + <<>>; + exit:{noproc, _} -> % Can be triggered by bogus Username. + ?DEBUG("Cannot retrieve password for ~ts", [Username]), + <<>> end. --spec get_services(binary(), jid(), request()) - -> {ok, [service()]} | {error, timeout}. + +-spec get_services(binary(), jid(), request()) -> + {ok, [service()]} | {error, timeout}. get_services(Host, JID, Request) -> try call(Host, {get_services, JID, Request}) of - {turn_disco, Services} -> - {ok, Services} + {turn_disco, Services} -> + {ok, Services} catch - exit:{timeout, _} -> - ?ERROR_MSG("Asking ~ts for services timed out", [Host]), - {error, timeout} + exit:{timeout, _} -> + ?ERROR_MSG("Asking ~ts for services timed out", [Host]), + {error, timeout} end. + -spec find_local_services() -> [service()]. find_local_services() -> ParseListener = fun(Listener) -> parse_listener(Listener) end, lists:flatmap(ParseListener, ejabberd_option:listen()). + -spec parse_listener(ejabberd_listener:listener()) -> [service()]. parse_listener({_EndPoint, ?STUN_MODULE, #{tls := true}}) -> ?DEBUG("Ignoring TLS-enabled STUN/TURN listener", []), - []; % Avoid certificate hostname issues. + []; % Avoid certificate hostname issues. parse_listener({{Port, _Addr, Transport}, ?STUN_MODULE, Opts}) -> case get_listener_ips(Opts) of - {undefined, undefined} -> - ?INFO_MSG("Won't auto-announce STUN/TURN service on port ~B (~s) " - "without public IP address, please specify " - "'turn_ipv4_address' and optionally 'turn_ipv6_address'", - [Port, Transport]), - []; - {IPv4Addr, IPv6Addr} -> - lists:flatmap( - fun(undefined) -> - []; - (Addr) -> - StunService = #service{host = Addr, - port = Port, - transport = Transport, - restricted = false, - type = stun}, - case Opts of - #{use_turn := true} -> - ?INFO_MSG("Going to offer STUN/TURN service: " - "~s (~s)", - [addr_to_str(Addr, Port), Transport]), - [StunService, - #service{host = Addr, - port = Port, - transport = Transport, - restricted = is_restricted(Opts), - type = turn}]; - #{use_turn := false} -> - ?INFO_MSG("Going to offer STUN service: " - "~s (~s)", - [addr_to_str(Addr, Port), Transport]), - [StunService] - end - end, [IPv4Addr, IPv6Addr]) + {undefined, undefined} -> + ?INFO_MSG("Won't auto-announce STUN/TURN service on port ~B (~s) " + "without public IP address, please specify " + "'turn_ipv4_address' and optionally 'turn_ipv6_address'", + [Port, Transport]), + []; + {IPv4Addr, IPv6Addr} -> + lists:flatmap( + fun(undefined) -> + []; + (Addr) -> + StunService = #service{ + host = Addr, + port = Port, + transport = Transport, + restricted = false, + type = stun + }, + case Opts of + #{use_turn := true} -> + ?INFO_MSG("Going to offer STUN/TURN service: " + "~s (~s)", + [addr_to_str(Addr, Port), Transport]), + [StunService, + #service{ + host = Addr, + port = Port, + transport = Transport, + restricted = is_restricted(Opts), + type = turn + }]; + #{use_turn := false} -> + ?INFO_MSG("Going to offer STUN service: " + "~s (~s)", + [addr_to_str(Addr, Port), Transport]), + [StunService] + end + end, + [IPv4Addr, IPv6Addr]) end; parse_listener({_EndPoint, Module, _Opts}) -> ?DEBUG("Ignoring ~s listener", [Module]), []. + -spec get_listener_ips(map()) -> {inet:ip4_address() | undefined, - inet:ip6_address() | undefined}. + inet:ip6_address() | undefined}. get_listener_ips(#{ip := {0, 0, 0, 0}} = Opts) -> {get_turn_ipv4_addr(Opts), undefined}; get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 0}} = Opts) -> - {get_turn_ipv4_addr(Opts), get_turn_ipv6_addr(Opts)}; % Assume dual-stack. + {get_turn_ipv4_addr(Opts), get_turn_ipv6_addr(Opts)}; % Assume dual-stack. get_listener_ips(#{ip := {127, _, _, _}} = Opts) -> {get_turn_ipv4_addr(Opts), undefined}; get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) -> @@ -659,62 +761,72 @@ get_listener_ips(#{ip := {_, _, _, _} = IP}) -> get_listener_ips(#{ip := {_, _, _, _, _, _, _, _} = IP}) -> {undefined, IP}. + -spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined. get_turn_ipv4_addr(#{turn_ipv4_address := {_, _, _, _} = TurnIP}) -> TurnIP; get_turn_ipv4_addr(#{turn_ipv4_address := undefined}) -> case misc:get_my_ipv4_address() of - {127, _, _, _} -> - undefined; - IP -> - IP + {127, _, _, _} -> + undefined; + IP -> + IP end. + -spec get_turn_ipv6_addr(map()) -> inet:ip6_address() | undefined. get_turn_ipv6_addr(#{turn_ipv6_address := {_, _, _, _, _, _, _, _} = TurnIP}) -> TurnIP; get_turn_ipv6_addr(#{turn_ipv6_address := undefined}) -> case misc:get_my_ipv6_address() of - {0, 0, 0, 0, 0, 0, 0, 1} -> - undefined; - IP -> - IP + {0, 0, 0, 0, 0, 0, 0, 1} -> + undefined; + IP -> + IP end. + -spec is_restricted(map()) -> boolean(). is_restricted(#{auth_type := user}) -> true; is_restricted(#{auth_type := anonymous}) -> false. + -spec call(host_or_hash(), term()) -> term(). call(Host, Request) -> Proc = get_proc_name(Host), gen_server:call(Proc, Request, timer:seconds(15)). + -spec cast(host_or_hash(), term()) -> ok. cast(Host, Request) -> Proc = get_proc_name(Host), gen_server:cast(Proc, Request). + -spec get_proc_name(host_or_hash()) -> atom(). get_proc_name(Host) when is_binary(Host) -> get_proc_name({hash, hash(Host)}); get_proc_name({hash, HostHash}) -> gen_mod:get_module_proc(HostHash, ?MODULE). + -spec hash(binary()) -> binary(). hash(Host) -> str:to_hexlist(binary_part(crypto:hash(sha, Host), 0, 4)). + -spec dedup(list()) -> list(). dedup([]) -> []; -dedup([H | T]) -> [H | [E || E <- dedup(T), E /= H]]. +dedup([H | T]) -> [H | [ E || E <- dedup(T), E /= H ]]. + -spec seconds_to_timestamp(non_neg_integer()) -> erlang:timestamp(). seconds_to_timestamp(Seconds) -> {Seconds div 1000000, Seconds rem 1000000, 0}. + -spec addr_to_str(inet:ip_address(), 0..65535) -> iolist(). addr_to_str({_, _, _, _, _, _, _, _} = Addr, Port) -> [$[, inet_parse:ntoa(Addr), $], $:, integer_to_list(Port)]; diff --git a/src/mod_stun_disco_opt.erl b/src/mod_stun_disco_opt.erl index 43b8102e6..632f659fe 100644 --- a/src/mod_stun_disco_opt.erl +++ b/src/mod_stun_disco_opt.erl @@ -9,33 +9,37 @@ -export([secret/1]). -export([services/1]). + -spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, access). + -spec credentials_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). credentials_lifetime(Opts) when is_map(Opts) -> gen_mod:get_opt(credentials_lifetime, Opts); credentials_lifetime(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, credentials_lifetime). + -spec offer_local_services(gen_mod:opts() | global | binary()) -> boolean(). offer_local_services(Opts) when is_map(Opts) -> gen_mod:get_opt(offer_local_services, Opts); offer_local_services(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, offer_local_services). + -spec secret(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). secret(Opts) when is_map(Opts) -> gen_mod:get_opt(secret, Opts); secret(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, secret). + -spec services(gen_mod:opts() | global | binary()) -> [tuple()]. services(Opts) when is_map(Opts) -> gen_mod:get_opt(services, Opts); services(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, services). - diff --git a/src/mod_time.erl b/src/mod_time.erl index fbebf03a7..e562e3df9 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -32,22 +32,33 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_local_iq/1, - mod_options/1, depends/2, mod_doc/0]). +-export([start/2, + stop/1, + reload/3, + process_local_iq/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). + start(_Host, _Opts) -> {ok, [{iq_handler, ejabberd_local, ?NS_TIME, process_local_iq}]}. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -57,20 +68,25 @@ process_local_iq(#iq{type = get} = IQ) -> Now_universal = calendar:now_to_universal_time(Now), Now_local = calendar:universal_time_to_local_time(Now_universal), Seconds_diff = - calendar:datetime_to_gregorian_seconds(Now_local) - - calendar:datetime_to_gregorian_seconds(Now_universal), + calendar:datetime_to_gregorian_seconds(Now_local) - + calendar:datetime_to_gregorian_seconds(Now_universal), {Hd, Md, _} = calendar:seconds_to_time(abs(Seconds_diff)), xmpp:make_iq_result(IQ, #time{tzo = {Hd, Md}, utc = Now}). + depends(_Host, _Opts) -> []. + mod_options(_Host) -> []. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0202.html" "[XEP-0202: Entity Time]. In other words, " - "the module reports server's system time.")}. + "the module reports server's system time.") + }. diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index a225ba23b..1ef6a7637 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -34,21 +34,45 @@ -behaviour(gen_server). -behaviour(gen_mod). --export([start/2, stop/1, get_sm_features/5, mod_options/1, mod_doc/0, - process_local_iq/1, process_sm_iq/1, string2lower/1, - remove_user/2, export/1, import_info/0, import/5, import_start/2, - depends/2, process_search/1, process_vcard/1, get_vcard/2, - disco_items/5, disco_features/5, disco_identity/5, - vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). --export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). +-export([start/2, + stop/1, + get_sm_features/5, + mod_options/1, + mod_doc/0, + process_local_iq/1, + process_sm_iq/1, + string2lower/1, + remove_user/2, + export/1, + import_info/0, + import/5, + import_start/2, + depends/2, + process_search/1, + process_vcard/1, + get_vcard/2, + disco_items/5, + disco_features/5, + disco_identity/5, + vcard_iq_set/1, + mod_opt_type/1, + set_vcard/3, + make_vcard_search/4]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -export([route/1]). -export([webadmin_menu_hostuser/4, webadmin_page_hostuser/4]). -import(ejabberd_web_admin, [make_command/4, make_command/2, make_table/2]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_vcard.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). @@ -57,16 +81,21 @@ -define(VCARD_CACHE, vcard_cache). + -callback init(binary(), gen_mod:opts()) -> any(). -callback stop(binary()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback get_vcard(binary(), binary()) -> {ok, [xmlel()]} | error. --callback set_vcard(binary(), binary(), - xmlel(), #vcard_search{}) -> {atomic, any()}. +-callback set_vcard(binary(), + binary(), + xmlel(), + #vcard_search{}) -> {atomic, any()}. -callback search_fields(binary()) -> [{binary(), binary()}]. -callback search_reported(binary()) -> [{binary(), binary()}]. --callback search(binary(), [{binary(), [binary()]}], boolean(), - infinity | pos_integer()) -> [{binary(), binary()}]. +-callback search(binary(), + [{binary(), [binary()]}], + boolean(), + infinity | pos_integer()) -> [{binary(), binary()}]. -callback remove_user(binary(), binary()) -> {atomic, any()}. -callback is_search_supported(binary()) -> boolean(). -callback use_cache(binary()) -> boolean(). @@ -76,92 +105,121 @@ -record(state, {hosts :: [binary()], server_host :: binary()}). + %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). + stop(Host) -> gen_mod:stop_child(?MODULE, Host). + %%==================================================================== %% gen_server callbacks %%==================================================================== -init([Host|_]) -> +init([Host | _]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), - ejabberd_hooks:add(remove_user, Host, ?MODULE, - remove_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_VCARD, ?MODULE, process_local_iq), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_VCARD, ?MODULE, process_sm_iq), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), + ejabberd_hooks:add(remove_user, + Host, + ?MODULE, + remove_user, + 50), + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_VCARD, + ?MODULE, + process_local_iq), + gen_iq_handler:add_iq_handler(ejabberd_sm, + Host, + ?NS_VCARD, + ?MODULE, + process_sm_iq), + ejabberd_hooks:add(disco_sm_features, + Host, + ?MODULE, + get_sm_features, + 50), ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE, webadmin_menu_hostuser, 50), ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE, webadmin_page_hostuser, 50), MyHosts = gen_mod:get_opt_hosts(Opts), Search = mod_vcard_opt:search(Opts), - if Search -> - lists:foreach( - fun(MyHost) -> - ejabberd_hooks:add( - disco_local_items, MyHost, ?MODULE, disco_items, 100), - ejabberd_hooks:add( - disco_local_features, MyHost, ?MODULE, disco_features, 100), - ejabberd_hooks:add( - disco_local_identity, MyHost, ?MODULE, disco_identity, 100), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco, - process_local_iq_items), - gen_iq_handler:add_iq_handler( - ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco, - process_local_iq_info), - case Mod:is_search_supported(Host) of - false -> - ?WARNING_MSG("vCard search functionality is " - "not implemented for ~ts backend", - [mod_vcard_opt:db_type(Opts)]); - true -> - ejabberd_router:register_route( - MyHost, Host, {apply, ?MODULE, route}) - end - end, MyHosts); - true -> - ok + if + Search -> + lists:foreach( + fun(MyHost) -> + ejabberd_hooks:add( + disco_local_items, MyHost, ?MODULE, disco_items, 100), + ejabberd_hooks:add( + disco_local_features, MyHost, ?MODULE, disco_features, 100), + ejabberd_hooks:add( + disco_local_identity, MyHost, ?MODULE, disco_identity, 100), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search), + gen_iq_handler:add_iq_handler( + ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler( + ejabberd_local, + MyHost, + ?NS_DISCO_ITEMS, + mod_disco, + process_local_iq_items), + gen_iq_handler:add_iq_handler( + ejabberd_local, + MyHost, + ?NS_DISCO_INFO, + mod_disco, + process_local_iq_info), + case Mod:is_search_supported(Host) of + false -> + ?WARNING_MSG("vCard search functionality is " + "not implemented for ~ts backend", + [mod_vcard_opt:db_type(Opts)]); + true -> + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}) + end + end, + MyHosts); + true -> + ok end, {ok, #state{hosts = MyHosts, server_host = Host}}. + handle_call(Call, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Call]), {noreply, State}. + handle_cast(Cast, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Cast]), {noreply, State}. + handle_info({route, Packet}, State) -> - try route(Packet) - catch ?EX_RULE(Class, Reason, St) -> - StackTrace = ?EX_STACK(St), - ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", - [xmpp:pp(Packet), - misc:format_exception(2, Class, Reason, StackTrace)]) + try + route(Packet) + catch + ?EX_RULE(Class, Reason, St) -> + StackTrace = ?EX_STACK(St), + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), @@ -174,42 +232,53 @@ terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> Mod:stop(Host), lists:foreach( fun(MyHost) -> - ejabberd_router:unregister_route(MyHost), - ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100), - ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100), - ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO) - end, MyHosts). + ejabberd_router:unregister_route(MyHost), + ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO) + end, + MyHosts). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + -spec route(stanza()) -> ok. route(#iq{} = IQ) -> ejabberd_router:process_iq(IQ); route(_) -> ok. + -spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | empty | {result, [binary()]}. -get_sm_features({error, _Error} = Acc, _From, _To, - _Node, _Lang) -> + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | empty | {result, [binary()]}. +get_sm_features({error, _Error} = Acc, + _From, + _To, + _Node, + _Lang) -> Acc; get_sm_features(Acc, _From, _To, Node, _Lang) -> case Node of - <<"">> -> - case Acc of - {result, Features} -> - {result, [?NS_VCARD | Features]}; - empty -> {result, [?NS_VCARD]} - end; - _ -> Acc + <<"">> -> + case Acc of + {result, Features} -> + {result, [?NS_VCARD | Features]}; + empty -> {result, [?NS_VCARD]} + end; + _ -> Acc end. + -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), @@ -217,59 +286,75 @@ process_local_iq(#iq{type = set, lang = Lang} = IQ) -> process_local_iq(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), VCard = case mod_vcard_opt:vcard(ServerHost) of - undefined -> - #vcard_temp{fn = <<"ejabberd">>, - url = ejabberd_config:get_uri(), - desc = misc:get_descr(Lang, ?T("Erlang XMPP Server")), - bday = <<"2002-11-16">>}; - V -> - V - end, + undefined -> + #vcard_temp{ + fn = <<"ejabberd">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("Erlang XMPP Server")), + bday = <<"2002-11-16">> + }; + V -> + V + end, xmpp:make_iq_result(IQ, VCard). + -spec process_sm_iq(iq()) -> iq(). process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) -> #jid{lserver = LServer} = From, case lists:member(LServer, ejabberd_option:hosts()) of - true -> - case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of - drop -> ignore; - #stanza_error{} = Err -> xmpp:make_error(IQ, Err); - _ -> xmpp:make_iq_result(IQ) - end; - false -> - Txt = ?T("The query is only allowed from local users"), - xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) + true -> + case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of + drop -> ignore; + #stanza_error{} = Err -> xmpp:make_error(IQ, Err); + _ -> xmpp:make_iq_result(IQ) + end; + false -> + Txt = ?T("The query is only allowed from local users"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) end; process_sm_iq(#iq{type = get, from = From, to = To, lang = Lang} = IQ) -> #jid{luser = LUser, lserver = LServer} = To, case get_vcard(LUser, LServer) of - error -> - Txt = ?T("Database failure"), - xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); - [] -> - xmpp:make_iq_result(IQ, #vcard_temp{}); - Els -> - IQ#iq{type = result, to = From, from = To, sub_els = Els} + error -> + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + [] -> + xmpp:make_iq_result(IQ, #vcard_temp{}); + Els -> + IQ#iq{type = result, to = From, from = To, sub_els = Els} end. + -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_vcard(#iq{type = get, lang = Lang} = IQ) -> xmpp:make_iq_result( - IQ, #vcard_temp{fn = <<"ejabberd/mod_vcard">>, - url = ejabberd_config:get_uri(), - desc = misc:get_descr(Lang, ?T("ejabberd vCard module"))}). + IQ, + #vcard_temp{ + fn = <<"ejabberd/mod_vcard">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, ?T("ejabberd vCard module")) + }). + -spec process_search(iq()) -> iq(). process_search(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), xmpp:make_iq_result(IQ, mk_search_form(To, ServerHost, Lang)); -process_search(#iq{type = set, to = To, lang = Lang, - sub_els = [#search{xdata = #xdata{type = submit, - fields = Fs}}]} = IQ) -> +process_search(#iq{ + type = set, + to = To, + lang = Lang, + sub_els = [#search{ + xdata = #xdata{ + type = submit, + fields = Fs + } + }] + } = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), ResultXData = search_result(Lang, To, ServerHost, Fs), xmpp:make_iq_result(IQ, #search{xdata = ResultXData}); @@ -277,9 +362,13 @@ process_search(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Incorrect data form"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). + -spec disco_items({error, stanza_error()} | {result, [disco_item()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [disco_item()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [disco_item()]}. disco_items(empty, _From, _To, <<"">>, _Lang) -> {result, []}; disco_items(empty, _From, _To, _Node, Lang) -> @@ -287,81 +376,95 @@ disco_items(empty, _From, _To, _Node, Lang) -> disco_items(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, - jid(), jid(), binary(), binary()) -> - {error, stanza_error()} | {result, [binary()]}. + jid(), + jid(), + binary(), + binary()) -> + {error, stanza_error()} | {result, [binary()]}. disco_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; disco_features(Acc, _From, _To, <<"">>, _Lang) -> Features = case Acc of - {result, Fs} -> Fs; - empty -> [] - end, + {result, Fs} -> Fs; + empty -> [] + end, {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, - ?NS_VCARD, ?NS_SEARCH | Features]}; + ?NS_VCARD, ?NS_SEARCH | Features]}; disco_features(empty, _From, _To, _Node, Lang) -> Txt = ?T("No features available"), {error, xmpp:err_item_not_found(Txt, Lang)}; disco_features(Acc, _From, _To, _Node, _Lang) -> Acc. --spec disco_identity([identity()], jid(), jid(), - binary(), binary()) -> [identity()]. + +-spec disco_identity([identity()], + jid(), + jid(), + binary(), + binary()) -> [identity()]. disco_identity(Acc, _From, To, <<"">>, Lang) -> Host = ejabberd_router:host_of_route(To#jid.lserver), Name = mod_vcard_opt:name(Host), - [#identity{category = <<"directory">>, - type = <<"user">>, - name = translate:translate(Lang, Name)}|Acc]; + [#identity{ + category = <<"directory">>, + type = <<"user">>, + name = translate:translate(Lang, Name) + } | Acc]; disco_identity(Acc, _From, _To, _Node, _Lang) -> Acc. + -spec get_vcard(binary(), binary()) -> [xmlel()] | error. get_vcard(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Result = case use_cache(Mod, LServer) of - true -> - ets_cache:lookup( - ?VCARD_CACHE, {LUser, LServer}, - fun() -> Mod:get_vcard(LUser, LServer) end); - false -> - Mod:get_vcard(LUser, LServer) - end, + true -> + ets_cache:lookup( + ?VCARD_CACHE, + {LUser, LServer}, + fun() -> Mod:get_vcard(LUser, LServer) end); + false -> + Mod:get_vcard(LUser, LServer) + end, case Result of - {ok, Els} -> Els; - error -> error + {ok, Els} -> Els; + error -> error end. + -spec make_vcard_search(binary(), binary(), binary(), xmlel()) -> #vcard_search{}. make_vcard_search(User, LUser, LServer, VCARD) -> FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), Family = fxml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), + [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), Given = fxml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), + [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), Middle = fxml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), + [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), Nickname = fxml:get_path_s(VCARD, - [{elem, <<"NICKNAME">>}, cdata]), + [{elem, <<"NICKNAME">>}, cdata]), BDay = fxml:get_path_s(VCARD, - [{elem, <<"BDAY">>}, cdata]), + [{elem, <<"BDAY">>}, cdata]), CTRY = fxml:get_path_s(VCARD, - [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), + [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), Locality = fxml:get_path_s(VCARD, - [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, - cdata]), + [{elem, <<"ADR">>}, + {elem, <<"LOCALITY">>}, + cdata]), EMail1 = fxml:get_path_s(VCARD, - [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), + [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), EMail2 = fxml:get_path_s(VCARD, - [{elem, <<"EMAIL">>}, cdata]), + [{elem, <<"EMAIL">>}, cdata]), OrgName = fxml:get_path_s(VCARD, - [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), + [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), OrgUnit = fxml:get_path_s(VCARD, - [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), + [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), EMail = case EMail1 of - <<"">> -> EMail2; - _ -> EMail1 - end, + <<"">> -> EMail2; + _ -> EMail1 + end, LFN = string2lower(FN), LFamily = string2lower(Family), LGiven = string2lower(Given), @@ -374,146 +477,171 @@ make_vcard_search(User, LUser, LServer, VCARD) -> LOrgName = string2lower(OrgName), LOrgUnit = string2lower(OrgUnit), US = {LUser, LServer}, - #vcard_search{us = US, - user = {User, LServer}, - luser = LUser, fn = FN, - lfn = LFN, - family = Family, - lfamily = LFamily, - given = Given, - lgiven = LGiven, - middle = Middle, - lmiddle = LMiddle, - nickname = Nickname, - lnickname = LNickname, - bday = BDay, - lbday = LBDay, - ctry = CTRY, - lctry = LCTRY, - locality = Locality, - llocality = LLocality, - email = EMail, - lemail = LEMail, - orgname = OrgName, - lorgname = LOrgName, - orgunit = OrgUnit, - lorgunit = LOrgUnit}. + #vcard_search{ + us = US, + user = {User, LServer}, + luser = LUser, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit + }. + -spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}. -vcard_iq_set(#iq{from = #jid{user = FromUser, lserver = FromLServer}, - to = #jid{user = ToUser, lserver = ToLServer}, - lang = Lang}) +vcard_iq_set(#iq{ + from = #jid{user = FromUser, lserver = FromLServer}, + to = #jid{user = ToUser, lserver = ToLServer}, + lang = Lang + }) when (FromUser /= ToUser) or (FromLServer /= ToLServer) -> Txt = ?T("User not allowed to perform an IQ set on another user's vCard."), {stop, xmpp:err_forbidden(Txt, Lang)}; vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> #jid{user = User, lserver = LServer} = From, case set_vcard(User, LServer, VCard) of - {error, badarg} -> - %% Should not be here? - Txt = ?T("Nodeprep has failed"), - {stop, xmpp:err_internal_server_error(Txt, Lang)}; - {error, not_implemented} -> - Txt = ?T("Updating the vCard is not supported by the vCard storage backend"), - {stop, xmpp:err_feature_not_implemented(Txt, Lang)}; - ok -> - IQ + {error, badarg} -> + %% Should not be here? + Txt = ?T("Nodeprep has failed"), + {stop, xmpp:err_internal_server_error(Txt, Lang)}; + {error, not_implemented} -> + Txt = ?T("Updating the vCard is not supported by the vCard storage backend"), + {stop, xmpp:err_feature_not_implemented(Txt, Lang)}; + ok -> + IQ end; vcard_iq_set(Acc) -> Acc. + -spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) -> - {error, badarg | not_implemented | binary()} | ok. + {error, badarg | not_implemented | binary()} | ok. set_vcard(User, LServer, VCARD) -> case jid:nodeprep(User) of - error -> - {error, badarg}; - LUser -> - VCardEl = xmpp:encode(VCARD), - VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl), - Mod = gen_mod:db_mod(LServer, ?MODULE), + error -> + {error, badarg}; + LUser -> + VCardEl = xmpp:encode(VCARD), + VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl), + Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch) of {atomic, ok} -> - ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)), + ets_cache:delete(?VCARD_CACHE, + {LUser, LServer}, + cache_nodes(Mod, LServer)), ok; {atomic, Error} -> {error, Error} end end. + -spec string2lower(binary()) -> binary(). string2lower(String) -> case stringprep:tolower_nofilter(String) of - Lower when is_binary(Lower) -> Lower; - error -> String + Lower when is_binary(Lower) -> Lower; + error -> String end. + -spec mk_tfield(binary(), binary(), binary()) -> xdata_field(). mk_tfield(Label, Var, Lang) -> - #xdata_field{type = 'text-single', - label = translate:translate(Lang, Label), - var = Var}. + #xdata_field{ + type = 'text-single', + label = translate:translate(Lang, Label), + var = Var + }. + -spec mk_field(binary(), binary()) -> xdata_field(). mk_field(Var, Val) -> #xdata_field{var = Var, values = [Val]}. + -spec mk_search_form(jid(), binary(), binary()) -> search(). mk_search_form(JID, ServerHost, Lang) -> Title = <<(translate:translate(Lang, ?T("Search users in ")))/binary, - (jid:encode(JID))/binary>>, + (jid:encode(JID))/binary>>, Mod = gen_mod:db_mod(ServerHost, ?MODULE), SearchFields = Mod:search_fields(ServerHost), - Fs = [mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields], - X = #xdata{type = form, - title = Title, - instructions = [make_instructions(Mod, Lang)], - fields = Fs}, - #search{instructions = - translate:translate( - Lang, ?T("You need an x:data capable client to search")), - xdata = X}. + Fs = [ mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields ], + X = #xdata{ + type = form, + title = Title, + instructions = [make_instructions(Mod, Lang)], + fields = Fs + }, + #search{ + instructions = + translate:translate( + Lang, ?T("You need an x:data capable client to search")), + xdata = X + }. + -spec make_instructions(module(), binary()) -> binary(). make_instructions(Mod, Lang) -> Fill = translate:translate( - Lang, - ?T("Fill in the form to search for any matching " - "XMPP User")), + Lang, + ?T("Fill in the form to search for any matching " + "XMPP User")), Add = translate:translate( - Lang, - ?T(" (Add * to the end of field to match substring)")), + Lang, + ?T(" (Add * to the end of field to match substring)")), case Mod of - mod_vcard_mnesia -> Fill; - _ -> str:concat(Fill, Add) + mod_vcard_mnesia -> Fill; + _ -> str:concat(Fill, Add) end. + -spec search_result(binary(), jid(), binary(), [xdata_field()]) -> xdata(). search_result(Lang, JID, ServerHost, XFields) -> Mod = gen_mod:db_mod(ServerHost, ?MODULE), - Reported = [mk_tfield(Label, Var, Lang) || - {Label, Var} <- Mod:search_reported(ServerHost)], - #xdata{type = result, - title = <<(translate:translate(Lang, - ?T("Search Results for ")))/binary, - (jid:encode(JID))/binary>>, - reported = Reported, - items = lists:map(fun (Item) -> item_to_field(Item) end, - search(ServerHost, XFields))}. + Reported = [ mk_tfield(Label, Var, Lang) + || {Label, Var} <- Mod:search_reported(ServerHost) ], + #xdata{ + type = result, + title = <<(translate:translate(Lang, + ?T("Search Results for ")))/binary, + (jid:encode(JID))/binary>>, + reported = Reported, + items = lists:map(fun(Item) -> item_to_field(Item) end, + search(ServerHost, XFields)) + }. + -spec item_to_field([{binary(), binary()}]) -> [xdata_field()]. item_to_field(Items) -> - [mk_field(Var, Value) || {Var, Value} <- Items]. + [ mk_field(Var, Value) || {Var, Value} <- Items ]. + -spec search(binary(), [xdata_field()]) -> [binary()]. search(LServer, XFields) -> - Data = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- XFields], + Data = [ {Var, Vals} || #xdata_field{var = Var, values = Vals} <- XFields ], Mod = gen_mod:db_mod(LServer, ?MODULE), AllowReturnAll = mod_vcard_opt:allow_return_all(LServer), MaxMatch = mod_vcard_opt:matches(LServer), Mod:search(LServer, Data, AllowReturnAll, MaxMatch). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> @@ -523,16 +651,18 @@ remove_user(User, Server) -> Mod:remove_user(LUser, LServer), ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). + -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of - true -> - CacheOpts = cache_opts(Host, Opts), - ets_cache:new(?VCARD_CACHE, CacheOpts); - false -> - ets_cache:delete(?VCARD_CACHE) + true -> + CacheOpts = cache_opts(Host, Opts), + ets_cache:new(?VCARD_CACHE, CacheOpts); + false -> + ets_cache:delete(?VCARD_CACHE) end. + -spec cache_opts(binary(), gen_mod:opts()) -> [proplists:property()]. cache_opts(_Host, Opts) -> MaxSize = mod_vcard_opt:cache_size(Opts), @@ -540,50 +670,61 @@ cache_opts(_Host, Opts) -> LifeTime = mod_vcard_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of - true -> Mod:use_cache(Host); - false -> mod_vcard_opt:use_cache(Host) + true -> Mod:use_cache(Host); + false -> mod_vcard_opt:use_cache(Host) end. + -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of - true -> Mod:cache_nodes(Host); - false -> ejabberd_cluster:get_nodes() + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() end. + import_info() -> [{<<"vcard">>, 3}, {<<"vcard_search">>, 24}]. + import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). + import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). + export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). + %%% %%% WebAdmin %%% + webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) -> Acc ++ [{<<"vcard">>, <<"vCard">>}]. -webadmin_page_hostuser(_, Host, User, - #request{path = [<<"vcard">>]} = R) -> + +webadmin_page_hostuser(_, + Host, + User, + #request{path = [<<"vcard">>]} = R) -> Head = ?H1GL(<<"vCard">>, <<"modules/#mod_vcard">>, <<"mod_vcard">>), Set = [make_command(set_nickname, R, [{<<"user">>, User}, {<<"host">>, Host}], []), make_command(set_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []), make_command(set_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []), make_command(set_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], - timer:sleep(100), % setting vcard takes a while, let's delay the get commands + timer:sleep(100), % setting vcard takes a while, let's delay the get commands FieldNames = [<<"VERSION">>, <<"FN">>, <<"NICKNAME">>, <<"BDAY">>], FieldNames2 = [{<<"N">>, <<"FAMILY">>}, @@ -595,38 +736,41 @@ webadmin_page_hostuser(_, Host, User, Get = [make_command(get_vcard, R, [{<<"user">>, User}, {<<"host">>, Host}], []), ?XE(<<"blockquote">>, [make_table([<<"name">>, <<"value">>], - [{?C(FieldName), - make_command(get_vcard, - R, - [{<<"user">>, User}, - {<<"host">>, Host}, - {<<"name">>, FieldName}], - [{only, value}])} - || FieldName <- FieldNames])]), + [ {?C(FieldName), + make_command(get_vcard, + R, + [{<<"user">>, User}, + {<<"host">>, Host}, + {<<"name">>, FieldName}], + [{only, value}])} + || FieldName <- FieldNames ])]), make_command(get_vcard2, R, [{<<"user">>, User}, {<<"host">>, Host}], []), ?XE(<<"blockquote">>, [make_table([<<"name">>, <<"subname">>, <<"value">>], - [{?C(FieldName), - ?C(FieldSubName), - make_command(get_vcard2, - R, - [{<<"user">>, User}, - {<<"host">>, Host}, - {<<"name">>, FieldName}, - {<<"subname">>, FieldSubName}], - [{only, value}])} - || {FieldName, FieldSubName} <- FieldNames2])]), + [ {?C(FieldName), + ?C(FieldSubName), + make_command(get_vcard2, + R, + [{<<"user">>, User}, + {<<"host">>, Host}, + {<<"name">>, FieldName}, + {<<"subname">>, FieldSubName}], + [{only, value}])} + || {FieldName, FieldSubName} <- FieldNames2 ])]), make_command(get_vcard2_multi, R, [{<<"user">>, User}, {<<"host">>, Host}], [])], {stop, Head ++ Get ++ Set}; webadmin_page_hostuser(Acc, _, _, _) -> Acc. + %%% %%% Documentation %%% + depends(_Host, _Opts) -> []. + mod_opt_type(allow_return_all) -> econf:bool(); mod_opt_type(name) -> @@ -652,6 +796,7 @@ mod_opt_type(cache_life_time) -> mod_opt_type(vcard) -> econf:vcard_temp(). + mod_options(Host) -> [{allow_return_all, false}, {host, <<"vjud.", Host/binary>>}, @@ -666,8 +811,10 @@ mod_options(Host) -> {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module allows end users to store and retrieve " "their vCard, and to retrieve other users vCards, " "as defined in https://xmpp.org/extensions/xep-0054.html" @@ -677,63 +824,84 @@ mod_doc() -> "its vCard when queried."), opts => [{allow_return_all, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This option enables you to specify if search " "operations with empty input fields should return " "all users who added some information to their vCard. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, - #{value => ?T("[Host, ...]"), + #{ + value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"vjud.\". " - "The keyword '@HOST@' is replaced with the real virtual host name.")}}, + "The keyword '@HOST@' is replaced with the real virtual host name.") + }}, {name, - #{value => ?T("Name"), + #{ + value => ?T("Name"), desc => ?T("The value of the service name. This name is only visible in some " "clients that support https://xmpp.org/extensions/xep-0030.html" - "[XEP-0030: Service Discovery]. The default is 'vCard User Search'.")}}, + "[XEP-0030: Service Discovery]. The default is 'vCard User Search'.") + }}, {matches, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => ?T("With this option, the number of reported search results " "can be limited. If the option's value is set to 'infinity', " - "all search results are reported. The default value is '30'.")}}, + "all search results are reported. The default value is '30'.") + }}, {search, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("This option specifies whether the search functionality " "is enabled or not. If disabled, the options 'hosts', 'name' " "and 'vcard' will be ignored and the Jabber User Directory " "service will not appear in the Service Discovery item list. " - "The default value is 'false'.")}}, + "The default value is 'false'.") + }}, {db_type, - #{value => "mnesia | sql | ldap", + #{ + value => "mnesia | sql | ldap", desc => - ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.") + }}, {use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}, {vcard, - #{value => ?T("vCard"), + #{ + value => ?T("vCard"), desc => ?T("A custom vCard of the server that will be displayed " "by some XMPP clients in Service Discovery. The value of " @@ -758,4 +926,6 @@ mod_doc() -> " adr:", " -", " work: true", - " street: Elm Street"]}}]}. + " street: Elm Street"] + }}] + }. diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 0d67fb564..f21c31bb2 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -29,18 +29,33 @@ %% API -export([start_link/2]). --export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, - remove_user/2, import/3, search_fields/1, search_reported/1, - mod_opt_type/1, mod_options/1, mod_doc/0]). +-export([init/2, + stop/1, + get_vcard/2, + set_vcard/4, + search/4, + remove_user/2, + import/3, + search_fields/1, + search_reported/1, + mod_opt_type/1, + mod_options/1, + mod_doc/0]). -export([is_search_supported/1]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). -include("eldap.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -define(PROCNAME, ejabberd_mod_vcard_ldap). @@ -50,31 +65,32 @@ %% @indent-begin -record(state, - {serverhost = <<"">> :: binary(), + {serverhost = <<"">> :: binary(), myhosts = [] :: [binary()], eldap_id = <<"">> :: binary(), search = false :: boolean(), servers = [] :: [binary()], backups = [] :: [binary()], - port = ?LDAP_PORT :: inet:port_number(), + port = ?LDAP_PORT :: inet:port_number(), tls_options = [] :: list(), dn = <<"">> :: binary(), base = <<"">> :: binary(), password = <<"">> :: binary(), uids = [] :: [{binary(), binary()}], vcard_map = [] :: [{binary(), [{binary(), [binary()]}]}], - vcard_map_attrs = [] :: [binary()], + vcard_map_attrs = [] :: [binary()], user_filter = <<"">> :: binary(), search_filter :: eldap:filter(), - search_fields = [] :: [{binary(), binary()}], + search_fields = [] :: [{binary(), binary()}], search_reported = [] :: [{binary(), binary()}], search_reported_attrs = [] :: [binary()], - deref_aliases = never :: never | searching | finding | always, + deref_aliases = never :: never | searching | finding | always, matches = 0 :: non_neg_integer()}). %% @indent-end %% @efmt:on -%% + %% + %%%=================================================================== %%% API @@ -83,44 +99,57 @@ start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + init(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 1000, worker, [?MODULE]}, + ChildSpec = {Proc, + {?MODULE, start_link, [Host, Opts]}, + transient, + 1000, + worker, + [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, ChildSpec). + stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), supervisor:terminate_child(ejabberd_backend_sup, Proc), supervisor:delete_child(ejabberd_backend_sup, Proc), ok. + is_search_supported(_LServer) -> true. + get_vcard(LUser, LServer) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), VCardMap = State#state.vcard_map, case find_ldap_user(LUser, State) of - #eldap_entry{attributes = Attributes} -> - VCard = ldap_attributes_to_vcard(Attributes, VCardMap, - {LUser, LServer}), - {ok, [xmpp:encode(VCard)]}; - _ -> - {ok, []} + #eldap_entry{attributes = Attributes} -> + VCard = ldap_attributes_to_vcard(Attributes, + VCardMap, + {LUser, LServer}), + {ok, [xmpp:encode(VCard)]}; + _ -> + {ok, []} end. + set_vcard(_LUser, _LServer, _VCard, _VCardSearch) -> {atomic, not_implemented}. + search_fields(LServer) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), State#state.search_fields. + search_reported(LServer) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), State#state.search_reported. + search(LServer, Data, _AllowReturnAll, MaxMatch) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), Base = State#state.base, @@ -129,65 +158,71 @@ search(LServer, Data, _AllowReturnAll, MaxMatch) -> UIDs = State#state.uids, ReportedAttrs = State#state.search_reported_attrs, Filter = eldap:'and'([SearchFilter, - eldap_utils:make_filter(Data, UIDs)]), + eldap_utils:make_filter(Data, UIDs)]), case eldap_pool:search(Eldap_ID, - [{base, Base}, {filter, Filter}, {limit, MaxMatch}, - {deref_aliases, State#state.deref_aliases}, - {attributes, ReportedAttrs}]) - of - #eldap_search_result{entries = E} -> - search_items(E, State); - _ -> - [] + [{base, Base}, + {filter, Filter}, + {limit, MaxMatch}, + {deref_aliases, State#state.deref_aliases}, + {attributes, ReportedAttrs}]) of + #eldap_search_result{entries = E} -> + search_items(E, State); + _ -> + [] end. + search_items(Entries, State) -> LServer = State#state.serverhost, SearchReported = State#state.search_reported, VCardMap = State#state.vcard_map, UIDs = State#state.uids, - Attributes = lists:map(fun (E) -> - #eldap_entry{attributes = Attrs} = E, Attrs - end, - Entries), + Attributes = lists:map(fun(E) -> + #eldap_entry{attributes = Attrs} = E, Attrs + end, + Entries), lists:filtermap( fun(Attrs) -> - case eldap_utils:find_ldap_attrs(UIDs, Attrs) of - {U, UIDAttrFormat} -> - case eldap_utils:get_user_part(U, UIDAttrFormat) of - {ok, Username} -> - case ejabberd_auth:user_exists(Username, - LServer) of - true -> - RFields = lists:map( - fun({_, VCardName}) -> - {VCardName, - map_vcard_attr(VCardName, - Attrs, - VCardMap, - {Username, - ejabberd_config:get_myname()})} - end, - SearchReported), - J = <>, - {true, [{<<"jid">>, J} | RFields]}; - _ -> - false - end; - _ -> - false - end; - <<"">> -> - false - end - end, Attributes). + case eldap_utils:find_ldap_attrs(UIDs, Attrs) of + {U, UIDAttrFormat} -> + case eldap_utils:get_user_part(U, UIDAttrFormat) of + {ok, Username} -> + case ejabberd_auth:user_exists(Username, + LServer) of + true -> + RFields = lists:map( + fun({_, VCardName}) -> + {VCardName, + map_vcard_attr(VCardName, + Attrs, + VCardMap, + {Username, + ejabberd_config:get_myname()})} + end, + SearchReported), + J = <>, + {true, [{<<"jid">>, J} | RFields]}; + _ -> + false + end; + _ -> + false + end; + <<"">> -> + false + end + end, + Attributes). + remove_user(_User, _Server) -> {atomic, not_implemented}. + import(_, _, _) -> ok. + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== @@ -195,31 +230,40 @@ init([Host, Opts]) -> process_flag(trap_exit, true), State = parse_options(Host, Opts), eldap_pool:start_link(State#state.eldap_id, - State#state.servers, State#state.backups, - State#state.port, State#state.dn, - State#state.password, State#state.tls_options), + State#state.servers, + State#state.backups, + State#state.port, + State#state.dn, + State#state.password, + State#state.tls_options), {ok, State}. + handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -229,90 +273,103 @@ find_ldap_user(User, State) -> Eldap_ID = State#state.eldap_id, VCardAttrs = State#state.vcard_map_attrs, case eldap_filter:parse(RFC2254_Filter, - [{<<"%u">>, User}]) - of - {ok, EldapFilter} -> - case eldap_pool:search(Eldap_ID, - [{base, Base}, {filter, EldapFilter}, - {deref_aliases, State#state.deref_aliases}, - {attributes, VCardAttrs}]) - of - #eldap_search_result{entries = [E | _]} -> E; - _ -> false - end; - _ -> false + [{<<"%u">>, User}]) of + {ok, EldapFilter} -> + case eldap_pool:search(Eldap_ID, + [{base, Base}, + {filter, EldapFilter}, + {deref_aliases, State#state.deref_aliases}, + {attributes, VCardAttrs}]) of + #eldap_search_result{entries = [E | _]} -> E; + _ -> false + end; + _ -> false end. + ldap_attributes_to_vcard(Attributes, VCardMap, UD) -> Attrs = lists:map( - fun({VCardName, _}) -> - {VCardName, map_vcard_attr(VCardName, Attributes, VCardMap, UD)} - end, VCardMap), + fun({VCardName, _}) -> + {VCardName, map_vcard_attr(VCardName, Attributes, VCardMap, UD)} + end, + VCardMap), lists:foldl(fun ldap_attribute_to_vcard/2, #vcard_temp{}, Attrs). + -spec ldap_attribute_to_vcard({binary(), binary()}, vcard_temp()) -> vcard_temp(). ldap_attribute_to_vcard({Attr, Value}, V) -> Ts = V#vcard_temp.tel, Es = V#vcard_temp.email, N = case V#vcard_temp.n of - undefined -> #vcard_name{}; - _ -> V#vcard_temp.n - end, + undefined -> #vcard_name{}; + _ -> V#vcard_temp.n + end, O = case V#vcard_temp.org of - undefined -> #vcard_org{}; - _ -> V#vcard_temp.org - end, + undefined -> #vcard_org{}; + _ -> V#vcard_temp.org + end, A = case V#vcard_temp.adr of - [] -> #vcard_adr{}; - As -> hd(As) - end, + [] -> #vcard_adr{}; + As -> hd(As) + end, case str:to_lower(Attr) of - <<"fn">> -> V#vcard_temp{fn = Value}; - <<"nickname">> -> V#vcard_temp{nickname = Value}; - <<"title">> -> V#vcard_temp{title = Value}; - <<"bday">> -> V#vcard_temp{bday = Value}; - <<"url">> -> V#vcard_temp{url = Value}; - <<"desc">> -> V#vcard_temp{desc = Value}; - <<"role">> -> V#vcard_temp{role = Value}; - <<"tel">> -> V#vcard_temp{tel = [#vcard_tel{number = Value}|Ts]}; - <<"email">> -> V#vcard_temp{email = [#vcard_email{userid = Value}|Es]}; - <<"photo">> -> V#vcard_temp{photo = #vcard_photo{binval = Value, - type = photo_type(Value)}}; - <<"family">> -> V#vcard_temp{n = N#vcard_name{family = Value}}; - <<"given">> -> V#vcard_temp{n = N#vcard_name{given = Value}}; - <<"middle">> -> V#vcard_temp{n = N#vcard_name{middle = Value}}; - <<"orgname">> -> V#vcard_temp{org = O#vcard_org{name = Value}}; - <<"orgunit">> -> V#vcard_temp{org = O#vcard_org{units = [Value]}}; - <<"locality">> -> V#vcard_temp{adr = [A#vcard_adr{locality = Value}]}; - <<"street">> -> V#vcard_temp{adr = [A#vcard_adr{street = Value}]}; - <<"ctry">> -> V#vcard_temp{adr = [A#vcard_adr{ctry = Value}]}; - <<"region">> -> V#vcard_temp{adr = [A#vcard_adr{region = Value}]}; - <<"pcode">> -> V#vcard_temp{adr = [A#vcard_adr{pcode = Value}]}; - _ -> V + <<"fn">> -> V#vcard_temp{fn = Value}; + <<"nickname">> -> V#vcard_temp{nickname = Value}; + <<"title">> -> V#vcard_temp{title = Value}; + <<"bday">> -> V#vcard_temp{bday = Value}; + <<"url">> -> V#vcard_temp{url = Value}; + <<"desc">> -> V#vcard_temp{desc = Value}; + <<"role">> -> V#vcard_temp{role = Value}; + <<"tel">> -> V#vcard_temp{tel = [#vcard_tel{number = Value} | Ts]}; + <<"email">> -> V#vcard_temp{email = [#vcard_email{userid = Value} | Es]}; + <<"photo">> -> + V#vcard_temp{ + photo = #vcard_photo{ + binval = Value, + type = photo_type(Value) + } + }; + <<"family">> -> V#vcard_temp{n = N#vcard_name{family = Value}}; + <<"given">> -> V#vcard_temp{n = N#vcard_name{given = Value}}; + <<"middle">> -> V#vcard_temp{n = N#vcard_name{middle = Value}}; + <<"orgname">> -> V#vcard_temp{org = O#vcard_org{name = Value}}; + <<"orgunit">> -> V#vcard_temp{org = O#vcard_org{units = [Value]}}; + <<"locality">> -> V#vcard_temp{adr = [A#vcard_adr{locality = Value}]}; + <<"street">> -> V#vcard_temp{adr = [A#vcard_adr{street = Value}]}; + <<"ctry">> -> V#vcard_temp{adr = [A#vcard_adr{ctry = Value}]}; + <<"region">> -> V#vcard_temp{adr = [A#vcard_adr{region = Value}]}; + <<"pcode">> -> V#vcard_temp{adr = [A#vcard_adr{pcode = Value}]}; + _ -> V end. + -spec photo_type(binary()) -> binary(). photo_type(Value) -> Type = eimp:get_type(Value), <<"image/", (atom_to_binary(Type, latin1))/binary>>. + map_vcard_attr(VCardName, Attributes, Pattern, UD) -> Res = lists:filter( - fun({Name, _}) -> - eldap_utils:case_insensitive_match(Name, VCardName) - end, Pattern), + fun({Name, _}) -> + eldap_utils:case_insensitive_match(Name, VCardName) + end, + Pattern), case Res of - [{_, [{Str, Attrs}|_]}] -> - process_pattern(Str, UD, - [eldap_utils:get_ldap_attr(X, Attributes) - || X <- Attrs]); - _ -> <<"">> + [{_, [{Str, Attrs} | _]}] -> + process_pattern(Str, + UD, + [ eldap_utils:get_ldap_attr(X, Attributes) + || X <- Attrs ]); + _ -> <<"">> end. + process_pattern(Str, {User, Domain}, AttrValues) -> eldap_filter:do_sub(Str, - [{<<"%u">>, User}, {<<"%d">>, Domain}] ++ - [{<<"%s">>, V, 1} || V <- AttrValues]). + [{<<"%u">>, User}, {<<"%d">>, Domain}] ++ + [ {<<"%s">>, V, 1} || V <- AttrValues ]). + default_vcard_map() -> [{<<"NICKNAME">>, [{<<"%u">>, []}]}, @@ -336,6 +393,7 @@ default_vcard_map() -> {<<"ROLE">>, [{<<"%s">>, [<<"employeeType">>]}]}, {<<"PHOTO">>, [{<<"%s">>, [<<"jpegPhoto">>]}]}]. + default_search_fields() -> [{?T("User"), <<"%u">>}, {?T("Full Name"), <<"displayName">>}, @@ -350,6 +408,7 @@ default_search_fields() -> {?T("Organization Name"), <<"o">>}, {?T("Organization Unit"), <<"ou">>}]. + default_search_reported() -> [{?T("Full Name"), <<"FN">>}, {?T("Given Name"), <<"FIRST">>}, @@ -363,6 +422,7 @@ default_search_reported() -> {?T("Organization Name"), <<"ORGNAME">>}, {?T("Organization Unit"), <<"ORGUNIT">>}]. + parse_options(Host, Opts) -> MyHosts = gen_mod:get_opt_hosts(Opts), Search = mod_vcard_opt:search(Opts), @@ -374,51 +434,60 @@ parse_options(Host, Opts) -> SubFilter = eldap_utils:generate_subfilter(UIDs), UserFilter = case mod_vcard_ldap_opt:ldap_filter(Opts) of <<"">> -> - SubFilter; + SubFilter; F -> <<"(&", SubFilter/binary, F/binary, ")">> end, {ok, SearchFilter} = - eldap_filter:parse(eldap_filter:do_sub(UserFilter, - [{<<"%u">>, <<"*">>}])), + eldap_filter:parse(eldap_filter:do_sub(UserFilter, + [{<<"%u">>, <<"*">>}])), VCardMap = mod_vcard_ldap_opt:ldap_vcard_map(Opts), SearchFields = mod_vcard_ldap_opt:ldap_search_fields(Opts), SearchReported = mod_vcard_ldap_opt:ldap_search_reported(Opts), - UIDAttrs = [UAttr || {UAttr, _} <- UIDs], + UIDAttrs = [ UAttr || {UAttr, _} <- UIDs ], VCardMapAttrs = lists:usort( - lists:flatten( - lists:map( - fun({_, Map}) -> - [Attrs || {_, Attrs} <- Map] - end, VCardMap) ++ UIDAttrs)), + lists:flatten( + lists:map( + fun({_, Map}) -> + [ Attrs || {_, Attrs} <- Map ] + end, + VCardMap) ++ UIDAttrs)), SearchReportedAttrs = lists:usort( - lists:flatten( - lists:map( - fun ({_, N}) -> - case lists:keyfind(N, 1, VCardMap) of - {_, Map} -> - [Attrs || {_, Attrs} <- Map]; - false -> - [] - end - end, SearchReported) ++ UIDAttrs)), - #state{serverhost = Host, myhosts = MyHosts, - eldap_id = Eldap_ID, search = Search, - servers = Cfg#eldap_config.servers, - backups = Cfg#eldap_config.backups, - port = Cfg#eldap_config.port, - tls_options = Cfg#eldap_config.tls_options, - dn = Cfg#eldap_config.dn, - password = Cfg#eldap_config.password, - base = Cfg#eldap_config.base, - deref_aliases = Cfg#eldap_config.deref_aliases, - uids = UIDs, vcard_map = VCardMap, - vcard_map_attrs = VCardMapAttrs, - user_filter = UserFilter, search_filter = SearchFilter, - search_fields = SearchFields, - search_reported = SearchReported, - search_reported_attrs = SearchReportedAttrs, - matches = Matches}. + lists:flatten( + lists:map( + fun({_, N}) -> + case lists:keyfind(N, 1, VCardMap) of + {_, Map} -> + [ Attrs || {_, Attrs} <- Map ]; + false -> + [] + end + end, + SearchReported) ++ UIDAttrs)), + #state{ + serverhost = Host, + myhosts = MyHosts, + eldap_id = Eldap_ID, + search = Search, + servers = Cfg#eldap_config.servers, + backups = Cfg#eldap_config.backups, + port = Cfg#eldap_config.port, + tls_options = Cfg#eldap_config.tls_options, + dn = Cfg#eldap_config.dn, + password = Cfg#eldap_config.password, + base = Cfg#eldap_config.base, + deref_aliases = Cfg#eldap_config.deref_aliases, + uids = UIDs, + vcard_map = VCardMap, + vcard_map_attrs = VCardMapAttrs, + user_filter = UserFilter, + search_filter = SearchFilter, + search_fields = SearchFields, + search_reported = SearchReported, + search_reported_attrs = SearchReportedAttrs, + matches = Matches + }. + mod_opt_type(ldap_search_fields) -> econf:map( @@ -432,9 +501,9 @@ mod_opt_type(ldap_vcard_map) -> econf:map( econf:binary(), econf:map( - econf:binary(), - econf:list( - econf:binary()))); + econf:binary(), + econf:list( + econf:binary()))); mod_opt_type(ldap_backups) -> econf:list(econf:domain(), [unique]); mod_opt_type(ldap_base) -> @@ -469,8 +538,9 @@ mod_opt_type(ldap_uids) -> fun(U) -> {U, <<"%u">>} end)), econf:map(econf:binary(), econf:binary(), [unique])). + -spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} | - {atom(), any()}]. + {atom(), any()}]. mod_options(Host) -> [{ldap_search_fields, default_search_fields()}, {ldap_search_reported, default_search_reported()}, @@ -490,99 +560,108 @@ mod_options(Host) -> {ldap_tls_depth, ejabberd_option:ldap_tls_depth(Host)}, {ldap_tls_verify, ejabberd_option:ldap_tls_verify(Host)}]. + mod_doc() -> - #{opts => + #{ + opts => [{ldap_search_fields, - #{value => "{Name: Attribute, ...}", + #{ + value => "{Name: Attribute, ...}", desc => ?T("This option defines the search form and the LDAP " - "attributes to search within. 'Name' is the name of a " - "search form field which will be automatically " - "translated by using the translation files " - "(see 'msgs/*.msg' for available words). " - "'Attribute' is the LDAP attribute or the pattern '%u'."), - example => - [{?T("The default is:"), - ["User: \"%u\"", - "\"Full Name\": displayName", - "\"Given Name\": givenName", - "\"Middle Name\": initials", - "\"Family Name\": sn", - "Nickname: \"%u\"", - "Birthday: birthDay", - "Country: c", - "City: l", - "Email: mail", - "\"Organization Name\": o", - "\"Organization Unit\": ou"] - }]}}, - {ldap_search_reported, - #{value => "{SearchField: VcardField}, ...}", - desc => - ?T("This option defines which search fields should be " - "reported. 'SearchField' is the name of a search form " - "field which will be automatically translated by using " - "the translation files (see 'msgs/*.msg' for available " - "words). 'VcardField' is the vCard field name defined " - "in the 'ldap_vcard_map' option."), - example => - [{?T("The default is:"), - ["\"Full Name\": FN", - "\"Given Name\": FIRST", - "\"Middle Name\": MIDDLE", - "\"Family Name\": LAST", - "\"Nickname\": NICKNAME", - "\"Birthday\": BDAY", - "\"Country\": CTRY", - "\"City\": LOCALITY", - "\"Email\": EMAIL", - "\"Organization Name\": ORGNAME", - "\"Organization Unit\": ORGUNIT"] - }]}}, - {ldap_vcard_map, - #{value => "{Name: {Pattern, LDAPattributes}, ...}", - desc => - ?T("With this option you can set the table that maps LDAP " - "attributes to vCard fields. 'Name' is the type name of " - "the vCard as defined in " - "https://tools.ietf.org/html/rfc2426[RFC 2426]. " - "'Pattern' is a string which contains " - "pattern variables '%u', '%d' or '%s'. " - "'LDAPattributes' is the list containing LDAP attributes. " - "The pattern variables '%s' will be sequentially replaced " - "with the values of LDAP attributes from " - "'List_of_LDAP_attributes', '%u' will be replaced with " - "the user part of a JID, and '%d' will be replaced with " - "the domain part of a JID."), - example => - [{?T("The default is:"), - ["NICKNAME: {\"%u\": []}", - "FN: {\"%s\": [displayName]}", - "LAST: {\"%s\": [sn]}", - "FIRST: {\"%s\": [givenName]}", - "MIDDLE: {\"%s\": [initials]}", - "ORGNAME: {\"%s\": [o]}", - "ORGUNIT: {\"%s\": [ou]}", - "CTRY: {\"%s\": [c]}", - "LOCALITY: {\"%s\": [l]}", - "STREET: {\"%s\": [street]}", - "REGION: {\"%s\": [st]}", - "PCODE: {\"%s\": [postalCode]}", - "TITLE: {\"%s\": [title]}", - "URL: {\"%s\": [labeleduri]}", - "DESC: {\"%s\": [description]}", - "TEL: {\"%s\": [telephoneNumber]}", - "EMAIL: {\"%s\": [mail]}", - "BDAY: {\"%s\": [birthDay]}", - "ROLE: {\"%s\": [employeeType]}", - "PHOTO: {\"%s\": [jpegPhoto]}"] - }]}}] ++ - [{Opt, - #{desc => - {?T("Same as top-level _`~s`_ option, but " - "applied to this module only."), [Opt]}}} - || Opt <- [ldap_base, ldap_servers, ldap_uids, - ldap_deref_aliases, ldap_encrypt, ldap_password, - ldap_port, ldap_rootdn, ldap_filter, - ldap_tls_certfile, ldap_tls_cacertfile, - ldap_tls_depth, ldap_tls_verify, ldap_backups]]}. + "attributes to search within. 'Name' is the name of a " + "search form field which will be automatically " + "translated by using the translation files " + "(see 'msgs/*.msg' for available words). " + "'Attribute' is the LDAP attribute or the pattern '%u'."), + example => + [{?T("The default is:"), + ["User: \"%u\"", + "\"Full Name\": displayName", + "\"Given Name\": givenName", + "\"Middle Name\": initials", + "\"Family Name\": sn", + "Nickname: \"%u\"", + "Birthday: birthDay", + "Country: c", + "City: l", + "Email: mail", + "\"Organization Name\": o", + "\"Organization Unit\": ou"]}] + }}, + {ldap_search_reported, + #{ + value => "{SearchField: VcardField}, ...}", + desc => + ?T("This option defines which search fields should be " + "reported. 'SearchField' is the name of a search form " + "field which will be automatically translated by using " + "the translation files (see 'msgs/*.msg' for available " + "words). 'VcardField' is the vCard field name defined " + "in the 'ldap_vcard_map' option."), + example => + [{?T("The default is:"), + ["\"Full Name\": FN", + "\"Given Name\": FIRST", + "\"Middle Name\": MIDDLE", + "\"Family Name\": LAST", + "\"Nickname\": NICKNAME", + "\"Birthday\": BDAY", + "\"Country\": CTRY", + "\"City\": LOCALITY", + "\"Email\": EMAIL", + "\"Organization Name\": ORGNAME", + "\"Organization Unit\": ORGUNIT"]}] + }}, + {ldap_vcard_map, + #{ + value => "{Name: {Pattern, LDAPattributes}, ...}", + desc => + ?T("With this option you can set the table that maps LDAP " + "attributes to vCard fields. 'Name' is the type name of " + "the vCard as defined in " + "https://tools.ietf.org/html/rfc2426[RFC 2426]. " + "'Pattern' is a string which contains " + "pattern variables '%u', '%d' or '%s'. " + "'LDAPattributes' is the list containing LDAP attributes. " + "The pattern variables '%s' will be sequentially replaced " + "with the values of LDAP attributes from " + "'List_of_LDAP_attributes', '%u' will be replaced with " + "the user part of a JID, and '%d' will be replaced with " + "the domain part of a JID."), + example => + [{?T("The default is:"), + ["NICKNAME: {\"%u\": []}", + "FN: {\"%s\": [displayName]}", + "LAST: {\"%s\": [sn]}", + "FIRST: {\"%s\": [givenName]}", + "MIDDLE: {\"%s\": [initials]}", + "ORGNAME: {\"%s\": [o]}", + "ORGUNIT: {\"%s\": [ou]}", + "CTRY: {\"%s\": [c]}", + "LOCALITY: {\"%s\": [l]}", + "STREET: {\"%s\": [street]}", + "REGION: {\"%s\": [st]}", + "PCODE: {\"%s\": [postalCode]}", + "TITLE: {\"%s\": [title]}", + "URL: {\"%s\": [labeleduri]}", + "DESC: {\"%s\": [description]}", + "TEL: {\"%s\": [telephoneNumber]}", + "EMAIL: {\"%s\": [mail]}", + "BDAY: {\"%s\": [birthDay]}", + "ROLE: {\"%s\": [employeeType]}", + "PHOTO: {\"%s\": [jpegPhoto]}"]}] + }}] ++ + [ {Opt, + #{ + desc => + {?T("Same as top-level _`~s`_ option, but " + "applied to this module only."), + [Opt]} + }} + || Opt <- [ldap_base, ldap_servers, ldap_uids, + ldap_deref_aliases, ldap_encrypt, ldap_password, + ldap_port, ldap_rootdn, ldap_filter, + ldap_tls_certfile, ldap_tls_cacertfile, + ldap_tls_depth, ldap_tls_verify, ldap_backups] ] + }. diff --git a/src/mod_vcard_ldap_opt.erl b/src/mod_vcard_ldap_opt.erl index 2d2af6f2b..9a2577235 100644 --- a/src/mod_vcard_ldap_opt.erl +++ b/src/mod_vcard_ldap_opt.erl @@ -21,105 +21,121 @@ -export([ldap_uids/1]). -export([ldap_vcard_map/1]). + -spec ldap_backups(gen_mod:opts() | global | binary()) -> [binary()]. ldap_backups(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_backups, Opts); ldap_backups(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_backups). + -spec ldap_base(gen_mod:opts() | global | binary()) -> binary(). ldap_base(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_base, Opts); ldap_base(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_base). + -spec ldap_deref_aliases(gen_mod:opts() | global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. ldap_deref_aliases(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_deref_aliases, Opts); ldap_deref_aliases(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_deref_aliases). + -spec ldap_encrypt(gen_mod:opts() | global | binary()) -> 'none' | 'starttls' | 'tls'. ldap_encrypt(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_encrypt, Opts); ldap_encrypt(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_encrypt). + -spec ldap_filter(gen_mod:opts() | global | binary()) -> binary(). ldap_filter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_filter, Opts); ldap_filter(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_filter). + -spec ldap_password(gen_mod:opts() | global | binary()) -> binary(). ldap_password(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_password, Opts); ldap_password(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_password). + -spec ldap_port(gen_mod:opts() | global | binary()) -> 1..1114111. ldap_port(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_port, Opts); ldap_port(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_port). + -spec ldap_rootdn(gen_mod:opts() | global | binary()) -> binary(). ldap_rootdn(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_rootdn, Opts); ldap_rootdn(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_rootdn). --spec ldap_search_fields(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec ldap_search_fields(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. ldap_search_fields(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_search_fields, Opts); ldap_search_fields(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_search_fields). --spec ldap_search_reported(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec ldap_search_reported(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. ldap_search_reported(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_search_reported, Opts); ldap_search_reported(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_search_reported). + -spec ldap_servers(gen_mod:opts() | global | binary()) -> [binary()]. ldap_servers(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_servers, Opts); ldap_servers(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_servers). + -spec ldap_tls_cacertfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_cacertfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_cacertfile, Opts); ldap_tls_cacertfile(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_cacertfile). + -spec ldap_tls_certfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_certfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_certfile, Opts); ldap_tls_certfile(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_certfile). + -spec ldap_tls_depth(gen_mod:opts() | global | binary()) -> non_neg_integer(). ldap_tls_depth(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_depth, Opts); ldap_tls_depth(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_depth). + -spec ldap_tls_verify(gen_mod:opts() | global | binary()) -> 'false' | 'hard' | 'soft'. ldap_tls_verify(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_verify, Opts); ldap_tls_verify(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_verify). --spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. + +-spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(), binary()}]. ldap_uids(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_uids, Opts); ldap_uids(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_uids). --spec ldap_vcard_map(gen_mod:opts() | global | binary()) -> [{binary(),[{binary(),[binary()]}]}]. + +-spec ldap_vcard_map(gen_mod:opts() | global | binary()) -> [{binary(), [{binary(), [binary()]}]}]. ldap_vcard_map(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_vcard_map, Opts); ldap_vcard_map(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_vcard_map). - diff --git a/src/mod_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl index e70b13fc0..e217d0838 100644 --- a/src/mod_vcard_mnesia.erl +++ b/src/mod_vcard_mnesia.erl @@ -27,74 +27,91 @@ -behaviour(mod_vcard). %% API --export([init/2, stop/1, import/3, get_vcard/2, set_vcard/4, search/4, - search_fields/1, search_reported/1, remove_user/2]). +-export([init/2, + stop/1, + import/3, + get_vcard/2, + set_vcard/4, + search/4, + search_fields/1, + search_reported/1, + remove_user/2]). -export([is_search_supported/1]). -export([need_transform/1, transform/1]). -export([mod_opt_type/1, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_vcard.hrl"). -include("logger.hrl"). -include("translate.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> - ejabberd_mnesia:create(?MODULE, vcard, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, vcard)}]), - ejabberd_mnesia:create(?MODULE, vcard_search, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, vcard_search)}, - {index, [ luser, lfn, lfamily, - lgiven, lmiddle, lnickname, - lbday, lctry, llocality, - lemail, lorgname, lorgunit - ]}]). + ejabberd_mnesia:create(?MODULE, + vcard, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, vcard)}]), + ejabberd_mnesia:create(?MODULE, + vcard_search, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, vcard_search)}, + {index, [luser, lfn, lfamily, + lgiven, lmiddle, lnickname, + lbday, lctry, llocality, + lemail, lorgname, lorgunit]}]). + stop(_Host) -> ok. + is_search_supported(_ServerHost) -> true. + get_vcard(LUser, LServer) -> US = {LUser, LServer}, Rs = mnesia:dirty_read(vcard, US), - {ok, lists:map(fun (R) -> R#vcard.vcard end, Rs)}. + {ok, lists:map(fun(R) -> R#vcard.vcard end, Rs)}. + set_vcard(LUser, LServer, VCARD, VCardSearch) -> US = {LUser, LServer}, - F = fun () -> - mnesia:write(#vcard{us = US, vcard = VCARD}), - mnesia:write(VCardSearch) - end, + F = fun() -> + mnesia:write(#vcard{us = US, vcard = VCARD}), + mnesia:write(VCardSearch) + end, mnesia:transaction(F). + search(LServer, Data, AllowReturnAll, MaxMatch) -> MatchSpec = make_matchspec(LServer, Data), - if (MatchSpec == #vcard_search{_ = '_'}) and - not AllowReturnAll -> - []; - true -> - case catch mnesia:dirty_select(vcard_search, - [{MatchSpec, [], ['$_']}]) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]), []; - Rs -> - Fields = lists:map(fun record_to_item/1, Rs), - case MaxMatch of - infinity -> - Fields; - Val -> - lists:sublist(Fields, Val) - end - end + if + (MatchSpec == #vcard_search{_ = '_'}) and + not AllowReturnAll -> + []; + true -> + case catch mnesia:dirty_select(vcard_search, + [{MatchSpec, [], ['$_']}]) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]), []; + Rs -> + Fields = lists:map(fun record_to_item/1, Rs), + case MaxMatch of + infinity -> + Fields; + Val -> + lists:sublist(Fields, Val) + end + end end. + search_fields(_LServer) -> [{?T("User"), <<"user">>}, {?T("Full Name"), <<"fn">>}, @@ -109,6 +126,7 @@ search_fields(_LServer) -> {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. + search_reported(_LServer) -> [{?T("Jabber ID"), <<"jid">>}, {?T("Full Name"), <<"fn">>}, @@ -123,65 +141,89 @@ search_reported(_LServer) -> {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. + remove_user(LUser, LServer) -> US = {LUser, LServer}, - F = fun () -> - mnesia:delete({vcard, US}), - mnesia:delete({vcard_search, US}) - end, + F = fun() -> + mnesia:delete({vcard, US}), + mnesia:delete({vcard_search, US}) + end, mnesia:transaction(F). + import(LServer, <<"vcard">>, [LUser, XML, _TimeStamp]) -> #xmlel{} = El = fxml_stream:parse_element(XML), VCard = #vcard{us = {LUser, LServer}, vcard = El}, mnesia:dirty_write(VCard); -import(LServer, <<"vcard_search">>, +import(LServer, + <<"vcard_search">>, [User, LUser, FN, LFN, Family, LFamily, Given, LGiven, Middle, LMiddle, Nickname, LNickname, BDay, LBDay, CTRY, LCTRY, Locality, LLocality, EMail, LEMail, OrgName, LOrgName, OrgUnit, LOrgUnit]) -> mnesia:dirty_write( - #vcard_search{us = {LUser, LServer}, - user = {User, LServer}, luser = LUser, - fn = FN, lfn = LFN, family = Family, - lfamily = LFamily, given = Given, - lgiven = LGiven, middle = Middle, - lmiddle = LMiddle, nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, llocality = LLocality, - email = EMail, lemail = LEMail, - orgname = OrgName, lorgname = LOrgName, - orgunit = OrgUnit, lorgunit = LOrgUnit}). + #vcard_search{ + us = {LUser, LServer}, + user = {User, LServer}, + luser = LUser, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit + }). + need_transform({vcard, {U, S}, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'vcard' will be converted to binary", []), true; need_transform(R) when element(1, R) == vcard_search -> case element(2, R) of - {U, S} when is_list(U) orelse is_list(S) -> - ?INFO_MSG("Mnesia table 'vcard_search' will be converted to binary", []), - true; - _ -> - false + {U, S} when is_list(U) orelse is_list(S) -> + ?INFO_MSG("Mnesia table 'vcard_search' will be converted to binary", []), + true; + _ -> + false end; need_transform(_) -> false. + transform(#vcard{us = {U, S}, vcard = El} = R) -> - R#vcard{us = {iolist_to_binary(U), iolist_to_binary(S)}, - vcard = fxml:to_xmlel(El)}; + R#vcard{ + us = {iolist_to_binary(U), iolist_to_binary(S)}, + vcard = fxml:to_xmlel(El) + }; transform(#vcard_search{} = VS) -> [vcard_search | L] = tuple_to_list(VS), NewL = lists:map( - fun({U, S}) -> - {iolist_to_binary(U), iolist_to_binary(S)}; - (Str) -> - iolist_to_binary(Str) - end, L), + fun({U, S}) -> + {iolist_to_binary(U), iolist_to_binary(S)}; + (Str) -> + iolist_to_binary(Str) + end, + L), list_to_tuple([vcard_search | NewL]). + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -190,68 +232,75 @@ make_matchspec(LServer, Data) -> Match = filter_fields(Data, GlobMatch, LServer), Match. + filter_fields([], Match, _LServer) -> Match; filter_fields([{SVar, [Val]} | Ds], Match, LServer) when is_binary(Val) and (Val /= <<"">>) -> LVal = mod_vcard:string2lower(Val), NewMatch = case SVar of - <<"user">> -> - case mod_vcard_mnesia_opt:search_all_hosts(LServer) of - true -> Match#vcard_search{luser = make_val(LVal)}; - false -> - Host = find_my_host(LServer), - Match#vcard_search{us = {make_val(LVal), Host}} - end; - <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; - <<"last">> -> - Match#vcard_search{lfamily = make_val(LVal)}; - <<"first">> -> - Match#vcard_search{lgiven = make_val(LVal)}; - <<"middle">> -> - Match#vcard_search{lmiddle = make_val(LVal)}; - <<"nick">> -> - Match#vcard_search{lnickname = make_val(LVal)}; - <<"bday">> -> - Match#vcard_search{lbday = make_val(LVal)}; - <<"ctry">> -> - Match#vcard_search{lctry = make_val(LVal)}; - <<"locality">> -> - Match#vcard_search{llocality = make_val(LVal)}; - <<"email">> -> - Match#vcard_search{lemail = make_val(LVal)}; - <<"orgname">> -> - Match#vcard_search{lorgname = make_val(LVal)}; - <<"orgunit">> -> - Match#vcard_search{lorgunit = make_val(LVal)}; - _ -> Match - end, + <<"user">> -> + case mod_vcard_mnesia_opt:search_all_hosts(LServer) of + true -> Match#vcard_search{luser = make_val(LVal)}; + false -> + Host = find_my_host(LServer), + Match#vcard_search{us = {make_val(LVal), Host}} + end; + <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; + <<"last">> -> + Match#vcard_search{lfamily = make_val(LVal)}; + <<"first">> -> + Match#vcard_search{lgiven = make_val(LVal)}; + <<"middle">> -> + Match#vcard_search{lmiddle = make_val(LVal)}; + <<"nick">> -> + Match#vcard_search{lnickname = make_val(LVal)}; + <<"bday">> -> + Match#vcard_search{lbday = make_val(LVal)}; + <<"ctry">> -> + Match#vcard_search{lctry = make_val(LVal)}; + <<"locality">> -> + Match#vcard_search{llocality = make_val(LVal)}; + <<"email">> -> + Match#vcard_search{lemail = make_val(LVal)}; + <<"orgname">> -> + Match#vcard_search{lorgname = make_val(LVal)}; + <<"orgunit">> -> + Match#vcard_search{lorgunit = make_val(LVal)}; + _ -> Match + end, filter_fields(Ds, NewMatch, LServer); filter_fields([_ | Ds], Match, LServer) -> filter_fields(Ds, Match, LServer). + make_val(Val) -> case str:suffix(<<"*">>, Val) of - true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; - _ -> Val + true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; + _ -> Val end. + find_my_host(LServer) -> Parts = str:tokens(LServer, <<".">>), find_my_host(Parts, ejabberd_option:hosts()). + find_my_host([], _Hosts) -> ejabberd_config:get_myname(); find_my_host([_ | Tail] = Parts, Hosts) -> Domain = parts_to_string(Parts), case lists:member(Domain, Hosts) of - true -> Domain; - false -> find_my_host(Tail, Hosts) + true -> Domain; + false -> find_my_host(Tail, Hosts) end. + parts_to_string(Parts) -> str:strip(list_to_binary( - lists:map(fun (S) -> <> end, Parts)), - right, $.). + lists:map(fun(S) -> <> end, Parts)), + right, + $.). + -spec record_to_item(#vcard_search{}) -> [{binary(), binary()}]. record_to_item(R) -> @@ -269,17 +318,24 @@ record_to_item(R) -> {<<"orgname">>, (R#vcard_search.orgname)}, {<<"orgunit">>, (R#vcard_search.orgunit)}]. + mod_opt_type(search_all_hosts) -> econf:bool(). + mod_options(_) -> [{search_all_hosts, true}]. + mod_doc() -> - #{opts => + #{ + opts => [{search_all_hosts, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Whether to perform search on all " "virtual hosts or not. The default " - "value is 'true'.")}}]}. + "value is 'true'.") + }}] + }. diff --git a/src/mod_vcard_mnesia_opt.erl b/src/mod_vcard_mnesia_opt.erl index a128c086d..b5ac50c45 100644 --- a/src/mod_vcard_mnesia_opt.erl +++ b/src/mod_vcard_mnesia_opt.erl @@ -5,9 +5,9 @@ -export([search_all_hosts/1]). + -spec search_all_hosts(gen_mod:opts() | global | binary()) -> boolean(). search_all_hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(search_all_hosts, Opts); search_all_hosts(Host) -> gen_mod:get_module_opt(Host, mod_vcard, search_all_hosts). - diff --git a/src/mod_vcard_opt.erl b/src/mod_vcard_opt.erl index 3a7cc7754..119c669ec 100644 --- a/src/mod_vcard_opt.erl +++ b/src/mod_vcard_opt.erl @@ -16,75 +16,86 @@ -export([use_cache/1]). -export([vcard/1]). + -spec allow_return_all(gen_mod:opts() | global | binary()) -> boolean(). allow_return_all(Opts) when is_map(Opts) -> gen_mod:get_opt(allow_return_all, Opts); allow_return_all(Host) -> gen_mod:get_module_opt(Host, mod_vcard, allow_return_all). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_vcard, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_vcard, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_vcard, cache_size). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_vcard, db_type). + -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_vcard, host). + -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_vcard, hosts). + -spec matches(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). matches(Opts) when is_map(Opts) -> gen_mod:get_opt(matches, Opts); matches(Host) -> gen_mod:get_module_opt(Host, mod_vcard, matches). + -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_vcard, name). + -spec search(gen_mod:opts() | global | binary()) -> boolean(). search(Opts) when is_map(Opts) -> gen_mod:get_opt(search, Opts); search(Host) -> gen_mod:get_module_opt(Host, mod_vcard, search). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_vcard, use_cache). + -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_vcard, vcard). - diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl index 18456f402..e0fdbb1f9 100644 --- a/src/mod_vcard_sql.erl +++ b/src/mod_vcard_sql.erl @@ -24,21 +24,30 @@ -module(mod_vcard_sql). - -behaviour(mod_vcard). %% API --export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, remove_user/2, - search_fields/1, search_reported/1, import/3, export/1]). +-export([init/2, + stop/1, + get_vcard/2, + set_vcard/4, + search/4, + remove_user/2, + search_fields/1, + search_reported/1, + import/3, + export/1]). -export([is_search_supported/1]). -export([sql_schemas/0]). -include_lib("xmpp/include/xmpp.hrl"). + -include("mod_vcard.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("translate.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -46,128 +55,158 @@ init(Host, _Opts) -> ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()), ok. + sql_schemas() -> [#sql_schema{ - version = 1, - tables = - [#sql_table{ - name = <<"vcard">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"vcard">>, type = {text, big}}, - #sql_column{name = <<"created_at">>, type = timestamp, - default = true}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"username">>], - unique = true}]}, - #sql_table{ - name = <<"vcard_search">>, - columns = - [#sql_column{name = <<"username">>, type = text}, - #sql_column{name = <<"lusername">>, type = text}, - #sql_column{name = <<"server_host">>, type = text}, - #sql_column{name = <<"fn">>, type = text}, - #sql_column{name = <<"lfn">>, type = text}, - #sql_column{name = <<"family">>, type = text}, - #sql_column{name = <<"lfamily">>, type = text}, - #sql_column{name = <<"given">>, type = text}, - #sql_column{name = <<"lgiven">>, type = text}, - #sql_column{name = <<"middle">>, type = text}, - #sql_column{name = <<"lmiddle">>, type = text}, - #sql_column{name = <<"nickname">>, type = text}, - #sql_column{name = <<"lnickname">>, type = text}, - #sql_column{name = <<"bday">>, type = text}, - #sql_column{name = <<"lbday">>, type = text}, - #sql_column{name = <<"ctry">>, type = text}, - #sql_column{name = <<"lctry">>, type = text}, - #sql_column{name = <<"locality">>, type = text}, - #sql_column{name = <<"llocality">>, type = text}, - #sql_column{name = <<"email">>, type = text}, - #sql_column{name = <<"lemail">>, type = text}, - #sql_column{name = <<"orgname">>, type = text}, - #sql_column{name = <<"lorgname">>, type = text}, - #sql_column{name = <<"orgunit">>, type = text}, - #sql_column{name = <<"lorgunit">>, type = text}], - indices = [#sql_index{ - columns = [<<"server_host">>, <<"lusername">>], - unique = true}, - #sql_index{ - columns = [<<"server_host">>, <<"lfn">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lfamily">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lgiven">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lmiddle">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lnickname">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lbday">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lctry">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"llocality">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lemail">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lorgname">>]}, - #sql_index{ - columns = [<<"server_host">>, <<"lorgunit">>]}]}]}]. + version = 1, + tables = + [#sql_table{ + name = <<"vcard">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"vcard">>, type = {text, big}}, + #sql_column{ + name = <<"created_at">>, + type = timestamp, + default = true + }], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"username">>], + unique = true + }] + }, + #sql_table{ + name = <<"vcard_search">>, + columns = + [#sql_column{name = <<"username">>, type = text}, + #sql_column{name = <<"lusername">>, type = text}, + #sql_column{name = <<"server_host">>, type = text}, + #sql_column{name = <<"fn">>, type = text}, + #sql_column{name = <<"lfn">>, type = text}, + #sql_column{name = <<"family">>, type = text}, + #sql_column{name = <<"lfamily">>, type = text}, + #sql_column{name = <<"given">>, type = text}, + #sql_column{name = <<"lgiven">>, type = text}, + #sql_column{name = <<"middle">>, type = text}, + #sql_column{name = <<"lmiddle">>, type = text}, + #sql_column{name = <<"nickname">>, type = text}, + #sql_column{name = <<"lnickname">>, type = text}, + #sql_column{name = <<"bday">>, type = text}, + #sql_column{name = <<"lbday">>, type = text}, + #sql_column{name = <<"ctry">>, type = text}, + #sql_column{name = <<"lctry">>, type = text}, + #sql_column{name = <<"locality">>, type = text}, + #sql_column{name = <<"llocality">>, type = text}, + #sql_column{name = <<"email">>, type = text}, + #sql_column{name = <<"lemail">>, type = text}, + #sql_column{name = <<"orgname">>, type = text}, + #sql_column{name = <<"lorgname">>, type = text}, + #sql_column{name = <<"orgunit">>, type = text}, + #sql_column{name = <<"lorgunit">>, type = text}], + indices = [#sql_index{ + columns = [<<"server_host">>, <<"lusername">>], + unique = true + }, + #sql_index{ + columns = [<<"server_host">>, <<"lfn">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lfamily">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lgiven">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lmiddle">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lnickname">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lbday">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lctry">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"llocality">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lemail">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lorgname">>] + }, + #sql_index{ + columns = [<<"server_host">>, <<"lorgunit">>] + }] + }] + }]. + stop(_Host) -> ok. + is_search_supported(_LServer) -> true. + get_vcard(LUser, LServer) -> case ejabberd_sql:sql_query( - LServer, - ?SQL("select @(vcard)s from vcard" + LServer, + ?SQL("select @(vcard)s from vcard" " where username=%(LUser)s and %(LServer)H")) of - {selected, [{SVCARD}]} -> - case fxml_stream:parse_element(SVCARD) of - {error, _Reason} -> error; - VCARD -> {ok, [VCARD]} - end; - {selected, []} -> {ok, []}; - _ -> error + {selected, [{SVCARD}]} -> + case fxml_stream:parse_element(SVCARD) of + {error, _Reason} -> error; + VCARD -> {ok, [VCARD]} + end; + {selected, []} -> {ok, []}; + _ -> error end. -set_vcard(LUser, LServer, VCARD, - #vcard_search{user = {User, _}, - fn = FN, - lfn = LFN, - family = Family, - lfamily = LFamily, - given = Given, - lgiven = LGiven, - middle = Middle, - lmiddle = LMiddle, - nickname = Nickname, - lnickname = LNickname, - bday = BDay, - lbday = LBDay, - ctry = CTRY, - lctry = LCTRY, - locality = Locality, - llocality = LLocality, - email = EMail, - lemail = LEMail, - orgname = OrgName, - lorgname = LOrgName, - orgunit = OrgUnit, - lorgunit = LOrgUnit}) -> + +set_vcard(LUser, + LServer, + VCARD, + #vcard_search{ + user = {User, _}, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit + }) -> SVCARD = fxml:element_to_binary(VCARD), ejabberd_sql:sql_transaction( LServer, fun() -> - ?SQL_UPSERT(LServer, "vcard", + ?SQL_UPSERT(LServer, + "vcard", ["!username=%(LUser)s", "!server_host=%(LServer)s", "vcard=%(SVCARD)s"]), - ?SQL_UPSERT(LServer, "vcard_search", + ?SQL_UPSERT(LServer, + "vcard_search", ["username=%(User)s", "!lusername=%(LUser)s", "!server_host=%(LServer)s", @@ -195,34 +234,48 @@ set_vcard(LUser, LServer, VCARD, "lorgunit=%(LOrgUnit)s"]) end). + search(LServer, Data, AllowReturnAll, MaxMatch) -> MatchSpec = make_matchspec(LServer, Data), - if (MatchSpec == <<"">>) and not AllowReturnAll -> []; - true -> - Limit = case MaxMatch of - infinity -> - <<"">>; - Val -> - [<<" LIMIT ">>, integer_to_binary(Val)] - end, - case catch ejabberd_sql:sql_query( - LServer, - [<<"select username, fn, family, given, " - "middle, nickname, bday, ctry, " - "locality, email, orgname, orgunit " - "from vcard_search ">>, - MatchSpec, Limit, <<";">>]) of - {selected, - [<<"username">>, <<"fn">>, <<"family">>, <<"given">>, - <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, - <<"locality">>, <<"email">>, <<"orgname">>, - <<"orgunit">>], Rs} when is_list(Rs) -> - [row_to_item(LServer, R) || R <- Rs]; - Error -> - ?ERROR_MSG("~p", [Error]), [] - end + if + (MatchSpec == <<"">>) and not AllowReturnAll -> []; + true -> + Limit = case MaxMatch of + infinity -> + <<"">>; + Val -> + [<<" LIMIT ">>, integer_to_binary(Val)] + end, + case catch ejabberd_sql:sql_query( + LServer, + [<<"select username, fn, family, given, " + "middle, nickname, bday, ctry, " + "locality, email, orgname, orgunit " + "from vcard_search ">>, + MatchSpec, + Limit, + <<";">>]) of + {selected, + [<<"username">>, + <<"fn">>, + <<"family">>, + <<"given">>, + <<"middle">>, + <<"nickname">>, + <<"bday">>, + <<"ctry">>, + <<"locality">>, + <<"email">>, + <<"orgname">>, + <<"orgunit">>], + Rs} when is_list(Rs) -> + [ row_to_item(LServer, R) || R <- Rs ]; + Error -> + ?ERROR_MSG("~p", [Error]), [] + end end. + search_fields(_LServer) -> [{?T("User"), <<"user">>}, {?T("Full Name"), <<"fn">>}, @@ -237,6 +290,7 @@ search_fields(_LServer) -> {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. + search_reported(_LServer) -> [{?T("Jabber ID"), <<"jid">>}, {?T("Full Name"), <<"fn">>}, @@ -251,6 +305,7 @@ search_reported(_LServer) -> {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. + remove_user(LUser, LServer) -> ejabberd_sql:sql_transaction( LServer, @@ -263,6 +318,7 @@ remove_user(LUser, LServer) -> " where lusername=%(LUser)s and %(LServer)H")) end). + export(_Server) -> [{vcard, fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) @@ -278,17 +334,33 @@ export(_Server) -> [] end}, {vcard_search, - fun(Host, #vcard_search{user = {User, LServer}, luser = LUser, - fn = FN, lfn = LFN, family = Family, - lfamily = LFamily, given = Given, - lgiven = LGiven, middle = Middle, - lmiddle = LMiddle, nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, llocality = LLocality, - email = EMail, lemail = LEMail, - orgname = OrgName, lorgname = LOrgName, - orgunit = OrgUnit, lorgunit = LOrgUnit}) + fun(Host, + #vcard_search{ + user = {User, LServer}, + luser = LUser, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit + }) when LServer == Host -> [?SQL("delete from vcard_search" " where lusername=%(LUser)s and %(LServer)H;"), @@ -322,15 +394,18 @@ export(_Server) -> [] end}]. + import(_, _, _) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== make_matchspec(LServer, Data) -> filter_fields(Data, <<"">>, LServer). + filter_fields([], Match, LServer) -> case ejabberd_sql:use_new_schema() of true -> @@ -350,46 +425,49 @@ filter_fields([{SVar, [Val]} | Ds], Match, LServer) when is_binary(Val) and (Val /= <<"">>) -> LVal = mod_vcard:string2lower(Val), NewMatch = case SVar of - <<"user">> -> make_val(LServer, Match, <<"lusername">>, LVal); - <<"fn">> -> make_val(LServer, Match, <<"lfn">>, LVal); - <<"last">> -> make_val(LServer, Match, <<"lfamily">>, LVal); - <<"first">> -> make_val(LServer, Match, <<"lgiven">>, LVal); - <<"middle">> -> make_val(LServer, Match, <<"lmiddle">>, LVal); - <<"nick">> -> make_val(LServer, Match, <<"lnickname">>, LVal); - <<"bday">> -> make_val(LServer, Match, <<"lbday">>, LVal); - <<"ctry">> -> make_val(LServer, Match, <<"lctry">>, LVal); - <<"locality">> -> - make_val(LServer, Match, <<"llocality">>, LVal); - <<"email">> -> make_val(LServer, Match, <<"lemail">>, LVal); - <<"orgname">> -> make_val(LServer, Match, <<"lorgname">>, LVal); - <<"orgunit">> -> make_val(LServer, Match, <<"lorgunit">>, LVal); - _ -> Match - end, + <<"user">> -> make_val(LServer, Match, <<"lusername">>, LVal); + <<"fn">> -> make_val(LServer, Match, <<"lfn">>, LVal); + <<"last">> -> make_val(LServer, Match, <<"lfamily">>, LVal); + <<"first">> -> make_val(LServer, Match, <<"lgiven">>, LVal); + <<"middle">> -> make_val(LServer, Match, <<"lmiddle">>, LVal); + <<"nick">> -> make_val(LServer, Match, <<"lnickname">>, LVal); + <<"bday">> -> make_val(LServer, Match, <<"lbday">>, LVal); + <<"ctry">> -> make_val(LServer, Match, <<"lctry">>, LVal); + <<"locality">> -> + make_val(LServer, Match, <<"llocality">>, LVal); + <<"email">> -> make_val(LServer, Match, <<"lemail">>, LVal); + <<"orgname">> -> make_val(LServer, Match, <<"lorgname">>, LVal); + <<"orgunit">> -> make_val(LServer, Match, <<"lorgunit">>, LVal); + _ -> Match + end, filter_fields(Ds, NewMatch, LServer); filter_fields([_ | Ds], Match, LServer) -> filter_fields(Ds, Match, LServer). + make_val(LServer, Match, Field, Val) -> Condition = case str:suffix(<<"*">>, Val) of - true -> - Val1 = str:substr(Val, 1, byte_size(Val) - 1), - SVal = <<(ejabberd_sql:escape( - ejabberd_sql:escape_like_arg_circumflex( - Val1)))/binary, - "%">>, - [Field, <<" LIKE '">>, SVal, <<"' ESCAPE '^'">>]; - _ -> - SQLType = ejabberd_option:sql_type(LServer), - SVal = ejabberd_sql:to_string_literal(SQLType, Val), - [Field, <<" = ">>, SVal] - end, + true -> + Val1 = str:substr(Val, 1, byte_size(Val) - 1), + SVal = <<(ejabberd_sql:escape( + ejabberd_sql:escape_like_arg_circumflex( + Val1)))/binary, + "%">>, + [Field, <<" LIKE '">>, SVal, <<"' ESCAPE '^'">>]; + _ -> + SQLType = ejabberd_option:sql_type(LServer), + SVal = ejabberd_sql:to_string_literal(SQLType, Val), + [Field, <<" = ">>, SVal] + end, case Match of - <<"">> -> Condition; - _ -> [Match, <<" and ">>, Condition] + <<"">> -> Condition; + _ -> [Match, <<" and ">>, Condition] end. -row_to_item(LServer, [Username, FN, Family, Given, Middle, Nickname, BDay, - CTRY, Locality, EMail, OrgName, OrgUnit]) -> + +row_to_item(LServer, + [Username, FN, Family, Given, Middle, Nickname, BDay, + CTRY, Locality, EMail, OrgName, OrgUnit]) -> [{<<"jid">>, <>}, {<<"fn">>, FN}, {<<"last">>, Family}, diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 70ed707b2..55694388d 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -29,13 +29,21 @@ %% gen_mod callbacks -export([start/2, stop/1, reload/3]). --export([update_presence/1, vcard_set/1, remove_user/2, mod_doc/0, - user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]). +-export([update_presence/1, + vcard_set/1, + remove_user/2, + mod_doc/0, + user_send_packet/1, + mod_opt_type/1, + mod_options/1, + depends/2]). %% API -export([compute_hash/1]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). -define(VCARD_XUPDATE_CACHE, vcard_xupdate_cache). @@ -44,6 +52,7 @@ %% gen_mod callbacks %%==================================================================== + start(Host, Opts) -> init_cache(Host, Opts), {ok, [{hook, c2s_self_presence, update_presence, 100}, @@ -51,49 +60,60 @@ start(Host, Opts) -> {hook, vcard_iq_set, vcard_set, 90}, {hook, remove_user, remove_user, 50}]}. + stop(_Host) -> ok. + reload(Host, NewOpts, _OldOpts) -> init_cache(Host, NewOpts). + depends(_Host, _Opts) -> [{mod_vcard, hard}]. + %%==================================================================== %% Hooks %%==================================================================== --spec update_presence({presence(), ejabberd_c2s:state()}) - -> {presence(), ejabberd_c2s:state()}. +-spec update_presence({presence(), ejabberd_c2s:state()}) -> + {presence(), ejabberd_c2s:state()}. update_presence({#presence{type = available} = Pres, - #{jid := #jid{luser = LUser, lserver = LServer}} = State}) -> + #{jid := #jid{luser = LUser, lserver = LServer}} = State}) -> case xmpp:get_subtag(Pres, #vcard_xupdate{}) of - #vcard_xupdate{hash = <<>>} -> - %% XEP-0398 forbids overwriting vcard:x:update - %% tags with empty element - {Pres, State}; - _ -> - Pres1 = case get_xupdate(LUser, LServer) of - undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{}); - XUpdate -> xmpp:set_subtag(Pres, XUpdate) - end, - {Pres1, State} + #vcard_xupdate{hash = <<>>} -> + %% XEP-0398 forbids overwriting vcard:x:update + %% tags with empty element + {Pres, State}; + _ -> + Pres1 = case get_xupdate(LUser, LServer) of + undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{}); + XUpdate -> xmpp:set_subtag(Pres, XUpdate) + end, + {Pres1, State} end; update_presence(Acc) -> Acc. --spec user_send_packet({presence(), ejabberd_c2s:state()}) - -> {presence(), ejabberd_c2s:state()}. -user_send_packet({#presence{type = available, - to = #jid{luser = U, lserver = S, - lresource = <<"">>}}, - #{jid := #jid{luser = U, lserver = S}}} = Acc) -> + +-spec user_send_packet({presence(), ejabberd_c2s:state()}) -> + {presence(), ejabberd_c2s:state()}. +user_send_packet({#presence{ + type = available, + to = #jid{ + luser = U, + lserver = S, + lresource = <<"">> + } + }, + #{jid := #jid{luser = U, lserver = S}}} = Acc) -> %% This is processed by update_presence/2 explicitly, we don't %% want to call this multiple times for performance reasons Acc; user_send_packet(Acc) -> update_presence(Acc). + -spec vcard_set(iq()) -> iq(). vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}, ejabberd_cluster:get_nodes()), @@ -102,50 +122,56 @@ vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> vcard_set(Acc) -> Acc. + -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}, ejabberd_cluster:get_nodes()). + %%==================================================================== %% Storage %%==================================================================== -spec get_xupdate(binary(), binary()) -> vcard_xupdate() | undefined. get_xupdate(LUser, LServer) -> Result = case use_cache(LServer) of - true -> - ets_cache:lookup( - ?VCARD_XUPDATE_CACHE, {LUser, LServer}, - fun() -> db_get_xupdate(LUser, LServer) end); - false -> - db_get_xupdate(LUser, LServer) - end, + true -> + ets_cache:lookup( + ?VCARD_XUPDATE_CACHE, + {LUser, LServer}, + fun() -> db_get_xupdate(LUser, LServer) end); + false -> + db_get_xupdate(LUser, LServer) + end, case Result of - {ok, external} -> undefined; - {ok, Hash} -> #vcard_xupdate{hash = Hash}; - error -> #vcard_xupdate{} + {ok, external} -> undefined; + {ok, Hash} -> #vcard_xupdate{hash = Hash}; + error -> #vcard_xupdate{} end. + -spec db_get_xupdate(binary(), binary()) -> {ok, binary() | external} | error. db_get_xupdate(LUser, LServer) -> case mod_vcard:get_vcard(LUser, LServer) of - [VCard] -> - {ok, compute_hash(VCard)}; - _ -> - error + [VCard] -> + {ok, compute_hash(VCard)}; + _ -> + error end. + -spec init_cache(binary(), gen_mod:opts()) -> ok. init_cache(Host, Opts) -> case use_cache(Host) of - true -> - CacheOpts = cache_opts(Opts), - ets_cache:new(?VCARD_XUPDATE_CACHE, CacheOpts); - false -> - ets_cache:delete(?VCARD_XUPDATE_CACHE) + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?VCARD_XUPDATE_CACHE, CacheOpts); + false -> + ets_cache:delete(?VCARD_XUPDATE_CACHE) end. + -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_vcard_xupdate_opt:cache_size(Opts), @@ -153,28 +179,32 @@ cache_opts(Opts) -> LifeTime = mod_vcard_xupdate_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + -spec use_cache(binary()) -> boolean(). use_cache(Host) -> mod_vcard_xupdate_opt:use_cache(Host). + -spec compute_hash(xmlel()) -> binary() | external. compute_hash(VCard) -> case fxml:get_subtag(VCard, <<"PHOTO">>) of - false -> - <<>>; - Photo -> - try xmpp:decode(Photo, ?NS_VCARD, []) of - #vcard_photo{binval = <<_, _/binary>> = BinVal} -> - str:sha(BinVal); - #vcard_photo{extval = <<_, _/binary>>} -> - external; - _ -> - <<>> - catch _:{xmpp_codec, _} -> - <<>> - end + false -> + <<>>; + Photo -> + try xmpp:decode(Photo, ?NS_VCARD, []) of + #vcard_photo{binval = <<_, _/binary>> = BinVal} -> + str:sha(BinVal); + #vcard_photo{extval = <<_, _/binary>>} -> + external; + _ -> + <<>> + catch + _:{xmpp_codec, _} -> + <<>> + end end. + %%==================================================================== %% Options %%==================================================================== @@ -187,20 +217,24 @@ mod_opt_type(cache_missed) -> mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). + mod_options(Host) -> [{use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. + mod_doc() -> - #{desc => + #{ + desc => [?T("The user's client can store an avatar in the " "user vCard. The vCard-Based Avatars protocol " "(https://xmpp.org/extensions/xep-0153.html[XEP-0153]) " "provides a method for clients to inform the contacts " "what is the avatar hash value. However, simple or small " - "clients may not implement that protocol."), "", + "clients may not implement that protocol."), + "", ?T("If this module is enabled, all the outgoing client presence " "stanzas get automatically the avatar hash on behalf of the " "client. So, the contacts receive the presence stanzas with " @@ -208,7 +242,8 @@ mod_doc() -> "https://xmpp.org/extensions/xep-0153.html[XEP-0153] as if the " "client would had inserted it itself. If the client had already " "included such element in the presence stanza, it is replaced " - "with the element generated by ejabberd."), "", + "with the element generated by ejabberd."), + "", ?T("By enabling this module, each vCard modification produces " "a hash recalculation, and each presence sent by a client " "produces hash retrieval and a presence stanza rewrite. " @@ -216,8 +251,10 @@ mod_doc() -> "computational overhead in servers with clients that change " "frequently their presence. However, the overhead is significantly " "reduced by the use of caching, so you probably don't want " - "to set 'use_cache' to 'false'."), "", - ?T("The module depends on _`mod_vcard`_."), "", + "to set 'use_cache' to 'false'."), + "", + ?T("The module depends on _`mod_vcard`_."), + "", ?T("NOTE: Nowadays https://xmpp.org/extensions/xep-0153.html" "[XEP-0153] is used mostly as \"read-only\", i.e. modern " "clients don't publish their avatars inside vCards. Thus " @@ -225,18 +262,27 @@ mod_doc() -> "with _`mod_avatar`_ for providing backward compatibility.")], opts => [{use_cache, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.") + }}, {cache_size, - #{value => "pos_integer() | infinity", + #{ + value => "pos_integer() | infinity", desc => - ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.") + }}, {cache_missed, - #{value => "true | false", + #{ + value => "true | false", desc => - ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.") + }}, {cache_life_time, - #{value => "timeout()", + #{ + value => "timeout()", desc => - ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.") + }}] + }. diff --git a/src/mod_vcard_xupdate_opt.erl b/src/mod_vcard_xupdate_opt.erl index a51e6884f..e64f5fbdf 100644 --- a/src/mod_vcard_xupdate_opt.erl +++ b/src/mod_vcard_xupdate_opt.erl @@ -8,27 +8,30 @@ -export([cache_size/1]). -export([use_cache/1]). + -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_life_time). + -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_missed). + -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_size). + -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, use_cache). - diff --git a/src/mod_version.erl b/src/mod_version.erl index e10168c84..b2d7803de 100644 --- a/src/mod_version.erl +++ b/src/mod_version.erl @@ -31,66 +31,94 @@ -behaviour(gen_mod). --export([start/2, stop/1, reload/3, process_local_iq/1, - mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). +-export([start/2, + stop/1, + reload/3, + process_local_iq/1, + mod_opt_type/1, + mod_options/1, + depends/2, + mod_doc/0]). -include("logger.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). + start(Host, _Opts) -> - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_VERSION, ?MODULE, process_local_iq). + gen_iq_handler:add_iq_handler(ejabberd_local, + Host, + ?NS_VERSION, + ?MODULE, + process_local_iq). + stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_VERSION). + gen_iq_handler:remove_iq_handler(ejabberd_local, + Host, + ?NS_VERSION). + reload(_Host, _NewOpts, _OldOpts) -> ok. + process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq(#iq{type = get, to = To} = IQ) -> Host = To#jid.lserver, OS = case mod_version_opt:show_os(Host) of - true -> get_os(); - false -> undefined - end, - xmpp:make_iq_result(IQ, #version{name = <<"ejabberd">>, - ver = ejabberd_option:version(), - os = OS}). + true -> get_os(); + false -> undefined + end, + xmpp:make_iq_result(IQ, + #version{ + name = <<"ejabberd">>, + ver = ejabberd_option:version(), + os = OS + }). + get_os() -> {Osfamily, Osname} = os:type(), OSType = list_to_binary([atom_to_list(Osfamily), $/, atom_to_list(Osname)]), OSVersion = case os:version() of - {Major, Minor, Release} -> - (str:format("~w.~w.~w", - [Major, Minor, Release])); - VersionString -> VersionString - end, + {Major, Minor, Release} -> + (str:format("~w.~w.~w", + [Major, Minor, Release])); + VersionString -> VersionString + end, <>. + depends(_Host, _Opts) -> []. + mod_opt_type(show_os) -> econf:bool(). + mod_options(_Host) -> [{show_os, true}]. + mod_doc() -> - #{desc => + #{ + desc => ?T("This module implements " "https://xmpp.org/extensions/xep-0092.html" "[XEP-0092: Software Version]. Consequently, " "it answers ejabberd's version when queried."), opts => [{show_os, - #{value => "true | false", + #{ + value => "true | false", desc => ?T("Should the operating system be revealed or not. " - "The default value is 'true'.")}}]}. + "The default value is 'true'.") + }}] + }. diff --git a/src/mod_version_opt.erl b/src/mod_version_opt.erl index 78d6231fb..db3edbbb6 100644 --- a/src/mod_version_opt.erl +++ b/src/mod_version_opt.erl @@ -5,9 +5,9 @@ -export([show_os/1]). + -spec show_os(gen_mod:opts() | global | binary()) -> boolean(). show_os(Opts) when is_map(Opts) -> gen_mod:get_opt(show_os, Opts); show_os(Host) -> gen_mod:get_module_opt(Host, mod_version, show_os). - diff --git a/src/mqtt_codec.erl b/src/mqtt_codec.erl index c67adfb12..5590278c7 100644 --- a/src/mqtt_codec.erl +++ b/src/mqtt_codec.erl @@ -37,38 +37,39 @@ -record(codec_state, {version :: undefined | mqtt_version(), type :: undefined | non_neg_integer(), - flags :: undefined | non_neg_integer(), - size :: undefined | non_neg_integer(), - max_size :: pos_integer() | infinity, - buf = <<>> :: binary()}). + flags :: undefined | non_neg_integer(), + size :: undefined | non_neg_integer(), + max_size :: pos_integer() | infinity, + buf = <<>> :: binary()}). -type error_reason() :: bad_varint | - {payload_too_big, integer()} | - {bad_packet_type, char()} | - {bad_packet, atom()} | + {payload_too_big, integer()} | + {bad_packet_type, char()} | + {bad_packet, atom()} | {unexpected_packet, atom()} | - {bad_reason_code, atom(), char()} | + {bad_reason_code, atom(), char()} | {bad_properties, atom()} | {bad_property, atom(), atom()} | {duplicated_property, atom(), atom()} | - bad_will_topic_or_message | - bad_connect_username_or_password | - bad_publish_id_or_payload | - {bad_topic_filters, atom()} | - {bad_qos, char()} | - bad_topic | bad_topic_filter | bad_utf8_string | - {unsupported_protocol_name, binary(), binary()} | + bad_will_topic_or_message | + bad_connect_username_or_password | + bad_publish_id_or_payload | + {bad_topic_filters, atom()} | + {bad_qos, char()} | + bad_topic | bad_topic_filter | bad_utf8_string | + {unsupported_protocol_name, binary(), binary()} | {unsupported_protocol_version, char(), iodata()} | - {{bad_flag, atom()}, char(), term()} | - {{bad_flags, atom()}, char(), char()}. + {{bad_flag, atom()}, char(), term()} | + {{bad_flags, atom()}, char(), char()}. %% @indent-end %% @efmt:on -%% + %% -opaque state() :: #codec_state{}. -export_type([state/0, error_reason/0]). + %%%=================================================================== %%% API %%%=================================================================== @@ -76,75 +77,92 @@ new(MaxSize) -> new(MaxSize, undefined). + -spec new(pos_integer() | infinity, undefined | mqtt_version()) -> state(). new(MaxSize, Version) -> #codec_state{max_size = MaxSize, version = Version}. + -spec renew(state()) -> state(). renew(#codec_state{version = Version, max_size = MaxSize}) -> #codec_state{version = Version, max_size = MaxSize}. + -spec decode(state(), binary()) -> {ok, mqtt_packet(), state()} | - {more, state()} | - {error, error_reason()}. + {more, state()} | + {error, error_reason()}. decode(#codec_state{size = undefined, buf = Buf} = State, Data) -> Buf1 = <>, case Buf1 of - <> -> - try - case decode_varint(Data1) of - {Len, _} when Len >= State#codec_state.max_size -> - err({payload_too_big, State#codec_state.max_size}); - {Len, Data2} when size(Data2) >= Len -> - <> = Data2, + <> -> + try + case decode_varint(Data1) of + {Len, _} when Len >= State#codec_state.max_size -> + err({payload_too_big, State#codec_state.max_size}); + {Len, Data2} when size(Data2) >= Len -> + <> = Data2, Version = State#codec_state.version, Pkt = decode_pkt(Version, Type, Flags, Payload), - State1 = case Pkt of + State1 = case Pkt of #connect{proto_level = V} -> State#codec_state{version = V}; _ -> State end, {ok, Pkt, State1#codec_state{buf = Data3}}; - {Len, Data2} -> - {more, State#codec_state{type = Type, - flags = Flags, - size = Len, - buf = Data2}}; - more -> - {more, State#codec_state{buf = Buf1}} - end - catch _:{?MODULE, Why} -> - {error, Why} - end; - <<>> -> - {more, State} + {Len, Data2} -> + {more, State#codec_state{ + type = Type, + flags = Flags, + size = Len, + buf = Data2 + }}; + more -> + {more, State#codec_state{buf = Buf1}} + end + catch + _:{?MODULE, Why} -> + {error, Why} + end; + <<>> -> + {more, State} end; -decode(#codec_state{size = Len, buf = Buf, - version = Version, - type = Type, flags = Flags} = State, Data) -> +decode(#codec_state{ + size = Len, + buf = Buf, + version = Version, + type = Type, + flags = Flags + } = State, + Data) -> Buf1 = <>, - if size(Buf1) >= Len -> - <> = Buf1, - try - Pkt = decode_pkt(Version, Type, Flags, Payload), + if + size(Buf1) >= Len -> + <> = Buf1, + try + Pkt = decode_pkt(Version, Type, Flags, Payload), State1 = case Pkt of #connect{proto_level = V} -> State#codec_state{version = V}; _ -> State end, - {ok, Pkt, State1#codec_state{type = undefined, - flags = undefined, - size = undefined, - buf = Data1}} - catch _:{?MODULE, Why} -> - {error, Why} - end; - true -> - {more, State#codec_state{buf = Buf1}} + {ok, Pkt, + State1#codec_state{ + type = undefined, + flags = undefined, + size = undefined, + buf = Data1 + }} + catch + _:{?MODULE, Why} -> + {error, Why} + end; + true -> + {more, State#codec_state{buf = Buf1}} end. + -spec encode(mqtt_version(), mqtt_packet()) -> binary(). encode(Version, Pkt) -> case Pkt of @@ -165,10 +183,12 @@ encode(Version, Pkt) -> #auth{} -> encode_auth(Pkt) end. + -spec pp(any()) -> iolist(). pp(Term) -> io_lib_pretty:print(Term, fun pp/2). + -spec format_error(error_reason()) -> string(). format_error({payload_too_big, Max}) -> format("Payload exceeds ~B bytes", [Max]); @@ -200,7 +220,7 @@ format_error(bad_publish_id_or_payload) -> "Malformed id or payload of PUBLISH packet"; format_error({bad_topic_filters, Name}) -> format("Malformed topic filters of ~ts packet", - [string:to_upper(atom_to_list(Name))]); + [string:to_upper(atom_to_list(Name))]); format_error({bad_qos, Q}) -> format_got_expected("Malformed QoS value", Q, "0, 1 or 2"); format_error(bad_topic) -> @@ -222,6 +242,7 @@ format_error({{bad_flags, Name}, Got, Expected}) -> format_error(Reason) -> format("Unexpected error: ~w", [Reason]). + -spec error_reason_code(error_reason()) -> reason_code(). error_reason_code({unsupported_protocol_name, _, _}) -> 'unsupported-protocol-version'; @@ -231,6 +252,7 @@ error_reason_code({payload_too_big, _}) -> 'packet-too-large'; error_reason_code({unexpected_packet, _}) -> 'protocol-error'; error_reason_code(_) -> 'malformed-packet'. + -spec format_reason_code(reason_code()) -> string(). format_reason_code('success') -> "Success"; format_reason_code('normal-disconnection') -> "Normal disconnection"; @@ -288,6 +310,7 @@ format_reason_code('wildcard-subscriptions-not-supported') -> format_reason_code(Code) -> format("Unexpected error: ~w", [Code]). + -spec is_error_code(char() | reason_code()) -> boolean(). is_error_code('success') -> false; is_error_code('normal-disconnection') -> false; @@ -302,6 +325,7 @@ is_error_code('re-authenticate') -> false; is_error_code(Code) when is_integer(Code) -> Code >= 128; is_error_code(_) -> true. + %%%=================================================================== %%% Decoder %%%=================================================================== @@ -309,60 +333,70 @@ is_error_code(_) -> true. decode_varint(Data) -> decode_varint(Data, 0, 1). + -spec decode_varint(binary(), non_neg_integer(), pos_integer()) -> - {non_neg_integer(), binary()} | more. + {non_neg_integer(), binary()} | more. decode_varint(<>, Val, Mult) -> NewVal = Val + (C band 127) * Mult, - NewMult = Mult*128, - if NewMult > ?MAX_VARINT -> - err(bad_varint); - (C band 128) == 0 -> - {NewVal, Data}; - true -> - decode_varint(Data, NewVal, NewMult) + NewMult = Mult * 128, + if + NewMult > ?MAX_VARINT -> + err(bad_varint); + (C band 128) == 0 -> + {NewVal, Data}; + true -> + decode_varint(Data, NewVal, NewMult) end; decode_varint(_, _, _) -> more. + -spec decode_pkt(mqtt_version() | undefined, - non_neg_integer(), non_neg_integer(), binary()) -> mqtt_packet(). + non_neg_integer(), + non_neg_integer(), + binary()) -> mqtt_packet(). decode_pkt(undefined, 1, Flags, Data) -> decode_connect(Flags, Data); -decode_pkt(Version, Type, Flags, Data) when Version /= undefined, Type>1 -> +decode_pkt(Version, Type, Flags, Data) when Version /= undefined, Type > 1 -> case Type of - 2 -> decode_connack(Version, Flags, Data); - 3 -> decode_publish(Version, Flags, Data); - 4 -> decode_puback(Version, Flags, Data); - 5 -> decode_pubrec(Version, Flags, Data); - 6 -> decode_pubrel(Version, Flags, Data); - 7 -> decode_pubcomp(Version, Flags, Data); - 8 -> decode_subscribe(Version, Flags, Data); - 9 -> decode_suback(Version, Flags, Data); - 10 -> decode_unsubscribe(Version, Flags, Data); - 11 -> decode_unsuback(Version, Flags, Data); - 12 -> decode_pingreq(Flags, Data); - 13 -> decode_pingresp(Flags, Data); - 14 -> decode_disconnect(Version, Flags, Data); + 2 -> decode_connack(Version, Flags, Data); + 3 -> decode_publish(Version, Flags, Data); + 4 -> decode_puback(Version, Flags, Data); + 5 -> decode_pubrec(Version, Flags, Data); + 6 -> decode_pubrel(Version, Flags, Data); + 7 -> decode_pubcomp(Version, Flags, Data); + 8 -> decode_subscribe(Version, Flags, Data); + 9 -> decode_suback(Version, Flags, Data); + 10 -> decode_unsubscribe(Version, Flags, Data); + 11 -> decode_unsuback(Version, Flags, Data); + 12 -> decode_pingreq(Flags, Data); + 13 -> decode_pingresp(Flags, Data); + 14 -> decode_disconnect(Version, Flags, Data); 15 when Version == ?MQTT_VERSION_5 -> decode_auth(Flags, Data); _ -> err({bad_packet_type, Type}) end; decode_pkt(_, Type, _, _) -> err({unexpected_packet, decode_packet_type(Type)}). + -spec decode_connect(non_neg_integer(), binary()) -> connect(). -decode_connect(Flags, <>) -> +decode_connect(Flags, + <>) -> assert(Proto, <<"MQTT">>, unsupported_protocol_name), - if ProtoLevel == ?MQTT_VERSION_4; ProtoLevel == ?MQTT_VERSION_5 -> + if + ProtoLevel == ?MQTT_VERSION_4; ProtoLevel == ?MQTT_VERSION_5 -> decode_connect(ProtoLevel, Flags, Data); - true -> + true -> err({unsupported_protocol_version, ProtoLevel, "4 or 5"}) end; decode_connect(_, _) -> err({bad_packet, connect}). + -spec decode_connect(mqtt_version(), non_neg_integer(), binary()) -> connect(). -decode_connect(Version, Flags, +decode_connect(Version, + Flags, <>) -> @@ -377,80 +411,94 @@ decode_connect(Version, Flags, {Will, WillProps, Data3} = decode_will(Version, WillFlag, WillRetain, WillQoS, Data2), {Username, Password} = decode_user_pass(UserFlag, PassFlag, Data3), - #connect{proto_level = Version, - will = Will, - will_properties = WillProps, - properties = Props, - clean_start = dec_bool(CleanStart), - keep_alive = KeepAlive, - client_id = utf8(ClientID), - username = utf8(Username), - password = Password}; + #connect{ + proto_level = Version, + will = Will, + will_properties = WillProps, + properties = Props, + clean_start = dec_bool(CleanStart), + keep_alive = KeepAlive, + client_id = utf8(ClientID), + username = utf8(Username), + password = Password + }; _ -> err({bad_packet, connect}) end; decode_connect(_, _, _) -> err({bad_packet, connect}). + -spec decode_connack(mqtt_version(), non_neg_integer(), binary()) -> connack(). decode_connack(Version, Flags, <<0:7, SessionPresent:1, Data/binary>>) -> assert(Flags, 0, {bad_flags, connack}), {Code, PropMap} = decode_code_with_props(Version, connack, Data), - #connack{session_present = dec_bool(SessionPresent), - code = Code, properties = PropMap}; + #connack{ + session_present = dec_bool(SessionPresent), + code = Code, + properties = PropMap + }; decode_connack(_, _, _) -> err({bad_packet, connack}). + -spec decode_publish(mqtt_version(), non_neg_integer(), binary()) -> publish(). decode_publish(Version, Flags, <>) -> Retain = Flags band 1, QoS = qos((Flags bsr 1) band 3), DUP = Flags band 8, {ID, Props, Payload} = decode_id_props_payload(Version, QoS, Data), - #publish{dup = dec_bool(DUP), - qos = QoS, - retain = dec_bool(Retain), - topic = topic(Topic, Props), - id = ID, - properties = Props, - payload = Payload}; + #publish{ + dup = dec_bool(DUP), + qos = QoS, + retain = dec_bool(Retain), + topic = topic(Topic, Props), + id = ID, + properties = Props, + payload = Payload + }; decode_publish(_, _, _) -> err({bad_packet, publish}). + -spec decode_puback(mqtt_version(), non_neg_integer(), binary()) -> puback(). -decode_puback(Version, Flags, <>) when ID>0 -> +decode_puback(Version, Flags, <>) when ID > 0 -> assert(Flags, 0, {bad_flags, puback}), {Code, PropMap} = decode_code_with_props(Version, puback, Data), #puback{id = ID, code = Code, properties = PropMap}; decode_puback(_, _, _) -> err({bad_packet, puback}). + -spec decode_pubrec(mqtt_version(), non_neg_integer(), binary()) -> pubrec(). -decode_pubrec(Version, Flags, <>) when ID>0 -> +decode_pubrec(Version, Flags, <>) when ID > 0 -> assert(Flags, 0, {bad_flags, pubrec}), {Code, PropMap} = decode_code_with_props(Version, pubrec, Data), #pubrec{id = ID, code = Code, properties = PropMap}; decode_pubrec(_, _, _) -> err({bad_packet, pubrec}). + -spec decode_pubrel(mqtt_version(), non_neg_integer(), binary()) -> pubrel(). -decode_pubrel(Version, Flags, <>) when ID>0 -> +decode_pubrel(Version, Flags, <>) when ID > 0 -> assert(Flags, 2, {bad_flags, pubrel}), {Code, PropMap} = decode_code_with_props(Version, pubrel, Data), #pubrel{id = ID, code = Code, properties = PropMap}; decode_pubrel(_, _, _) -> err({bad_packet, pubrel}). + -spec decode_pubcomp(mqtt_version(), non_neg_integer(), binary()) -> pubcomp(). -decode_pubcomp(Version, Flags, <>) when ID>0 -> +decode_pubcomp(Version, Flags, <>) when ID > 0 -> assert(Flags, 0, {bad_flags, pubcomp}), {Code, PropMap} = decode_code_with_props(Version, pubcomp, Data), #pubcomp{id = ID, code = Code, properties = PropMap}; decode_pubcomp(_, _, _) -> err({bad_packet, pubcomp}). + -spec decode_subscribe(mqtt_version(), non_neg_integer(), binary()) -> subscribe(). -decode_subscribe(Version, Flags, <>) when ID>0 -> +decode_subscribe(Version, Flags, <>) when ID > 0 -> assert(Flags, 2, {bad_flags, subscribe}), case Version of ?MQTT_VERSION_4 -> @@ -464,24 +512,30 @@ decode_subscribe(Version, Flags, <>) when ID>0 -> decode_subscribe(_, _, _) -> err({bad_packet, subscribe}). + -spec decode_suback(mqtt_version(), non_neg_integer(), binary()) -> suback(). -decode_suback(Version, Flags, <>) when ID>0 -> +decode_suback(Version, Flags, <>) when ID > 0 -> assert(Flags, 0, {bad_flags, suback}), case Version of ?MQTT_VERSION_4 -> - #suback{id = ID, - codes = decode_suback_codes(Data)}; + #suback{ + id = ID, + codes = decode_suback_codes(Data) + }; ?MQTT_VERSION_5 -> {PropMap, Tail} = decode_props(suback, Data), - #suback{id = ID, - codes = decode_suback_codes(Tail), - properties = PropMap} + #suback{ + id = ID, + codes = decode_suback_codes(Tail), + properties = PropMap + } end; decode_suback(_, _, _) -> err({bad_packet, suback}). + -spec decode_unsubscribe(mqtt_version(), non_neg_integer(), binary()) -> unsubscribe(). -decode_unsubscribe(Version, Flags, <>) when ID>0 -> +decode_unsubscribe(Version, Flags, <>) when ID > 0 -> assert(Flags, 2, {bad_flags, unsubscribe}), case Version of ?MQTT_VERSION_4 -> @@ -495,21 +549,25 @@ decode_unsubscribe(Version, Flags, <>) when ID>0 -> decode_unsubscribe(_, _, _) -> err({bad_packet, unsubscribe}). + -spec decode_unsuback(mqtt_version(), non_neg_integer(), binary()) -> unsuback(). -decode_unsuback(Version, Flags, <>) when ID>0 -> +decode_unsuback(Version, Flags, <>) when ID > 0 -> assert(Flags, 0, {bad_flags, unsuback}), case Version of ?MQTT_VERSION_4 -> #unsuback{id = ID}; ?MQTT_VERSION_5 -> {PropMap, Tail} = decode_props(unsuback, Data), - #unsuback{id = ID, - codes = decode_unsuback_codes(Tail), - properties = PropMap} + #unsuback{ + id = ID, + codes = decode_unsuback_codes(Tail), + properties = PropMap + } end; decode_unsuback(_, _, _) -> err({bad_packet, unsuback}). + -spec decode_pingreq(non_neg_integer(), binary()) -> pingreq(). decode_pingreq(Flags, <<>>) -> assert(Flags, 0, {bad_flags, pingreq}), @@ -517,6 +575,7 @@ decode_pingreq(Flags, <<>>) -> decode_pingreq(_, _) -> err({bad_packet, pingreq}). + -spec decode_pingresp(non_neg_integer(), binary()) -> pingresp(). decode_pingresp(Flags, <<>>) -> assert(Flags, 0, {bad_flags, pingresp}), @@ -524,18 +583,21 @@ decode_pingresp(Flags, <<>>) -> decode_pingresp(_, _) -> err({bad_packet, pingresp}). + -spec decode_disconnect(mqtt_version(), non_neg_integer(), binary()) -> disconnect(). decode_disconnect(Version, Flags, Payload) -> assert(Flags, 0, {bad_flags, disconnect}), {Code, PropMap} = decode_code_with_props(Version, disconnect, Payload), #disconnect{code = Code, properties = PropMap}. + -spec decode_auth(non_neg_integer(), binary()) -> auth(). decode_auth(Flags, Payload) -> assert(Flags, 0, {bad_flags, auth}), {Code, PropMap} = decode_code_with_props(?MQTT_VERSION_5, auth, Payload), #auth{code = Code, properties = PropMap}. + -spec decode_packet_type(char()) -> atom(). decode_packet_type(1) -> connect; decode_packet_type(2) -> connack; @@ -554,8 +616,9 @@ decode_packet_type(14) -> disconnect; decode_packet_type(15) -> auth; decode_packet_type(T) -> err({bad_packet_type, T}). --spec decode_will(mqtt_version(), 0|1, 0|1, qos(), binary()) -> - {undefined | publish(), properties(), binary()}. + +-spec decode_will(mqtt_version(), 0 | 1, 0 | 1, qos(), binary()) -> + {undefined | publish(), properties(), binary()}. decode_will(_, 0, WillRetain, WillQoS, Data) -> assert(WillRetain, 0, {bad_flag, will_retain}), assert(WillQoS, 0, {bad_flag, will_qos}), @@ -568,21 +631,28 @@ decode_will(Version, 1, WillRetain, WillQoS, Data) -> case Data1 of <> -> - {#publish{retain = dec_bool(WillRetain), - qos = qos(WillQoS), - topic = topic(Topic), - payload = Message}, - Props, Data2}; + {#publish{ + retain = dec_bool(WillRetain), + qos = qos(WillQoS), + topic = topic(Topic), + payload = Message + }, + Props, + Data2}; _ -> err(bad_will_topic_or_message) end. --spec decode_user_pass(non_neg_integer(), non_neg_integer(), - binary()) -> {binary(), binary()}. + +-spec decode_user_pass(non_neg_integer(), + non_neg_integer(), + binary()) -> {binary(), binary()}. decode_user_pass(1, 0, <>) -> {utf8(User), <<>>}; -decode_user_pass(1, 1, <>) -> +decode_user_pass(1, + 1, + <>) -> {utf8(User), Pass}; decode_user_pass(0, Flag, <<>>) -> assert(Flag, 0, {bad_flag, password}), @@ -590,8 +660,9 @@ decode_user_pass(0, Flag, <<>>) -> decode_user_pass(_, _, _) -> err(bad_connect_username_or_password). + -spec decode_id_props_payload(mqtt_version(), non_neg_integer(), binary()) -> - {undefined | non_neg_integer(), properties(), binary()}. + {undefined | non_neg_integer(), properties(), binary()}. decode_id_props_payload(Version, 0, Data) -> case Version of ?MQTT_VERSION_4 -> @@ -600,7 +671,7 @@ decode_id_props_payload(Version, 0, Data) -> {Props, Payload} = decode_props(publish, Data), {undefined, Props, Payload} end; -decode_id_props_payload(Version, _, <>) when ID>0 -> +decode_id_props_payload(Version, _, <>) when ID > 0 -> case Version of ?MQTT_VERSION_4 -> {ID, #{}, Data}; @@ -611,6 +682,7 @@ decode_id_props_payload(Version, _, <>) when ID>0 -> decode_id_props_payload(_, _, _) -> err(bad_publish_id_or_payload). + -spec decode_subscribe_filters(binary()) -> [{binary(), sub_opts()}]. decode_subscribe_filters(< err({{bad_flag, retain_handling}, RH, "0, 1 or 2"}); _ -> ok end, - Opts = #sub_opts{qos = qos(QoS), - no_local = dec_bool(NL), - retain_as_published = dec_bool(RAP), - retain_handling = RH}, - [{topic_filter(Filter), Opts}|decode_subscribe_filters(Tail)]; + Opts = #sub_opts{ + qos = qos(QoS), + no_local = dec_bool(NL), + retain_as_published = dec_bool(RAP), + retain_handling = RH + }, + [{topic_filter(Filter), Opts} | decode_subscribe_filters(Tail)]; decode_subscribe_filters(<<>>) -> []; decode_subscribe_filters(_) -> err({bad_topic_filters, subscribe}). + -spec decode_unsubscribe_filters(binary()) -> [binary()]. decode_unsubscribe_filters(<>) -> - [topic_filter(Filter)|decode_unsubscribe_filters(Tail)]; + [topic_filter(Filter) | decode_unsubscribe_filters(Tail)]; decode_unsubscribe_filters(<<>>) -> []; decode_unsubscribe_filters(_) -> err({bad_topic_filters, unsubscribe}). + -spec decode_suback_codes(binary()) -> [reason_code()]. decode_suback_codes(<>) -> - [decode_suback_code(Code)|decode_suback_codes(Data)]; + [decode_suback_code(Code) | decode_suback_codes(Data)]; decode_suback_codes(<<>>) -> []. + -spec decode_unsuback_codes(binary()) -> [reason_code()]. decode_unsuback_codes(<>) -> - [decode_unsuback_code(Code)|decode_unsuback_codes(Data)]; + [decode_unsuback_code(Code) | decode_unsuback_codes(Data)]; decode_unsuback_codes(<<>>) -> []. + -spec decode_utf8_pair(binary()) -> {utf8_pair(), binary()}. decode_utf8_pair(<>) -> @@ -657,16 +735,19 @@ decode_utf8_pair(< err(bad_utf8_pair). + -spec decode_props(atom(), binary()) -> {properties(), binary()}. decode_props(Pkt, Data) -> try {Len, Data1} = decode_varint(Data), <> = Data1, {decode_props(Pkt, PData, #{}), Tail} - catch _:{badmatch, _} -> + catch + _:{badmatch, _} -> err({bad_properties, Pkt}) end. + -spec decode_props(atom(), binary(), properties()) -> properties(). decode_props(_, <<>>, Props) -> Props; @@ -679,9 +760,12 @@ decode_props(Pkt, Data, Props) -> Vals ++ Val; (_) -> err({duplicated_property, Pkt, Name}) - end, Val, Props), + end, + Val, + Props), decode_props(Pkt, Tail, Props1). + -spec decode_prop(atom(), char(), binary()) -> {property(), term(), binary()}. decode_prop(_, 18, <>) -> {assigned_client_identifier, utf8(Data), Bin}; @@ -693,7 +777,7 @@ decode_prop(_, 3, <>) -> {content_type, utf8(Data), Bin}; decode_prop(_, 9, <>) -> {correlation_data, Data, Bin}; -decode_prop(_, 39, <>) when Size>0 -> +decode_prop(_, 39, <>) when Size > 0 -> {maximum_packet_size, Size, Bin}; decode_prop(Pkt, 36, <>) -> {maximum_qos, @@ -701,7 +785,8 @@ decode_prop(Pkt, 36, <>) -> 0 -> 0; 1 -> 1; _ -> err({bad_property, Pkt, maximum_qos}) - end, Bin}; + end, + Bin}; decode_prop(_, 2, <>) -> {message_expiry_interval, I, Bin}; decode_prop(Pkt, 1, <>) -> @@ -710,10 +795,11 @@ decode_prop(Pkt, 1, <>) -> 0 -> binary; 1 -> utf8; _ -> err({bad_property, Pkt, payload_format_indicator}) - end, Bin}; + end, + Bin}; decode_prop(_, 31, <>) -> {reason_string, utf8(Data), Bin}; -decode_prop(_, 33, <>) when Max>0 -> +decode_prop(_, 33, <>) when Max > 0 -> {receive_maximum, Max, Bin}; decode_prop(Pkt, 23, Data) -> decode_bool_prop(Pkt, request_problem_information, Data); @@ -741,10 +827,10 @@ decode_prop(Pkt, 11, Data) when Pkt == publish; Pkt == subscribe -> {subscription_identifier, ID, Bin}; _ -> err({bad_property, publish, subscription_identifier}) - end; + end; decode_prop(Pkt, 41, Data) -> decode_bool_prop(Pkt, subscription_identifiers_available, Data); -decode_prop(_, 35, <>) when Alias>0 -> +decode_prop(_, 35, <>) when Alias > 0 -> {topic_alias, Alias, Bin}; decode_prop(_, 34, <>) -> {topic_alias_maximum, Max, Bin}; @@ -758,6 +844,7 @@ decode_prop(_, 24, <>) -> decode_prop(Pkt, _, _) -> err({bad_properties, Pkt}). + decode_bool_prop(Pkt, Name, <>) -> case Val of 0 -> {Name, false, Bin}; @@ -767,8 +854,9 @@ decode_bool_prop(Pkt, Name, <>) -> decode_bool_prop(Pkt, Name, _) -> err({bad_property, Pkt, Name}). + -spec decode_code_with_props(mqtt_version(), atom(), binary()) -> - {reason_code(), properties()}. + {reason_code(), properties()}. decode_code_with_props(_, connack, <>) -> {decode_connack_code(Code), case Props of @@ -788,11 +876,13 @@ decode_code_with_props(?MQTT_VERSION_5, Pkt, <>) -> decode_code_with_props(_, Pkt, _) -> err({bad_packet, Pkt}). + -spec decode_pubcomp_code(char()) -> reason_code(). decode_pubcomp_code(0) -> 'success'; decode_pubcomp_code(146) -> 'packet-identifier-not-found'; decode_pubcomp_code(Code) -> err({bad_reason_code, pubcomp, Code}). + -spec decode_pubrec_code(char()) -> reason_code(). decode_pubrec_code(0) -> 'success'; decode_pubrec_code(16) -> 'no-matching-subscribers'; @@ -805,6 +895,7 @@ decode_pubrec_code(151) -> 'quota-exceeded'; decode_pubrec_code(153) -> 'payload-format-invalid'; decode_pubrec_code(Code) -> err({bad_reason_code, pubrec, Code}). + -spec decode_disconnect_code(char()) -> reason_code(). decode_disconnect_code(0) -> 'normal-disconnection'; decode_disconnect_code(4) -> 'disconnect-with-will-message'; @@ -838,12 +929,14 @@ decode_disconnect_code(161) -> 'subscription-identifiers-not-supported'; decode_disconnect_code(162) -> 'wildcard-subscriptions-not-supported'; decode_disconnect_code(Code) -> err({bad_reason_code, disconnect, Code}). + -spec decode_auth_code(char()) -> reason_code(). decode_auth_code(0) -> 'success'; decode_auth_code(24) -> 'continue-authentication'; decode_auth_code(25) -> 're-authenticate'; decode_auth_code(Code) -> err({bad_reason_code, auth, Code}). + -spec decode_suback_code(char()) -> 0..2 | reason_code(). decode_suback_code(0) -> 0; decode_suback_code(1) -> 1; @@ -859,6 +952,7 @@ decode_suback_code(161) -> 'subscription-identifiers-not-supported'; decode_suback_code(162) -> 'wildcard-subscriptions-not-supported'; decode_suback_code(Code) -> err({bad_reason_code, suback, Code}). + -spec decode_unsuback_code(char()) -> reason_code(). decode_unsuback_code(0) -> 'success'; decode_unsuback_code(17) -> 'no-subscription-existed'; @@ -869,6 +963,7 @@ decode_unsuback_code(143) -> 'topic-filter-invalid'; decode_unsuback_code(145) -> 'packet-identifier-in-use'; decode_unsuback_code(Code) -> err({bad_reason_code, unsuback, Code}). + -spec decode_puback_code(char()) -> reason_code(). decode_puback_code(0) -> 'success'; decode_puback_code(16) -> 'no-matching-subscribers'; @@ -881,11 +976,13 @@ decode_puback_code(151) -> 'quota-exceeded'; decode_puback_code(153) -> 'payload-format-invalid'; decode_puback_code(Code) -> err({bad_reason_code, puback, Code}). + -spec decode_pubrel_code(char()) -> reason_code(). decode_pubrel_code(0) -> 'success'; decode_pubrel_code(146) -> 'packet-identifier-not-found'; decode_pubrel_code(Code) -> err({bad_reason_code, pubrel, Code}). + -spec decode_connack_code(char()) -> reason_code(). decode_connack_code(0) -> 'success'; decode_connack_code(1) -> 'unsupported-protocol-version'; @@ -916,6 +1013,7 @@ decode_connack_code(157) -> 'server-moved'; decode_connack_code(159) -> 'connection-rate-exceeded'; decode_connack_code(Code) -> err({bad_reason_code, connack, Code}). + -spec decode_reason_code(atom(), char()) -> reason_code(). decode_reason_code(pubcomp, Code) -> decode_pubcomp_code(Code); decode_reason_code(pubrec, Code) -> decode_pubrec_code(Code); @@ -925,152 +1023,232 @@ decode_reason_code(puback, Code) -> decode_puback_code(Code); decode_reason_code(pubrel, Code) -> decode_pubrel_code(Code); decode_reason_code(connack, Code) -> decode_connack_code(Code). + %%%=================================================================== %%% Encoder %%%=================================================================== -encode_connect(#connect{proto_level = Version, properties = Props, - will = Will, will_properties = WillProps, - clean_start = CleanStart, - keep_alive = KeepAlive, client_id = ClientID, - username = Username, password = Password}) -> +encode_connect(#connect{ + proto_level = Version, + properties = Props, + will = Will, + will_properties = WillProps, + clean_start = CleanStart, + keep_alive = KeepAlive, + client_id = ClientID, + username = Username, + password = Password + }) -> UserFlag = Username /= <<>>, PassFlag = UserFlag andalso Password /= <<>>, WillFlag = is_record(Will, publish), WillRetain = WillFlag andalso Will#publish.retain, - WillQoS = if WillFlag -> Will#publish.qos; - true -> 0 - end, - Header = <<4:16, "MQTT", Version, (enc_bool(UserFlag)):1, - (enc_bool(PassFlag)):1, (enc_bool(WillRetain)):1, - WillQoS:2, (enc_bool(WillFlag)):1, - (enc_bool(CleanStart)):1, 0:1, - KeepAlive:16>>, + WillQoS = if + WillFlag -> Will#publish.qos; + true -> 0 + end, + Header = <<4:16, + "MQTT", + Version, + (enc_bool(UserFlag)):1, + (enc_bool(PassFlag)):1, + (enc_bool(WillRetain)):1, + WillQoS:2, + (enc_bool(WillFlag)):1, + (enc_bool(CleanStart)):1, + 0:1, + KeepAlive:16>>, EncClientID = <<(size(ClientID)):16, ClientID/binary>>, EncWill = encode_will(Will), EncUserPass = encode_user_pass(Username, Password), Payload = case Version of ?MQTT_VERSION_5 -> - [Header, encode_props(Props), EncClientID, - if WillFlag -> encode_props(WillProps); - true -> <<>> + [Header, + encode_props(Props), + EncClientID, + if + WillFlag -> encode_props(WillProps); + true -> <<>> end, - EncWill, EncUserPass]; + EncWill, + EncUserPass]; _ -> [Header, EncClientID, EncWill, EncUserPass] end, <<1:4, 0:4, (encode_with_len(Payload))/binary>>. -encode_connack(Version, #connack{session_present = SP, - code = Code, properties = Props}) -> + +encode_connack(Version, + #connack{ + session_present = SP, + code = Code, + properties = Props + }) -> Payload = [enc_bool(SP), encode_connack_code(Version, Code), encode_props(Version, Props)], <<2:4, 0:4, (encode_with_len(Payload))/binary>>. -encode_publish(Version, #publish{qos = QoS, retain = Retain, dup = Dup, - topic = Topic, id = ID, payload = Payload, - properties = Props}) -> + +encode_publish(Version, + #publish{ + qos = QoS, + retain = Retain, + dup = Dup, + topic = Topic, + id = ID, + payload = Payload, + properties = Props + }) -> Data1 = <<(size(Topic)):16, Topic/binary>>, Data2 = case QoS of - 0 -> <<>>; - _ when ID>0 -> <> - end, + 0 -> <<>>; + _ when ID > 0 -> <> + end, Data3 = encode_props(Version, Props), Data4 = encode_with_len([Data1, Data2, Data3, Payload]), <<3:4, (enc_bool(Dup)):1, QoS:2, (enc_bool(Retain)):1, Data4/binary>>. -encode_puback(Version, #puback{id = ID, code = Code, - properties = Props}) when ID>0 -> - Data = encode_code_with_props(Version, Code, Props), - <<4:4, 0:4, (encode_with_len([<>|Data]))/binary>>. -encode_pubrec(Version, #pubrec{id = ID, code = Code, - properties = Props}) when ID>0 -> +encode_puback(Version, + #puback{ + id = ID, + code = Code, + properties = Props + }) when ID > 0 -> Data = encode_code_with_props(Version, Code, Props), - <<5:4, 0:4, (encode_with_len([<>|Data]))/binary>>. + <<4:4, 0:4, (encode_with_len([<> | Data]))/binary>>. -encode_pubrel(Version, #pubrel{id = ID, code = Code, - properties = Props}) when ID>0 -> + +encode_pubrec(Version, + #pubrec{ + id = ID, + code = Code, + properties = Props + }) when ID > 0 -> Data = encode_code_with_props(Version, Code, Props), - <<6:4, 2:4, (encode_with_len([<>|Data]))/binary>>. + <<5:4, 0:4, (encode_with_len([<> | Data]))/binary>>. -encode_pubcomp(Version, #pubcomp{id = ID, code = Code, - properties = Props}) when ID>0 -> + +encode_pubrel(Version, + #pubrel{ + id = ID, + code = Code, + properties = Props + }) when ID > 0 -> Data = encode_code_with_props(Version, Code, Props), - <<7:4, 0:4, (encode_with_len([<>|Data]))/binary>>. + <<6:4, 2:4, (encode_with_len([<> | Data]))/binary>>. -encode_subscribe(Version, #subscribe{id = ID, - filters = [_|_] = Filters, - properties = Props}) when ID>0 -> - EncFilters = [<<(size(Filter)):16, Filter/binary, - (encode_subscription_options(SubOpts))>> || - {Filter, SubOpts} <- Filters], + +encode_pubcomp(Version, + #pubcomp{ + id = ID, + code = Code, + properties = Props + }) when ID > 0 -> + Data = encode_code_with_props(Version, Code, Props), + <<7:4, 0:4, (encode_with_len([<> | Data]))/binary>>. + + +encode_subscribe(Version, + #subscribe{ + id = ID, + filters = [_ | _] = Filters, + properties = Props + }) when ID > 0 -> + EncFilters = [ <<(size(Filter)):16, + Filter/binary, + (encode_subscription_options(SubOpts))>> + || {Filter, SubOpts} <- Filters ], Payload = [<>, encode_props(Version, Props), EncFilters], <<8:4, 2:4, (encode_with_len(Payload))/binary>>. -encode_suback(Version, #suback{id = ID, codes = Codes, - properties = Props}) when ID>0 -> - Payload = [<>, encode_props(Version, Props) - |[encode_reason_code(Code) || Code <- Codes]], + +encode_suback(Version, + #suback{ + id = ID, + codes = Codes, + properties = Props + }) when ID > 0 -> + Payload = [<>, encode_props(Version, Props) | [ encode_reason_code(Code) || Code <- Codes ]], <<9:4, 0:4, (encode_with_len(Payload))/binary>>. -encode_unsubscribe(Version, #unsubscribe{id = ID, - filters = [_|_] = Filters, - properties = Props}) when ID>0 -> - EncFilters = [<<(size(Filter)):16, Filter/binary>> || Filter <- Filters], + +encode_unsubscribe(Version, + #unsubscribe{ + id = ID, + filters = [_ | _] = Filters, + properties = Props + }) when ID > 0 -> + EncFilters = [ <<(size(Filter)):16, Filter/binary>> || Filter <- Filters ], Payload = [<>, encode_props(Version, Props), EncFilters], <<10:4, 2:4, (encode_with_len(Payload))/binary>>. -encode_unsuback(Version, #unsuback{id = ID, codes = Codes, - properties = Props}) when ID>0 -> + +encode_unsuback(Version, + #unsuback{ + id = ID, + codes = Codes, + properties = Props + }) when ID > 0 -> EncCodes = case Version of ?MQTT_VERSION_5 -> - [encode_reason_code(Code) || Code <- Codes]; + [ encode_reason_code(Code) || Code <- Codes ]; ?MQTT_VERSION_4 -> [] end, - Payload = [<>, encode_props(Version, Props)|EncCodes], + Payload = [<>, encode_props(Version, Props) | EncCodes], <<11:4, 0:4, (encode_with_len(Payload))/binary>>. + encode_pingreq() -> <<12:4, 0:4, 0>>. + encode_pingresp() -> <<13:4, 0:4, 0>>. + encode_disconnect(Version, #disconnect{code = Code, properties = Props}) -> Data = encode_code_with_props(Version, Code, Props), <<14:4, 0:4, (encode_with_len(Data))/binary>>. + encode_auth(#auth{code = Code, properties = Props}) -> Data = encode_code_with_props(?MQTT_VERSION_5, Code, Props), <<15:4, 0:4, (encode_with_len(Data))/binary>>. + -spec encode_with_len(iodata()) -> binary(). encode_with_len(IOData) -> Data = iolist_to_binary(IOData), Len = encode_varint(size(Data)), <>. + -spec encode_varint(non_neg_integer()) -> binary(). encode_varint(X) when X < 128 -> <<0:1, X:7>>; encode_varint(X) when X < ?MAX_VARINT -> <<1:1, (X rem 128):7, (encode_varint(X div 128))/binary>>. + -spec encode_props(mqtt_version(), properties()) -> binary(). encode_props(?MQTT_VERSION_5, Props) -> encode_props(Props); encode_props(?MQTT_VERSION_4, _) -> <<>>. + -spec encode_props(properties()) -> binary(). encode_props(Props) -> encode_with_len( maps:fold( fun(Name, Val, Acc) -> - [encode_prop(Name, Val)|Acc] - end, [], Props)). + [encode_prop(Name, Val) | Acc] + end, + [], + Props)). + -spec encode_prop(property(), term()) -> iodata(). encode_prop(assigned_client_identifier, <<>>) -> @@ -1093,11 +1271,11 @@ encode_prop(correlation_data, <<>>) -> <<>>; encode_prop(correlation_data, Data) -> <<9, (size(Data)):16, Data/binary>>; -encode_prop(maximum_packet_size, Size) when Size>0, Size= +encode_prop(maximum_packet_size, Size) when Size > 0, Size =< ?MAX_UINT32 -> <<39, Size:32>>; -encode_prop(maximum_qos, QoS) when QoS>=0, QoS<2 -> +encode_prop(maximum_qos, QoS) when QoS >= 0, QoS < 2 -> <<36, QoS>>; -encode_prop(message_expiry_interval, I) when I>=0, I= +encode_prop(message_expiry_interval, I) when I >= 0, I =< ?MAX_UINT32 -> <<2, I:32>>; encode_prop(payload_format_indicator, binary) -> <<>>; @@ -1107,7 +1285,7 @@ encode_prop(reason_string, <<>>) -> <<>>; encode_prop(reason_string, S) -> <<31, (size(S)):16, S/binary>>; -encode_prop(receive_maximum, Max) when Max>0, Max= +encode_prop(receive_maximum, Max) when Max > 0, Max =< ?MAX_UINT16 -> <<33, Max:16>>; encode_prop(request_problem_information, true) -> <<>>; @@ -1129,43 +1307,44 @@ encode_prop(retain_available, true) -> <<>>; encode_prop(retain_available, false) -> <<37, 0>>; -encode_prop(server_keep_alive, Secs) when Secs>=0, Secs= +encode_prop(server_keep_alive, Secs) when Secs >= 0, Secs =< ?MAX_UINT16 -> <<19, Secs:16>>; encode_prop(server_reference, <<>>) -> <<>>; encode_prop(server_reference, S) -> <<28, (size(S)):16, S/binary>>; -encode_prop(session_expiry_interval, I) when I>=0, I= +encode_prop(session_expiry_interval, I) when I >= 0, I =< ?MAX_UINT32 -> <<17, I:32>>; encode_prop(shared_subscription_available, true) -> <<>>; encode_prop(shared_subscription_available, false) -> <<42, 0>>; -encode_prop(subscription_identifier, [_|_] = IDs) -> - [encode_prop(subscription_identifier, ID) || ID <- IDs]; -encode_prop(subscription_identifier, ID) when ID>0, ID +encode_prop(subscription_identifier, [_ | _] = IDs) -> + [ encode_prop(subscription_identifier, ID) || ID <- IDs ]; +encode_prop(subscription_identifier, ID) when ID > 0, ID < ?MAX_VARINT -> <<11, (encode_varint(ID))/binary>>; encode_prop(subscription_identifiers_available, true) -> <<>>; encode_prop(subscription_identifiers_available, false) -> <<41, 0>>; -encode_prop(topic_alias, Alias) when Alias>0, Alias= +encode_prop(topic_alias, Alias) when Alias > 0, Alias =< ?MAX_UINT16 -> <<35, Alias:16>>; encode_prop(topic_alias_maximum, 0) -> <<>>; -encode_prop(topic_alias_maximum, Max) when Max>0, Max= +encode_prop(topic_alias_maximum, Max) when Max > 0, Max =< ?MAX_UINT16 -> <<34, Max:16>>; encode_prop(user_property, Pairs) -> - [<<38, (encode_utf8_pair(Pair))/binary>> || Pair <- Pairs]; + [ <<38, (encode_utf8_pair(Pair))/binary>> || Pair <- Pairs ]; encode_prop(wildcard_subscription_available, true) -> <<>>; encode_prop(wildcard_subscription_available, false) -> <<40, 0>>; encode_prop(will_delay_interval, 0) -> <<>>; -encode_prop(will_delay_interval, I) when I>0, I= +encode_prop(will_delay_interval, I) when I > 0, I =< ?MAX_UINT32 -> <<24, I:32>>. + -spec encode_user_pass(binary(), binary()) -> binary(). encode_user_pass(User, Pass) when User /= <<>> andalso Pass /= <<>> -> <<(size(User)):16, User/binary, (size(Pass)):16, Pass/binary>>; @@ -1174,35 +1353,45 @@ encode_user_pass(User, _) when User /= <<>> -> encode_user_pass(_, _) -> <<>>. + -spec encode_will(undefined | publish()) -> binary(). encode_will(#publish{topic = Topic, payload = Payload}) -> - <<(size(Topic)):16, Topic/binary, - (size(Payload)):16, Payload/binary>>; + <<(size(Topic)):16, + Topic/binary, + (size(Payload)):16, + Payload/binary>>; encode_will(undefined) -> <<>>. -encode_subscription_options(#sub_opts{qos = QoS, - no_local = NL, - retain_as_published = RAP, - retain_handling = RH}) - when QoS>=0, RH>=0, QoS<3, RH<3 -> + +encode_subscription_options(#sub_opts{ + qos = QoS, + no_local = NL, + retain_as_published = RAP, + retain_handling = RH + }) + when QoS >= 0, RH >= 0, QoS < 3, RH < 3 -> (RH bsl 4) bor (enc_bool(RAP) bsl 3) bor (enc_bool(NL) bsl 2) bor QoS. + -spec encode_code_with_props(mqtt_version(), reason_code(), properties()) -> [binary()]. encode_code_with_props(Version, Code, Props) -> - if Version == ?MQTT_VERSION_4 orelse - (Code == success andalso Props == #{}) -> + if + Version == ?MQTT_VERSION_4 orelse + (Code == success andalso Props == #{}) -> []; - Props == #{} -> + Props == #{} -> [encode_reason_code(Code)]; - true -> + true -> [encode_reason_code(Code), encode_props(Props)] end. + -spec encode_utf8_pair({binary(), binary()}) -> binary(). encode_utf8_pair({Key, Val}) -> <<(size(Key)):16, Key/binary, (size(Val)):16, Val/binary>>. + -spec encode_connack_code(mqtt_version(), atom()) -> char(). encode_connack_code(?MQTT_VERSION_5, Reason) -> encode_reason_code(Reason); encode_connack_code(_, success) -> 0; @@ -1213,6 +1402,7 @@ encode_connack_code(_, 'bad-user-name-or-password') -> 4; encode_connack_code(_, 'not-authorized') -> 5; encode_connack_code(_, _) -> 128. + -spec encode_reason_code(char() | reason_code()) -> char(). encode_reason_code('success') -> 0; encode_reason_code('normal-disconnection') -> 0; @@ -1261,6 +1451,7 @@ encode_reason_code('subscription-identifiers-not-supported') -> 161; encode_reason_code('wildcard-subscriptions-not-supported') -> 162; encode_reason_code(Code) when is_integer(Code) -> Code. + %%%=================================================================== %%% Formatters %%%=================================================================== @@ -1283,16 +1474,19 @@ pp(disconnect, 2) -> record_info(fields, disconnect); pp(sub_opts, 4) -> record_info(fields, sub_opts); pp(_, _) -> no. + -spec format(io:format(), list()) -> string(). format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). + format_got_expected(Txt, Got, Expected) -> FmtGot = term_format(Got), FmtExp = term_format(Expected), format("~ts: " ++ FmtGot ++ " (expected: " ++ FmtExp ++ ")", [Txt, Got, Expected]). + term_format(I) when is_integer(I) -> "~B"; term_format(B) when is_binary(B) -> @@ -1305,6 +1499,7 @@ term_format(T) -> false -> "~w" end. + %%%=================================================================== %%% Validators %%%=================================================================== @@ -1314,16 +1509,19 @@ assert(Got, Got, _) -> assert(Got, Expected, Reason) -> err({Reason, Got, Expected}). + -spec qos(qos()) -> qos(). -qos(QoS) when is_integer(QoS), QoS>=0, QoS<3 -> +qos(QoS) when is_integer(QoS), QoS >= 0, QoS < 3 -> QoS; qos(QoS) -> err({bad_qos, QoS}). + -spec topic(binary()) -> binary(). topic(Topic) -> topic(Topic, #{}). + -spec topic(binary(), properties()) -> binary(). topic(<<>>, Props) -> case maps:is_key(topic_alias, Props) of @@ -1337,6 +1535,7 @@ topic(Bin, _) when is_binary(Bin) -> topic(_, _) -> err(bad_topic). + -spec topic_filter(binary()) -> binary(). topic_filter(<<>>) -> err(bad_topic_filter); @@ -1347,12 +1546,14 @@ topic_filter(Bin) when is_binary(Bin) -> topic_filter(_) -> err(bad_topic_filter). + -spec utf8(binary()) -> binary(). utf8(Bin) -> ok = check_utf8(Bin), ok = check_zero(Bin), Bin. + -spec check_topic(binary()) -> ok. check_topic(<>) when H == $#; H == $+; H == 0 -> err(bad_topic); @@ -1361,6 +1562,7 @@ check_topic(<<_, T/binary>>) -> check_topic(<<>>) -> ok. + -spec check_topic_filter(binary(), char()) -> ok. check_topic_filter(<<>>, _) -> ok; @@ -1377,15 +1579,17 @@ check_topic_filter(<<0, _/binary>>, _) -> check_topic_filter(<>, _) -> check_topic_filter(T, H). + -spec check_utf8(binary()) -> ok. check_utf8(Bin) -> case unicode:characters_to_binary(Bin, utf8) of - UTF8Str when is_binary(UTF8Str) -> - ok; - _ -> - err(bad_utf8_string) + UTF8Str when is_binary(UTF8Str) -> + ok; + _ -> + err(bad_utf8_string) end. + -spec check_zero(binary()) -> ok. check_zero(<<0, _/binary>>) -> err(bad_utf8_string); @@ -1394,6 +1598,7 @@ check_zero(<<_, T/binary>>) -> check_zero(<<>>) -> ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -1401,10 +1606,12 @@ check_zero(<<>>) -> dec_bool(0) -> false; dec_bool(_) -> true. + -spec enc_bool(boolean()) -> 0..1. enc_bool(true) -> 1; enc_bool(false) -> 0. + -spec err(any()) -> no_return(). err(Reason) -> erlang:error({?MODULE, Reason}). diff --git a/src/node_flat.erl b/src/node_flat.erl index 7093d4beb..33b07588a 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -34,88 +34,123 @@ -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, - remove_extra_items/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, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/7, get_items/3, get_item/7, - get_last_items/3, get_only_item/2, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1, can_fetch_item/2, is_subscribed/1, transform/1]). +-export([init/3, + terminate/2, + options/0, + features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/7, + delete_item/4, + remove_extra_items/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, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, get_items/3, + get_item/7, + get_last_items/3, + get_only_item/2, + get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1, + can_fetch_item/2, + is_subscribed/1, + transform/1]). + init(_Host, _ServerHost, _Opts) -> %pubsub_subscription:init(Host, ServerHost, Opts), - ejabberd_mnesia:create(?MODULE, pubsub_state, - [{disc_copies, [node()]}, {index, [nodeidx]}, - {type, ordered_set}, - {attributes, record_info(fields, pubsub_state)}]), - ejabberd_mnesia:create(?MODULE, pubsub_item, - [{disc_only_copies, [node()]}, {index, [nodeidx]}, - {attributes, record_info(fields, pubsub_item)}]), - ejabberd_mnesia:create(?MODULE, pubsub_orphan, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_orphan)}]), + ejabberd_mnesia:create(?MODULE, + pubsub_state, + [{disc_copies, [node()]}, + {index, [nodeidx]}, + {type, ordered_set}, + {attributes, record_info(fields, pubsub_state)}]), + ejabberd_mnesia:create(?MODULE, + pubsub_item, + [{disc_only_copies, [node()]}, + {index, [nodeidx]}, + {attributes, record_info(fields, pubsub_item)}]), + ejabberd_mnesia:create(?MODULE, + pubsub_orphan, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_orphan)}]), ItemsFields = record_info(fields, pubsub_item), case mnesia:table_info(pubsub_item, attributes) of - ItemsFields -> ok; - _ -> mnesia:transform_table(pubsub_item, ignore, ItemsFields) + ItemsFields -> ok; + _ -> mnesia:transform_table(pubsub_item, ignore, ItemsFields) end, ok. + terminate(_Host, _ServerHost) -> ok. + options() -> [{deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, true}, - {purge_offline, false}, - {persist_items, true}, - {max_items, ?MAXITEMS}, - {subscribe, true}, - {access_model, open}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}, - {itemreply, none}]. + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {purge_offline, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}, + {itemreply, none}]. + features() -> [<<"create-nodes">>, - <<"auto-create">>, - <<"access-authorize">>, - <<"delete-nodes">>, - <<"delete-items">>, - <<"get-pending">>, - <<"instant-nodes">>, - <<"manage-subscriptions">>, - <<"modify-affiliations">>, - <<"outcast-affiliation">>, - <<"persistent-items">>, - <<"multi-items">>, - <<"publish">>, - <<"publish-only-affiliation">>, - <<"publish-options">>, - <<"purge-nodes">>, - <<"retract-items">>, - <<"retrieve-affiliations">>, - <<"retrieve-items">>, - <<"retrieve-subscriptions">>, - <<"subscribe">>, - %%<<"subscription-options">>, - <<"subscription-notifications">>]. + <<"auto-create">>, + <<"access-authorize">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"get-pending">>, + <<"instant-nodes">>, + <<"manage-subscriptions">>, + <<"modify-affiliations">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"multi-items">>, + <<"publish">>, + <<"publish-only-affiliation">>, + <<"publish-options">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + %%<<"subscription-options">>, + <<"subscription-notifications">>]. + %% @doc Checks if the current user has the permission to create the requested node %%

    In flat node, any unused node name is allowed. The access parameter is also @@ -124,34 +159,42 @@ features() -> create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> LOwner = jid:tolower(Owner), Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - acl:match_rule(ServerHost, Access, LOwner) =:= allow - end, + {<<"">>, Host, <<"">>} -> + true; % pubsub service always allowed + _ -> + acl:match_rule(ServerHost, Access, LOwner) =:= allow + end, {result, Allowed}. + create_node(Nidx, Owner) -> OwnerKey = jid:tolower(jid:remove_resource(Owner)), - set_state(#pubsub_state{stateid = {OwnerKey, Nidx}, - nodeidx = Nidx, affiliation = owner}), + set_state(#pubsub_state{ + stateid = {OwnerKey, Nidx}, + nodeidx = Nidx, + affiliation = owner + }), {result, {default, broadcast}}. + delete_node(Nodes) -> - Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> - lists:map(fun (S) -> {J, S} end, Ss) - end, - Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> - {result, States} = get_states(Nidx), - lists:foreach(fun (State) -> - del_items(Nidx, State#pubsub_state.items), - del_state(State#pubsub_state{items = []}) - end, States), - del_orphan_items(Nidx), - {PubsubNode, lists:flatmap(Tr, States)} - end, Nodes), + Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> + lists:map(fun(S) -> {J, S} end, Ss) + end, + Reply = lists:map(fun(#pubsub_node{id = Nidx} = PubsubNode) -> + {result, States} = get_states(Nidx), + lists:foreach(fun(State) -> + del_items(Nidx, State#pubsub_state.items), + del_state(State#pubsub_state{items = []}) + end, + States), + del_orphan_items(Nidx), + {PubsubNode, lists:flatmap(Tr, States)} + end, + Nodes), {result, {default, broadcast, Reply}}. + %% @doc

    Accepts or rejects subcription requests on a PubSub node.

    %%

    The mechanism works as follow: %%

      @@ -183,73 +226,82 @@ delete_node(Nodes) -> %% to completely disable persistence.
    %%

    %%

    In the default plugin module, the record is unchanged.

    -subscribe_node(Nidx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, _Options) -> +subscribe_node(Nidx, + Sender, + Subscriber, + AccessModel, + SendLast, + PresenceSubscription, + RosterGroup, + _Options) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, GenState = get_state(Nidx, GenKey), SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(Nidx, SubKey) - end, + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, Affiliation = GenState#pubsub_state.affiliation, Subscriptions = SubState#pubsub_state.subscriptions, Whitelisted = lists:member(Affiliation, [member, publisher, owner]), - PendingSubscription = lists:any(fun - ({pending, _}) -> true; - (_) -> false - end, - Subscriptions), + PendingSubscription = lists:any(fun({pending, _}) -> true; + (_) -> false + end, + Subscriptions), Owner = Affiliation == owner, - if not Authorized -> - {error, - mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_invalid_jid())}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, xmpp:err_forbidden()}; - PendingSubscription -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_pending_subscription())}; - (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; - (AccessModel == roster) and (not RosterGroup) and (not Owner) -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; - (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> - {error, - mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, xmpp:err_forbidden()}; - true -> - %%SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options), - {NewSub, SubId} = case Subscriptions of - [{subscribed, Id}|_] -> - {subscribed, Id}; - [] -> - Id = pubsub_subscription:make_subid(), - Sub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - set_state(SubState#pubsub_state{subscriptions = - [{Sub, Id} | Subscriptions]}), - {Sub, Id} - end, - case {NewSub, SendLast} of - {subscribed, never} -> - {result, {default, subscribed, SubId}}; - {subscribed, _} -> - {result, {default, subscribed, SubId, send_last}}; - {_, _} -> - {result, {default, pending, SubId}} - end + if + not Authorized -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_invalid_jid())}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + PendingSubscription -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_pending_subscription())}; + (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and (not RosterGroup) and (not Owner) -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> + {error, + mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, xmpp:err_forbidden()}; + true -> + %%SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options), + {NewSub, SubId} = case Subscriptions of + [{subscribed, Id} | _] -> + {subscribed, Id}; + [] -> + Id = pubsub_subscription:make_subid(), + Sub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + set_state(SubState#pubsub_state{ + subscriptions = + [{Sub, Id} | Subscriptions] + }), + {Sub, Id} + end, + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end end. + %% @doc

    Unsubscribe the Subscriber from the Node.

    unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> SubKey = jid:tolower(Subscriber), @@ -257,72 +309,74 @@ unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, GenState = get_state(Nidx, GenKey), SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(Nidx, SubKey) - end, - Subscriptions = lists:filter(fun - ({_Sub, _SubId}) -> true; - (_SubId) -> false - end, - SubState#pubsub_state.subscriptions), + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Subscriptions = lists:filter(fun({_Sub, _SubId}) -> true; + (_SubId) -> false + end, + SubState#pubsub_state.subscriptions), SubIdExists = case SubId of - <<>> -> false; - Binary when is_binary(Binary) -> true; - _ -> false - end, + <<>> -> false; + Binary when is_binary(Binary) -> true; + _ -> false + end, if - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> - {error, xmpp:err_forbidden()}; - %% Entity did not specify SubId - %%SubId == "", ?? -> - %% {error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubId -> - %% {error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - Subscriptions == [] -> - {error, - mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun - ({_, S}) when S == SubId -> true; - (_) -> false - end, - SubState#pubsub_state.subscriptions), - case Sub of - {value, S} -> - delete_subscriptions(SubState, [S]), - {result, default}; - false -> - {error, - mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - delete_subscriptions(SubState, Subscriptions), - {result, default}; - %% No subid supplied, but there's only one matching subscription - length(Subscriptions) == 1 -> - delete_subscriptions(SubState, Subscriptions), - {result, default}; - %% No subid and more than one possible subscription match. - true -> - {error, - mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())} + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, xmpp:err_forbidden()}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, + mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun({_, S}) when S == SubId -> true; + (_) -> false + end, + SubState#pubsub_state.subscriptions), + case Sub of + {value, S} -> + delete_subscriptions(SubState, [S]), + {result, default}; + false -> + {error, + mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + delete_subscriptions(SubState, Subscriptions), + {result, default}; + %% No subid supplied, but there's only one matching subscription + length(Subscriptions) == 1 -> + delete_subscriptions(SubState, Subscriptions), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())} end. + delete_subscriptions(SubState, Subscriptions) -> - NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) -> - %%pubsub_subscription:delete_subscription(SubKey, Nidx, SubId), - Acc -- [{Subscription, SubId}] - end, SubState#pubsub_state.subscriptions, Subscriptions), + NewSubs = lists:foldl(fun({Subscription, SubId}, Acc) -> + %%pubsub_subscription:delete_subscription(SubKey, Nidx, SubId), + Acc -- [{Subscription, SubId}] + end, + SubState#pubsub_state.subscriptions, + Subscriptions), case {SubState#pubsub_state.affiliation, NewSubs} of - {none, []} -> del_state(SubState); - _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) + {none, []} -> del_state(SubState); + _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) end. + %% @doc

    Publishes the item passed as parameter.

    %%

    The mechanism works as follow: %%

      @@ -353,74 +407,88 @@ delete_subscriptions(SubState, Subscriptions) -> %% to completely disable persistence.
    %%

    %%

    In the default plugin module, the record is unchanged.

    -publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, - _PubOpts) -> +publish_item(Nidx, + Publisher, + PublishModel, + MaxItems, + ItemId, + Payload, + _PubOpts) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(Nidx, SubKey) - end, + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, Affiliation = GenState#pubsub_state.affiliation, Subscribed = case PublishModel of - subscribers -> is_subscribed(GenState#pubsub_state.subscriptions) orelse - is_subscribed(SubState#pubsub_state.subscriptions); - _ -> undefined - end, - if not ((PublishModel == open) or - (PublishModel == publishers) and - ((Affiliation == owner) - or (Affiliation == publisher) - or (Affiliation == publish_only)) - or (Subscribed == true)) -> - {error, xmpp:err_forbidden()}; - true -> - if MaxItems > 0; - MaxItems == unlimited -> - Now = erlang:timestamp(), - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> - set_item(OldItem#pubsub_item{ - modification = {Now, SubKey}, - payload = Payload}), - {result, {default, broadcast, []}}; - % Allow node owner to modify any item, he can also delete it and recreate - {result, #pubsub_item{creation = {CreationTime, _}} = OldItem} when Affiliation == owner-> - set_item(OldItem#pubsub_item{ - creation = {CreationTime, GenKey}, - modification = {Now, SubKey}, - payload = Payload}), - {result, {default, broadcast, []}}; - {result, _} -> - {error, xmpp:err_forbidden()}; - _ -> - Items = [ItemId | GenState#pubsub_state.items], - {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), - set_state(GenState#pubsub_state{items = NI}), - set_item(#pubsub_item{ - itemid = {ItemId, Nidx}, - nodeidx = Nidx, - creation = {Now, GenKey}, - modification = {Now, SubKey}, - payload = Payload}), - {result, {default, broadcast, OI}} - end; - true -> - {result, {default, broadcast, []}} - end + subscribers -> + is_subscribed(GenState#pubsub_state.subscriptions) orelse + is_subscribed(SubState#pubsub_state.subscriptions); + _ -> undefined + end, + if + not ((PublishModel == open) or + (PublishModel == publishers) and + ((Affiliation == owner) or + (Affiliation == publisher) or + (Affiliation == publish_only)) or + (Subscribed == true)) -> + {error, xmpp:err_forbidden()}; + true -> + if + MaxItems > 0; + MaxItems == unlimited -> + Now = erlang:timestamp(), + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> + set_item(OldItem#pubsub_item{ + modification = {Now, SubKey}, + payload = Payload + }), + {result, {default, broadcast, []}}; + % Allow node owner to modify any item, he can also delete it and recreate + {result, #pubsub_item{creation = {CreationTime, _}} = OldItem} when Affiliation == owner -> + set_item(OldItem#pubsub_item{ + creation = {CreationTime, GenKey}, + modification = {Now, SubKey}, + payload = Payload + }), + {result, {default, broadcast, []}}; + {result, _} -> + {error, xmpp:err_forbidden()}; + _ -> + Items = [ItemId | GenState#pubsub_state.items], + {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), + set_state(GenState#pubsub_state{items = NI}), + set_item(#pubsub_item{ + itemid = {ItemId, Nidx}, + nodeidx = Nidx, + creation = {Now, GenKey}, + modification = {Now, SubKey}, + payload = Payload + }), + {result, {default, broadcast, OI}} + end; + true -> + {result, {default, broadcast, []}} + end end. + remove_extra_items(Nidx, MaxItems) -> {result, States} = get_states(Nidx), Records = States ++ mnesia:read({pubsub_orphan, Nidx}), ItemIds = lists:flatmap(fun(#pubsub_state{items = Is}) -> - Is; - (#pubsub_orphan{items = Is}) -> - Is - end, Records), + Is; + (#pubsub_orphan{items = Is}) -> + Is + end, + Records), remove_extra_items(Nidx, MaxItems, ItemIds). + %% @doc

    This function is used to remove extra items, most notably when the %% maximum number of items has been reached.

    %%

    This function is used internally by the core PubSub module, as no @@ -439,22 +507,27 @@ 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)), + 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), + 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.

    @@ -464,75 +537,77 @@ delete_item(Nidx, Publisher, PublishModel, ItemId) -> GenState = get_state(Nidx, GenKey), #pubsub_state{affiliation = Affiliation, items = Items} = GenState, Allowed = Affiliation == publisher orelse - Affiliation == owner orelse - (PublishModel == open andalso - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end), - if not Allowed -> - {error, xmpp:err_forbidden()}; - true -> - case lists:member(ItemId, Items) of - true -> - del_item(Nidx, ItemId), - set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), - {result, {default, broadcast}}; - false -> - case Affiliation of - owner -> - {result, States} = get_states(Nidx), - Records = States ++ mnesia:read({pubsub_orphan, Nidx}), - lists:foldl(fun - (#pubsub_state{items = RI} = S, Res) -> - case lists:member(ItemId, RI) of - true -> - NI = lists:delete(ItemId, RI), - del_item(Nidx, ItemId), - mnesia:write(S#pubsub_state{items = NI}), - {result, {default, broadcast}}; - false -> - Res - end; - (#pubsub_orphan{items = RI} = S, Res) -> - case lists:member(ItemId, RI) of - true -> - NI = lists:delete(ItemId, RI), - del_item(Nidx, ItemId), - mnesia:write(S#pubsub_orphan{items = NI}), - {result, {default, broadcast}}; - false -> - Res - end - end, - {error, xmpp:err_item_not_found()}, Records); - _ -> - {error, xmpp:err_forbidden()} - end - end + Affiliation == owner orelse + (PublishModel == open andalso + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end), + if + not Allowed -> + {error, xmpp:err_forbidden()}; + true -> + case lists:member(ItemId, Items) of + true -> + del_item(Nidx, ItemId), + set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), + {result, {default, broadcast}}; + false -> + case Affiliation of + owner -> + {result, States} = get_states(Nidx), + Records = States ++ mnesia:read({pubsub_orphan, Nidx}), + lists:foldl(fun(#pubsub_state{items = RI} = S, Res) -> + case lists:member(ItemId, RI) of + true -> + NI = lists:delete(ItemId, RI), + del_item(Nidx, ItemId), + mnesia:write(S#pubsub_state{items = NI}), + {result, {default, broadcast}}; + false -> + Res + end; + (#pubsub_orphan{items = RI} = S, Res) -> + case lists:member(ItemId, RI) of + true -> + NI = lists:delete(ItemId, RI), + del_item(Nidx, ItemId), + mnesia:write(S#pubsub_orphan{items = NI}), + {result, {default, broadcast}}; + false -> + Res + end + end, + {error, xmpp:err_item_not_found()}, + Records); + _ -> + {error, xmpp:err_forbidden()} + end + end end. + purge_node(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(Nidx), - lists:foreach(fun - (#pubsub_state{items = []}) -> - ok; - (#pubsub_state{items = Items} = S) -> - del_items(Nidx, Items), - set_state(S#pubsub_state{items = []}) - end, - States), - del_orphan_items(Nidx), - {result, {default, broadcast}}; - _ -> - {error, xmpp:err_forbidden()} + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(Nidx), + lists:foreach(fun(#pubsub_state{items = []}) -> + ok; + (#pubsub_state{items = Items} = S) -> + del_items(Nidx, Items), + set_state(S#pubsub_state{items = []}) + end, + States), + del_orphan_items(Nidx), + {result, {default, broadcast}}; + _ -> + {error, xmpp:err_forbidden()} end. + %% @doc

    Return the current affiliations for the given user

    %%

    The default module reads affiliations in the main Mnesia %% pubsub_state table. If a plugin stores its data in the same @@ -545,35 +620,40 @@ get_entity_affiliations(Host, Owner) -> GenKey = jid:remove_resource(SubKey), States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = mod_pubsub:tree(Host), - Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc]; - _ -> Acc - end - end, - [], States), + Reply = lists:foldl(fun(#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc]; + _ -> Acc + end + end, + [], + States), {result, Reply}. + get_node_affiliations(Nidx) -> {result, States} = get_states(Nidx), - Tr = fun (#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end, + Tr = fun(#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end, {result, lists:map(Tr, States)}. + get_affiliation(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), #pubsub_state{affiliation = Affiliation} = get_state(Nidx, GenKey), {result, Affiliation}. + set_affiliation(Nidx, Owner, Affiliation) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), case {Affiliation, GenState#pubsub_state.subscriptions} of - {none, []} -> {result, del_state(GenState)}; - _ -> {result, set_state(GenState#pubsub_state{affiliation = Affiliation})} + {none, []} -> {result, del_state(GenState)}; + _ -> {result, set_state(GenState#pubsub_state{affiliation = Affiliation})} end. + %% @doc

    Return the current subscriptions for the given user

    %%

    The default module reads subscriptions in the main Mnesia %% pubsub_state table. If a plugin stores its data in the same @@ -585,78 +665,86 @@ get_entity_subscriptions(Host, Owner) -> {U, D, _} = SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = case SubKey of - GenKey -> - mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); - _ -> - mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) - ++ - mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) - end, + GenKey -> + mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> + mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) ++ + mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, NodeTree = mod_pubsub:tree(Host), - Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, J} | Acc2] - end, - Acc, Ss); - _ -> - Acc - end - end, - [], States), + Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> + lists:foldl(fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, J} | Acc2] + end, + Acc, + Ss); + _ -> + Acc + end + end, + [], + States), {result, Reply}. + get_node_subscriptions(Nidx) -> {result, States} = get_states(Nidx), - Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> - lists:foldl(fun ({S, SubId}, Acc) -> - [{J, S, SubId} | Acc] - end, - [], Subscriptions) - end, + Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> + lists:foldl(fun({S, SubId}, Acc) -> + [{J, S, SubId} | Acc] + end, + [], + Subscriptions) + end, {result, lists:flatmap(Tr, States)}. + get_subscriptions(Nidx, Owner) -> SubKey = jid:tolower(Owner), SubState = get_state(Nidx, SubKey), {result, SubState#pubsub_state.subscriptions}. + set_subscriptions(Nidx, Owner, Subscription, SubId) -> SubKey = jid:tolower(Owner), SubState = get_state(Nidx, SubKey), case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> - {error, - mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_not_subscribed())}; - _ -> - new_subscription(Nidx, Owner, Subscription, SubState) - end; - {<<>>, [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(SubState, SID); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {<<>>, [_ | _]} -> - {error, - mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())}; - _ -> - case Subscription of - none -> unsub_with_subid(SubState, SubId); - _ -> replace_subscription({Subscription, SubId}, SubState) - end + {_, []} -> + case Subscription of + none -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_not_subscribed())}; + _ -> + new_subscription(Nidx, Owner, Subscription, SubState) + end; + {<<>>, [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(SubState, SID); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {<<>>, [_ | _]} -> + {error, + mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())}; + _ -> + case Subscription of + none -> unsub_with_subid(SubState, SubId); + _ -> replace_subscription({Subscription, SubId}, SubState) + end end. + replace_subscription(NewSub, SubState) -> NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})}. + replace_subscription(_, [], Acc) -> Acc; replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). + new_subscription(_Nidx, _Owner, Sub, SubState) -> %%SubId = pubsub_subscription:add_subscription(Owner, Nidx, []), SubId = pubsub_subscription:make_subid(), @@ -664,55 +752,61 @@ new_subscription(_Nidx, _Owner, Sub, SubState) -> set_state(SubState#pubsub_state{subscriptions = [{Sub, SubId} | Subs]}), {result, {Sub, SubId}}. + unsub_with_subid(SubState, SubId) -> %%pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, Nidx, SubId), - NewSubs = [{S, Sid} - || {S, Sid} <- SubState#pubsub_state.subscriptions, - SubId =/= Sid], + NewSubs = [ {S, Sid} + || {S, Sid} <- SubState#pubsub_state.subscriptions, + SubId =/= Sid ], case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> {result, del_state(SubState)}; - _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} + {[], none} -> {result, del_state(SubState)}; + _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} end. + %% @doc

    Returns a list of Owner's nodes on Host with pending %% subscriptions.

    get_pending_nodes(Host, Owner) -> GenKey = jid:remove_resource(jid:tolower(Owner)), - States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, - affiliation = owner, - _ = '_'}), - NodeIdxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], + States = mnesia:match_object(#pubsub_state{ + stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_' + }), + NodeIdxs = [ Nidx || #pubsub_state{stateid = {_, Nidx}} <- States ], NodeTree = mod_pubsub:tree(Host), - Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> - case lists:member(Nidx, NodeIdxs) of - true -> - case get_nodes_helper(NodeTree, S) of - {value, Node} -> [Node | Acc]; - false -> Acc - end; - false -> - Acc - end - end, - [], pubsub_state), + Reply = mnesia:foldl(fun(#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> + case lists:member(Nidx, NodeIdxs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, + [], + pubsub_state), {result, Reply}. + get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> - HasPending = fun - ({pending, _}) -> true; - (pending) -> true; - (_) -> false - end, + HasPending = fun({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, case lists:any(HasPending, Subs) of - true -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {_, Node}} -> {value, Node}; - _ -> false - end; - false -> - false + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> {value, Node}; + _ -> false + end; + false -> + false end. + %% @doc Returns the list of stored states for a given node. %%

    For the default PubSub module, states are stored in Mnesia database.

    %%

    We can consider that the pubsub_state table have been created by the main @@ -725,41 +819,48 @@ get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs} %% node_default:get_states(Nidx).'''

    get_states(Nidx) -> States = case catch mnesia:index_read(pubsub_state, Nidx, #pubsub_state.nodeidx) of - List when is_list(List) -> List; - _ -> [] - end, + List when is_list(List) -> List; + _ -> [] + end, {result, States}. + %% @doc

    Returns a state (one state list), given its reference.

    get_state(Nidx, Key) -> StateId = {Key, Nidx}, case catch mnesia:read({pubsub_state, StateId}) of - [State] when is_record(State, pubsub_state) -> State; - _ -> #pubsub_state{stateid = StateId, nodeidx = Nidx} + [State] when is_record(State, pubsub_state) -> State; + _ -> #pubsub_state{stateid = StateId, nodeidx = Nidx} end. + %% @doc

    Write a state into database.

    set_state(State) when is_record(State, pubsub_state) -> mnesia:write(State). %set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. + %% @doc

    Delete a state from database.

    del_state(#pubsub_state{stateid = {Key, Nidx}, items = Items}) -> case Items of - [] -> - ok; - _ -> - Orphan = #pubsub_orphan{nodeid = Nidx, items = - case mnesia:read({pubsub_orphan, Nidx}) of - [#pubsub_orphan{items = ItemIds}] -> - lists:usort(ItemIds++Items); - _ -> - Items - end}, - mnesia:write(Orphan) + [] -> + ok; + _ -> + Orphan = #pubsub_orphan{ + nodeid = Nidx, + items = + case mnesia:read({pubsub_orphan, Nidx}) of + [#pubsub_orphan{items = ItemIds}] -> + lists:usort(ItemIds ++ Items); + _ -> + Items + end + }, + mnesia:write(Orphan) end, mnesia:delete({pubsub_state, {Key, Nidx}}). + %% @doc Returns the list of stored items for a given node. %%

    For the default PubSub module, items are stored in Mnesia database.

    %%

    We can consider that the pubsub_item table have been created by the main @@ -768,11 +869,17 @@ del_state(#pubsub_state{stateid = {Key, Nidx}, items = Items}) -> %% relational database), or they can even decide not to persist any items.

    get_items(Nidx, _From, undefined) -> RItems = lists:keysort(#pubsub_item.creation, - mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)), + mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)), {result, {RItems, undefined}}; -get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, - 'after' = After, before = Before}) -> +get_items(Nidx, + _From, + #rsm_set{ + max = Max, + index = IncIndex, + 'after' = After, + before = Before + }) -> case lists:keysort(#pubsub_item.creation, mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)) of [] -> @@ -793,22 +900,27 @@ get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, {_, <<>>, undefined} -> %% 2.5 Requesting the Last Page in a Result Set SubList = lists:reverse(RItems), - {Count-Limit, lists:reverse(lists:sublist(SubList, Limit))}; + {Count - Limit, lists:reverse(lists:sublist(SubList, Limit))}; {_, Stamp, undefined} -> BeforeNow = encode_stamp(Stamp), - {NewIndex, SubList} = extract_sublist(before_now, BeforeNow, - 0, lists:reverse(RItems)), - {Count-NewIndex-Limit, lists:reverse(lists:sublist(SubList, Limit))}; + {NewIndex, SubList} = extract_sublist(before_now, + BeforeNow, + 0, + lists:reverse(RItems)), + {Count - NewIndex - Limit, lists:reverse(lists:sublist(SubList, Limit))}; {_, undefined, Stamp} -> AfterNow = encode_stamp(Stamp), - {NewIndex, SubList} = extract_sublist(after_now, AfterNow, - 0, RItems), + {NewIndex, SubList} = extract_sublist(after_now, + AfterNow, + 0, + RItems), {NewIndex, lists:sublist(SubList, Limit)} end, Rsm = rsm_page(Count, IncIndex, Offset, ItemsPage), {result, {ItemsPage, Rsm}} end. + get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), @@ -818,43 +930,46 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM BareSubscriptions = GenState#pubsub_state.subscriptions, FullSubscriptions = SubState#pubsub_state.subscriptions, Whitelisted = can_fetch_item(Affiliation, BareSubscriptions) orelse - can_fetch_item(Affiliation, FullSubscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, xmpp:err_forbidden()}; - (AccessModel == presence) and not PresenceSubscription -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; - (AccessModel == roster) and not RosterGroup -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; - (AccessModel == authorize) and not Whitelisted -> - {error, xmpp:err_forbidden()}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_items(Nidx, JID, RSM) + can_fetch_item(Affiliation, FullSubscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(Nidx, JID, RSM) end. + extract_sublist(A, Now, Index, [#pubsub_item{creation = {Creation, _}} | RItems]) - when ((A == before_now) and (Creation >= Now)) - or ((A == after_now) and (Creation =< Now)) -> - extract_sublist(A, Now, Index+1, RItems); + when ((A == before_now) and (Creation >= Now)) or + ((A == after_now) and (Creation =< Now)) -> + extract_sublist(A, Now, Index + 1, RItems); extract_sublist(_, _, Index, RItems) -> {Index, RItems}. + get_only_item(Nidx, From) -> get_last_items(Nidx, From, 1). + get_last_items(Nidx, _From, Count) when Count > 0 -> Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), LastItems = lists:reverse(lists:keysort(#pubsub_item.modification, Items)), @@ -862,14 +977,17 @@ get_last_items(Nidx, _From, Count) when Count > 0 -> get_last_items(_Nidx, _From, _Count) -> {result, []}. + %% @doc

    Returns an item (one item list), given its reference.

    + get_item(Nidx, ItemId) -> case mnesia:read({pubsub_item, {ItemId, Nidx}}) of - [Item] when is_record(Item, pubsub_item) -> {result, Item}; - _ -> {error, xmpp:err_item_not_found()} + [Item] when is_record(Item, pubsub_item) -> {result, Item}; + _ -> {error, xmpp:err_item_not_found()} end. + get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), @@ -877,74 +995,82 @@ get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _Sub Affiliation = GenState#pubsub_state.affiliation, Subscriptions = GenState#pubsub_state.subscriptions, Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, xmpp:err_forbidden()}; - (AccessModel == presence) and not PresenceSubscription -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; - (AccessModel == roster) and not RosterGroup -> - {error, - mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; - (AccessModel == authorize) and not Whitelisted -> - {error, xmpp:err_forbidden()}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_item(Nidx, ItemId) + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, + mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(Nidx, ItemId) end. + %% @doc

    Write an item into database.

    set_item(Item) when is_record(Item, pubsub_item) -> mnesia:write(Item). %set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. + %% @doc

    Delete an item from database.

    del_item(Nidx, ItemId) -> mnesia:delete({pubsub_item, {ItemId, Nidx}}). + del_items(Nidx, ItemIds) -> - lists:foreach(fun (ItemId) -> del_item(Nidx, ItemId) - end, - ItemIds). + lists:foreach(fun(ItemId) -> del_item(Nidx, ItemId) + end, + ItemIds). + del_orphan_items(Nidx) -> case mnesia:read({pubsub_orphan, Nidx}) of - [#pubsub_orphan{items = ItemIds}] -> - del_items(Nidx, ItemIds), - mnesia:delete({pubsub_orphan, Nidx}); - _ -> - ok + [#pubsub_orphan{items = ItemIds}] -> + del_items(Nidx, ItemIds), + mnesia:delete({pubsub_orphan, Nidx}); + _ -> + ok end. + get_item_name(_Host, _Node, Id) -> {result, Id}. + %% @doc

    Return the path of the node. In flat it's just node id.

    node_to_path(Node) -> {result, [Node]}. + path_to_node(Path) -> {result, case Path of - %% default slot - [Node] -> iolist_to_binary(Node); - %% handle old possible entries, used when migrating database content to new format - [Node | _] when is_binary(Node) -> - iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); - %% default case (used by PEP for example) - _ -> iolist_to_binary(Path) + %% default slot + [Node] -> iolist_to_binary(Node); + %% handle old possible entries, used when migrating database content to new format + [Node | _] when is_binary(Node) -> + iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); + %% default case (used by PEP for example) + _ -> iolist_to_binary(Path) end}. + can_fetch_item(owner, _) -> true; can_fetch_item(member, _) -> true; can_fetch_item(publisher, _) -> true; @@ -953,21 +1079,23 @@ can_fetch_item(outcast, _) -> false; can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). %can_fetch_item(_Affiliation, _Subscription) -> false. + is_subscribed(Subscriptions) -> - lists:any(fun - ({subscribed, _SubId}) -> true; - (_) -> false - end, - Subscriptions). + lists:any(fun({subscribed, _SubId}) -> true; + (_) -> false + end, + Subscriptions). + first_in_list(_Pred, []) -> false; first_in_list(Pred, [H | T]) -> case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) + true -> {value, H}; + _ -> first_in_list(Pred, T) end. + rsm_page(Count, _, _, []) -> #rsm_set{count = Count}; rsm_page(Count, Index, Offset, Items) -> @@ -975,18 +1103,27 @@ rsm_page(Count, Index, Offset, Items) -> LastItem = lists:last(Items), First = decode_stamp(element(1, FirstItem#pubsub_item.creation)), Last = decode_stamp(element(1, LastItem#pubsub_item.creation)), - #rsm_set{count = Count, index = Index, - first = #rsm_first{index = Offset, data = First}, - last = Last}. + #rsm_set{ + count = Count, + index = Index, + first = #rsm_first{index = Offset, data = First}, + last = Last + }. + encode_stamp(Stamp) -> - try xmpp_util:decode_timestamp(Stamp) - catch _:{bad_timestamp, _} -> - Stamp % We should return a proper error to the client instead. + try + xmpp_util:decode_timestamp(Stamp) + catch + _:{bad_timestamp, _} -> + Stamp % We should return a proper error to the client instead. end. + + decode_stamp(Stamp) -> xmpp_util:encode_timestamp(Stamp). + transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) -> {pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss}; transform({pubsub_item, {Id, Nidx}, C, M, P}) -> diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index acfdf3331..6a85f9882 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -33,49 +33,81 @@ -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). - -include("pubsub.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("ejabberd_sql_pt.hrl"). -include("translate.hrl"). --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, purge_node/2, - subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, - remove_extra_items/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, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/7, get_items/3, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1, - get_entity_subscriptions_for_send_last/2, get_last_items/3, - get_only_item/2]). +-export([init/3, + terminate/2, + options/0, + features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/7, + delete_item/4, + remove_extra_items/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, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, get_items/3, + get_item/7, get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1, + get_entity_subscriptions_for_send_last/2, + get_last_items/3, + get_only_item/2]). + +-export([decode_jid/1, + encode_jid/1, + encode_jid_like/1, + decode_affiliation/1, + decode_subscriptions/1, + encode_affiliation/1, + encode_subscriptions/1, + encode_host/1, + encode_host_like/1]). --export([decode_jid/1, encode_jid/1, encode_jid_like/1, - decode_affiliation/1, decode_subscriptions/1, - encode_affiliation/1, encode_subscriptions/1, - encode_host/1, encode_host_like/1]). init(_Host, _ServerHost, _Opts) -> %%pubsub_subscription_sql:init(Host, ServerHost, Opts), ok. + terminate(_Host, _ServerHost) -> ok. + options() -> [{sql, true}, {rsm, true} | node_flat:options()]. + features() -> [<<"rsm">> | node_flat:features()]. + create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + create_node(Nidx, Owner) -> {_U, _S, _R} = OwnerKey = jid:tolower(jid:remove_resource(Owner)), J = encode_jid(OwnerKey), @@ -83,207 +115,232 @@ create_node(Nidx, Owner) -> S = encode_subscriptions([]), ejabberd_sql:sql_query_t( ?SQL("insert into pubsub_state(" - "nodeid, jid, affiliation, subscriptions) " - "values (%(Nidx)d, %(J)s, %(A)s, %(S)s)")), + "nodeid, jid, affiliation, subscriptions) " + "values (%(Nidx)d, %(J)s, %(A)s, %(S)s)")), {result, {default, broadcast}}. + delete_node(Nodes) -> Reply = lists:map( - fun(#pubsub_node{id = Nidx} = PubsubNode) -> - Subscriptions = - case ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s, @(subscriptions)s " - "from pubsub_state where nodeid=%(Nidx)d")) of - {selected, RItems} -> - [{decode_jid(SJID), decode_subscriptions(Subs)} - || {SJID, Subs} <- RItems]; - _ -> - [] - end, - {PubsubNode, Subscriptions} - end, Nodes), + fun(#pubsub_node{id = Nidx} = PubsubNode) -> + Subscriptions = + case ejabberd_sql:sql_query_t( + ?SQL("select @(jid)s, @(subscriptions)s " + "from pubsub_state where nodeid=%(Nidx)d")) of + {selected, RItems} -> + [ {decode_jid(SJID), decode_subscriptions(Subs)} + || {SJID, Subs} <- RItems ]; + _ -> + [] + end, + {PubsubNode, Subscriptions} + end, + Nodes), {result, {default, broadcast, Reply}}. -subscribe_node(Nidx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, _Options) -> + +subscribe_node(Nidx, + Sender, + Subscriber, + AccessModel, + SendLast, + PresenceSubscription, + RosterGroup, + _Options) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Whitelisted = lists:member(Affiliation, [member, publisher, owner]), - PendingSubscription = lists:any(fun - ({pending, _}) -> true; - (_) -> false - end, - Subscriptions), + PendingSubscription = lists:any(fun({pending, _}) -> true; + (_) -> false + end, + Subscriptions), Owner = Affiliation == owner, - if not Authorized -> - {error, mod_pubsub:extended_error( - xmpp:err_bad_request(), mod_pubsub:err_invalid_jid())}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, xmpp:err_forbidden()}; - PendingSubscription -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_pending_subscription())}; - (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_presence_subscription_required())}; - (AccessModel == roster) and (not RosterGroup) and (not Owner) -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_not_in_roster_group())}; - (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> - {error, mod_pubsub:extended_error( - xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, ?ERR_FORBIDDEN}; - true -> - %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Subscriber, Nidx, Options), - {NewSub, SubId} = case Subscriptions of - [{subscribed, Id}|_] -> - {subscribed, Id}; - [] -> - Id = pubsub_subscription_sql:make_subid(), - Sub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - update_subscription(Nidx, SubKey, [{Sub, Id} | Subscriptions]), - {Sub, Id} - end, - case {NewSub, SendLast} of - {subscribed, never} -> - {result, {default, subscribed, SubId}}; - {subscribed, _} -> - {result, {default, subscribed, SubId, send_last}}; - {_, _} -> - {result, {default, pending, SubId}} - end + if + not Authorized -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), mod_pubsub:err_invalid_jid())}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + PendingSubscription -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_pending_subscription())}; + (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and (not RosterGroup) and (not Owner) -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> + {error, mod_pubsub:extended_error( + xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Subscriber, Nidx, Options), + {NewSub, SubId} = case Subscriptions of + [{subscribed, Id} | _] -> + {subscribed, Id}; + [] -> + Id = pubsub_subscription_sql:make_subid(), + Sub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + update_subscription(Nidx, SubKey, [{Sub, Id} | Subscriptions]), + {Sub, Id} + end, + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end end. + unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, SubKey), SubIdExists = case SubId of - <<>> -> false; - Binary when is_binary(Binary) -> true; - _ -> false - end, + <<>> -> false; + Binary when is_binary(Binary) -> true; + _ -> false + end, if - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> - {error, xmpp:err_forbidden()}; - %% Entity did not specify SubId - %%SubId == "", ?? -> - %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubId -> - %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - Subscriptions == [] -> - {error, mod_pubsub:extended_error( - xmpp:err_unexpected_request(), - mod_pubsub:err_not_subscribed())}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun - ({_, S}) when S == SubId -> true; - (_) -> false - end, - Subscriptions), - case Sub of - {value, S} -> - delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions), - {result, default}; - false -> - {error, mod_pubsub:extended_error( - xmpp:err_unexpected_request(), - mod_pubsub:err_not_subscribed())} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - [delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions) - || S <- Subscriptions], - {result, default}; - %% No subid supplied, but there's only one matching subscription - length(Subscriptions) == 1 -> - delete_subscription(SubKey, Nidx, hd(Subscriptions), Affiliation, Subscriptions), - {result, default}; - %% No subid and more than one possible subscription match. - true -> - {error, mod_pubsub:extended_error( - xmpp:err_bad_request(), mod_pubsub:err_subid_required())} + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, xmpp:err_forbidden()}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, mod_pubsub:extended_error( + xmpp:err_unexpected_request(), + mod_pubsub:err_not_subscribed())}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun({_, S}) when S == SubId -> true; + (_) -> false + end, + Subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions), + {result, default}; + false -> + {error, mod_pubsub:extended_error( + xmpp:err_unexpected_request(), + mod_pubsub:err_not_subscribed())} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + [ delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions) + || S <- Subscriptions ], + {result, default}; + %% No subid supplied, but there's only one matching subscription + length(Subscriptions) == 1 -> + delete_subscription(SubKey, Nidx, hd(Subscriptions), Affiliation, Subscriptions), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), mod_pubsub:err_subid_required())} end. + delete_subscription(SubKey, Nidx, {Subscription, SubId}, Affiliation, Subscriptions) -> NewSubs = Subscriptions -- [{Subscription, SubId}], %%pubsub_subscription_sql:unsubscribe_node(SubKey, Nidx, SubId), case {Affiliation, NewSubs} of - {none, []} -> del_state(Nidx, SubKey); - _ -> update_subscription(Nidx, SubKey, NewSubs) + {none, []} -> del_state(Nidx, SubKey); + _ -> update_subscription(Nidx, SubKey, NewSubs) end. -publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, - _PubOpts) -> + +publish_item(Nidx, + Publisher, + PublishModel, + MaxItems, + ItemId, + Payload, + _PubOpts) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Subscribed = case PublishModel of - subscribers -> node_flat:is_subscribed(Subscriptions); - _ -> undefined - end, - if not ((PublishModel == open) or - (PublishModel == publishers) and - ((Affiliation == owner) - or (Affiliation == publisher) - or (Affiliation == publish_only)) - or (Subscribed == true)) -> - {error, xmpp:err_forbidden()}; - true -> - if MaxItems > 0; - MaxItems == unlimited -> - Now = erlang:timestamp(), - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> - set_item(OldItem#pubsub_item{ - modification = {Now, SubKey}, - payload = Payload}), - {result, {default, broadcast, []}}; - % Allow node owner to modify any item, he can also delete it and recreate - {result, #pubsub_item{creation = {CreationTime, _}} = OldItem} when Affiliation == owner-> - set_item(OldItem#pubsub_item{ - creation = {CreationTime, GenKey}, - modification = {Now, SubKey}, - payload = Payload}), - {result, {default, broadcast, []}}; - {result, _} -> - {error, xmpp:err_forbidden()}; - _ -> - OldIds = maybe_remove_extra_items(Nidx, MaxItems, - GenKey, ItemId), - set_item(#pubsub_item{ - itemid = {ItemId, Nidx}, - creation = {Now, GenKey}, - modification = {Now, SubKey}, - payload = Payload}), - {result, {default, broadcast, OldIds}} - end; - true -> - {result, {default, broadcast, []}} - end + subscribers -> node_flat:is_subscribed(Subscriptions); + _ -> undefined + end, + if + not ((PublishModel == open) or + (PublishModel == publishers) and + ((Affiliation == owner) or + (Affiliation == publisher) or + (Affiliation == publish_only)) or + (Subscribed == true)) -> + {error, xmpp:err_forbidden()}; + true -> + if + MaxItems > 0; + MaxItems == unlimited -> + Now = erlang:timestamp(), + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> + set_item(OldItem#pubsub_item{ + modification = {Now, SubKey}, + payload = Payload + }), + {result, {default, broadcast, []}}; + % Allow node owner to modify any item, he can also delete it and recreate + {result, #pubsub_item{creation = {CreationTime, _}} = OldItem} when Affiliation == owner -> + set_item(OldItem#pubsub_item{ + creation = {CreationTime, GenKey}, + modification = {Now, SubKey}, + payload = Payload + }), + {result, {default, broadcast, []}}; + {result, _} -> + {error, xmpp:err_forbidden()}; + _ -> + OldIds = maybe_remove_extra_items(Nidx, + MaxItems, + GenKey, + ItemId), + set_item(#pubsub_item{ + itemid = {ItemId, Nidx}, + creation = {Now, GenKey}, + modification = {Now, SubKey}, + payload = Payload + }), + {result, {default, broadcast, OldIds}} + end; + true -> + {result, {default, broadcast, []}} + end end. + remove_extra_items(Nidx, MaxItems) -> remove_extra_items(Nidx, MaxItems, itemids(Nidx)). + remove_extra_items(_Nidx, unlimited, ItemIds) -> {result, {ItemIds, []}}; remove_extra_items(Nidx, MaxItems, ItemIds) -> @@ -292,74 +349,78 @@ 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))), + 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, []} + ?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), {result, Affiliation} = get_affiliation(Nidx, GenKey), Allowed = Affiliation == publisher orelse - Affiliation == owner orelse - (PublishModel == open andalso - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end), - if not Allowed -> - {error, xmpp:err_forbidden()}; - true -> - Items = itemids(Nidx, GenKey), - case lists:member(ItemId, Items) of - true -> - case del_item(Nidx, ItemId) of - {updated, 1} -> {result, {default, broadcast}}; - _ -> {error, xmpp:err_item_not_found()} - end; - false -> - case Affiliation of - owner -> - case del_item(Nidx, ItemId) of - {updated, 1} -> {result, {default, broadcast}}; - _ -> {error, xmpp:err_item_not_found()} - end; - _ -> - {error, xmpp:err_forbidden()} - end - end + Affiliation == owner orelse + (PublishModel == open andalso + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end), + if + not Allowed -> + {error, xmpp:err_forbidden()}; + true -> + Items = itemids(Nidx, GenKey), + case lists:member(ItemId, Items) of + true -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, xmpp:err_item_not_found()} + end; + false -> + case Affiliation of + owner -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, xmpp:err_item_not_found()} + end; + _ -> + {error, xmpp:err_forbidden()} + end + end end. + purge_node(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(Nidx), - lists:foreach(fun - (#pubsub_state{items = []}) -> ok; - (#pubsub_state{items = Items}) -> del_items(Nidx, Items) - end, - States), - {result, {default, broadcast}}; - _ -> - {error, xmpp:err_forbidden()} + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(Nidx), + lists:foreach(fun(#pubsub_state{items = []}) -> ok; + (#pubsub_state{items = Items}) -> del_items(Nidx, Items) + end, + States), + {result, {default, broadcast}}; + _ -> + {error, xmpp:err_forbidden()} end. + get_entity_affiliations(Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), @@ -367,91 +428,100 @@ get_entity_affiliations(Host, Owner) -> J = encode_jid(GenKey), {result, case ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(affiliation)s " - "from pubsub_state i, pubsub_node n where " - "i.nodeid = n.nodeid and jid=%(J)s and host=%(H)s")) of - {selected, RItems} -> - [{nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), - decode_affiliation(A)} || {N, T, I, A} <- RItems]; - _ -> - [] + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(affiliation)s " + "from pubsub_state i, pubsub_node n where " + "i.nodeid = n.nodeid and jid=%(J)s and host=%(H)s")) of + {selected, RItems} -> + [ {nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), + decode_affiliation(A)} || {N, T, I, A} <- RItems ]; + _ -> + [] end}. + get_node_affiliations(Nidx) -> {result, case ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s, @(affiliation)s from pubsub_state " - "where nodeid=%(Nidx)d")) of - {selected, RItems} -> - [{decode_jid(J), decode_affiliation(A)} || {J, A} <- RItems]; - _ -> - [] + ?SQL("select @(jid)s, @(affiliation)s from pubsub_state " + "where nodeid=%(Nidx)d")) of + {selected, RItems} -> + [ {decode_jid(J), decode_affiliation(A)} || {J, A} <- RItems ]; + _ -> + [] end}. + get_affiliation(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), J = encode_jid(GenKey), {result, case ejabberd_sql:sql_query_t( - ?SQL("select @(affiliation)s from pubsub_state " - "where nodeid=%(Nidx)d and jid=%(J)s")) of - {selected, [{A}]} -> - decode_affiliation(A); - _ -> - none + ?SQL("select @(affiliation)s from pubsub_state " + "where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{A}]} -> + decode_affiliation(A); + _ -> + none end}. + set_affiliation(Nidx, Owner, Affiliation) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), {_, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey), case {Affiliation, Subscriptions} of - {none, []} -> {result, del_state(Nidx, GenKey)}; - _ -> {result, update_affiliation(Nidx, GenKey, Affiliation)} + {none, []} -> {result, del_state(Nidx, GenKey)}; + _ -> {result, update_affiliation(Nidx, GenKey, Affiliation)} end. + get_entity_subscriptions(Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), H = encode_host(Host), GJ = encode_jid(GenKey), Query = case SubKey of - GenKey -> - GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, - ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and " - "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); - _ -> - SJ = encode_jid(SubKey), - ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and " - "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") - end, + GenKey -> + GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); + _ -> + SJ = encode_jid(SubKey), + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") + end, {result, case ejabberd_sql:sql_query_t(Query) of - {selected, RItems} -> - lists:foldl( - fun({N, T, I, J, S}, Acc) -> - Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), - Jid = decode_jid(J), - lists:foldl( - fun({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid} | Acc2] - end, Acc, decode_subscriptions(S)) - end, [], RItems); - _ -> - [] + {selected, RItems} -> + lists:foldl( + fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), + Jid = decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, + Acc, + decode_subscriptions(S)) + end, + [], + RItems); + _ -> + [] end}. + -spec get_entity_subscriptions_for_send_last(Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) -> - {result, [{mod_pubsub:pubsubNode(), - mod_pubsub:subscription(), - mod_pubsub:subId(), - ljid()}]}. + Owner :: jid()) -> + {result, [{mod_pubsub:pubsubNode(), + mod_pubsub:subscription(), + mod_pubsub:subId(), + ljid()}]}. get_entity_subscriptions_for_send_last(Host, Owner) -> SubKey = jid:tolower(Owner), @@ -459,105 +529,119 @@ get_entity_subscriptions_for_send_last(Host, Owner) -> H = encode_host(Host), GJ = encode_jid(GenKey), Query = case SubKey of - GenKey -> - GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, - ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and val='on_sub_and_presence' and " - "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); - _ -> - SJ = encode_jid(SubKey), - ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and val='on_sub_and_presence' and " - "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") - end, + GenKey -> + GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); + _ -> + SJ = encode_jid(SubKey), + ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") + end, {result, case ejabberd_sql:sql_query_t(Query) of - {selected, RItems} -> - lists:foldl( - fun ({N, T, I, J, S}, Acc) -> - Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), - Jid = decode_jid(J), - lists:foldl( - fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid}| Acc2] - end, Acc, decode_subscriptions(S)) - end, [], RItems); - _ -> - [] + {selected, RItems} -> + lists:foldl( + fun({N, T, I, J, S}, Acc) -> + Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), + Jid = decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, + Acc, + decode_subscriptions(S)) + end, + [], + RItems); + _ -> + [] end}. + get_node_subscriptions(Nidx) -> {result, case ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s, @(subscriptions)s from pubsub_state " - "where nodeid=%(Nidx)d")) of - {selected, RItems} -> - lists:foldl( - fun ({J, S}, Acc) -> - Jid = decode_jid(J), - lists:foldl( - fun ({Sub, SubId}, Acc2) -> - [{Jid, Sub, SubId} | Acc2] - end, Acc, decode_subscriptions(S)) - end, [], RItems); - _ -> - [] + ?SQL("select @(jid)s, @(subscriptions)s from pubsub_state " + "where nodeid=%(Nidx)d")) of + {selected, RItems} -> + lists:foldl( + fun({J, S}, Acc) -> + Jid = decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Jid, Sub, SubId} | Acc2] + end, + Acc, + decode_subscriptions(S)) + end, + [], + RItems); + _ -> + [] end}. + get_subscriptions(Nidx, Owner) -> SubKey = jid:tolower(Owner), J = encode_jid(SubKey), {result, case ejabberd_sql:sql_query_t( - ?SQL("select @(subscriptions)s from pubsub_state" - " where nodeid=%(Nidx)d and jid=%(J)s")) of - {selected, [{S}]} -> - decode_subscriptions(S); - _ -> - [] + ?SQL("select @(subscriptions)s from pubsub_state" + " where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{S}]} -> + decode_subscriptions(S); + _ -> + [] end}. + set_subscriptions(Nidx, Owner, Subscription, SubId) -> SubKey = jid:tolower(Owner), SubState = get_state_without_itemids(Nidx, SubKey), case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> - {error, mod_pubsub:extended_error( - xmpp:err_bad_request(), - mod_pubsub:err_not_subscribed())}; - _ -> - new_subscription(Nidx, Owner, Subscription, SubState) - end; - {<<>>, [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(Nidx, SID, SubState); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {<<>>, [_ | _]} -> - {error, mod_pubsub:extended_error( - xmpp:err_bad_request(), - mod_pubsub:err_subid_required())}; - _ -> - case Subscription of - none -> unsub_with_subid(Nidx, SubId, SubState); - _ -> replace_subscription({Subscription, SubId}, SubState) - end + {_, []} -> + case Subscription of + none -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), + mod_pubsub:err_not_subscribed())}; + _ -> + new_subscription(Nidx, Owner, Subscription, SubState) + end; + {<<>>, [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(Nidx, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {<<>>, [_ | _]} -> + {error, mod_pubsub:extended_error( + xmpp:err_bad_request(), + mod_pubsub:err_subid_required())}; + _ -> + case Subscription of + none -> unsub_with_subid(Nidx, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end end. + replace_subscription(NewSub, SubState) -> NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})}. + replace_subscription(_, [], Acc) -> Acc; replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). + new_subscription(_Nidx, _Owner, Subscription, SubState) -> %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Owner, Nidx, []), SubId = pubsub_subscription_sql:make_subid(), @@ -565,97 +649,110 @@ new_subscription(_Nidx, _Owner, Subscription, SubState) -> set_state(SubState#pubsub_state{subscriptions = Subscriptions}), {result, {Subscription, SubId}}. + unsub_with_subid(Nidx, SubId, SubState) -> %%pubsub_subscription_sql:unsubscribe_node(SubState#pubsub_state.stateid, Nidx, SubId), - NewSubs = [{S, Sid} - || {S, Sid} <- SubState#pubsub_state.subscriptions, - SubId =/= Sid], + NewSubs = [ {S, Sid} + || {S, Sid} <- SubState#pubsub_state.subscriptions, + SubId =/= Sid ], case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> {result, del_state(Nidx, element(1, SubState#pubsub_state.stateid))}; - _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} + {[], none} -> {result, del_state(Nidx, element(1, SubState#pubsub_state.stateid))}; + _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} end. + get_pending_nodes(Host, Owner) -> GenKey = encode_jid(jid:remove_resource(jid:tolower(Owner))), PendingIdxs = case ejabberd_sql:sql_query_t( ?SQL("select @(nodeid)d from pubsub_state " "where subscriptions like '%p%' and affiliation='o'" "and jid=%(GenKey)s")) of - {selected, RItems} -> - [Nidx || {Nidx} <- RItems]; - _ -> - [] - end, + {selected, RItems} -> + [ Nidx || {Nidx} <- RItems ]; + _ -> + [] + end, NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun(Nidx, Acc) -> - case NodeTree:get_node(Nidx) of - #pubsub_node{nodeid = {_, Node}} -> [Node | Acc]; - _ -> Acc - end + case NodeTree:get_node(Nidx) of + #pubsub_node{nodeid = {_, Node}} -> [Node | Acc]; + _ -> Acc + end end, - [], PendingIdxs), + [], + PendingIdxs), {result, Reply}. + get_states(Nidx) -> case ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " - "from pubsub_state where nodeid=%(Nidx)d")) of - {selected, RItems} -> - {result, - lists:map( - fun({SJID, Aff, Subs}) -> - JID = decode_jid(SJID), - #pubsub_state{stateid = {JID, Nidx}, - nodeidx = Nidx, - items = itemids(Nidx, JID), - affiliation = decode_affiliation(Aff), - subscriptions = decode_subscriptions(Subs)} - end, RItems)}; - _ -> - {result, []} + ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " + "from pubsub_state where nodeid=%(Nidx)d")) of + {selected, RItems} -> + {result, + lists:map( + fun({SJID, Aff, Subs}) -> + JID = decode_jid(SJID), + #pubsub_state{ + stateid = {JID, Nidx}, + nodeidx = Nidx, + items = itemids(Nidx, JID), + affiliation = decode_affiliation(Aff), + subscriptions = decode_subscriptions(Subs) + } + end, + RItems)}; + _ -> + {result, []} end. + get_state(Nidx, JID) -> State = get_state_without_itemids(Nidx, JID), {SJID, _} = State#pubsub_state.stateid, State#pubsub_state{items = itemids(Nidx, SJID)}. + -spec get_state_without_itemids(Nidx :: mod_pubsub:nodeIdx(), Key :: ljid()) -> - mod_pubsub:pubsubState(). + mod_pubsub:pubsubState(). get_state_without_itemids(Nidx, JID) -> J = encode_jid(JID), case ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " - "from pubsub_state " - "where nodeid=%(Nidx)d and jid=%(J)s")) of - {selected, [{SJID, Aff, Subs}]} -> - #pubsub_state{stateid = {decode_jid(SJID), Nidx}, - nodeidx = Nidx, - affiliation = decode_affiliation(Aff), - subscriptions = decode_subscriptions(Subs)}; - _ -> - #pubsub_state{stateid = {JID, Nidx}, nodeidx = Nidx} + ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " + "from pubsub_state " + "where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{SJID, Aff, Subs}]} -> + #pubsub_state{ + stateid = {decode_jid(SJID), Nidx}, + nodeidx = Nidx, + affiliation = decode_affiliation(Aff), + subscriptions = decode_subscriptions(Subs) + }; + _ -> + #pubsub_state{stateid = {JID, Nidx}, nodeidx = Nidx} end. + set_state(State) -> {_, Nidx} = State#pubsub_state.stateid, set_state(Nidx, State). + set_state(Nidx, State) -> {JID, _} = State#pubsub_state.stateid, J = encode_jid(JID), S = encode_subscriptions(State#pubsub_state.subscriptions), A = encode_affiliation(State#pubsub_state.affiliation), ?SQL_UPSERT_T( - "pubsub_state", - ["!nodeid=%(Nidx)d", - "!jid=%(J)s", - "affiliation=%(A)s", - "subscriptions=%(S)s" - ]), + "pubsub_state", + ["!nodeid=%(Nidx)d", + "!jid=%(J)s", + "affiliation=%(A)s", + "subscriptions=%(S)s"]), ok. + del_state(Nidx, JID) -> J = encode_jid(JID), catch ejabberd_sql:sql_query_t( @@ -663,222 +760,261 @@ del_state(Nidx, JID) -> " where jid=%(J)s and nodeid=%(Nidx)d")), ok. + get_items(Nidx, _From, undefined) -> SNidx = misc:i2l(Nidx), case ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload", - " from pubsub_item where nodeid='", SNidx/binary, "'", - " order by creation asc">>]) of - {selected, _, AllItems} -> - {result, {[raw_to_item(Nidx, RItem) || RItem <- AllItems], undefined}}; - _ -> - {result, {[], undefined}} + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'", + " order by creation asc">>]) of + {selected, _, AllItems} -> + {result, {[ raw_to_item(Nidx, RItem) || RItem <- AllItems ], undefined}}; + _ -> + {result, {[], undefined}} end; -get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, - 'after' = After, before = Before}) -> +get_items(Nidx, + _From, + #rsm_set{ + max = Max, + index = IncIndex, + 'after' = After, + before = Before + }) -> Count = case catch ejabberd_sql:sql_query_t( - ?SQL("select @(count(itemid))d from pubsub_item" - " where nodeid=%(Nidx)d")) of - {selected, [{C}]} -> C; - _ -> 0 - end, + ?SQL("select @(count(itemid))d from pubsub_item" + " where nodeid=%(Nidx)d")) of + {selected, [{C}]} -> C; + _ -> 0 + end, Offset = case {IncIndex, Before, After} of - {I, undefined, undefined} when is_integer(I) -> I; - _ -> 0 - end, + {I, undefined, undefined} when is_integer(I) -> I; + _ -> 0 + end, Limit = case Max of - undefined -> ?MAXITEMS; - _ -> Max - end, + undefined -> ?MAXITEMS; + _ -> Max + end, Filters = rsm_filters(misc:i2l(Nidx), Before, After), Query = fun(mssql, _) -> - ejabberd_sql:sql_query_t( - [<<"select top ", (integer_to_binary(Limit))/binary, - " itemid, publisher, creation, modification, payload", - " from pubsub_item", Filters/binary>>]); - %OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; - (_, _) -> - ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload", - " from pubsub_item", Filters/binary, - " limit ", (integer_to_binary(Limit))/binary, - " offset ", (integer_to_binary(Offset))/binary>>]) - end, + ejabberd_sql:sql_query_t( + [<<"select top ", + (integer_to_binary(Limit))/binary, + " itemid, publisher, creation, modification, payload", + " from pubsub_item", + Filters/binary>>]); + %OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item", + Filters/binary, + " limit ", + (integer_to_binary(Limit))/binary, + " offset ", + (integer_to_binary(Offset))/binary>>]) + end, case ejabberd_sql:sql_query_t(Query) of - {selected, _, []} -> - {result, {[], #rsm_set{count = Count}}}; - {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], RItems} -> - Rsm = rsm_page(Count, IncIndex, Offset, RItems), - {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}}; - _ -> - {result, {[], undefined}} + {selected, _, []} -> + {result, {[], #rsm_set{count = Count}}}; + {selected, [<<"itemid">>, + <<"publisher">>, + <<"creation">>, + <<"modification">>, + <<"payload">>], + RItems} -> + Rsm = rsm_page(Count, IncIndex, Offset, RItems), + {result, {[ raw_to_item(Nidx, RItem) || RItem <- RItems ], Rsm}}; + _ -> + {result, {[], undefined}} end. + get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, xmpp:err_forbidden()}; - (AccessModel == presence) and not PresenceSubscription -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_presence_subscription_required())}; - (AccessModel == roster) and not RosterGroup -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_not_in_roster_group())}; - (AccessModel == whitelist) and not Whitelisted -> - {error, mod_pubsub:extended_error( - xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; - (AccessModel == authorize) and not Whitelisted -> - {error, xmpp:err_forbidden()}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_items(Nidx, JID, RSM) + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, mod_pubsub:extended_error( + xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(Nidx, JID, RSM) end. + get_last_items(Nidx, _From, Limit) -> SNidx = misc:i2l(Nidx), Query = fun(mssql, _) -> - ejabberd_sql:sql_query_t( - [<<"select top ", (integer_to_binary(Limit))/binary, - " itemid, publisher, creation, modification, payload", - " from pubsub_item where nodeid='", SNidx/binary, - "' order by modification desc">>]); - (_, _) -> - ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload", - " from pubsub_item where nodeid='", SNidx/binary, - "' order by modification desc ", - " limit ", (integer_to_binary(Limit))/binary>>]) - end, + ejabberd_sql:sql_query_t( + [<<"select top ", + (integer_to_binary(Limit))/binary, + " itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", + SNidx/binary, + "' order by modification desc">>]); + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", + SNidx/binary, + "' order by modification desc ", + " limit ", + (integer_to_binary(Limit))/binary>>]) + end, case catch ejabberd_sql:sql_query_t(Query) of - {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], RItems} -> - {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; - _ -> - {result, []} + {selected, [<<"itemid">>, + <<"publisher">>, + <<"creation">>, + <<"modification">>, + <<"payload">>], + RItems} -> + {result, [ raw_to_item(Nidx, RItem) || RItem <- RItems ]}; + _ -> + {result, []} end. + get_only_item(Nidx, _From) -> SNidx = misc:i2l(Nidx), Query = fun(mssql, _) -> - ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload", - " from pubsub_item where nodeid='", SNidx/binary, "'">>]); - (_, _) -> - ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload", - " from pubsub_item where nodeid='", SNidx/binary, "'">>]) - end, + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'">>]); + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'">>]) + end, case catch ejabberd_sql:sql_query_t(Query) of - {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, - <<"modification">>, <<"payload">>], RItems} -> - {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; - _ -> - {result, []} + {selected, [<<"itemid">>, + <<"publisher">>, + <<"creation">>, + <<"modification">>, + <<"payload">>], + RItems} -> + {result, [ raw_to_item(Nidx, RItem) || RItem <- RItems ]}; + _ -> + {result, []} end. + get_item(Nidx, ItemId) -> case catch ejabberd_sql:sql_query_t( - ?SQL("select @(itemid)s, @(publisher)s, @(creation)s," - " @(modification)s, @(payload)s from pubsub_item" - " where nodeid=%(Nidx)d and itemid=%(ItemId)s")) - of - {selected, [RItem]} -> - {result, raw_to_item(Nidx, RItem)}; - {selected, []} -> - {error, xmpp:err_item_not_found()}; - {'EXIT', _} -> - {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} + ?SQL("select @(itemid)s, @(publisher)s, @(creation)s," + " @(modification)s, @(payload)s from pubsub_item" + " where nodeid=%(Nidx)d and itemid=%(ItemId)s")) of + {selected, [RItem]} -> + {result, raw_to_item(Nidx, RItem)}; + {selected, []} -> + {error, xmpp:err_item_not_found()}; + {'EXIT', _} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} end. + get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, xmpp:err_forbidden()}; - (AccessModel == presence) and not PresenceSubscription -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_presence_subscription_required())}; - (AccessModel == roster) and not RosterGroup -> - {error, mod_pubsub:extended_error( - xmpp:err_not_authorized(), - mod_pubsub:err_not_in_roster_group())}; - (AccessModel == whitelist) and not Whitelisted -> - {error, mod_pubsub:extended_error( - xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; - (AccessModel == authorize) and not Whitelisted -> - {error, xmpp:err_forbidden()}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_item(Nidx, ItemId) + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, xmpp:err_forbidden()}; + (AccessModel == presence) and not PresenceSubscription -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_presence_subscription_required())}; + (AccessModel == roster) and not RosterGroup -> + {error, mod_pubsub:extended_error( + xmpp:err_not_authorized(), + mod_pubsub:err_not_in_roster_group())}; + (AccessModel == whitelist) and not Whitelisted -> + {error, mod_pubsub:extended_error( + xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; + (AccessModel == authorize) and not Whitelisted -> + {error, xmpp:err_forbidden()}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(Nidx, ItemId) end. + set_item(Item) -> {ItemId, Nidx} = Item#pubsub_item.itemid, {C, _} = Item#pubsub_item.creation, {M, JID} = Item#pubsub_item.modification, P = encode_jid(JID), Payload = Item#pubsub_item.payload, - XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), + XML = str:join([ fxml:element_to_binary(X) || X <- Payload ], <<>>), SM = encode_now(M), SC = encode_now(C), ?SQL_UPSERT_T( - "pubsub_item", - ["!nodeid=%(Nidx)d", - "!itemid=%(ItemId)s", - "publisher=%(P)s", - "modification=%(SM)s", - "payload=%(XML)s", - "-creation=%(SC)s" - ]), + "pubsub_item", + ["!nodeid=%(Nidx)d", + "!itemid=%(ItemId)s", + "publisher=%(P)s", + "modification=%(SM)s", + "payload=%(XML)s", + "-creation=%(SC)s"]), ok. + del_item(Nidx, ItemId) -> catch ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_item where itemid=%(ItemId)s" " and nodeid=%(Nidx)d")). + del_items(_, []) -> ok; del_items(Nidx, [ItemId]) -> del_item(Nidx, ItemId); del_items(Nidx, ItemIds) -> - I = str:join([ejabberd_sql:to_string_literal_t(X) || X <- ItemIds], <<",">>), + I = str:join([ ejabberd_sql:to_string_literal_t(X) || X <- ItemIds ], <<",">>), SNidx = misc:i2l(Nidx), - catch - ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>, - I, <<") and nodeid='">>, SNidx, <<"';">>]). + catch ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>, + I, + <<") and nodeid='">>, + SNidx, + <<"';">>]). + get_item_name(_Host, _Node, Id) -> {result, Id}. + node_to_path(Node) -> node_flat:node_to_path(Node). + path_to_node(Path) -> node_flat:path_to_node(Path). @@ -887,96 +1023,97 @@ first_in_list(_Pred, []) -> false; first_in_list(Pred, [H | T]) -> case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) + true -> {value, H}; + _ -> first_in_list(Pred, T) end. + itemids(Nidx) -> - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(itemid)s from pubsub_item where " - "nodeid=%(Nidx)d order by modification desc")) - of - {selected, RItems} -> - [ItemId || {ItemId} <- RItems]; - _ -> - [] + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s from pubsub_item where " + "nodeid=%(Nidx)d order by modification desc")) of + {selected, RItems} -> + [ ItemId || {ItemId} <- RItems ]; + _ -> + [] end. + itemids(Nidx, {_U, _S, _R} = JID) -> SJID = encode_jid(JID), SJIDLike = <<(encode_jid_like(JID))/binary, "/%">>, - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(itemid)s from pubsub_item where " - "nodeid=%(Nidx)d and (publisher=%(SJID)s" - " or publisher like %(SJIDLike)s %ESCAPE) " - "order by modification desc")) - of - {selected, RItems} -> - [ItemId || {ItemId} <- RItems]; - _ -> - [] + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s from pubsub_item where " + "nodeid=%(Nidx)d and (publisher=%(SJID)s" + " or publisher like %(SJIDLike)s %ESCAPE) " + "order by modification desc")) of + {selected, RItems} -> + [ ItemId || {ItemId} <- RItems ]; + _ -> + [] end. + select_affiliation_subscriptions(Nidx, JID) -> J = encode_jid(JID), - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(affiliation)s, @(subscriptions)s from " - " pubsub_state where nodeid=%(Nidx)d and jid=%(J)s")) - of - {selected, [{A, S}]} -> - {decode_affiliation(A), decode_subscriptions(S)}; - _ -> - {none, []} + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(affiliation)s, @(subscriptions)s from " + " pubsub_state where nodeid=%(Nidx)d and jid=%(J)s")) of + {selected, [{A, S}]} -> + {decode_affiliation(A), decode_subscriptions(S)}; + _ -> + {none, []} end. + select_affiliation_subscriptions(Nidx, JID, JID) -> select_affiliation_subscriptions(Nidx, JID); select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> GJ = encode_jid(GenKey), SJ = encode_jid(SubKey), case catch ejabberd_sql:sql_query_t( - ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s from " - " pubsub_state where nodeid=%(Nidx)d and jid in (%(GJ)s, %(SJ)s)")) - of - {selected, Res} -> - lists:foldr( - fun({Jid, A, S}, {_, Subs}) when Jid == GJ -> - {decode_affiliation(A), Subs ++ decode_subscriptions(S)}; - ({_, _, S}, {Aff, Subs}) -> - {Aff, Subs ++ decode_subscriptions(S)} - end, {none, []}, Res); - _ -> - {none, []} + ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s from " + " pubsub_state where nodeid=%(Nidx)d and jid in (%(GJ)s, %(SJ)s)")) of + {selected, Res} -> + lists:foldr( + fun({Jid, A, S}, {_, Subs}) when Jid == GJ -> + {decode_affiliation(A), Subs ++ decode_subscriptions(S)}; + ({_, _, S}, {Aff, Subs}) -> + {Aff, Subs ++ decode_subscriptions(S)} + end, + {none, []}, + Res); + _ -> + {none, []} end. + update_affiliation(Nidx, JID, Affiliation) -> J = encode_jid(JID), A = encode_affiliation(Affiliation), ?SQL_UPSERT_T( - "pubsub_state", - ["!nodeid=%(Nidx)d", - "!jid=%(J)s", - "affiliation=%(A)s", - "-subscriptions=''" - ]). + "pubsub_state", + ["!nodeid=%(Nidx)d", + "!jid=%(J)s", + "affiliation=%(A)s", + "-subscriptions=''"]). + update_subscription(Nidx, JID, Subscription) -> J = encode_jid(JID), S = encode_subscriptions(Subscription), ?SQL_UPSERT_T( - "pubsub_state", - ["!nodeid=%(Nidx)d", - "!jid=%(J)s", - "subscriptions=%(S)s", - "-affiliation='n'" - ]). + "pubsub_state", + ["!nodeid=%(Nidx)d", + "!jid=%(J)s", + "subscriptions=%(S)s", + "-affiliation='n'"]). + -spec maybe_remove_extra_items(mod_pubsub:nodeIdx(), - non_neg_integer() | unlimited, ljid(), - mod_pubsub:itemId()) -> [mod_pubsub:itemId()]. + non_neg_integer() | unlimited, + ljid(), + mod_pubsub:itemId()) -> [mod_pubsub:itemId()]. maybe_remove_extra_items(_Nidx, unlimited, _GenKey, _ItemId) -> []; maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) -> @@ -984,10 +1121,12 @@ maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) -> {result, {_NewIds, OldIds}} = remove_extra_items(Nidx, MaxItems, ItemIds), OldIds. + -spec decode_jid(SJID :: binary()) -> ljid(). decode_jid(SJID) -> jid:tolower(jid:decode(SJID)). + -spec decode_affiliation(Arg :: binary()) -> atom(). decode_affiliation(<<"o">>) -> owner; decode_affiliation(<<"p">>) -> publisher; @@ -996,39 +1135,47 @@ decode_affiliation(<<"m">>) -> member; decode_affiliation(<<"c">>) -> outcast; decode_affiliation(_) -> none. + -spec decode_subscription(Arg :: binary()) -> atom(). decode_subscription(<<"s">>) -> subscribed; decode_subscription(<<"p">>) -> pending; decode_subscription(<<"u">>) -> unconfigured; decode_subscription(_) -> none. --spec decode_subscriptions(Subscriptions :: binary()) -> [] | [{atom(), binary()},...]. + +-spec decode_subscriptions(Subscriptions :: binary()) -> [] | [{atom(), binary()}, ...]. decode_subscriptions(Subscriptions) -> - lists:foldl(fun (Subscription, Acc) -> - case str:tokens(Subscription, <<":">>) of - [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; - _ -> Acc - end - end, - [], str:tokens(Subscriptions, <<",">>)). + lists:foldl(fun(Subscription, Acc) -> + case str:tokens(Subscription, <<":">>) of + [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; + _ -> Acc + end + end, + [], + str:tokens(Subscriptions, <<",">>)). + -spec encode_jid(JID :: ljid()) -> binary(). encode_jid(JID) -> jid:encode(JID). + -spec encode_jid_like(JID :: ljid()) -> binary(). encode_jid_like(JID) -> ejabberd_sql:escape_like_arg(jid:encode(JID)). + -spec encode_host(Host :: host()) -> binary(). encode_host({_U, _S, _R} = LJID) -> encode_jid(LJID); encode_host(Host) -> Host. + -spec encode_host_like(Host :: host()) -> binary(). encode_host_like({_U, _S, _R} = LJID) -> encode_jid_like(LJID); encode_host_like(Host) -> ejabberd_sql:escape_like_arg(Host). + -spec encode_affiliation(Arg :: atom()) -> binary(). encode_affiliation(owner) -> <<"o">>; encode_affiliation(publisher) -> <<"p">>; @@ -1037,70 +1184,99 @@ encode_affiliation(member) -> <<"m">>; encode_affiliation(outcast) -> <<"c">>; encode_affiliation(_) -> <<"n">>. + -spec encode_subscription(Arg :: atom()) -> binary(). encode_subscription(subscribed) -> <<"s">>; encode_subscription(pending) -> <<"p">>; encode_subscription(unconfigured) -> <<"u">>; encode_subscription(_) -> <<"n">>. --spec encode_subscriptions(Subscriptions :: [] | [{atom(), binary()},...]) -> binary(). + +-spec encode_subscriptions(Subscriptions :: [] | [{atom(), binary()}, ...]) -> binary(). encode_subscriptions(Subscriptions) -> - str:join([<<(encode_subscription(S))/binary, ":", SubId/binary>> - || {S, SubId} <- Subscriptions], <<",">>). + str:join([ <<(encode_subscription(S))/binary, ":", SubId/binary>> + || {S, SubId} <- Subscriptions ], + <<",">>). + %%% record getter/setter + raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}); raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) -> JID = decode_jid(SJID), Payload = case fxml_stream:parse_element(XML) of - {error, _Reason} -> []; - El -> [El] - end, - #pubsub_item{itemid = {ItemId, Nidx}, - nodeidx = Nidx, - creation = {decode_now(Creation), jid:remove_resource(JID)}, - modification = {decode_now(Modification), JID}, - payload = Payload}. + {error, _Reason} -> []; + El -> [El] + end, + #pubsub_item{ + itemid = {ItemId, Nidx}, + nodeidx = Nidx, + creation = {decode_now(Creation), jid:remove_resource(JID)}, + modification = {decode_now(Modification), JID}, + payload = Payload + }. + rsm_filters(SNidx, undefined, undefined) -> <<" where nodeid='", SNidx/binary, "'", " order by creation asc">>; -rsm_filters(SNidx, undefined, After) -> - <<" where nodeid='", SNidx/binary, "'", - " and creation>'", (encode_stamp(After))/binary, "'", +rsm_filters(SNidx, undefined, After) -> + <<" where nodeid='", + SNidx/binary, + "'", + " and creation>'", + (encode_stamp(After))/binary, + "'", " order by creation asc">>; rsm_filters(SNidx, <<>>, undefined) -> %% 2.5 Requesting the Last Page in a Result Set <<" where nodeid='", SNidx/binary, "'", " order by creation desc">>; rsm_filters(SNidx, Before, undefined) -> - <<" where nodeid='", SNidx/binary, "'", - " and creation<'", (encode_stamp(Before))/binary, "'", + <<" where nodeid='", + SNidx/binary, + "'", + " and creation<'", + (encode_stamp(Before))/binary, + "'", " order by creation desc">>. + rsm_page(Count, Index, Offset, Items) -> First = decode_stamp(lists:nth(3, hd(Items))), Last = decode_stamp(lists:nth(3, lists:last(Items))), - #rsm_set{count = Count, index = Index, - first = #rsm_first{index = Offset, data = First}, - last = Last}. + #rsm_set{ + count = Count, + index = Index, + first = #rsm_first{index = Offset, data = First}, + last = Last + }. + encode_stamp(Stamp) -> try xmpp_util:decode_timestamp(Stamp) of - Now -> - encode_now(Now) - catch _:{bad_timestamp, _} -> - Stamp % We should return a proper error to the client instead. + Now -> + encode_now(Now) + catch + _:{bad_timestamp, _} -> + Stamp % We should return a proper error to the client instead. end. + + decode_stamp(Stamp) -> xmpp_util:encode_timestamp(decode_now(Stamp)). + encode_now({T1, T2, T3}) -> - <<(misc:i2l(T1, 6))/binary, ":", - (misc:i2l(T2, 6))/binary, ":", + <<(misc:i2l(T1, 6))/binary, + ":", + (misc:i2l(T2, 6))/binary, + ":", (misc:i2l(T3, 6))/binary>>. + + decode_now(NowStr) -> [MS, S, US] = binary:split(NowStr, <<":">>, [global]), {binary_to_integer(MS), binary_to_integer(S), binary_to_integer(US)}. diff --git a/src/node_pep.erl b/src/node_pep.erl index 3d208c73b..377b8ba27 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -34,234 +34,321 @@ -include("pubsub.hrl"). --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, - remove_extra_items/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, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/7, get_items/3, get_item/7, - get_last_items/3, get_only_item/2, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1, depends/3]). +-export([init/3, + terminate/2, + options/0, + features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/7, + delete_item/4, + remove_extra_items/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, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, get_items/3, + get_item/7, + get_last_items/3, + get_only_item/2, + get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1, + depends/3]). + depends(_Host, _ServerHost, _Opts) -> [{mod_caps, hard}]. + init(Host, ServerHost, Opts) -> node_flat:init(Host, ServerHost, Opts), ok. + terminate(Host, ServerHost) -> node_flat:terminate(Host, ServerHost), ok. + options() -> [{deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, false}, - {purge_offline, false}, - {persist_items, true}, - {max_items, 1}, - {subscribe, true}, - {access_model, presence}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, true}, - {itemreply, none}]. + {notify_config, false}, + {notify_delete, false}, + {notify_retract, false}, + {purge_offline, false}, + {persist_items, true}, + {max_items, 1}, + {subscribe, true}, + {access_model, presence}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, true}, + {itemreply, none}]. + features() -> [<<"create-nodes">>, - <<"auto-create">>, - <<"auto-subscribe">>, - <<"config-node">>, - <<"config-node-max">>, - <<"delete-nodes">>, - <<"delete-items">>, - <<"filtered-notifications">>, - <<"item-ids">>, - <<"modify-affiliations">>, - <<"multi-items">>, - <<"outcast-affiliation">>, - <<"persistent-items">>, - <<"publish">>, - <<"publish-options">>, - <<"purge-nodes">>, - <<"retract-items">>, - <<"retrieve-affiliations">>, - <<"retrieve-items">>, - <<"retrieve-subscriptions">>, - <<"subscribe">>]. + <<"auto-create">>, + <<"auto-subscribe">>, + <<"config-node">>, + <<"config-node-max">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"filtered-notifications">>, + <<"item-ids">>, + <<"modify-affiliations">>, + <<"multi-items">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"publish-options">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>]. + create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> LOwner = jid:tolower(Owner), {User, Server, _Resource} = LOwner, Allowed = case LOwner of - {<<"">>, Host, <<"">>} -> - true; % pubsub service always allowed - _ -> - case acl:match_rule(ServerHost, Access, LOwner) of - allow -> - case Host of - {User, Server, _} -> true; - _ -> false - end; - _ -> - false - end - end, + {<<"">>, Host, <<"">>} -> + true; % pubsub service always allowed + _ -> + case acl:match_rule(ServerHost, Access, LOwner) of + allow -> + case Host of + {User, Server, _} -> true; + _ -> false + end; + _ -> + false + end + end, {result, Allowed}. + create_node(Nidx, Owner) -> node_flat:create_node(Nidx, Owner). + delete_node(Nodes) -> node_flat:delete_node(Nodes). -subscribe_node(Nidx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup, Options). + +subscribe_node(Nidx, + Sender, + Subscriber, + AccessModel, + SendLast, + PresenceSubscription, + RosterGroup, + Options) -> + node_flat:subscribe_node(Nidx, + Sender, + Subscriber, + AccessModel, + SendLast, + PresenceSubscription, + RosterGroup, + Options). + unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> case node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of - {error, Error} -> {error, Error}; - {result, _} -> {result, default} + {error, Error} -> {error, Error}; + {result, _} -> {result, default} end. + publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> - node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, - Payload, PubOpts). + node_flat:publish_item(Nidx, + Publisher, + Model, + MaxItems, + ItemId, + Payload, + PubOpts). + remove_extra_items(Nidx, MaxItems) -> node_flat:remove_extra_items(Nidx, MaxItems). + remove_extra_items(Nidx, MaxItems, ItemIds) -> node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). + remove_expired_items(Nidx, Seconds) -> node_flat:remove_expired_items(Nidx, Seconds). + delete_item(Nidx, Publisher, PublishModel, ItemId) -> node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). + purge_node(Nidx, Owner) -> node_flat:purge_node(Nidx, Owner). + get_entity_affiliations(Host, Owner) -> {_, D, _} = SubKey = jid:tolower(Owner), SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = mod_pubsub:tree(Host), - Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> [{Node, A} | Acc]; - _ -> Acc - end - end, - [], States), + Reply = lists:foldl(fun(#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> [{Node, A} | Acc]; + _ -> Acc + end + end, + [], + States), {result, Reply}. get_node_affiliations(Nidx) -> node_flat:get_node_affiliations(Nidx). + get_affiliation(Nidx, Owner) -> node_flat:get_affiliation(Nidx, Owner). + set_affiliation(Nidx, Owner, Affiliation) -> node_flat:set_affiliation(Nidx, Owner, Affiliation). + get_entity_subscriptions(Host, Owner) -> {U, D, _} = SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = case SubKey of - GenKey -> - mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); - _ -> - mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) - ++ - mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) - end, + GenKey -> + mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> + mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) ++ + mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, NodeTree = mod_pubsub:tree(Host), - Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> - lists:foldl(fun - ({subscribed, SubId}, Acc2) -> - [{Node, subscribed, SubId, J} | Acc2]; - ({pending, _SubId}, Acc2) -> - [{Node, pending, J} | Acc2]; - (S, Acc2) -> - [{Node, S, J} | Acc2] - end, - Acc, Ss); - _ -> - Acc - end - end, - [], States), + Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> + lists:foldl(fun({subscribed, SubId}, Acc2) -> + [{Node, subscribed, SubId, J} | Acc2]; + ({pending, _SubId}, Acc2) -> + [{Node, pending, J} | Acc2]; + (S, Acc2) -> + [{Node, S, J} | Acc2] + end, + Acc, + Ss); + _ -> + Acc + end + end, + [], + States), {result, Reply}. + get_node_subscriptions(Nidx) -> node_flat:get_node_subscriptions(Nidx). + get_subscriptions(Nidx, Owner) -> node_flat:get_subscriptions(Nidx, Owner). + set_subscriptions(Nidx, Owner, Subscription, SubId) -> node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). + get_pending_nodes(Host, Owner) -> node_flat:get_pending_nodes(Host, Owner). + get_states(Nidx) -> node_flat:get_states(Nidx). + get_state(Nidx, JID) -> node_flat:get_state(Nidx, JID). + set_state(State) -> node_flat:set_state(State). + get_items(Nidx, From, RSM) -> node_flat:get_items(Nidx, From, RSM). + get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_flat:get_items(Nidx, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM). + node_flat:get_items(Nidx, + JID, + AccessModel, + PresenceSubscription, + RosterGroup, + SubId, + RSM). + get_last_items(Nidx, From, Count) -> node_flat:get_last_items(Nidx, From, Count). + get_only_item(Nidx, From) -> node_flat:get_only_item(Nidx, From). + get_item(Nidx, ItemId) -> node_flat:get_item(Nidx, ItemId). + get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_flat:get_item(Nidx, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). + node_flat:get_item(Nidx, + ItemId, + JID, + AccessModel, + PresenceSubscription, + RosterGroup, + SubId). + set_item(Item) -> node_flat:set_item(Item). + get_item_name(Host, Node, Id) -> node_flat:get_item_name(Host, Node, Id). + node_to_path(Node) -> node_flat:node_to_path(Node). + path_to_node(Path) -> node_flat:path_to_node(Path). diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index 5d35c7bfe..858f89690 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -30,220 +30,313 @@ -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). - -include("pubsub.hrl"). -include("ejabberd_sql_pt.hrl"). --export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, - remove_extra_items/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, - get_subscriptions/2, set_subscriptions/4, - get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/7, get_items/3, get_item/7, - get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1, depends/3, - get_entity_subscriptions_for_send_last/2, get_last_items/3, - get_only_item/2]). +-export([init/3, + terminate/2, + options/0, + features/0, + create_node_permission/6, + create_node/2, + delete_node/1, + purge_node/2, + subscribe_node/8, + unsubscribe_node/4, + publish_item/7, + delete_item/4, + remove_extra_items/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, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, + get_states/1, + get_state/2, + set_state/1, + get_items/7, get_items/3, + get_item/7, get_item/2, + set_item/1, + get_item_name/3, + node_to_path/1, + path_to_node/1, + depends/3, + get_entity_subscriptions_for_send_last/2, + get_last_items/3, + get_only_item/2]). + depends(_Host, _ServerHost, _Opts) -> [{mod_caps, hard}]. + init(Host, ServerHost, Opts) -> node_flat_sql:init(Host, ServerHost, Opts), ok. + terminate(Host, ServerHost) -> node_flat_sql:terminate(Host, ServerHost), ok. + options() -> [{sql, true}, {rsm, true} | node_pep:options()]. + features() -> [<<"rsm">> | node_pep:features()]. + create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_pep:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + create_node(Nidx, Owner) -> node_flat_sql:create_node(Nidx, Owner), {result, {default, broadcast}}. + delete_node(Nodes) -> node_flat_sql:delete_node(Nodes). -subscribe_node(Nidx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_flat_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup, Options). + +subscribe_node(Nidx, + Sender, + Subscriber, + AccessModel, + SendLast, + PresenceSubscription, + RosterGroup, + Options) -> + node_flat_sql:subscribe_node(Nidx, + Sender, + Subscriber, + AccessModel, + SendLast, + PresenceSubscription, + RosterGroup, + Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> case node_flat_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of - {error, Error} -> {error, Error}; - {result, _} -> {result, default} + {error, Error} -> {error, Error}; + {result, _} -> {result, default} end. + publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> - node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, - Payload, PubOpts). + node_flat_sql:publish_item(Nidx, + Publisher, + Model, + MaxItems, + ItemId, + Payload, + PubOpts). + remove_extra_items(Nidx, MaxItems) -> node_flat_sql:remove_extra_items(Nidx, MaxItems). + remove_extra_items(Nidx, MaxItems, ItemIds) -> node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). + remove_expired_items(Nidx, Seconds) -> node_flat_sql:remove_expired_items(Nidx, Seconds). + delete_item(Nidx, Publisher, PublishModel, ItemId) -> node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). + purge_node(Nidx, Owner) -> node_flat_sql:purge_node(Nidx, Owner). + get_entity_affiliations(_Host, Owner) -> OwnerKey = jid:tolower(jid:remove_resource(Owner)), node_flat_sql:get_entity_affiliations(OwnerKey, Owner). + get_node_affiliations(Nidx) -> node_flat_sql:get_node_affiliations(Nidx). + get_affiliation(Nidx, Owner) -> node_flat_sql:get_affiliation(Nidx, Owner). + set_affiliation(Nidx, Owner, Affiliation) -> node_flat_sql:set_affiliation(Nidx, Owner, Affiliation). + get_entity_subscriptions(_Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), HLike = <<"%@", (node_flat_sql:encode_host_like(element(2, SubKey)))/binary>>, GJ = node_flat_sql:encode_jid(GenKey), Query = case SubKey of - GenKey -> - GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, - ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and " - "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); - _ -> - SJ = node_flat_sql:encode_jid(SubKey), - ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and " - "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") - end, + GenKey -> + GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); + _ -> + SJ = node_flat_sql:encode_jid(SubKey), + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and " + "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") + end, {result, case ejabberd_sql:sql_query_t(Query) of - {selected, RItems} -> - lists:foldl( - fun({H, N, T, I, J, S}, Acc) -> - O = node_flat_sql:decode_jid(H), - Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), - Jid = node_flat_sql:decode_jid(J), - lists:foldl( - fun({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid} | Acc2] - end, Acc, node_flat_sql:decode_subscriptions(S)) - end, [], RItems); - _ -> - [] + {selected, RItems} -> + lists:foldl( + fun({H, N, T, I, J, S}, Acc) -> + O = node_flat_sql:decode_jid(H), + Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), + Jid = node_flat_sql:decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, + Acc, + node_flat_sql:decode_subscriptions(S)) + end, + [], + RItems); + _ -> + [] end}. + get_entity_subscriptions_for_send_last(_Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), HLike = <<"%@", (node_flat_sql:encode_host_like(element(2, SubKey)))/binary>>, GJ = node_flat_sql:encode_jid(GenKey), Query = case SubKey of - GenKey -> - GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, - ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and val='on_sub_and_presence' and " - "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); - _ -> - SJ = node_flat_sql:encode_jid(SubKey), - ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " - "name='send_last_published_item' and val='on_sub_and_presence' and " - "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") - end, + GenKey -> + GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); + _ -> + SJ = node_flat_sql:encode_jid(SubKey), + ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " + "name='send_last_published_item' and val='on_sub_and_presence' and " + "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") + end, {result, case ejabberd_sql:sql_query_t(Query) of - {selected, RItems} -> - lists:foldl( - fun ({H, N, T, I, J, S}, Acc) -> - O = node_flat_sql:decode_jid(H), - Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), - Jid = node_flat_sql:decode_jid(J), - lists:foldl( - fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid}| Acc2] - end, Acc, node_flat_sql:decode_subscriptions(S)) - end, [], RItems); - _ -> - [] + {selected, RItems} -> + lists:foldl( + fun({H, N, T, I, J, S}, Acc) -> + O = node_flat_sql:decode_jid(H), + Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), + Jid = node_flat_sql:decode_jid(J), + lists:foldl( + fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, + Acc, + node_flat_sql:decode_subscriptions(S)) + end, + [], + RItems); + _ -> + [] end}. + get_node_subscriptions(Nidx) -> node_flat_sql:get_node_subscriptions(Nidx). + get_subscriptions(Nidx, Owner) -> node_flat_sql:get_subscriptions(Nidx, Owner). + set_subscriptions(Nidx, Owner, Subscription, SubId) -> node_flat_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). + get_pending_nodes(Host, Owner) -> node_flat_sql:get_pending_nodes(Host, Owner). + get_states(Nidx) -> node_flat_sql:get_states(Nidx). + get_state(Nidx, JID) -> node_flat_sql:get_state(Nidx, JID). + set_state(State) -> node_flat_sql:set_state(State). + get_items(Nidx, From, RSM) -> node_flat_sql:get_items(Nidx, From, RSM). + get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_flat_sql:get_items(Nidx, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM). + node_flat_sql:get_items(Nidx, + JID, + AccessModel, + PresenceSubscription, + RosterGroup, + SubId, + RSM). + get_last_items(Nidx, JID, Count) -> node_flat_sql:get_last_items(Nidx, JID, Count). + get_only_item(Nidx, JID) -> node_flat_sql:get_only_item(Nidx, JID). + get_item(Nidx, ItemId) -> node_flat_sql:get_item(Nidx, ItemId). + get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_flat_sql:get_item(Nidx, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). + node_flat_sql:get_item(Nidx, + ItemId, + JID, + AccessModel, + PresenceSubscription, + RosterGroup, + SubId). + set_item(Item) -> node_flat_sql:set_item(Item). + get_item_name(Host, Node, Id) -> node_flat_sql:get_item_name(Host, Node, Id). + node_to_path(Node) -> node_flat_sql:node_to_path(Node). + path_to_node(Path) -> node_flat_sql:path_to_node(Path). diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl index facb4fd74..4ae328d82 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -41,219 +41,266 @@ -include_lib("stdlib/include/ms_transform.hrl"). -include("pubsub.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). --export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_all_nodes/1, - get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, get_node/2, get_node/1, + get_nodes/2, get_nodes/1, + get_all_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/6, + delete_node/2]). + init(_Host, _ServerHost, _Options) -> - ejabberd_mnesia:create(?MODULE, pubsub_node, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_node)}, - {index, [id]}]), + ejabberd_mnesia:create(?MODULE, + pubsub_node, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_node)}, + {index, [id]}]), %% mnesia:transform_table(pubsub_state, ignore, StatesFields) ok. + terminate(_Host, _ServerHost) -> ok. + options() -> [{virtual_tree, false}]. + set_node(Node) when is_record(Node, pubsub_node) -> mnesia:write(Node). + get_node(Host, Node, _From) -> get_node(Host, Node). + get_node(Host, Node) -> case mnesia:read({pubsub_node, {Host, Node}}) of - [#pubsub_node{} = Record] -> fixup_node(Record); - _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} + [#pubsub_node{} = Record] -> fixup_node(Record); + _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. + get_node(Nidx) -> case mnesia:index_read(pubsub_node, Nidx, #pubsub_node.id) of - [#pubsub_node{} = Record] -> fixup_node(Record); - _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} + [#pubsub_node{} = Record] -> fixup_node(Record); + _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. + get_nodes(Host) -> get_nodes(Host, infinity). + get_nodes(Host, infinity) -> Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}), - [fixup_node(N) || N <- Nodes]; + [ fixup_node(N) || N <- Nodes ]; get_nodes(Host, Limit) -> case mnesia:select( - pubsub_node, - ets:fun2ms( - fun(#pubsub_node{nodeid = {H, _}} = Node) when H == Host -> - Node - end), Limit, read) of - '$end_of_table' -> []; - {Nodes, _} -> [fixup_node(N) || N <- Nodes] + pubsub_node, + ets:fun2ms( + fun(#pubsub_node{nodeid = {H, _}} = Node) when H == Host -> + Node + end), + Limit, + read) of + '$end_of_table' -> []; + {Nodes, _} -> [ fixup_node(N) || N <- Nodes ] end. + get_all_nodes({_U, _S, _R} = Owner) -> Host = jid:tolower(jid:remove_resource(Owner)), Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}), - [fixup_node(N) || N <- Nodes]; + [ fixup_node(N) || N <- Nodes ]; get_all_nodes(Host) -> - Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) - ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'}, - _ = '_'}), - [fixup_node(N) || N <- Nodes]. + Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) ++ + mnesia:match_object(#pubsub_node{ + nodeid = {{'_', Host, '_'}, '_'}, + _ = '_' + }), + [ fixup_node(N) || N <- Nodes ]. + get_parentnodes(Host, Node, _From) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of - [Record] when is_record(Record, pubsub_node) -> - Record#pubsub_node.parents; - _ -> - [] + [Record] when is_record(Record, pubsub_node) -> + Record#pubsub_node.parents; + _ -> + [] end. + get_parentnodes_tree(Host, Node, _From) -> get_parentnodes_tree(Host, Node, 0, []). + + get_parentnodes_tree(Host, Node, Level, Acc) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of - [#pubsub_node{} = Record0] -> - Record = fixup_node(Record0), - Tree = [{Level, [Record]}|Acc], - case Record#pubsub_node.parents of - [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); - _ -> Tree - end; - _ -> - Acc + [#pubsub_node{} = Record0] -> + Record = fixup_node(Record0), + Tree = [{Level, [Record]} | Acc], + case Record#pubsub_node.parents of + [Parent] -> get_parentnodes_tree(Host, Parent, Level + 1, Tree); + _ -> Tree + end; + _ -> + Acc end. + get_subnodes(Host, <<>>, infinity) -> Nodes = mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, parents = [], _ = '_'}), - [fixup_node(N) || N <- Nodes]; + [ fixup_node(N) || N <- Nodes ]; get_subnodes(Host, <<>>, Limit) -> case mnesia:select( - pubsub_node, - ets:fun2ms( - fun(#pubsub_node{nodeid = {H, _}, parents = []} = Node) when H == Host -> - Node - end), Limit, read) of - '$end_of_table' -> []; - {Nodes, _} -> [fixup_node(N) || N <- Nodes] + pubsub_node, + ets:fun2ms( + fun(#pubsub_node{nodeid = {H, _}, parents = []} = Node) when H == Host -> + Node + end), + Limit, + read) of + '$end_of_table' -> []; + {Nodes, _} -> [ fixup_node(N) || N <- Nodes ] end; get_subnodes(Host, Node, infinity) -> - Q = qlc:q([fixup_node(N) - || #pubsub_node{nodeid = {NHost, _}, - parents = Parents} = - N - <- mnesia:table(pubsub_node), - Host == NHost, lists:member(Node, Parents)]), + Q = qlc:q([ fixup_node(N) + || #pubsub_node{ + nodeid = {NHost, _}, + parents = Parents + } = + N <- mnesia:table(pubsub_node), + Host == NHost, + lists:member(Node, Parents) ]), qlc:e(Q); get_subnodes(Host, Node, Limit) -> case mnesia:select( - pubsub_node, - ets:fun2ms( - fun(#pubsub_node{nodeid = {H, _}, parents = Ps} = N) - when H == Host andalso Ps /= [] -> N - end), Limit, read) of - '$end_of_table' -> []; - {Nodes, _} -> - lists:filtermap( - fun(#pubsub_node{parents = Parents} = N2) -> - case lists:member(Node, Parents) of - true -> {true, fixup_node(N2)}; - _ -> false - end - end, Nodes) + pubsub_node, + ets:fun2ms( + fun(#pubsub_node{nodeid = {H, _}, parents = Ps} = N) + when H == Host andalso Ps /= [] -> N + end), + Limit, + read) of + '$end_of_table' -> []; + {Nodes, _} -> + lists:filtermap( + fun(#pubsub_node{parents = Parents} = N2) -> + case lists:member(Node, Parents) of + true -> {true, fixup_node(N2)}; + _ -> false + end + end, + Nodes) end. + get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). + get_subnodes_tree(Host, Node) -> case get_node(Host, Node) of - {error, _} -> - []; - Rec -> - BasePlugin = misc:binary_to_atom(<<"node_", - (Rec#pubsub_node.type)/binary>>), - {result, BasePath} = BasePlugin:node_to_path(Node), - mnesia:foldl(fun (#pubsub_node{nodeid = {H, N}} = R, Acc) -> - Plugin = misc:binary_to_atom(<<"node_", - (R#pubsub_node.type)/binary>>), - {result, Path} = Plugin:node_to_path(N), - case lists:prefix(BasePath, Path) and (H == Host) of - true -> [R | Acc]; - false -> Acc - end - end, - [], pubsub_node) + {error, _} -> + []; + Rec -> + BasePlugin = misc:binary_to_atom(<<"node_", + (Rec#pubsub_node.type)/binary>>), + {result, BasePath} = BasePlugin:node_to_path(Node), + mnesia:foldl(fun(#pubsub_node{nodeid = {H, N}} = R, Acc) -> + Plugin = misc:binary_to_atom(<<"node_", + (R#pubsub_node.type)/binary>>), + {result, Path} = Plugin:node_to_path(N), + case lists:prefix(BasePath, Path) and (H == Host) of + true -> [R | Acc]; + false -> Acc + end + end, + [], + pubsub_node) end. + create_node(Host, Node, Type, Owner, Options, Parents) -> BJID = jid:tolower(jid:remove_resource(Owner)), case mnesia:read({pubsub_node, {Host, Node}}) of - [] -> - ParentExists = case Host of - {_U, _S, _R} -> - %% This is special case for PEP handling - %% PEP does not uses hierarchy - true; - _ -> - case Parents of - [] -> - true; - [Parent | _] -> - case catch mnesia:read({pubsub_node, {Host, Parent}}) of - [#pubsub_node{owners = [{<<>>, Host, <<>>}]}] -> - true; - [#pubsub_node{owners = Owners}] -> - lists:member(BJID, Owners); - _ -> - false - end; - _ -> - false - end - end, - case ParentExists of - true -> - Nidx = pubsub_index:new(node), - mnesia:write(#pubsub_node{nodeid = {Host, Node}, - id = Nidx, parents = Parents, - type = Type, owners = [BJID], - options = Options}), - {ok, Nidx}; - false -> - {error, xmpp:err_forbidden()} - end; - _ -> - {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())} + [] -> + ParentExists = case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + case Parents of + [] -> + true; + [Parent | _] -> + case catch mnesia:read({pubsub_node, {Host, Parent}}) of + [#pubsub_node{owners = [{<<>>, Host, <<>>}]}] -> + true; + [#pubsub_node{owners = Owners}] -> + lists:member(BJID, Owners); + _ -> + false + end; + _ -> + false + end + end, + case ParentExists of + true -> + Nidx = pubsub_index:new(node), + mnesia:write(#pubsub_node{ + nodeid = {Host, Node}, + id = Nidx, + parents = Parents, + type = Type, + owners = [BJID], + options = Options + }), + {ok, Nidx}; + false -> + {error, xmpp:err_forbidden()} + end; + _ -> + {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())} end. + delete_node(Host, Node) -> Removed = get_subnodes_tree(Host, Node), - lists:foreach(fun (#pubsub_node{nodeid = {_, SubNode}, id = SubNidx}) -> - pubsub_index:free(node, SubNidx), - mnesia:delete({pubsub_node, {Host, SubNode}}) - end, - Removed), + lists:foreach(fun(#pubsub_node{nodeid = {_, SubNode}, id = SubNidx}) -> + pubsub_index:free(node, SubNidx), + mnesia:delete({pubsub_node, {Host, SubNode}}) + end, + Removed), Removed. + fixup_node(#pubsub_node{options = Options} = Node) -> Res = lists:splitwith( - fun({max_items, infinity}) -> false; - (_) -> true - end, Options), + fun({max_items, infinity}) -> false; + (_) -> true + end, + Options), Options2 = case Res of - {Before, [_ | After]} -> - Before ++ [{max_items, max} | After]; - {Rest, []} -> - Rest - end, + {Before, [_ | After]} -> + Before ++ [{max_items, max} | After]; + {Rest, []} -> + Rest + end, Node#pubsub_node{options = Options2}. diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl index 09959099e..a81d67aa8 100644 --- a/src/nodetree_tree_sql.erl +++ b/src/nodetree_tree_sql.erl @@ -37,343 +37,368 @@ -behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). - -include("pubsub.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("ejabberd_sql_pt.hrl"). -include("translate.hrl"). --export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_all_nodes/1, - get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, get_node/2, get_node/1, + get_nodes/2, get_nodes/1, + get_all_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/6, + delete_node/2]). -export([raw_to_node/2]). + init(_Host, _ServerHost, _Opts) -> ok. + terminate(_Host, _ServerHost) -> ok. + options() -> [{sql, true} | nodetree_tree:options()]. + set_node(Record) when is_record(Record, pubsub_node) -> {Host, Node} = Record#pubsub_node.nodeid, Parent = case Record#pubsub_node.parents of - [] -> <<>>; - [First | _] -> First - end, + [] -> <<>>; + [First | _] -> First + end, Type = Record#pubsub_node.type, H = node_flat_sql:encode_host(Host), Nidx = case nodeidx(Host, Node) of - {result, OldNidx} -> - catch - ejabberd_sql:sql_query_t( - ?SQL("delete from pubsub_node_option " - "where nodeid=%(OldNidx)d")), - catch - ejabberd_sql:sql_query_t( - ?SQL("update pubsub_node set" - " host=%(H)s, node=%(Node)s," - " parent=%(Parent)s, plugin=%(Type)s " - "where nodeid=%(OldNidx)d")), - OldNidx; - {error, not_found} -> - catch - ejabberd_sql:sql_query_t( - ?SQL("insert into pubsub_node(host, node, parent, plugin) " - "values(%(H)s, %(Node)s, %(Parent)s, %(Type)s)")), - case nodeidx(Host, Node) of - {result, NewNidx} -> NewNidx; - {error, not_found} -> none; % this should not happen - {error, _} -> db_error - end; - {error, _} -> - db_error - end, + {result, OldNidx} -> + catch ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_node_option " + "where nodeid=%(OldNidx)d")), + catch ejabberd_sql:sql_query_t( + ?SQL("update pubsub_node set" + " host=%(H)s, node=%(Node)s," + " parent=%(Parent)s, plugin=%(Type)s " + "where nodeid=%(OldNidx)d")), + OldNidx; + {error, not_found} -> + catch ejabberd_sql:sql_query_t( + ?SQL("insert into pubsub_node(host, node, parent, plugin) " + "values(%(H)s, %(Node)s, %(Parent)s, %(Type)s)")), + case nodeidx(Host, Node) of + {result, NewNidx} -> NewNidx; + {error, not_found} -> none; % this should not happen + {error, _} -> db_error + end; + {error, _} -> + db_error + end, case Nidx of - db_error -> - {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; - none -> - Txt = ?T("Node index not found"), - {error, xmpp:err_internal_server_error(Txt, ejabberd_option:language())}; - _ -> - lists:foreach(fun ({Key, Value}) -> - SKey = iolist_to_binary(atom_to_list(Key)), - SValue = misc:term_to_expr(Value), - catch - ejabberd_sql:sql_query_t( - ?SQL("insert into pubsub_node_option(nodeid, name, val) " - "values (%(Nidx)d, %(SKey)s, %(SValue)s)")) - end, - Record#pubsub_node.options), - {result, Nidx} + db_error -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; + none -> + Txt = ?T("Node index not found"), + {error, xmpp:err_internal_server_error(Txt, ejabberd_option:language())}; + _ -> + lists:foreach(fun({Key, Value}) -> + SKey = iolist_to_binary(atom_to_list(Key)), + SValue = misc:term_to_expr(Value), + catch ejabberd_sql:sql_query_t( + ?SQL("insert into pubsub_node_option(nodeid, name, val) " + "values (%(Nidx)d, %(SKey)s, %(SValue)s)")) + end, + Record#pubsub_node.options), + {result, Nidx} end. + get_node(Host, Node, _From) -> get_node(Host, Node). + get_node(Host, Node) -> H = node_flat_sql:encode_host(Host), - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " - "where host=%(H)s and node=%(Node)s")) - of - {selected, [RItem]} -> - raw_to_node(Host, RItem); - {'EXIT', _Reason} -> - {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; - _ -> - {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " + "where host=%(H)s and node=%(Node)s")) of + {selected, [RItem]} -> + raw_to_node(Host, RItem); + {'EXIT', _Reason} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; + _ -> + {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. + get_node(Nidx) -> - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(host)s, @(node)s, @(parent)s, @(plugin)s from pubsub_node " - "where nodeid=%(Nidx)d")) - of - {selected, [{Host, Node, Parent, Type}]} -> - raw_to_node(Host, {Node, Parent, Type, Nidx}); - {'EXIT', _Reason} -> - {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; - _ -> - {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(host)s, @(node)s, @(parent)s, @(plugin)s from pubsub_node " + "where nodeid=%(Nidx)d")) of + {selected, [{Host, Node, Parent, Type}]} -> + raw_to_node(Host, {Node, Parent, Type, Nidx}); + {'EXIT', _Reason} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; + _ -> + {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. + get_nodes(Host) -> get_nodes(Host, infinity). + get_nodes(Host, Limit) -> H = node_flat_sql:encode_host(Host), - Query = fun(mssql, _) when is_integer(Limit), Limit>=0 -> - ejabberd_sql:sql_query_t( - ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(H)s")); - (_, _) when is_integer(Limit), Limit>=0 -> - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(H)s limit %(Limit)d")); - (_, _) -> - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(H)s")) - end, + Query = fun(mssql, _) when is_integer(Limit), Limit >= 0 -> + ejabberd_sql:sql_query_t( + ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s")); + (_, _) when is_integer(Limit), Limit >= 0 -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s limit %(Limit)d")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s")) + end, case ejabberd_sql:sql_query_t(Query) of - {selected, RItems} -> - [raw_to_node(Host, Item) || Item <- RItems]; - _ -> - [] + {selected, RItems} -> + [ raw_to_node(Host, Item) || Item <- RItems ]; + _ -> + [] end. + get_all_nodes({_U, _S, _R} = JID) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), EncKey = node_flat_sql:encode_jid(GenKey), Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, case ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(EncKey)s " - "or host like %(Pattern)s %ESCAPE")) of - {selected, RItems} -> - [raw_to_node(GenKey, Item) || Item <- RItems]; - _ -> - [] + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(EncKey)s " + "or host like %(Pattern)s %ESCAPE")) of + {selected, RItems} -> + [ raw_to_node(GenKey, Item) || Item <- RItems ]; + _ -> + [] end; get_all_nodes(Host) -> Pattern1 = <<"%@", Host/binary>>, Pattern2 = <<"%@", Host/binary, "/%">>, case ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(Host)s " - "or host like %(Pattern1)s " - "or host like %(Pattern2)s %ESCAPE")) of - {selected, RItems} -> - [raw_to_node(Host, Item) || Item <- RItems]; - _ -> - [] + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(Host)s " + "or host like %(Pattern1)s " + "or host like %(Pattern2)s %ESCAPE")) of + {selected, RItems} -> + [ raw_to_node(Host, Item) || Item <- RItems ]; + _ -> + [] end. + get_parentnodes(Host, Node, _From) -> case get_node(Host, Node) of - Record when is_record(Record, pubsub_node) -> - Record#pubsub_node.parents; - _ -> - [] + Record when is_record(Record, pubsub_node) -> + Record#pubsub_node.parents; + _ -> + [] end. + get_parentnodes_tree(Host, Node, _From) -> get_parentnodes_tree(Host, Node, 0, []). + + get_parentnodes_tree(Host, Node, Level, Acc) -> case get_node(Host, Node) of - Record when is_record(Record, pubsub_node) -> - Tree = [{Level, [Record]}|Acc], - case Record#pubsub_node.parents of - [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); - _ -> Tree - end; - _ -> - Acc + Record when is_record(Record, pubsub_node) -> + Tree = [{Level, [Record]} | Acc], + case Record#pubsub_node.parents of + [Parent] -> get_parentnodes_tree(Host, Parent, Level + 1, Tree); + _ -> Tree + end; + _ -> + Acc end. + get_subnodes(Host, Node, Limit) -> H = node_flat_sql:encode_host(Host), - Query = fun(mssql, _) when is_integer(Limit), Limit>=0 -> - ejabberd_sql:sql_query_t( - ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(H)s and parent=%(Node)s")); - (_, _) when is_integer(Limit), Limit>=0 -> - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(H)s and parent=%(Node)s " - "limit %(Limit)d")); - (_, _) -> - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " - "from pubsub_node where host=%(H)s and parent=%(Node)s")) - end, + Query = fun(mssql, _) when is_integer(Limit), Limit >= 0 -> + ejabberd_sql:sql_query_t( + ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s and parent=%(Node)s")); + (_, _) when is_integer(Limit), Limit >= 0 -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s and parent=%(Node)s " + "limit %(Limit)d")); + (_, _) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(H)s and parent=%(Node)s")) + end, case ejabberd_sql:sql_query_t(Query) of - {selected, RItems} -> - [raw_to_node(Host, Item) || Item <- RItems]; - _ -> - [] + {selected, RItems} -> + [ raw_to_node(Host, Item) || Item <- RItems ]; + _ -> + [] end. + get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). + get_subnodes_tree(Host, Node) -> case get_node(Host, Node) of - {error, _} -> - []; - Rec -> - Type = Rec#pubsub_node.type, - H = node_flat_sql:encode_host(Host), - N = <<(ejabberd_sql:escape_like_arg(Node))/binary, "/%">>, - Sub = case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " - "where host=%(H)s and plugin=%(Type)s and" - " (parent=%(Node)s or parent like %(N)s %ESCAPE)")) - of - {selected, RItems} -> - [raw_to_node(Host, Item) || Item <- RItems]; - _ -> - [] - end, - [Rec|Sub] + {error, _} -> + []; + Rec -> + Type = Rec#pubsub_node.type, + H = node_flat_sql:encode_host(Host), + N = <<(ejabberd_sql:escape_like_arg(Node))/binary, "/%">>, + Sub = case catch ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " + "where host=%(H)s and plugin=%(Type)s and" + " (parent=%(Node)s or parent like %(N)s %ESCAPE)")) of + {selected, RItems} -> + [ raw_to_node(Host, Item) || Item <- RItems ]; + _ -> + [] + end, + [Rec | Sub] end. + create_node(Host, Node, Type, Owner, Options, Parents) -> BJID = jid:tolower(jid:remove_resource(Owner)), case nodeidx(Host, Node) of - {error, not_found} -> - ParentExists = case Host of - {_U, _S, _R} -> - %% This is special case for PEP handling - %% PEP does not uses hierarchy - true; - _ -> - case Parents of - [] -> - true; - [Parent | _] -> - case nodeidx(Host, Parent) of - {result, PNode} -> - case nodeowners(PNode) of - [{<<>>, Host, <<>>}] -> true; - Owners -> lists:member(BJID, Owners) - end; - _ -> - false - end; - _ -> - false - end - end, - case ParentExists of - true -> - case set_node(#pubsub_node{nodeid = {Host, Node}, - parents = Parents, type = Type, - options = Options}) - of - {result, Nidx} -> {ok, Nidx}; - Other -> Other - end; - false -> - {error, xmpp:err_forbidden()} - end; - {result, _} -> - {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())}; - {error, db_fail} -> - {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} + {error, not_found} -> + ParentExists = case Host of + {_U, _S, _R} -> + %% This is special case for PEP handling + %% PEP does not uses hierarchy + true; + _ -> + case Parents of + [] -> + true; + [Parent | _] -> + case nodeidx(Host, Parent) of + {result, PNode} -> + case nodeowners(PNode) of + [{<<>>, Host, <<>>}] -> true; + Owners -> lists:member(BJID, Owners) + end; + _ -> + false + end; + _ -> + false + end + end, + case ParentExists of + true -> + case set_node(#pubsub_node{ + nodeid = {Host, Node}, + parents = Parents, + type = Type, + options = Options + }) of + {result, Nidx} -> {ok, Nidx}; + Other -> Other + end; + false -> + {error, xmpp:err_forbidden()} + end; + {result, _} -> + {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())}; + {error, db_fail} -> + {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} end. + delete_node(Host, Node) -> lists:map( - fun(Rec) -> - Nidx = Rec#pubsub_node.id, - catch ejabberd_sql:sql_query_t( - ?SQL("delete from pubsub_node where nodeid=%(Nidx)d")), - Rec - end, get_subnodes_tree(Host, Node)). + fun(Rec) -> + Nidx = Rec#pubsub_node.id, + catch ejabberd_sql:sql_query_t( + ?SQL("delete from pubsub_node where nodeid=%(Nidx)d")), + Rec + end, + get_subnodes_tree(Host, Node)). + %% helpers raw_to_node(Host, [Node, Parent, Type, Nidx]) -> raw_to_node(Host, {Node, Parent, Type, binary_to_integer(Nidx)}); raw_to_node(Host, {Node, Parent, Type, Nidx}) -> - Options = case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(name)s, @(val)s from pubsub_node_option " - "where nodeid=%(Nidx)d")) - of - {selected, ROptions} -> - DbOpts = lists:map( - fun({<<"max_items">>, <<"infinity">>}) -> - {max_items, max}; - ({Key, Value}) -> - RKey = misc:binary_to_atom(Key), - Tokens = element(2, erl_scan:string(binary_to_list(<>))), - RValue = element(2, erl_parse:parse_term(Tokens)), - {RKey, RValue} - end, - ROptions), - Module = misc:binary_to_atom(<<"node_", Type/binary, "_sql">>), - StdOpts = Module:options(), - lists:foldl(fun ({Key, Value}, Acc) -> - lists:keystore(Key, 1, Acc, {Key, Value}) - end, - StdOpts, DbOpts); - _ -> - [] - end, + Options = case catch ejabberd_sql:sql_query_t( + ?SQL("select @(name)s, @(val)s from pubsub_node_option " + "where nodeid=%(Nidx)d")) of + {selected, ROptions} -> + DbOpts = lists:map( + fun({<<"max_items">>, <<"infinity">>}) -> + {max_items, max}; + ({Key, Value}) -> + RKey = misc:binary_to_atom(Key), + Tokens = element(2, erl_scan:string(binary_to_list(<>))), + RValue = element(2, erl_parse:parse_term(Tokens)), + {RKey, RValue} + end, + ROptions), + Module = misc:binary_to_atom(<<"node_", Type/binary, "_sql">>), + StdOpts = Module:options(), + lists:foldl(fun({Key, Value}, Acc) -> + lists:keystore(Key, 1, Acc, {Key, Value}) + end, + StdOpts, + DbOpts); + _ -> + [] + end, Parents = case Parent of - <<>> -> []; - _ -> [Parent] - end, - #pubsub_node{nodeid = {Host, Node}, id = Nidx, - parents = Parents, type = Type, options = Options}. + <<>> -> []; + _ -> [Parent] + end, + #pubsub_node{ + nodeid = {Host, Node}, + id = Nidx, + parents = Parents, + type = Type, + options = Options + }. + nodeidx(Host, Node) -> H = node_flat_sql:encode_host(Host), - case catch - ejabberd_sql:sql_query_t( - ?SQL("select @(nodeid)d from pubsub_node " - "where host=%(H)s and node=%(Node)s")) - of - {selected, [{Nidx}]} -> - {result, Nidx}; - {'EXIT', _Reason} -> - {error, db_fail}; - _ -> - {error, not_found} + case catch ejabberd_sql:sql_query_t( + ?SQL("select @(nodeid)d from pubsub_node " + "where host=%(H)s and node=%(Node)s")) of + {selected, [{Nidx}]} -> + {result, Nidx}; + {'EXIT', _Reason} -> + {error, db_fail}; + _ -> + {error, not_found} end. + nodeowners(Nidx) -> {result, Res} = node_flat_sql:get_node_affiliations(Nidx), - [LJID || {LJID, Aff} <- Res, Aff =:= owner]. + [ LJID || {LJID, Aff} <- Res, Aff =:= owner ]. diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl index 18eb9ed30..8ea7419e1 100644 --- a/src/nodetree_virtual.erl +++ b/src/nodetree_virtual.erl @@ -36,90 +36,127 @@ -include("pubsub.hrl"). --export([init/3, terminate/2, options/0, set_node/1, - get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_all_nodes/1, - get_parentnodes/3, get_parentnodes_tree/3, - get_subnodes/3, get_subnodes_tree/3, create_node/6, - delete_node/2]). +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, get_node/2, get_node/1, + get_nodes/2, get_nodes/1, + get_all_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/6, + delete_node/2]). + init(_Host, _ServerHost, _Opts) -> ok. + terminate(_Host, _ServerHost) -> ok. + options() -> [{virtual_tree, true}]. + set_node(_Node) -> ok. + get_node(Host, Node, _From) -> get_node(Host, Node). + get_node(Host, Node) -> Nidx = nodeidx(Host, Node), node_record(Host, Node, Nidx). + get_node(Nidx) -> {Host, Node} = nodeid(Nidx), node_record(Host, Node, Nidx). + get_nodes(Host) -> get_nodes(Host, infinity). + get_nodes(_Host, _Limit) -> []. + get_all_nodes(_Host) -> []. + get_parentnodes(_Host, _Node, _From) -> []. + get_parentnodes_tree(Host, Node, From) -> [{0, [get_node(Host, Node, From)]}]. + get_subnodes(_Host, _Node, _From) -> []. + get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). + get_subnodes_tree(_Host, _Node) -> []. + create_node(Host, Node, _Type, _Owner, _Options, _Parents) -> {error, {virtual, nodeidx(Host, Node)}}. + delete_node(Host, Node) -> [get_node(Host, Node)]. + %% internal helper -node_record({U,S,R}, Node, Nidx) -> + +node_record({U, S, R}, Node, Nidx) -> Host = mod_pubsub:host(S), Type = <<"pep">>, Module = mod_pubsub:plugin(Host, Type), - #pubsub_node{nodeid = {{U,S,R},Node}, id = Nidx, type = Type, - owners = [{U,S,R}], - options = Module:options()}; + #pubsub_node{ + nodeid = {{U, S, R}, Node}, + id = Nidx, + type = Type, + owners = [{U, S, R}], + options = Module:options() + }; node_record(Host, Node, Nidx) -> - [Type|_] = mod_pubsub:plugins(Host), + [Type | _] = mod_pubsub:plugins(Host), Module = mod_pubsub:plugin(Host, Type), - #pubsub_node{nodeid = {Host, Node}, id = Nidx, type = Type, - owners = [{<<"">>, Host, <<"">>}], - options = Module:options()}. + #pubsub_node{ + nodeid = {Host, Node}, + id = Nidx, + type = Type, + owners = [{<<"">>, Host, <<"">>}], + options = Module:options() + }. -nodeidx({U,S,R}, Node) -> - JID = jid:encode(jid:make(U,S,R)), + +nodeidx({U, S, R}, Node) -> + JID = jid:encode(jid:make(U, S, R)), <>; nodeidx(Host, Node) -> <>. + + nodeid(Nidx) -> [Head, Node] = binary:split(Nidx, <<":">>), case jid:decode(Head) of - {jid,<<>>,Host,<<>>,_,_,_} -> {Host, Node}; - {jid,U,S,R,_,_,_} -> {{U,S,R}, Node} + {jid, <<>>, Host, <<>>, _, _, _} -> {Host, Node}; + {jid, U, S, R, _, _, _} -> {{U, S, R}, Node} end. diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index 045abdf90..7b5a64027 100644 --- a/src/prosody2ejabberd.erl +++ b/src/prosody2ejabberd.erl @@ -29,353 +29,398 @@ -include_lib("xmpp/include/scram.hrl"). -include_lib("xmpp/include/xmpp.hrl"). + -include("logger.hrl"). -include("mod_roster.hrl"). -include("mod_offline.hrl"). -include("mod_privacy.hrl"). + %%%=================================================================== %%% API %%%=================================================================== from_dir(ProsodyDir) -> case code:ensure_loaded(luerl) of - {module, _} -> - case file:list_dir(ProsodyDir) of - {ok, HostDirs} -> - lists:foreach( - fun(HostDir) -> - Host = list_to_binary(HostDir), - lists:foreach( - fun(SubDir) -> - Path = filename:join( - [ProsodyDir, HostDir, SubDir]), - convert_dir(Path, Host, SubDir) - end, ["vcard", "accounts", "roster", - "private", "config", "offline", - "privacy", "pep", "pubsub"]) - end, HostDirs); - {error, Why} = Err -> - ?ERROR_MSG("Failed to list ~ts: ~ts", - [ProsodyDir, file:format_error(Why)]), - Err - end; - {error, _} = Err -> - ?ERROR_MSG("The file 'luerl.beam' is not found: maybe " - "ejabberd is not compiled with Lua support", []), - Err + {module, _} -> + case file:list_dir(ProsodyDir) of + {ok, HostDirs} -> + lists:foreach( + fun(HostDir) -> + Host = list_to_binary(HostDir), + lists:foreach( + fun(SubDir) -> + Path = filename:join( + [ProsodyDir, HostDir, SubDir]), + convert_dir(Path, Host, SubDir) + end, + ["vcard", "accounts", "roster", + "private", "config", "offline", + "privacy", "pep", "pubsub"]) + end, + HostDirs); + {error, Why} = Err -> + ?ERROR_MSG("Failed to list ~ts: ~ts", + [ProsodyDir, file:format_error(Why)]), + Err + end; + {error, _} = Err -> + ?ERROR_MSG("The file 'luerl.beam' is not found: maybe " + "ejabberd is not compiled with Lua support", + []), + Err end. + %%%=================================================================== %%% Internal functions %%%=================================================================== convert_dir(Path, Host, Type) -> case file:list_dir(Path) of - {ok, Files} -> - lists:foreach( - fun(File) -> - FilePath = filename:join(Path, File), - case Type of - "pep" -> - case filelib:is_dir(FilePath) of - true -> - JID = list_to_binary(File ++ "@" ++ Host), - convert_dir(FilePath, JID, "pubsub"); - false -> - ok - end; - _ -> - case eval_file(FilePath) of - {ok, Data} -> - Name = iolist_to_binary(filename:rootname(File)), - convert_data(misc:uri_decode(Host), Type, - misc:uri_decode(Name), Data); - Err -> - Err - end - end - end, Files); - {error, enoent} -> - ok; - {error, Why} = Err -> - ?ERROR_MSG("Failed to list ~ts: ~ts", - [Path, file:format_error(Why)]), - Err + {ok, Files} -> + lists:foreach( + fun(File) -> + FilePath = filename:join(Path, File), + case Type of + "pep" -> + case filelib:is_dir(FilePath) of + true -> + JID = list_to_binary(File ++ "@" ++ Host), + convert_dir(FilePath, JID, "pubsub"); + false -> + ok + end; + _ -> + case eval_file(FilePath) of + {ok, Data} -> + Name = iolist_to_binary(filename:rootname(File)), + convert_data(misc:uri_decode(Host), + Type, + misc:uri_decode(Name), + Data); + Err -> + Err + end + end + end, + Files); + {error, enoent} -> + ok; + {error, Why} = Err -> + ?ERROR_MSG("Failed to list ~ts: ~ts", + [Path, file:format_error(Why)]), + Err end. + eval_file(Path) -> case file:read_file(Path) of - {ok, Data} -> - State0 = luerl:init(), - State1 = luerl:set_table([item], - fun([X], State) -> {[X], State} end, - State0), - NewData = case filename:extension(Path) of - ".list" -> - <<"return {", Data/binary, "};">>; - _ -> - Data - end, - case luerl:eval(NewData, State1) of - {ok, _} = Res -> - Res; - {error, Why, _} = Err -> - ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]), - Err - end; - {error, Why} = Err -> - ?ERROR_MSG("Failed to read file ~ts: ~ts", - [Path, file:format_error(Why)]), - Err + {ok, Data} -> + State0 = luerl:init(), + State1 = luerl:set_table([item], + fun([X], State) -> {[X], State} end, + State0), + NewData = case filename:extension(Path) of + ".list" -> + <<"return {", Data/binary, "};">>; + _ -> + Data + end, + case luerl:eval(NewData, State1) of + {ok, _} = Res -> + Res; + {error, Why, _} = Err -> + ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]), + Err + end; + {error, Why} = Err -> + ?ERROR_MSG("Failed to read file ~ts: ~ts", + [Path, file:format_error(Why)]), + Err end. + maybe_get_scram_auth(Data) -> case proplists:get_value(<<"iteration_count">>, Data, no_ic) of - IC when is_number(IC) -> - #scram{ - storedkey = misc:hex_to_base64(proplists:get_value(<<"stored_key">>, Data, <<"">>)), - serverkey = misc:hex_to_base64(proplists:get_value(<<"server_key">>, Data, <<"">>)), - salt = base64:encode(proplists:get_value(<<"salt">>, Data, <<"">>)), - iterationcount = round(IC) - }; - _ -> <<"">> + IC when is_number(IC) -> + #scram{ + storedkey = misc:hex_to_base64(proplists:get_value(<<"stored_key">>, Data, <<"">>)), + serverkey = misc:hex_to_base64(proplists:get_value(<<"server_key">>, Data, <<"">>)), + salt = base64:encode(proplists:get_value(<<"salt">>, Data, <<"">>)), + iterationcount = round(IC) + }; + _ -> <<"">> end. + convert_data(Host, "accounts", User, [Data]) -> Password = case proplists:get_value(<<"password">>, Data, no_pass) of - no_pass -> - maybe_get_scram_auth(Data); - Pass when is_binary(Pass) -> - Pass - end, + no_pass -> + maybe_get_scram_auth(Data); + Pass when is_binary(Pass) -> + Pass + end, case ejabberd_auth:try_register(User, Host, Password) of - ok -> - ok; - Err -> - ?ERROR_MSG("Failed to register user ~ts@~ts: ~p", - [User, Host, Err]), - Err + ok -> + ok; + Err -> + ?ERROR_MSG("Failed to register user ~ts@~ts: ~p", + [User, Host, Err]), + Err end; convert_data(Host, "roster", User, [Data]) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), Rosters = - lists:flatmap( - fun({<<"pending">>, L}) -> - convert_pending_item(LUser, LServer, L); - ({S, L}) when is_binary(S) -> - convert_roster_item(LUser, LServer, S, L); - (_) -> - [] - end, Data), + lists:flatmap( + fun({<<"pending">>, L}) -> + convert_pending_item(LUser, LServer, L); + ({S, L}) when is_binary(S) -> + convert_roster_item(LUser, LServer, S, L); + (_) -> + [] + end, + Data), lists:foreach(fun mod_roster:set_roster/1, Rosters); convert_data(Host, "private", User, [Data]) -> PrivData = lists:flatmap( - fun({_TagXMLNS, Raw}) -> - case deserialize(Raw) of - [El] -> - XMLNS = fxml:get_tag_attr_s(<<"xmlns">>, El), - [{XMLNS, El}]; - _ -> - [] - end - end, Data), + fun({_TagXMLNS, Raw}) -> + case deserialize(Raw) of + [El] -> + XMLNS = fxml:get_tag_attr_s(<<"xmlns">>, El), + [{XMLNS, El}]; + _ -> + [] + end + end, + Data), mod_private:set_data(jid:make(User, Host), PrivData); convert_data(Host, "vcard", User, [Data]) -> LServer = jid:nameprep(Host), case deserialize(Data) of - [VCard] -> - mod_vcard:set_vcard(User, LServer, VCard); - _ -> - ok + [VCard] -> + mod_vcard:set_vcard(User, LServer, VCard); + _ -> + ok end; convert_data(_Host, "config", _User, [Data]) -> RoomJID1 = case proplists:get_value(<<"jid">>, Data, not_found) of - not_found -> proplists:get_value(<<"_jid">>, Data, room_jid_not_found); - A when is_binary(A) -> A - end, + not_found -> proplists:get_value(<<"_jid">>, Data, room_jid_not_found); + A when is_binary(A) -> A + end, RoomJID = jid:decode(RoomJID1), Config = proplists:get_value(<<"_data">>, Data, []), RoomCfg = convert_room_config(Data), case proplists:get_bool(<<"persistent">>, Config) of - true when RoomJID /= error -> - mod_muc:store_room(find_serverhost(RoomJID#jid.lserver), RoomJID#jid.lserver, - RoomJID#jid.luser, RoomCfg); - _ -> - ok + true when RoomJID /= error -> + mod_muc:store_room(find_serverhost(RoomJID#jid.lserver), + RoomJID#jid.lserver, + RoomJID#jid.luser, + RoomCfg); + _ -> + ok end; convert_data(Host, "offline", User, [Data]) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), lists:foreach( fun({_, RawXML}) -> - case deserialize(RawXML) of - [El] -> - case el_to_offline_msg(LUser, LServer, El) of - [Msg] -> ok = mod_offline:store_offline_msg(Msg); - [] -> ok - end; - _ -> - ok - end - end, Data); + case deserialize(RawXML) of + [El] -> + case el_to_offline_msg(LUser, LServer, El) of + [Msg] -> ok = mod_offline:store_offline_msg(Msg); + [] -> ok + end; + _ -> + ok + end + end, + Data); convert_data(Host, "privacy", User, [Data]) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), Lists = proplists:get_value(<<"lists">>, Data, []), Priv = #privacy{ - us = {LUser, LServer}, - default = proplists:get_value(<<"default">>, Data, none), - lists = lists:flatmap( - fun({Name, Vals}) -> - Items = proplists:get_value(<<"items">>, Vals, []), - case lists:map(fun convert_privacy_item/1, - Items) of - [] -> []; - ListItems -> [{Name, ListItems}] - end - end, Lists)}, + us = {LUser, LServer}, + default = proplists:get_value(<<"default">>, Data, none), + lists = lists:flatmap( + fun({Name, Vals}) -> + Items = proplists:get_value(<<"items">>, Vals, []), + case lists:map(fun convert_privacy_item/1, + Items) of + [] -> []; + ListItems -> [{Name, ListItems}] + end + end, + Lists) + }, mod_privacy:set_list(Priv); convert_data(HostStr, "pubsub", Node, [Data]) -> case decode_pubsub_host(HostStr) of - Host when is_binary(Host); - is_tuple(Host) -> - Type = node_type(Host), - NodeData = convert_node_config(HostStr, Data), - DefaultConfig = mod_pubsub:config(Host, default_node_config, []), - Owner = proplists:get_value(owner, NodeData), - Options = lists:foldl( - fun({_Opt, undefined}, Acc) -> - Acc; - ({Opt, Val}, Acc) -> - lists:keystore(Opt, 1, Acc, {Opt, Val}) - end, DefaultConfig, proplists:get_value(options, NodeData)), - case mod_pubsub:tree_action(Host, create_node, [Host, Node, Type, Owner, Options, []]) of - {ok, Nidx} -> - case mod_pubsub:node_action(Host, Type, create_node, [Nidx, Owner]) of - {result, _} -> - Access = open, % always allow subscriptions proplists:get_value(access_model, Options), - Publish = open, % always allow publications proplists:get_value(publish_model, Options), - MaxItems = proplists:get_value(max_items, Options), - Affiliations = proplists:get_value(affiliations, NodeData), - Subscriptions = proplists:get_value(subscriptions, NodeData), - Items = proplists:get_value(items, NodeData), - [mod_pubsub:node_action(Host, Type, set_affiliation, - [Nidx, Entity, Aff]) - || {Entity, Aff} <- Affiliations, Entity =/= Owner], - [mod_pubsub:node_action(Host, Type, subscribe_node, - [Nidx, jid:make(Entity), Entity, Access, never, [], [], []]) - || Entity <- Subscriptions], - [mod_pubsub:node_action(Host, Type, publish_item, - [Nidx, Publisher, Publish, MaxItems, ItemId, Payload, []]) - || {ItemId, Publisher, Payload} <- Items]; - Error -> - Error - end; - Error -> - ?ERROR_MSG("Failed to import pubsub node ~ts on ~p:~n~p", - [Node, Host, NodeData]), - Error - end; - Error -> - ?ERROR_MSG("Failed to import pubsub node: ~p", [Error]), - Error + Host when is_binary(Host); + is_tuple(Host) -> + Type = node_type(Host), + NodeData = convert_node_config(HostStr, Data), + DefaultConfig = mod_pubsub:config(Host, default_node_config, []), + Owner = proplists:get_value(owner, NodeData), + Options = lists:foldl( + fun({_Opt, undefined}, Acc) -> + Acc; + ({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}) + end, + DefaultConfig, + proplists:get_value(options, NodeData)), + case mod_pubsub:tree_action(Host, create_node, [Host, Node, Type, Owner, Options, []]) of + {ok, Nidx} -> + case mod_pubsub:node_action(Host, Type, create_node, [Nidx, Owner]) of + {result, _} -> + Access = open, % always allow subscriptions proplists:get_value(access_model, Options), + Publish = open, % always allow publications proplists:get_value(publish_model, Options), + MaxItems = proplists:get_value(max_items, Options), + Affiliations = proplists:get_value(affiliations, NodeData), + Subscriptions = proplists:get_value(subscriptions, NodeData), + Items = proplists:get_value(items, NodeData), + [ mod_pubsub:node_action(Host, + Type, + set_affiliation, + [Nidx, Entity, Aff]) + || {Entity, Aff} <- Affiliations, Entity =/= Owner ], + [ mod_pubsub:node_action(Host, + Type, + subscribe_node, + [Nidx, jid:make(Entity), Entity, Access, never, [], [], []]) + || Entity <- Subscriptions ], + [ mod_pubsub:node_action(Host, + Type, + publish_item, + [Nidx, Publisher, Publish, MaxItems, ItemId, Payload, []]) + || {ItemId, Publisher, Payload} <- Items ]; + Error -> + Error + end; + Error -> + ?ERROR_MSG("Failed to import pubsub node ~ts on ~p:~n~p", + [Node, Host, NodeData]), + Error + end; + Error -> + ?ERROR_MSG("Failed to import pubsub node: ~p", [Error]), + Error end; convert_data(_Host, _Type, _User, _Data) -> ok. + convert_pending_item(LUser, LServer, LuaList) -> lists:flatmap( fun({S, true}) -> - try jid:decode(S) of - J -> - LJID = jid:tolower(J), - [#roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, - jid = LJID, - ask = in}] - catch _:{bad_jid, _} -> - [] - end; - (_) -> - [] - end, LuaList). + try jid:decode(S) of + J -> + LJID = jid:tolower(J), + [#roster{ + usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = LJID, + ask = in + }] + catch + _:{bad_jid, _} -> + [] + end; + (_) -> + [] + end, + LuaList). + convert_roster_item(LUser, LServer, JIDstring, LuaList) -> try jid:decode(JIDstring) of - JID -> - LJID = jid:tolower(JID), - InitR = #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, - jid = LJID}, - lists:foldl( - fun({<<"groups">>, Val}, [R]) -> - Gs = lists:flatmap( - fun({G, true}) -> [G]; - (_) -> [] - end, Val), - [R#roster{groups = Gs}]; - ({<<"subscription">>, Sub}, [R]) -> - [R#roster{subscription = misc:binary_to_atom(Sub)}]; - ({<<"ask">>, <<"subscribe">>}, [R]) -> - [R#roster{ask = out}]; - ({<<"name">>, Name}, [R]) -> - [R#roster{name = Name}]; - ({<<"persist">>, false}, _) -> - []; - ({<<"approved">>, _}, [R]) -> - [R]; - (A, [R]) -> - io:format("Warning: roster of user ~ts@~ts includes unknown " - "attribute:~n ~p~nand that one is discarded.~n", - [LUser, LServer, A]), - [R] - end, [InitR], LuaList) - catch _:{bad_jid, _} -> - [] + JID -> + LJID = jid:tolower(JID), + InitR = #roster{ + usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = LJID + }, + lists:foldl( + fun({<<"groups">>, Val}, [R]) -> + Gs = lists:flatmap( + fun({G, true}) -> [G]; + (_) -> [] + end, + Val), + [R#roster{groups = Gs}]; + ({<<"subscription">>, Sub}, [R]) -> + [R#roster{subscription = misc:binary_to_atom(Sub)}]; + ({<<"ask">>, <<"subscribe">>}, [R]) -> + [R#roster{ask = out}]; + ({<<"name">>, Name}, [R]) -> + [R#roster{name = Name}]; + ({<<"persist">>, false}, _) -> + []; + ({<<"approved">>, _}, [R]) -> + [R]; + (A, [R]) -> + io:format("Warning: roster of user ~ts@~ts includes unknown " + "attribute:~n ~p~nand that one is discarded.~n", + [LUser, LServer, A]), + [R] + end, + [InitR], + LuaList) + catch + _:{bad_jid, _} -> + [] end. + convert_room_affiliations(Data) -> lists:flatmap( fun({J, Aff}) -> - try jid:decode(J) of - #jid{luser = U, lserver = S} -> - [{{U, S, <<>>}, misc:binary_to_atom(Aff)}] - catch _:{bad_jid, _} -> - [] - end - end, proplists:get_value(<<"_affiliations">>, Data, [])). + try jid:decode(J) of + #jid{luser = U, lserver = S} -> + [{{U, S, <<>>}, misc:binary_to_atom(Aff)}] + catch + _:{bad_jid, _} -> + [] + end + end, + proplists:get_value(<<"_affiliations">>, Data, [])). + convert_room_config(Data) -> Config = proplists:get_value(<<"_data">>, Data, []), Pass = case proplists:get_value(<<"password">>, Config, <<"">>) of - <<"">> -> - []; - Password -> - [{password_protected, true}, - {password, Password}] - end, + <<"">> -> + []; + Password -> + [{password_protected, true}, + {password, Password}] + end, Subj = try jid:decode( - proplists:get_value( - <<"subject_from">>, Config, <<"">>)) of - #jid{lresource = Nick} when Nick /= <<"">> -> - [{subject, proplists:get_value(<<"subject">>, Config, <<"">>)}, - {subject_author, Nick}] - catch _:{bad_jid, _} -> - [] - end, + proplists:get_value( + <<"subject_from">>, Config, <<"">>)) of + #jid{lresource = Nick} when Nick /= <<"">> -> + [{subject, proplists:get_value(<<"subject">>, Config, <<"">>)}, + {subject_author, Nick}] + catch + _:{bad_jid, _} -> + [] + end, Anonymous = case proplists:get_value(<<"whois">>, Config, <<"moderators">>) of - <<"moderators">> -> true; - _ -> false - end, + <<"moderators">> -> true; + _ -> false + end, [{affiliations, convert_room_affiliations(Data)}, {allow_change_subj, proplists:get_bool(<<"changesubject">>, Config)}, {mam, proplists:get_bool(<<"archiving">>, Config)}, {description, proplists:get_value(<<"description">>, Config, <<"">>)}, - {members_only, proplists:get_bool(<<"members_only">>, Config)}, + {members_only, proplists:get_bool(<<"members_only">>, Config)}, {moderated, proplists:get_bool(<<"moderated">>, Config)}, {persistent, proplists:get_bool(<<"persistent">>, Config)}, {anonymous, Anonymous}] ++ Pass ++ Subj. + convert_privacy_item({_, Item}) -> Action = proplists:get_value(<<"action">>, Item, <<"allow">>), Order = proplists:get_value(<<"order">>, Item, 0), @@ -385,83 +430,102 @@ convert_privacy_item({_, Item}) -> MatchMsg = proplists:get_bool(<<"message">>, Item), MatchPresIn = proplists:get_bool(<<"presence-in">>, Item), MatchPresOut = proplists:get_bool(<<"presence-out">>, Item), - MatchAll = if (MatchIQ == false) and (MatchMsg == false) and - (MatchPresIn == false) and (MatchPresOut == false) -> - true; - true -> - false - end, - {Type, Value} = try case T of - none -> {T, none}; - group -> {T, V}; - jid -> {T, jid:tolower(jid:decode(V))}; - subscription -> {T, misc:binary_to_atom(V)} - end - catch _:_ -> - {none, none} - end, - #listitem{type = Type, - value = Value, - action = misc:binary_to_atom(Action), - order = erlang:trunc(Order), - match_all = MatchAll, - match_iq = MatchIQ, - match_message = MatchMsg, - match_presence_in = MatchPresIn, - match_presence_out = MatchPresOut}. + MatchAll = if + (MatchIQ == false) and (MatchMsg == false) and + (MatchPresIn == false) and (MatchPresOut == false) -> + true; + true -> + false + end, + {Type, Value} = try + case T of + none -> {T, none}; + group -> {T, V}; + jid -> {T, jid:tolower(jid:decode(V))}; + subscription -> {T, misc:binary_to_atom(V)} + end + catch + _:_ -> + {none, none} + end, + #listitem{ + type = Type, + value = Value, + action = misc:binary_to_atom(Action), + order = erlang:trunc(Order), + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMsg, + match_presence_in = MatchPresIn, + match_presence_out = MatchPresOut + }. + decode_pubsub_host(Host) -> try jid:decode(Host) of - #jid{luser = <<>>, lserver = LServer} -> LServer; - #jid{luser = LUser, lserver = LServer} -> {LUser, LServer, <<>>} - catch _:{bad_jid, _} -> bad_jid + #jid{luser = <<>>, lserver = LServer} -> LServer; + #jid{luser = LUser, lserver = LServer} -> {LUser, LServer, <<>>} + catch + _:{bad_jid, _} -> bad_jid end. + node_type({_U, _S, _R}) -> <<"pep">>; node_type(Host) -> hd(mod_pubsub:plugins(Host)). + max_items(Config, Default) -> case round(proplists:get_value(<<"max_items">>, Config, Default)) of - I when I =< 0 -> Default; - I -> I + I when I =< 0 -> Default; + I -> I end. + convert_node_affiliations(Data) -> lists:flatmap( fun({J, Aff}) -> - try jid:decode(J) of - JID -> - [{JID, misc:binary_to_atom(Aff)}] - catch _:{bad_jid, _} -> - [] - end - end, proplists:get_value(<<"affiliations">>, Data, [])). + try jid:decode(J) of + JID -> + [{JID, misc:binary_to_atom(Aff)}] + catch + _:{bad_jid, _} -> + [] + end + end, + proplists:get_value(<<"affiliations">>, Data, [])). + convert_node_subscriptions(Data) -> lists:flatmap( fun({J, true}) -> - try jid:decode(J) of - JID -> - [jid:tolower(JID)] - catch _:{bad_jid, _} -> - [] - end; - (_) -> - [] - end, proplists:get_value(<<"subscribers">>, Data, [])). + try jid:decode(J) of + JID -> + [jid:tolower(JID)] + catch + _:{bad_jid, _} -> + [] + end; + (_) -> + [] + end, + proplists:get_value(<<"subscribers">>, Data, [])). + convert_node_items(Host, Data) -> Authors = proplists:get_value(<<"data_author">>, Data, []), lists:flatmap( fun({ItemId, Item}) -> - try jid:decode(proplists:get_value(ItemId, Authors, Host)) of - JID -> - [El] = deserialize(Item), - [{ItemId, JID, El#xmlel.children}] - catch _:{bad_jid, _} -> - [] - end - end, proplists:get_value(<<"data">>, Data, [])). + try jid:decode(proplists:get_value(ItemId, Authors, Host)) of + JID -> + [El] = deserialize(Item), + [{ItemId, JID, El#xmlel.children}] + catch + _:{bad_jid, _} -> + [] + end + end, + proplists:get_value(<<"data">>, Data, [])). + convert_node_config(Host, Data) -> Config = proplists:get_value(<<"config">>, Data, []), @@ -469,82 +533,87 @@ convert_node_config(Host, Data) -> {subscriptions, convert_node_subscriptions(Data)}, {owner, jid:decode(proplists:get_value(<<"creator">>, Config, Host))}, {items, convert_node_items(Host, Data)}, - {options, [ - {deliver_notifications, - proplists:get_value(<<"deliver_notifications">>, Config, true)}, - {deliver_payloads, - proplists:get_value(<<"deliver_payloads">>, Config, true)}, - {persist_items, - proplists:get_value(<<"persist_items">>, Config, true)}, - {max_items, - max_items(Config, 10)}, - {access_model, - misc:binary_to_atom(proplists:get_value(<<"access_model">>, Config, <<"open">>))}, - {publish_model, - misc:binary_to_atom(proplists:get_value(<<"publish_model">>, Config, <<"publishers">>))}, - {title, - proplists:get_value(<<"title">>, Config, <<"">>)} - ]} - ]. + {options, [{deliver_notifications, + proplists:get_value(<<"deliver_notifications">>, Config, true)}, + {deliver_payloads, + proplists:get_value(<<"deliver_payloads">>, Config, true)}, + {persist_items, + proplists:get_value(<<"persist_items">>, Config, true)}, + {max_items, + max_items(Config, 10)}, + {access_model, + misc:binary_to_atom(proplists:get_value(<<"access_model">>, Config, <<"open">>))}, + {publish_model, + misc:binary_to_atom(proplists:get_value(<<"publish_model">>, Config, <<"publishers">>))}, + {title, + proplists:get_value(<<"title">>, Config, <<"">>)}]}]. + el_to_offline_msg(LUser, LServer, #xmlel{attrs = Attrs} = El) -> try - TS = xmpp_util:decode_timestamp( - fxml:get_attr_s(<<"stamp">>, Attrs)), - Attrs1 = lists:filter( - fun({<<"stamp">>, _}) -> false; - ({<<"stamp_legacy">>, _}) -> false; - (_) -> true - end, Attrs), - El1 = El#xmlel{attrs = Attrs1}, - case xmpp:decode(El1, ?NS_CLIENT, [ignore_els]) of - #message{from = #jid{} = From, to = #jid{} = To} = Packet -> - [#offline_msg{ - us = {LUser, LServer}, - timestamp = TS, - expire = never, - from = From, - to = To, - packet = Packet}]; - _ -> - [] - end - catch _:{bad_timestamp, _} -> - []; - _:{bad_jid, _} -> - []; - _:{xmpp_codec, _} -> - [] + TS = xmpp_util:decode_timestamp( + fxml:get_attr_s(<<"stamp">>, Attrs)), + Attrs1 = lists:filter( + fun({<<"stamp">>, _}) -> false; + ({<<"stamp_legacy">>, _}) -> false; + (_) -> true + end, + Attrs), + El1 = El#xmlel{attrs = Attrs1}, + case xmpp:decode(El1, ?NS_CLIENT, [ignore_els]) of + #message{from = #jid{} = From, to = #jid{} = To} = Packet -> + [#offline_msg{ + us = {LUser, LServer}, + timestamp = TS, + expire = never, + from = From, + to = To, + packet = Packet + }]; + _ -> + [] + end + catch + _:{bad_timestamp, _} -> + []; + _:{bad_jid, _} -> + []; + _:{xmpp_codec, _} -> + [] end. + find_serverhost(Host) -> [ServerHost] = - lists:filter( - fun(ServerHost) -> - case gen_mod:is_loaded(ServerHost, mod_muc) of - true -> - lists:member(Host, gen_mod:get_module_opt_hosts(ServerHost, mod_muc)); - false -> - false - end - end, ejabberd_option:hosts()), + lists:filter( + fun(ServerHost) -> + case gen_mod:is_loaded(ServerHost, mod_muc) of + true -> + lists:member(Host, gen_mod:get_module_opt_hosts(ServerHost, mod_muc)); + false -> + false + end + end, + ejabberd_option:hosts()), ServerHost. + deserialize(L) -> deserialize(L, #xmlel{}, []). -deserialize([{Other, _}|T], El, Acc) - when (Other == <<"key">>) - or (Other == <<"when">>) - or (Other == <<"with">>) -> + +deserialize([{Other, _} | T], El, Acc) + when (Other == <<"key">>) or + (Other == <<"when">>) or + (Other == <<"with">>) -> deserialize(T, El, Acc); -deserialize([{<<"attr">>, Attrs}|T], El, Acc) -> +deserialize([{<<"attr">>, Attrs} | T], El, Acc) -> deserialize(T, El#xmlel{attrs = Attrs ++ El#xmlel.attrs}, Acc); -deserialize([{<<"name">>, Name}|T], El, Acc) -> +deserialize([{<<"name">>, Name} | T], El, Acc) -> deserialize(T, El#xmlel{name = Name}, Acc); -deserialize([{_, S}|T], #xmlel{children = Els} = El, Acc) when is_binary(S) -> - deserialize(T, El#xmlel{children = [{xmlcdata, S}|Els]}, Acc); -deserialize([{_, L}|T], #xmlel{children = Els} = El, Acc) when is_list(L) -> +deserialize([{_, S} | T], #xmlel{children = Els} = El, Acc) when is_binary(S) -> + deserialize(T, El#xmlel{children = [{xmlcdata, S} | Els]}, Acc); +deserialize([{_, L} | T], #xmlel{children = Els} = El, Acc) when is_list(L) -> deserialize(T, El#xmlel{children = deserialize(L) ++ Els}, Acc); deserialize([], #xmlel{children = Els} = El, Acc) -> - [El#xmlel{children = lists:reverse(Els)}|Acc]. + [El#xmlel{children = lists:reverse(Els)} | Acc]. diff --git a/src/proxy_protocol.erl b/src/proxy_protocol.erl index 4ce0d31b4..445b57e08 100644 --- a/src/proxy_protocol.erl +++ b/src/proxy_protocol.erl @@ -28,157 +28,171 @@ %% API -export([decode/3]). + decode(SockMod, Socket, Timeout) -> V = SockMod:recv(Socket, 6, Timeout), case V of - {ok, <<"PROXY ">>} -> - decode_v1(SockMod, Socket, Timeout); - {ok, <<16#0d, 16#0a, 16#0d, 16#0a, 16#00, 16#0d>>} -> - decode_v2(SockMod, Socket, Timeout); - _ -> - {error, eproto} + {ok, <<"PROXY ">>} -> + decode_v1(SockMod, Socket, Timeout); + {ok, <<16#0d, 16#0a, 16#0d, 16#0a, 16#00, 16#0d>>} -> + decode_v2(SockMod, Socket, Timeout); + _ -> + {error, eproto} end. + decode_v1(SockMod, Socket, Timeout) -> case read_until_rn(SockMod, Socket, <<>>, false, Timeout) of - {error, _} = Err -> - Err; - Val -> - case binary:split(Val, <<" ">>, [global]) of - [<<"TCP4">>, SAddr, DAddr, SPort, DPort] -> - try {inet_parse:ipv4strict_address(binary_to_list(SAddr)), - inet_parse:ipv4strict_address(binary_to_list(DAddr)), - binary_to_integer(SPort), - binary_to_integer(DPort)} - of - {{ok, DA}, {ok, SA}, DP, SP} -> - {{SA, SP}, {DA, DP}}; - _ -> - {error, eproto} - catch - error:badarg -> - {error, eproto} - end; - [<<"TCP6">>, SAddr, DAddr, SPort, DPort] -> - try {inet_parse:ipv6strict_address(binary_to_list(SAddr)), - inet_parse:ipv6strict_address(binary_to_list(DAddr)), - binary_to_integer(SPort), - binary_to_integer(DPort)} - of - {{ok, DA}, {ok, SA}, DP, SP} -> - {{SA, SP}, {DA, DP}}; - _ -> - {error, eproto} - catch - error:badarg -> - {error, eproto} - end; - [<<"UNKNOWN">> | _] -> - {undefined, undefined} - end + {error, _} = Err -> + Err; + Val -> + case binary:split(Val, <<" ">>, [global]) of + [<<"TCP4">>, SAddr, DAddr, SPort, DPort] -> + try {inet_parse:ipv4strict_address(binary_to_list(SAddr)), + inet_parse:ipv4strict_address(binary_to_list(DAddr)), + binary_to_integer(SPort), + binary_to_integer(DPort)} of + {{ok, DA}, {ok, SA}, DP, SP} -> + {{SA, SP}, {DA, DP}}; + _ -> + {error, eproto} + catch + error:badarg -> + {error, eproto} + end; + [<<"TCP6">>, SAddr, DAddr, SPort, DPort] -> + try {inet_parse:ipv6strict_address(binary_to_list(SAddr)), + inet_parse:ipv6strict_address(binary_to_list(DAddr)), + binary_to_integer(SPort), + binary_to_integer(DPort)} of + {{ok, DA}, {ok, SA}, DP, SP} -> + {{SA, SP}, {DA, DP}}; + _ -> + {error, eproto} + catch + error:badarg -> + {error, eproto} + end; + [<<"UNKNOWN">> | _] -> + {undefined, undefined} + end end. + decode_v2(SockMod, Socket, Timeout) -> case SockMod:recv(Socket, 10, Timeout) of - {error, _} = Err -> - Err; - {ok, <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, - 2:4, Command:4, Transport:8, AddrLen:16/big-unsigned-integer>>} -> - case SockMod:recv(Socket, AddrLen, Timeout) of - {error, _} = Err -> - Err; - {ok, Data} -> - case Command of - 0 -> - case {inet:sockname(Socket), inet:peername(Socket)} of - {{ok, SA}, {ok, DA}} -> - {SA, DA}; - {{error, _} = E, _} -> - E; - {_, {error, _} = E} -> - E - end; - 1 -> - case Transport of - % UNSPEC or UNIX - V when V == 0; V == 16#31; V == 16#32 -> - {{unknown, unknown}, {unknown, unknown}}; - % IPV4 over TCP or UDP - V when V == 16#11; V == 16#12 -> - case Data of - <> -> - {{{S1, S2, S3, S4}, SP}, - {{D1, D2, D3, D4}, DP}}; - _ -> - {error, eproto} - end; - % IPV6 over TCP or UDP - V when V == 16#21; V == 16#22 -> - case Data of - <> -> - {{{S1, S2, S3, S4, S5, S6, S7, S8}, SP}, - {{D1, D2, D3, D4, D5, D6, D7, D8}, DP}}; - _ -> - {error, eproto} - end - end; - _ -> - {error, eproto} - end - end; - <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, _/binary>> -> - {error, eproto}; - _ -> - {error, eproto} + {error, _} = Err -> + Err; + {ok, <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, + 2:4, Command:4, Transport:8, AddrLen:16/big-unsigned-integer>>} -> + case SockMod:recv(Socket, AddrLen, Timeout) of + {error, _} = Err -> + Err; + {ok, Data} -> + case Command of + 0 -> + case {inet:sockname(Socket), inet:peername(Socket)} of + {{ok, SA}, {ok, DA}} -> + {SA, DA}; + {{error, _} = E, _} -> + E; + {_, {error, _} = E} -> + E + end; + 1 -> + case Transport of + % UNSPEC or UNIX + V when V == 0; V == 16#31; V == 16#32 -> + {{unknown, unknown}, {unknown, unknown}}; + % IPV4 over TCP or UDP + V when V == 16#11; V == 16#12 -> + case Data of + <> -> + {{{S1, S2, S3, S4}, SP}, + {{D1, D2, D3, D4}, DP}}; + _ -> + {error, eproto} + end; + % IPV6 over TCP or UDP + V when V == 16#21; V == 16#22 -> + case Data of + <> -> + {{{S1, S2, S3, S4, S5, S6, S7, S8}, SP}, + {{D1, D2, D3, D4, D5, D6, D7, D8}, DP}}; + _ -> + {error, eproto} + end + end; + _ -> + {error, eproto} + end + end; + <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, _/binary>> -> + {error, eproto}; + _ -> + {error, eproto} end. + read_until_rn(_SockMod, _Socket, Data, _, _) when size(Data) > 107 -> {error, eproto}; read_until_rn(SockMod, Socket, Data, true, Timeout) -> case SockMod:recv(Socket, 1, Timeout) of - {ok, <<"\n">>} -> - Data; - {ok, <<"\r">>} -> - read_until_rn(SockMod, Socket, <>, - true, Timeout); - {ok, Other} -> - read_until_rn(SockMod, Socket, <>, - false, Timeout); - {error, _} = Err -> - Err + {ok, <<"\n">>} -> + Data; + {ok, <<"\r">>} -> + read_until_rn(SockMod, + Socket, + <>, + true, + Timeout); + {ok, Other} -> + read_until_rn(SockMod, + Socket, + <>, + false, + Timeout); + {error, _} = Err -> + Err end; read_until_rn(SockMod, Socket, Data, false, Timeout) -> case SockMod:recv(Socket, 2, Timeout) of - {ok, <<"\r\n">>} -> - Data; - {ok, <>} -> - read_until_rn(SockMod, Socket, <>, - true, Timeout); - {ok, Other} -> - read_until_rn(SockMod, Socket, <>, - false, Timeout); - {error, _} = Err -> - Err + {ok, <<"\r\n">>} -> + Data; + {ok, <>} -> + read_until_rn(SockMod, + Socket, + <>, + true, + Timeout); + {ok, Other} -> + read_until_rn(SockMod, + Socket, + <>, + false, + Timeout); + {error, _} = Err -> + Err end. diff --git a/src/pubsub_db_sql.erl b/src/pubsub_db_sql.erl index 7a789e9ea..c9938b6fd 100644 --- a/src/pubsub_db_sql.erl +++ b/src/pubsub_db_sql.erl @@ -25,30 +25,33 @@ -module(pubsub_db_sql). - -author("pablo.polvorin@process-one.net"). -include("pubsub.hrl"). -include("ejabberd_sql_pt.hrl"). --export([add_subscription/1, read_subscription/1, - delete_subscription/1, update_subscription/1]). +-export([add_subscription/1, + read_subscription/1, + delete_subscription/1, + update_subscription/1]). -export([export/1]). --spec read_subscription(SubID :: mod_pubsub:subId()) -> {ok, #pubsub_subscription{}} | notfound. + +-spec read_subscription(SubID :: mod_pubsub:subId()) -> {ok, #pubsub_subscription{}} | notfound. read_subscription(SubID) -> - case - ejabberd_sql:sql_query_t( - ?SQL("select @(opt_name)s, @(opt_value)s from pubsub_subscription_opt where subid = %(SubID)s")) - of - {selected, []} -> - notfound; - {selected, Options} -> - {ok, - #pubsub_subscription{subid = SubID, - options = lists:map(fun subscription_opt_from_sql/1, Options)}} + case ejabberd_sql:sql_query_t( + ?SQL("select @(opt_name)s, @(opt_value)s from pubsub_subscription_opt where subid = %(SubID)s")) of + {selected, []} -> + notfound; + {selected, Options} -> + {ok, + #pubsub_subscription{ + subid = SubID, + options = lists:map(fun subscription_opt_from_sql/1, Options) + }} end. + -spec delete_subscription(SubID :: mod_pubsub:subId()) -> ok. delete_subscription(SubID) -> ejabberd_sql:sql_query_t( @@ -56,10 +59,12 @@ delete_subscription(SubID) -> "where subid = %(SubID)s")), ok. + -spec update_subscription(#pubsub_subscription{}) -> ok. update_subscription(#pubsub_subscription{subid = SubId} = Sub) -> delete_subscription(SubId), add_subscription(Sub). + -spec add_subscription(#pubsub_subscription{}) -> ok. add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> lists:foreach( @@ -73,6 +78,7 @@ add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> Opts), ok. + subscription_opt_from_sql({<<"DELIVER">>, Value}) -> {deliver, sql_to_boolean(Value)}; subscription_opt_from_sql({<<"DIGEST">>, Value}) -> @@ -89,16 +95,17 @@ subscription_opt_from_sql({<<"SHOW_VALUES">>, Value}) -> {show_values, Value}; subscription_opt_from_sql({<<"SUBSCRIPTION_TYPE">>, Value}) -> {subscription_type, - case Value of - <<"items">> -> items; - <<"nodes">> -> nodes - end}; + case Value of + <<"items">> -> items; + <<"nodes">> -> nodes + end}; subscription_opt_from_sql({<<"SUBSCRIPTION_DEPTH">>, Value}) -> {subscription_depth, - case Value of - <<"all">> -> all; - N -> sql_to_integer(N) - end}. + case Value of + <<"all">> -> all; + N -> sql_to_integer(N) + end}. + subscription_opt_to_sql({deliver, Bool}) -> {<<"DELIVER">>, boolean_to_sql(Bool)}; @@ -114,85 +121,107 @@ subscription_opt_to_sql({show_values, Values}) -> {<<"SHOW_VALUES">>, Values}; subscription_opt_to_sql({subscription_type, Type}) -> {<<"SUBSCRIPTION_TYPE">>, - case Type of - items -> <<"items">>; - nodes -> <<"nodes">> - end}; + case Type of + items -> <<"items">>; + nodes -> <<"nodes">> + end}; subscription_opt_to_sql({subscription_depth, Depth}) -> {<<"SUBSCRIPTION_DEPTH">>, - case Depth of - all -> <<"all">>; - N -> integer_to_sql(N) - end}. + case Depth of + all -> <<"all">>; + N -> integer_to_sql(N) + end}. + integer_to_sql(N) -> integer_to_binary(N). + boolean_to_sql(true) -> <<"1">>; boolean_to_sql(false) -> <<"0">>. + timestamp_to_sql(T) -> xmpp_util:encode_timestamp(T). + sql_to_integer(N) -> binary_to_integer(N). + sql_to_boolean(B) -> B == <<"1">>. + sql_to_timestamp(T) -> xmpp_util:decode_timestamp(T). + export(_Server) -> - [{pubsub_node, - fun(_Host, #pubsub_node{nodeid = {Host, Node}, id = Nidx, - parents = Parents, type = Type, - options = Options}) -> - H = node_flat_sql:encode_host(Host), - Parent = case Parents of - [] -> <<>>; - [First | _] -> First - end, - [?SQL("delete from pubsub_node where nodeid=%(Nidx)d;"), - ?SQL("delete from pubsub_node_option where nodeid=%(Nidx)d;"), - ?SQL("delete from pubsub_node_owner where nodeid=%(Nidx)d;"), - ?SQL("delete from pubsub_state where nodeid=%(Nidx)d;"), - ?SQL("delete from pubsub_item where nodeid=%(Nidx)d;"), - ?SQL("insert into pubsub_node(host,node,nodeid,parent,plugin)" - " values (%(H)s, %(Node)s, %(Nidx)d, %(Parent)s, %(Type)s);")] - ++ lists:map( - fun ({Key, Value}) -> - SKey = iolist_to_binary(atom_to_list(Key)), - SValue = misc:term_to_expr(Value), - ?SQL("insert into pubsub_node_option(nodeid,name,val)" - " values (%(Nidx)d, %(SKey)s, %(SValue)s);") - end, Options); - (_Host, _R) -> - [] - end}, - {pubsub_state, - fun(_Host, #pubsub_state{stateid = {JID, Nidx}, - affiliation = Affiliation, - subscriptions = Subscriptions}) -> - J = jid:encode(JID), - S = node_flat_sql:encode_subscriptions(Subscriptions), - A = node_flat_sql:encode_affiliation(Affiliation), - [?SQL("insert into pubsub_state(nodeid,jid,affiliation,subscriptions)" - " values (%(Nidx)d, %(J)s, %(A)s, %(S)s);")]; - (_Host, _R) -> - [] - end}, - {pubsub_item, - fun(_Host, #pubsub_item{itemid = {ItemId, Nidx}, - creation = {C, _}, - modification = {M, JID}, - payload = Payload}) -> - P = jid:encode(JID), - XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), - SM = encode_now(M), - SC = encode_now(C), - [?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload)" - " values (%(ItemId)s, %(Nidx)d, %(SC)s, %(SM)s, %(P)s, %(XML)s);")]; - (_Host, _R) -> - [] - end}]. + [{pubsub_node, + fun(_Host, + #pubsub_node{ + nodeid = {Host, Node}, + id = Nidx, + parents = Parents, + type = Type, + options = Options + }) -> + H = node_flat_sql:encode_host(Host), + Parent = case Parents of + [] -> <<>>; + [First | _] -> First + end, + [?SQL("delete from pubsub_node where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_node_option where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_node_owner where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_state where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_item where nodeid=%(Nidx)d;"), + ?SQL("insert into pubsub_node(host,node,nodeid,parent,plugin)" + " values (%(H)s, %(Node)s, %(Nidx)d, %(Parent)s, %(Type)s);")] ++ + lists:map( + fun({Key, Value}) -> + SKey = iolist_to_binary(atom_to_list(Key)), + SValue = misc:term_to_expr(Value), + ?SQL("insert into pubsub_node_option(nodeid,name,val)" + " values (%(Nidx)d, %(SKey)s, %(SValue)s);") + end, + Options); + (_Host, _R) -> + [] + end}, + {pubsub_state, + fun(_Host, + #pubsub_state{ + stateid = {JID, Nidx}, + affiliation = Affiliation, + subscriptions = Subscriptions + }) -> + J = jid:encode(JID), + S = node_flat_sql:encode_subscriptions(Subscriptions), + A = node_flat_sql:encode_affiliation(Affiliation), + [?SQL("insert into pubsub_state(nodeid,jid,affiliation,subscriptions)" + " values (%(Nidx)d, %(J)s, %(A)s, %(S)s);")]; + (_Host, _R) -> + [] + end}, + {pubsub_item, + fun(_Host, + #pubsub_item{ + itemid = {ItemId, Nidx}, + creation = {C, _}, + modification = {M, JID}, + payload = Payload + }) -> + P = jid:encode(JID), + XML = str:join([ fxml:element_to_binary(X) || X <- Payload ], <<>>), + SM = encode_now(M), + SC = encode_now(C), + [?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload)" + " values (%(ItemId)s, %(Nidx)d, %(SC)s, %(SM)s, %(P)s, %(XML)s);")]; + (_Host, _R) -> + [] + end}]. + encode_now({T1, T2, T3}) -> - <<(misc:i2l(T1, 6))/binary, ":", - (misc:i2l(T2, 6))/binary, ":", + <<(misc:i2l(T1, 6))/binary, + ":", + (misc:i2l(T2, 6))/binary, + ":", (misc:i2l(T3, 6))/binary>>. diff --git a/src/pubsub_index.erl b/src/pubsub_index.erl index 0c34ea63b..13044e63f 100644 --- a/src/pubsub_index.erl +++ b/src/pubsub_index.erl @@ -33,32 +33,36 @@ -export([init/3, new/1, free/2]). + init(_Host, _ServerHost, _Opts) -> - ejabberd_mnesia:create(?MODULE, pubsub_index, - [{disc_copies, [node()]}, - {attributes, record_info(fields, pubsub_index)}]). + ejabberd_mnesia:create(?MODULE, + pubsub_index, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_index)}]). + new(Index) -> case mnesia:read({pubsub_index, Index}) of - [I] -> - case I#pubsub_index.free of - [] -> - Id = I#pubsub_index.last + 1, - mnesia:write(I#pubsub_index{last = Id}), - Id; - [Id | Free] -> - mnesia:write(I#pubsub_index{free = Free}), Id - end; - _ -> - mnesia:write(#pubsub_index{index = Index, last = 1, free = []}), - 1 + [I] -> + case I#pubsub_index.free of + [] -> + Id = I#pubsub_index.last + 1, + mnesia:write(I#pubsub_index{last = Id}), + Id; + [Id | Free] -> + mnesia:write(I#pubsub_index{free = Free}), Id + end; + _ -> + mnesia:write(#pubsub_index{index = Index, last = 1, free = []}), + 1 end. + free(Index, Id) -> case mnesia:read({pubsub_index, Index}) of - [I] -> - Free = I#pubsub_index.free, - mnesia:write(I#pubsub_index{free = [Id | Free]}); - _ -> - ok + [I] -> + Free = I#pubsub_index.free, + mnesia:write(I#pubsub_index{free = [Id | Free]}); + _ -> + ok end. diff --git a/src/pubsub_migrate.erl b/src/pubsub_migrate.erl index 8d9fc6198..155547e72 100644 --- a/src/pubsub_migrate.erl +++ b/src/pubsub_migrate.erl @@ -31,420 +31,475 @@ -export([update_node_database/2, update_state_database/2]). -export([update_item_database/2, update_lastitem_database/2]). + update_item_database(_Host, _ServerHost) -> convert_list_items(). + update_node_database(Host, ServerHost) -> mnesia:del_table_index(pubsub_node, type), mnesia:del_table_index(pubsub_node, parentid), case catch mnesia:table_info(pubsub_node, attributes) of - [host_node, host_parent, info] -> - ?INFO_MSG("Upgrading pubsub nodes table...", []), - F = fun () -> - {Result, LastIdx} = lists:foldl(fun ({pubsub_node, - NodeId, ParentId, - {nodeinfo, Items, - Options, - Entities}}, - {RecList, - NodeIdx}) -> - ItemsList = - lists:foldl(fun - ({item, - IID, - Publisher, - Payload}, - Acc) -> - C = - {unknown, - Publisher}, - M = - {erlang:timestamp(), - Publisher}, - mnesia:write(#pubsub_item{itemid - = - {IID, - NodeIdx}, - creation - = - C, - modification - = - M, - payload - = - Payload}), - [{Publisher, - IID} - | Acc] - end, - [], - Items), - Owners = - dict:fold(fun - (JID, - {entity, - Aff, - Sub}, - Acc) -> - UsrItems = - lists:foldl(fun - ({P, - I}, - IAcc) -> - case - P - of - JID -> - [I - | IAcc]; - _ -> - IAcc - end - end, - [], - ItemsList), - mnesia:write({pubsub_state, - {JID, - NodeIdx}, - UsrItems, - Aff, - Sub}), - case - Aff - of - owner -> - [JID - | Acc]; - _ -> - Acc - end - end, - [], - Entities), - mnesia:delete({pubsub_node, - NodeId}), - {[#pubsub_node{nodeid - = - NodeId, - id - = - NodeIdx, - parents - = - [element(2, - ParentId)], - owners - = - Owners, - options - = - Options} - | RecList], - NodeIdx + 1} - end, - {[], 1}, - mnesia:match_object({pubsub_node, - {Host, - '_'}, - '_', - '_'})), - mnesia:write(#pubsub_index{index = node, last = LastIdx, - free = []}), - Result - end, - {atomic, NewRecords} = mnesia:transaction(F), - {atomic, ok} = mnesia:delete_table(pubsub_node), - {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_node, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, - pubsub_node)}]), - FNew = fun () -> - lists:foreach(fun (Record) -> mnesia:write(Record) end, - NewRecords) - end, - case mnesia:transaction(FNew) of - {atomic, Result} -> - ?INFO_MSG("Pubsub nodes table upgraded: ~p", - [Result]); - {aborted, Reason} -> - ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", - [Reason]) - end; - [nodeid, parentid, type, owners, options] -> - F = fun ({pubsub_node, NodeId, {_, Parent}, Type, - Owners, Options}) -> - #pubsub_node{nodeid = NodeId, id = 0, - parents = [Parent], type = Type, - owners = Owners, options = Options} - end, - mnesia:transform_table(pubsub_node, F, - [nodeid, id, parents, type, owners, options]), - FNew = fun () -> - LastIdx = lists:foldl(fun (#pubsub_node{nodeid = - NodeId} = - PubsubNode, - NodeIdx) -> - mnesia:write(PubsubNode#pubsub_node{id - = - NodeIdx}), - lists:foreach(fun - (#pubsub_state{stateid - = - StateId} = - State) -> - {JID, - _} = - StateId, - mnesia:delete({pubsub_state, - StateId}), - mnesia:write(State#pubsub_state{stateid - = - {JID, - NodeIdx}}) - end, - mnesia:match_object(#pubsub_state{stateid - = - {'_', - NodeId}, - _ - = - '_'})), - lists:foreach(fun - (#pubsub_item{itemid - = - ItemId} = - Item) -> - {IID, - _} = - ItemId, - {M1, - M2} = - Item#pubsub_item.modification, - {C1, - C2} = - Item#pubsub_item.creation, - mnesia:delete({pubsub_item, - ItemId}), - mnesia:write(Item#pubsub_item{itemid - = - {IID, - NodeIdx}, - modification - = - {M2, - M1}, - creation - = - {C2, - C1}}) - end, - mnesia:match_object(#pubsub_item{itemid - = - {'_', - NodeId}, - _ - = - '_'})), - NodeIdx + 1 - end, - 1, - mnesia:match_object({pubsub_node, - {Host, '_'}, - '_', '_', - '_', '_', - '_'}) - ++ - mnesia:match_object({pubsub_node, - {{'_', - ServerHost, - '_'}, - '_'}, - '_', '_', - '_', '_', - '_'})), - mnesia:write(#pubsub_index{index = node, - last = LastIdx, free = []}) - end, - case mnesia:transaction(FNew) of - {atomic, Result} -> - rename_default_nodeplugin(), - ?INFO_MSG("Pubsub nodes table upgraded: ~p", - [Result]); - {aborted, Reason} -> - ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", - [Reason]) - end; - [nodeid, id, parent, type, owners, options] -> - F = fun ({pubsub_node, NodeId, Id, Parent, Type, Owners, - Options}) -> - #pubsub_node{nodeid = NodeId, id = Id, - parents = [Parent], type = Type, - owners = Owners, options = Options} - end, - mnesia:transform_table(pubsub_node, F, - [nodeid, id, parents, type, owners, options]), - rename_default_nodeplugin(); - _ -> ok + [host_node, host_parent, info] -> + ?INFO_MSG("Upgrading pubsub nodes table...", []), + F = fun() -> + {Result, LastIdx} = lists:foldl(fun({pubsub_node, + NodeId, + ParentId, + {nodeinfo, Items, + Options, + Entities}}, + {RecList, + NodeIdx}) -> + ItemsList = + lists:foldl(fun({item, + IID, + Publisher, + Payload}, + Acc) -> + C = + {unknown, + Publisher}, + M = + {erlang:timestamp(), + Publisher}, + mnesia:write(#pubsub_item{ + itemid = + {IID, + NodeIdx}, + creation = + C, + modification = + M, + payload = + Payload + }), + [{Publisher, + IID} | Acc] + end, + [], + Items), + Owners = + dict:fold(fun(JID, + {entity, + Aff, + Sub}, + Acc) -> + UsrItems = + lists:foldl(fun({P, + I}, + IAcc) -> + case P of + JID -> + [I | IAcc]; + _ -> + IAcc + end + end, + [], + ItemsList), + mnesia:write({pubsub_state, + {JID, + NodeIdx}, + UsrItems, + Aff, + Sub}), + case Aff of + owner -> + [JID | Acc]; + _ -> + Acc + end + end, + [], + Entities), + mnesia:delete({pubsub_node, + NodeId}), + {[#pubsub_node{ + nodeid = + NodeId, + id = + NodeIdx, + parents = + [element(2, + ParentId)], + owners = + Owners, + options = + Options + } | RecList], + NodeIdx + 1} + end, + {[], 1}, + mnesia:match_object({pubsub_node, + {Host, + '_'}, + '_', + '_'})), + mnesia:write(#pubsub_index{ + index = node, + last = LastIdx, + free = [] + }), + Result + end, + {atomic, NewRecords} = mnesia:transaction(F), + {atomic, ok} = mnesia:delete_table(pubsub_node), + {atomic, ok} = ejabberd_mnesia:create(?MODULE, + pubsub_node, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, + pubsub_node)}]), + FNew = fun() -> + lists:foreach(fun(Record) -> mnesia:write(Record) end, + NewRecords) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + ?INFO_MSG("Pubsub nodes table upgraded: ~p", + [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", + [Reason]) + end; + [nodeid, parentid, type, owners, options] -> + F = fun({pubsub_node, NodeId, + {_, Parent}, + Type, + Owners, + Options}) -> + #pubsub_node{ + nodeid = NodeId, + id = 0, + parents = [Parent], + type = Type, + owners = Owners, + options = Options + } + end, + mnesia:transform_table(pubsub_node, + F, + [nodeid, id, parents, type, owners, options]), + FNew = fun() -> + LastIdx = lists:foldl(fun(#pubsub_node{ + nodeid = + NodeId + } = + PubsubNode, + NodeIdx) -> + mnesia:write(PubsubNode#pubsub_node{ + id = + NodeIdx + }), + lists:foreach(fun(#pubsub_state{ + stateid = + StateId + } = + State) -> + {JID, + _} = + StateId, + mnesia:delete({pubsub_state, + StateId}), + mnesia:write(State#pubsub_state{ + stateid = + {JID, + NodeIdx} + }) + end, + mnesia:match_object(#pubsub_state{ + stateid = + {'_', + NodeId}, + _ = + '_' + })), + lists:foreach(fun(#pubsub_item{ + itemid = + ItemId + } = + Item) -> + {IID, + _} = + ItemId, + {M1, + M2} = + Item#pubsub_item.modification, + {C1, + C2} = + Item#pubsub_item.creation, + mnesia:delete({pubsub_item, + ItemId}), + mnesia:write(Item#pubsub_item{ + itemid = + {IID, + NodeIdx}, + modification = + {M2, + M1}, + creation = + {C2, + C1} + }) + end, + mnesia:match_object(#pubsub_item{ + itemid = + {'_', + NodeId}, + _ = + '_' + })), + NodeIdx + 1 + end, + 1, + mnesia:match_object({pubsub_node, + {Host, '_'}, + '_', + '_', + '_', + '_', + '_'}) ++ + mnesia:match_object({pubsub_node, + {{'_', + ServerHost, + '_'}, + '_'}, + '_', + '_', + '_', + '_', + '_'})), + mnesia:write(#pubsub_index{ + index = node, + last = LastIdx, + free = [] + }) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + rename_default_nodeplugin(), + ?INFO_MSG("Pubsub nodes table upgraded: ~p", + [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", + [Reason]) + end; + [nodeid, id, parent, type, owners, options] -> + F = fun({pubsub_node, NodeId, Id, Parent, Type, Owners, + Options}) -> + #pubsub_node{ + nodeid = NodeId, + id = Id, + parents = [Parent], + type = Type, + owners = Owners, + options = Options + } + end, + mnesia:transform_table(pubsub_node, + F, + [nodeid, id, parents, type, owners, options]), + rename_default_nodeplugin(); + _ -> ok end, convert_list_nodes(). + rename_default_nodeplugin() -> - lists:foreach(fun (Node) -> - mnesia:dirty_write(Node#pubsub_node{type = - <<"hometree">>}) - end, - mnesia:dirty_match_object(#pubsub_node{type = - <<"default">>, - _ = '_'})). + lists:foreach(fun(Node) -> + mnesia:dirty_write(Node#pubsub_node{ + type = + <<"hometree">> + }) + end, + mnesia:dirty_match_object(#pubsub_node{ + type = + <<"default">>, + _ = '_' + })). + update_state_database(_Host, _ServerHost) -> -% useless starting from ejabberd 17.04 -% case catch mnesia:table_info(pubsub_state, attributes) of -% [stateid, nodeidx, items, affiliation, subscriptions] -> -% ?INFO_MSG("Upgrading pubsub states table...", []), -% F = fun ({pubsub_state, {{U,S,R}, NodeID}, _NodeIdx, Items, Aff, Sub}, Acc) -> -% JID = {U,S,R}, -% Subs = case Sub of -% none -> -% []; -% [] -> -% []; -% _ -> -% SubID = pubsub_subscription:make_subid(), -% [{Sub, SubID}] -% end, -% NewState = #pubsub_state{stateid = {JID, NodeID}, -% items = Items, -% affiliation = Aff, -% subscriptions = Subs}, -% [NewState | Acc] -% end, -% {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, -% [F, [], pubsub_state]), -% {atomic, ok} = mnesia:delete_table(pubsub_state), -% {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_state, -% [{disc_copies, [node()]}, -% {attributes, record_info(fields, pubsub_state)}]), -% FNew = fun () -> -% lists:foreach(fun mnesia:write/1, NewRecs) -% end, -% case mnesia:transaction(FNew) of -% {atomic, Result} -> -% ?INFO_MSG("Pubsub states table upgraded: ~p", -% [Result]); -% {aborted, Reason} -> -% ?ERROR_MSG("Problem upgrading Pubsub states table:~n~p", -% [Reason]) -% end; -% _ -> -% ok -% end, + % useless starting from ejabberd 17.04 + % case catch mnesia:table_info(pubsub_state, attributes) of + % [stateid, nodeidx, items, affiliation, subscriptions] -> + % ?INFO_MSG("Upgrading pubsub states table...", []), + % F = fun ({pubsub_state, {{U,S,R}, NodeID}, _NodeIdx, Items, Aff, Sub}, Acc) -> + % JID = {U,S,R}, + % Subs = case Sub of + % none -> + % []; + % [] -> + % []; + % _ -> + % SubID = pubsub_subscription:make_subid(), + % [{Sub, SubID}] + % end, + % NewState = #pubsub_state{stateid = {JID, NodeID}, + % items = Items, + % affiliation = Aff, + % subscriptions = Subs}, + % [NewState | Acc] + % end, + % {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, + % [F, [], pubsub_state]), + % {atomic, ok} = mnesia:delete_table(pubsub_state), + % {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_state, + % [{disc_copies, [node()]}, + % {attributes, record_info(fields, pubsub_state)}]), + % FNew = fun () -> + % lists:foreach(fun mnesia:write/1, NewRecs) + % end, + % case mnesia:transaction(FNew) of + % {atomic, Result} -> + % ?INFO_MSG("Pubsub states table upgraded: ~p", + % [Result]); + % {aborted, Reason} -> + % ?ERROR_MSG("Problem upgrading Pubsub states table:~n~p", + % [Reason]) + % end; + % _ -> + % ok + % end, convert_list_subscriptions(), convert_list_states(). + update_lastitem_database(_Host, _ServerHost) -> convert_list_lasts(). + %% binarization from old 2.1.x + convert_list_items() -> convert_list_records( - pubsub_item, - record_info(fields, pubsub_item), - fun(#pubsub_item{itemid = {I, _}}) -> I end, - fun(#pubsub_item{itemid = {I, Nidx}, - creation = {C, CKey}, - modification = {M, MKey}, - payload = Els} = R) -> - R#pubsub_item{itemid = {bin(I), Nidx}, - creation = {C, binusr(CKey)}, - modification = {M, binusr(MKey)}, - payload = [fxml:to_xmlel(El) || El<-Els]} - end). + pubsub_item, + record_info(fields, pubsub_item), + fun(#pubsub_item{itemid = {I, _}}) -> I end, + fun(#pubsub_item{ + itemid = {I, Nidx}, + creation = {C, CKey}, + modification = {M, MKey}, + payload = Els + } = R) -> + R#pubsub_item{ + itemid = {bin(I), Nidx}, + creation = {C, binusr(CKey)}, + modification = {M, binusr(MKey)}, + payload = [ fxml:to_xmlel(El) || El <- Els ] + } + end). + convert_list_states() -> convert_list_records( - pubsub_state, - record_info(fields, pubsub_state), - fun(#pubsub_state{stateid = {{U,_,_}, _}}) -> U end, - fun(#pubsub_state{stateid = {U, Nidx}, - items = Is, - affiliation = A, - subscriptions = Ss} = R) -> - R#pubsub_state{stateid = {binusr(U), Nidx}, - items = [bin(I) || I<-Is], - affiliation = A, - subscriptions = [{S,bin(Sid)} || {S,Sid}<-Ss]} - end). + pubsub_state, + record_info(fields, pubsub_state), + fun(#pubsub_state{stateid = {{U, _, _}, _}}) -> U end, + fun(#pubsub_state{ + stateid = {U, Nidx}, + items = Is, + affiliation = A, + subscriptions = Ss + } = R) -> + R#pubsub_state{ + stateid = {binusr(U), Nidx}, + items = [ bin(I) || I <- Is ], + affiliation = A, + subscriptions = [ {S, bin(Sid)} || {S, Sid} <- Ss ] + } + end). + convert_list_nodes() -> convert_list_records( - pubsub_node, - record_info(fields, pubsub_node), - fun(#pubsub_node{nodeid = {{U,_,_}, _}}) -> U; - (#pubsub_node{nodeid = {H, _}}) -> H end, - fun(#pubsub_node{nodeid = {H, N}, - id = I, - parents = Ps, - type = T, - owners = Os, - options = Opts} = R) -> - R#pubsub_node{nodeid = {binhost(H), bin(N)}, - id = I, - parents = [bin(P) || P<-Ps], - type = bin(T), - owners = [binusr(O) || O<-Os], - options = Opts} - end). + pubsub_node, + record_info(fields, pubsub_node), + fun(#pubsub_node{nodeid = {{U, _, _}, _}}) -> U; + (#pubsub_node{nodeid = {H, _}}) -> H + end, + fun(#pubsub_node{ + nodeid = {H, N}, + id = I, + parents = Ps, + type = T, + owners = Os, + options = Opts + } = R) -> + R#pubsub_node{ + nodeid = {binhost(H), bin(N)}, + id = I, + parents = [ bin(P) || P <- Ps ], + type = bin(T), + owners = [ binusr(O) || O <- Os ], + options = Opts + } + end). + convert_list_subscriptions() -> - [convert_list_records( - pubsub_subscription, - record_info(fields, pubsub_subscription), - fun(#pubsub_subscription{subid = I}) -> I end, - fun(#pubsub_subscription{subid = I, - options = Opts} = R) -> - R#pubsub_subscription{subid = bin(I), - options = Opts} - end) || lists:member(pubsub_subscription, mnesia:system_info(tables))]. + [ convert_list_records( + pubsub_subscription, + record_info(fields, pubsub_subscription), + fun(#pubsub_subscription{subid = I}) -> I end, + fun(#pubsub_subscription{ + subid = I, + options = Opts + } = R) -> + R#pubsub_subscription{ + subid = bin(I), + options = Opts + } + end) || lists:member(pubsub_subscription, mnesia:system_info(tables)) ]. + convert_list_lasts() -> convert_list_records( - pubsub_last_item, - record_info(fields, pubsub_last_item), - fun(#pubsub_last_item{itemid = I}) -> I end, - fun(#pubsub_last_item{itemid = I, - nodeid = Nidx, - creation = {C, CKey}, - payload = Payload} = R) -> - R#pubsub_last_item{itemid = bin(I), - nodeid = Nidx, - creation = {C, binusr(CKey)}, - payload = fxml:to_xmlel(Payload)} - end). + pubsub_last_item, + record_info(fields, pubsub_last_item), + fun(#pubsub_last_item{itemid = I}) -> I end, + fun(#pubsub_last_item{ + itemid = I, + nodeid = Nidx, + creation = {C, CKey}, + payload = Payload + } = R) -> + R#pubsub_last_item{ + itemid = bin(I), + nodeid = Nidx, + creation = {C, binusr(CKey)}, + payload = fxml:to_xmlel(Payload) + } + end). + %% internal tools + convert_list_records(Tab, Fields, DetectFun, ConvertFun) -> case mnesia:table_info(Tab, attributes) of - Fields -> - convert_table_to_binary( - Tab, Fields, set, DetectFun, ConvertFun); - _ -> - ?INFO_MSG("Recreating ~p table", [Tab]), - mnesia:transform_table(Tab, ignore, Fields), - convert_list_records(Tab, Fields, DetectFun, ConvertFun) + Fields -> + convert_table_to_binary( + Tab, Fields, set, DetectFun, ConvertFun); + _ -> + ?INFO_MSG("Recreating ~p table", [Tab]), + mnesia:transform_table(Tab, ignore, Fields), + convert_list_records(Tab, Fields, DetectFun, ConvertFun) end. -binhost({U,S,R}) -> binusr({U,S,R}); + +binhost({U, S, R}) -> binusr({U, S, R}); binhost(L) -> bin(L). -binusr({U,S,R}) -> {bin(U), bin(S), bin(R)}. + +binusr({U, S, R}) -> {bin(U), bin(S), bin(R)}. + bin(L) -> iolist_to_binary(L). + %% The code should be updated to support new ejabberd_mnesia %% transform functions (i.e. need_transform/1 and transform/1) convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> @@ -453,12 +508,13 @@ convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> ?INFO_MSG("Converting '~ts' table from strings to binaries.", [Tab]), TmpTab = list_to_atom(atom_to_list(Tab) ++ "_tmp_table"), catch mnesia:delete_table(TmpTab), - case ejabberd_mnesia:create(?MODULE, TmpTab, - [{disc_only_copies, [node()]}, - {type, Type}, - {local_content, true}, - {record_name, Tab}, - {attributes, Fields}]) of + case ejabberd_mnesia:create(?MODULE, + TmpTab, + [{disc_only_copies, [node()]}, + {type, Type}, + {local_content, true}, + {record_name, Tab}, + {attributes, Fields}]) of {atomic, ok} -> mnesia:transform_table(Tab, ignore, Fields), case mnesia:transaction( @@ -468,7 +524,9 @@ convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> fun(R, _) -> NewR = ConvertFun(R), mnesia:dirty_write(TmpTab, NewR) - end, ok, Tab) + end, + ok, + Tab) end) of {atomic, ok} -> mnesia:clear_table(Tab), @@ -478,7 +536,9 @@ convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> mnesia:foldl( fun(R, _) -> mnesia:dirty_write(R) - end, ok, TmpTab) + end, + ok, + TmpTab) end) of {atomic, ok} -> mnesia:delete_table(TmpTab); @@ -495,9 +555,11 @@ convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> ok end. + is_table_still_list(Tab, DetectFun) -> is_table_still_list(Tab, DetectFun, mnesia:dirty_first(Tab)). + is_table_still_list(_Tab, _DetectFun, '$end_of_table') -> false; is_table_still_list(Tab, DetectFun, Key) -> @@ -513,7 +575,9 @@ is_table_still_list(Tab, DetectFun, Key) -> El -> is_list(El) end - end, '$next', Rs), + end, + '$next', + Rs), case Res of true -> true; @@ -523,6 +587,7 @@ is_table_still_list(Tab, DetectFun, Key) -> is_table_still_list(Tab, DetectFun, mnesia:dirty_next(Tab, Key)) end. + report_and_stop(Tab, Err) -> ErrTxt = lists:flatten( io_lib:format( diff --git a/src/pubsub_subscription.erl b/src/pubsub_subscription.erl index 6db643af6..3d4507ba8 100644 --- a/src/pubsub_subscription.erl +++ b/src/pubsub_subscription.erl @@ -28,118 +28,134 @@ -author("bjc@kublai.com"). %% API --export([init/3, subscribe_node/3, unsubscribe_node/3, - get_subscription/3, set_subscription/4, - make_subid/0, - get_options_xform/2, parse_options_xform/1]). +-export([init/3, + subscribe_node/3, + unsubscribe_node/3, + get_subscription/3, + set_subscription/4, + make_subid/0, + get_options_xform/2, + parse_options_xform/1]). % Internal function also exported for use in transactional bloc from pubsub plugins --export([add_subscription/3, delete_subscription/3, - read_subscription/3, write_subscription/4]). +-export([add_subscription/3, + delete_subscription/3, + read_subscription/3, + write_subscription/4]). -include("pubsub.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). --define(PUBSUB_DELIVER, <<"pubsub#deliver">>). --define(PUBSUB_DIGEST, <<"pubsub#digest">>). --define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). --define(PUBSUB_EXPIRE, <<"pubsub#expire">>). --define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). --define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). --define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). --define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). --define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). --define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " - "(aggregations) of notifications or all notifications individually">>). --define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " - "sending any two notification digests">>). --define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). --define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " - "XMPP message body in addition to the payload format">>). --define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). --define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). --define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). --define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). --define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). --define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). --define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). --define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). +-define(PUBSUB_DELIVER, <<"pubsub#deliver">>). +-define(PUBSUB_DIGEST, <<"pubsub#digest">>). +-define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). +-define(PUBSUB_EXPIRE, <<"pubsub#expire">>). +-define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). +-define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). +-define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). +-define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). +-define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). +-define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " + "(aggregations) of notifications or all notifications individually">>). +-define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " + "sending any two notification digests">>). +-define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). +-define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " + "XMPP message body in addition to the payload format">>). +-define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). +-define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). +-define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). +-define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). +-define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). +-define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). +-define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). +-define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). -define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, <<"Receive notification of new items only">>). -define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, <<"Receive notification of new nodes only">>). --define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). --define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). + %%==================================================================== %% API %%==================================================================== init(_Host, _ServerHost, _Opts) -> ok = create_table(). + subscribe_node(JID, NodeId, Options) -> - case catch mnesia:sync_dirty(fun add_subscription/3, [JID, NodeId, Options]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} + case catch mnesia:sync_dirty(fun add_subscription/3, [JID, NodeId, Options]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. + unsubscribe_node(JID, NodeId, SubID) -> - case catch mnesia:sync_dirty(fun delete_subscription/3, [JID, NodeId, SubID]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} + case catch mnesia:sync_dirty(fun delete_subscription/3, [JID, NodeId, SubID]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. + get_subscription(JID, NodeId, SubID) -> - case catch mnesia:sync_dirty(fun read_subscription/3, [JID, NodeId, SubID]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} + case catch mnesia:sync_dirty(fun read_subscription/3, [JID, NodeId, SubID]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. + set_subscription(JID, NodeId, SubID, Options) -> - case catch mnesia:sync_dirty(fun write_subscription/4, [JID, NodeId, SubID, Options]) - of - {'EXIT', {aborted, Error}} -> Error; - {error, Error} -> {error, Error}; - Result -> {result, Result} + case catch mnesia:sync_dirty(fun write_subscription/4, [JID, NodeId, SubID, Options]) of + {'EXIT', {aborted, Error}} -> Error; + {error, Error} -> {error, Error}; + Result -> {result, Result} end. get_options_xform(Lang, Options) -> Keys = [deliver, show_values, subscription_type, subscription_depth], - XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], + XFields = [ get_option_xfield(Lang, Key, Options) || Key <- Keys ], {result, - #xdata{type = form, - fields = [#xdata_field{type = hidden, - var = <<"FORM_TYPE">>, - values = [?NS_PUBSUB_SUB_OPTIONS]}| - XFields]}}. + #xdata{ + type = form, + fields = [#xdata_field{ + type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_SUB_OPTIONS] + } | XFields] + }}. + parse_options_xform(XFields) -> Opts = set_xoption(XFields, []), {result, Opts}. + %%==================================================================== %% Internal functions %%==================================================================== create_table() -> - case ejabberd_mnesia:create(?MODULE, pubsub_subscription, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, pubsub_subscription)}, - {type, set}]) - of - {atomic, ok} -> ok; - {aborted, {already_exists, _}} -> ok; - Other -> Other + case ejabberd_mnesia:create(?MODULE, + pubsub_subscription, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, pubsub_subscription)}, + {type, set}]) of + {atomic, ok} -> ok; + {aborted, {already_exists, _}} -> ok; + Other -> Other end. --spec add_subscription(_JID :: ljid(), _NodeId :: mod_pubsub:nodeIdx(), - Options :: [] | mod_pubsub:subOptions()) -> - SubId :: mod_pubsub:subId(). + +-spec add_subscription(_JID :: ljid(), + _NodeId :: mod_pubsub:nodeIdx(), + Options :: [] | mod_pubsub:subOptions()) -> + SubId :: mod_pubsub:subId(). add_subscription(_JID, _NodeId, []) -> make_subid(); add_subscription(_JID, _NodeId, Options) -> @@ -147,47 +163,56 @@ add_subscription(_JID, _NodeId, Options) -> mnesia:write(#pubsub_subscription{subid = SubID, options = Options}), SubID. + -spec delete_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId()) -> ok. delete_subscription(_JID, _NodeId, SubID) -> mnesia:delete({pubsub_subscription, SubID}). + -spec read_subscription(_JID :: ljid(), _NodeId :: _, SubID :: mod_pubsub:subId()) -> - mod_pubsub:pubsubSubscription() | {error, notfound}. + mod_pubsub:pubsubSubscription() | {error, notfound}. read_subscription(_JID, _NodeId, SubID) -> case mnesia:read({pubsub_subscription, SubID}) of - [Sub] -> Sub; - _ -> {error, notfound} + [Sub] -> Sub; + _ -> {error, notfound} end. --spec write_subscription(_JID :: ljid(), _NodeId :: _, SubID :: mod_pubsub:subId(), - Options :: mod_pubsub:subOptions()) -> ok. + +-spec write_subscription(_JID :: ljid(), + _NodeId :: _, + SubID :: mod_pubsub:subId(), + Options :: mod_pubsub:subOptions()) -> ok. write_subscription(_JID, _NodeId, SubID, Options) -> mnesia:write(#pubsub_subscription{subid = SubID, options = Options}). --spec make_subid() -> SubId::mod_pubsub:subId(). + +-spec make_subid() -> SubId :: mod_pubsub:subId(). make_subid() -> {T1, T2, T3} = erlang:timestamp(), (str:format("~.16B~.16B~.16B", [T1, T2, T3])). + %% %% Subscription XForm processing. %% + %% Return processed options, with types converted and so forth, using %% Opts as defaults. set_xoption([], Opts) -> Opts; set_xoption([{Var, Value} | T], Opts) -> NewOpts = case var_xfield(Var) of - {error, _} -> Opts; - Key -> - Val = val_xfield(Key, Value), - lists:keystore(Key, 1, Opts, {Key, Val}) - end, + {error, _} -> Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, set_xoption(T, NewOpts). + %% Return the options list's key for an XForm var. %% Convert Values for option list's Key. var_xfield(?PUBSUB_DELIVER) -> deliver; @@ -200,20 +225,23 @@ var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. + val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest_frequency = Opt, [Val]) -> case catch binary_to_integer(Val) of - N when is_integer(N) -> N; - _ -> - Txt = {?T("Value of '~s' should be integer"), [Opt]}, - {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(expire = Opt, [Val]) -> - try xmpp_util:decode_timestamp(Val) - catch _:{bad_timestamp, _} -> - Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, - {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + try + xmpp_util:decode_timestamp(Val) + catch + _:{bad_timestamp, _} -> + Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; @@ -222,12 +250,13 @@ val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; val_xfield(subscription_depth = Opt, [Depth]) -> case catch binary_to_integer(Depth) of - N when is_integer(N) -> N; - _ -> - Txt = {?T("Value of '~s' should be integer"), [Opt]}, - {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end. + %% Convert XForm booleans to Erlang booleans. xopt_to_bool(_, <<"0">>) -> false; xopt_to_bool(_, <<"1">>) -> true; @@ -237,6 +266,7 @@ xopt_to_bool(Option, _) -> Txt = {?T("Value of '~s' should be boolean"), [Option]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())}. + %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. get_option_xfield(Lang, Key, Options) -> @@ -244,23 +274,31 @@ get_option_xfield(Lang, Key, Options) -> Label = xfield_label(Key), {Type, OptEls} = type_and_options(xfield_type(Key), Lang), Vals = case lists:keysearch(Key, 1, Options) of - {value, {_, Val}} -> - [xfield_val(Key, Val)]; - false -> - [] - end, - #xdata_field{type = Type, var = Var, - label = translate:translate(Lang, Label), - values = Vals, - options = OptEls}. + {value, {_, Val}} -> + [xfield_val(Key, Val)]; + false -> + [] + end, + #xdata_field{ + type = Type, + var = Var, + label = translate:translate(Lang, Label), + values = Vals, + options = OptEls + }. + type_and_options({Type, Options}, Lang) -> - {Type, [tr_xfield_options(O, Lang) || O <- Options]}; + {Type, [ tr_xfield_options(O, Lang) || O <- Options ]}; type_and_options(Type, _Lang) -> {Type, []}. + tr_xfield_options({Value, Label}, Lang) -> - #xdata_option{label = translate:translate(Lang, Label), - value = Value}. + #xdata_option{ + label = translate:translate(Lang, Label), + value = Value + }. + xfield_var(deliver) -> ?PUBSUB_DELIVER; %xfield_var(digest) -> ?PUBSUB_DIGEST; @@ -271,6 +309,7 @@ xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. + xfield_type(deliver) -> boolean; %xfield_type(digest) -> boolean; %xfield_type(digest_frequency) -> 'text-single'; @@ -278,19 +317,20 @@ xfield_type(deliver) -> boolean; %xfield_type(include_body) -> boolean; xfield_type(show_values) -> {'list-multi', - [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, - {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, - {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, - {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, - {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; + [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, + {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, + {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, + {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, + {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; xfield_type(subscription_type) -> {'list-single', - [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, - {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; + [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, + {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; xfield_type(subscription_depth) -> {'list-single', - [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, - {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, + {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + %% Return the XForm variable label for a subscription option key. xfield_label(deliver) -> ?DELIVER_LABEL; @@ -304,6 +344,7 @@ xfield_label(show_values) -> ?SHOW_VALUES_LABEL; xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. + xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest_frequency, Val) -> diff --git a/src/pubsub_subscription_sql.erl b/src/pubsub_subscription_sql.erl index 8f1361b47..625c4b5b4 100644 --- a/src/pubsub_subscription_sql.erl +++ b/src/pubsub_subscription_sql.erl @@ -28,131 +28,158 @@ -author("pablo.polvorin@process-one.net"). %% API --export([init/3, subscribe_node/3, unsubscribe_node/3, - get_subscription/3, set_subscription/4, - make_subid/0, - get_options_xform/2, parse_options_xform/1]). +-export([init/3, + subscribe_node/3, + unsubscribe_node/3, + get_subscription/3, + set_subscription/4, + make_subid/0, + get_options_xform/2, + parse_options_xform/1]). -include("pubsub.hrl"). + -include_lib("xmpp/include/xmpp.hrl"). + -include("translate.hrl"). --define(PUBSUB_DELIVER, <<"pubsub#deliver">>). --define(PUBSUB_DIGEST, <<"pubsub#digest">>). --define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). --define(PUBSUB_EXPIRE, <<"pubsub#expire">>). --define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). --define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). --define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). --define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). --define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). --define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " - "(aggregations) of notifications or all notifications individually">>). --define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " - "sending any two notification digests">>). --define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). --define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " - "XMPP message body in addition to the payload format">>). --define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). --define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). --define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). --define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). --define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). --define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). --define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). --define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). +-define(PUBSUB_DELIVER, <<"pubsub#deliver">>). +-define(PUBSUB_DIGEST, <<"pubsub#digest">>). +-define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). +-define(PUBSUB_EXPIRE, <<"pubsub#expire">>). +-define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). +-define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). +-define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). +-define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). +-define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). +-define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " + "(aggregations) of notifications or all notifications individually">>). +-define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " + "sending any two notification digests">>). +-define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). +-define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " + "XMPP message body in addition to the payload format">>). +-define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). +-define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). +-define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). +-define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). +-define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). +-define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). +-define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). +-define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). -define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, <<"Receive notification of new items only">>). -define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, <<"Receive notification of new nodes only">>). --define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). --define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). -define(DB_MOD, pubsub_db_sql). %%==================================================================== %% API %%==================================================================== + init(_Host, _ServerHost, _Opts) -> ok = create_table(). + -spec subscribe_node(_JID :: _, _NodeId :: _, Options :: [] | mod_pubsub:subOptions()) -> - {result, mod_pubsub:subId()}. + {result, mod_pubsub:subId()}. subscribe_node(_JID, _NodeId, Options) -> SubID = make_subid(), (?DB_MOD):add_subscription(#pubsub_subscription{subid = SubID, options = Options}), {result, SubID}. + -spec unsubscribe_node(_JID :: _, _NodeId :: _, SubID :: mod_pubsub:subId()) -> - {result, mod_pubsub:subscription()} | {error, notfound}. + {result, mod_pubsub:subscription()} | {error, notfound}. unsubscribe_node(_JID, _NodeId, SubID) -> case (?DB_MOD):read_subscription(SubID) of - {ok, Sub} -> (?DB_MOD):delete_subscription(SubID), {result, Sub}; - notfound -> {error, notfound} + {ok, Sub} -> (?DB_MOD):delete_subscription(SubID), {result, Sub}; + notfound -> {error, notfound} end. + -spec get_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId()) -> - {result, mod_pubsub:subscription()} | {error, notfound}. + {result, mod_pubsub:subscription()} | {error, notfound}. get_subscription(_JID, _NodeId, SubID) -> case (?DB_MOD):read_subscription(SubID) of - {ok, Sub} -> {result, Sub}; - notfound -> {error, notfound} + {ok, Sub} -> {result, Sub}; + notfound -> {error, notfound} end. --spec set_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId(), - Options :: mod_pubsub:subOptions()) -> {result, ok}. + +-spec set_subscription(_JID :: _, + _NodeId :: _, + SubId :: mod_pubsub:subId(), + Options :: mod_pubsub:subOptions()) -> {result, ok}. set_subscription(_JID, _NodeId, SubID, Options) -> case (?DB_MOD):read_subscription(SubID) of - {ok, _} -> - (?DB_MOD):update_subscription(#pubsub_subscription{subid = SubID, - options = Options}), - {result, ok}; - notfound -> - (?DB_MOD):add_subscription(#pubsub_subscription{subid = SubID, - options = Options}), - {result, ok} + {ok, _} -> + (?DB_MOD):update_subscription(#pubsub_subscription{ + subid = SubID, + options = Options + }), + {result, ok}; + notfound -> + (?DB_MOD):add_subscription(#pubsub_subscription{ + subid = SubID, + options = Options + }), + {result, ok} end. + get_options_xform(Lang, Options) -> Keys = [deliver, show_values, subscription_type, subscription_depth], - XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], + XFields = [ get_option_xfield(Lang, Key, Options) || Key <- Keys ], {result, - #xdata{type = form, - fields = [#xdata_field{type = hidden, - var = <<"FORM_TYPE">>, - values = [?NS_PUBSUB_SUB_OPTIONS]}| - XFields]}}. + #xdata{ + type = form, + fields = [#xdata_field{ + type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_SUB_OPTIONS] + } | XFields] + }}. + parse_options_xform(XFields) -> Opts = set_xoption(XFields, []), {result, Opts}. + %%==================================================================== %% Internal functions %%==================================================================== create_table() -> ok. + -spec make_subid() -> mod_pubsub:subId(). make_subid() -> {T1, T2, T3} = erlang:timestamp(), (str:format("~.16B~.16B~.16B", [T1, T2, T3])). + %% %% Subscription XForm processing. %% + %% Return processed options, with types converted and so forth, using %% Opts as defaults. set_xoption([], Opts) -> Opts; set_xoption([{Var, Value} | T], Opts) -> NewOpts = case var_xfield(Var) of - {error, _} -> Opts; - Key -> - Val = val_xfield(Key, Value), - lists:keystore(Key, 1, Opts, {Key, Val}) - end, + {error, _} -> Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, set_xoption(T, NewOpts). + %% Return the options list's key for an XForm var. %% Convert Values for option list's Key. var_xfield(?PUBSUB_DELIVER) -> deliver; @@ -165,20 +192,23 @@ var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. + val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest_frequency = Opt, [Val]) -> case catch binary_to_integer(Val) of - N when is_integer(N) -> N; - _ -> - Txt = {?T("Value of '~s' should be integer"), [Opt]}, - {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(expire = Opt, [Val]) -> - try xmpp_util:decode_timestamp(Val) - catch _:{bad_timestamp, _} -> - Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, - {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + try + xmpp_util:decode_timestamp(Val) + catch + _:{bad_timestamp, _} -> + Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; @@ -187,12 +217,13 @@ val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; val_xfield(subscription_depth = Opt, [Depth]) -> case catch binary_to_integer(Depth) of - N when is_integer(N) -> N; - _ -> - Txt = {?T("Value of '~s' should be integer"), [Opt]}, - {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} + N when is_integer(N) -> N; + _ -> + Txt = {?T("Value of '~s' should be integer"), [Opt]}, + {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end. + %% Convert XForm booleans to Erlang booleans. xopt_to_bool(_, <<"0">>) -> false; xopt_to_bool(_, <<"1">>) -> true; @@ -202,6 +233,7 @@ xopt_to_bool(Option, _) -> Txt = {?T("Value of '~s' should be boolean"), [Option]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())}. + %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. get_option_xfield(Lang, Key, Options) -> @@ -209,23 +241,31 @@ get_option_xfield(Lang, Key, Options) -> Label = xfield_label(Key), {Type, OptEls} = type_and_options(xfield_type(Key), Lang), Vals = case lists:keysearch(Key, 1, Options) of - {value, {_, Val}} -> - [xfield_val(Key, Val)]; - false -> - [] - end, - #xdata_field{type = Type, var = Var, - label = translate:translate(Lang, Label), - values = Vals, - options = OptEls}. + {value, {_, Val}} -> + [xfield_val(Key, Val)]; + false -> + [] + end, + #xdata_field{ + type = Type, + var = Var, + label = translate:translate(Lang, Label), + values = Vals, + options = OptEls + }. + type_and_options({Type, Options}, Lang) -> - {Type, [tr_xfield_options(O, Lang) || O <- Options]}; + {Type, [ tr_xfield_options(O, Lang) || O <- Options ]}; type_and_options(Type, _Lang) -> {Type, []}. + tr_xfield_options({Value, Label}, Lang) -> - #xdata_option{label = translate:translate(Lang, Label), - value = Value}. + #xdata_option{ + label = translate:translate(Lang, Label), + value = Value + }. + xfield_var(deliver) -> ?PUBSUB_DELIVER; %xfield_var(digest) -> ?PUBSUB_DIGEST; @@ -236,6 +276,7 @@ xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. + xfield_type(deliver) -> boolean; %xfield_type(digest) -> boolean; %xfield_type(digest_frequency) -> 'text-single'; @@ -257,6 +298,7 @@ xfield_type(subscription_depth) -> [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + %% Return the XForm variable label for a subscription option key. xfield_label(deliver) -> ?DELIVER_LABEL; %xfield_label(digest) -> ?DIGEST_LABEL; @@ -269,6 +311,7 @@ xfield_label(show_values) -> ?SHOW_VALUES_LABEL; xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. + xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest_frequency, Val) -> @@ -283,5 +326,6 @@ xfield_val(subscription_depth, all) -> [<<"all">>]; xfield_val(subscription_depth, N) -> [integer_to_binary(N)]. + bool_to_xopt(false) -> <<"false">>; bool_to_xopt(true) -> <<"true">>. diff --git a/src/rest.erl b/src/rest.erl index b456fdaac..d2a3b5d3e 100644 --- a/src/rest.erl +++ b/src/rest.erl @@ -25,15 +25,23 @@ -module(rest). --export([start/1, stop/1, get/2, get/3, post/4, delete/2, - put/4, patch/4, request/6, with_retry/4, +-export([start/1, + stop/1, + get/2, get/3, + post/4, + delete/2, + put/4, + patch/4, + request/6, + with_retry/4, encode_json/1]). -include("logger.hrl"). --define(HTTP_TIMEOUT, 10000). +-define(HTTP_TIMEOUT, 10000). -define(CONNECT_TIMEOUT, 8000). --define(CONTENT_TYPE, "application/json"). +-define(CONTENT_TYPE, "application/json"). + start(Host) -> application:start(inets), @@ -47,57 +55,71 @@ start(Host) -> end, httpc:set_options([{max_sessions, Size}] ++ Proxy). + stop(_Host) -> ok. + with_retry(Method, Args, MaxRetries, Backoff) -> with_retry(Method, Args, 0, MaxRetries, Backoff). + + with_retry(Method, Args, Retries, MaxRetries, Backoff) -> case apply(?MODULE, Method, Args) of %% Only retry on timeout errors - {error, {http_error,{error,Error}}} - when Retries < MaxRetries - andalso (Error == 'timeout' orelse Error == 'connect_timeout') -> + {error, {http_error, {error, Error}}} + when Retries < MaxRetries andalso + (Error == 'timeout' orelse Error == 'connect_timeout') -> timer:sleep(round(math:pow(2, Retries)) * Backoff), - with_retry(Method, Args, Retries+1, MaxRetries, Backoff); + with_retry(Method, Args, Retries + 1, MaxRetries, Backoff); Result -> Result end. + get(Server, Path) -> request(Server, get, Path, [], ?CONTENT_TYPE, <<>>). + + get(Server, Path, Params) -> request(Server, get, Path, Params, ?CONTENT_TYPE, <<>>). + delete(Server, Path) -> request(Server, delete, Path, [], ?CONTENT_TYPE, <<>>). + post(Server, Path, Params, Content) -> Data = encode_json(Content), request(Server, post, Path, Params, ?CONTENT_TYPE, Data). + put(Server, Path, Params, Content) -> Data = encode_json(Content), request(Server, put, Path, Params, ?CONTENT_TYPE, Data). + patch(Server, Path, Params, Content) -> Data = encode_json(Content), request(Server, patch, Path, Params, ?CONTENT_TYPE, Data). + request(Server, Method, Path, _Params, _Mime, {error, Error}) -> - ejabberd_hooks:run(backend_api_error, Server, + ejabberd_hooks:run(backend_api_error, + Server, [Server, Method, Path, Error]), {error, Error}; request(Server, Method, Path, Params, Mime, Data) -> {Query, Opts} = case Params of - {_, _} -> Params; - _ -> {Params, []} - end, + {_, _} -> Params; + _ -> {Params, []} + end, URI = to_list(url(Server, Path, Query)), HttpOpts = case {ejabberd_option:rest_proxy_username(Server), ejabberd_option:rest_proxy_password(Server)} of - {"", _} -> [{connect_timeout, ?CONNECT_TIMEOUT}, - {timeout, ?HTTP_TIMEOUT}]; + {"", _} -> + [{connect_timeout, ?CONNECT_TIMEOUT}, + {timeout, ?HTTP_TIMEOUT}]; {User, Pass} -> [{connect_timeout, ?CONNECT_TIMEOUT}, {timeout, ?HTTP_TIMEOUT}, @@ -105,8 +127,8 @@ request(Server, Method, Path, Params, Mime, Data) -> end, Hdrs = [{"connection", "keep-alive"}, {"Accept", "application/json"}, - {"User-Agent", "ejabberd"}] - ++ custom_headers(Server), + {"User-Agent", "ejabberd"}] ++ + custom_headers(Server), Req = if (Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse (Method =:= delete) -> {URI, Hdrs, to_list(Mime), Data}; @@ -116,50 +138,57 @@ request(Server, Method, Path, Params, Mime, Data) -> Begin = os:timestamp(), ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]), Result = try httpc:request(Method, Req, HttpOpts, [{body_format, binary}]) of - {ok, {{_, Code, _}, RetHdrs, Body}} -> - try decode_json(Body) of - JSon -> - case proplists:get_bool(return_headers, Opts) of - true -> {ok, Code, RetHdrs, JSon}; - false -> {ok, Code, JSon} - end - catch - _:Reason -> - {error, {invalid_json, Body, Reason}} - end; - {error, Reason} -> - {error, {http_error, {error, Reason}}} - catch - exit:Reason -> - {error, {http_error, {error, Reason}}} - end, + {ok, {{_, Code, _}, RetHdrs, Body}} -> + try decode_json(Body) of + JSon -> + case proplists:get_bool(return_headers, Opts) of + true -> {ok, Code, RetHdrs, JSon}; + false -> {ok, Code, JSon} + end + catch + _:Reason -> + {error, {invalid_json, Body, Reason}} + end; + {error, Reason} -> + {error, {http_error, {error, Reason}}} + catch + exit:Reason -> + {error, {http_error, {error, Reason}}} + end, case Result of {error, {http_error, {error, timeout}}} -> - ejabberd_hooks:run(backend_api_timeout, Server, + ejabberd_hooks:run(backend_api_timeout, + Server, [Server, Method, Path]); {error, {http_error, {error, connect_timeout}}} -> - ejabberd_hooks:run(backend_api_timeout, Server, + ejabberd_hooks:run(backend_api_timeout, + Server, [Server, Method, Path]); {error, Error} -> - ejabberd_hooks:run(backend_api_error, Server, + ejabberd_hooks:run(backend_api_error, + Server, [Server, Method, Path, Error]); _ -> End = os:timestamp(), - Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms - ejabberd_hooks:run(backend_api_response_time, Server, + Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms + ejabberd_hooks:run(backend_api_response_time, + Server, [Server, Method, Path, Elapsed]) end, Result. + %%%---------------------------------------------------------------------- %%% HTTP helpers %%%---------------------------------------------------------------------- + to_list(V) when is_binary(V) -> binary_to_list(V); to_list(V) when is_list(V) -> V. + encode_json(Content) -> case catch misc:json_encode(Content) of {'EXIT', Reason} -> @@ -168,70 +197,89 @@ encode_json(Content) -> Encoded end. + decode_json(<<>>) -> []; decode_json(<<" ">>) -> []; decode_json(<<"\r\n">>) -> []; decode_json(Data) -> misc:json_decode(Data). + custom_headers(Server) -> - case ejabberd_option:ext_api_headers(Server) of + case ejabberd_option:ext_api_headers(Server) of <<>> -> []; Hdrs -> lists:foldr(fun(Hdr, Acc) -> - case binary:split(Hdr, <<":">>) of - [K, V] -> [{binary_to_list(K), binary_to_list(V)}|Acc]; - _ -> Acc - end - end, [], binary:split(Hdrs, <<",">>)) + case binary:split(Hdr, <<":">>) of + [K, V] -> [{binary_to_list(K), binary_to_list(V)} | Acc]; + _ -> Acc + end + end, + [], + binary:split(Hdrs, <<",">>)) end. + base_url(Server, Path) -> BPath = case iolist_to_binary(Path) of - <<$/, Ok/binary>> -> Ok; - Ok -> Ok - end, + <<$/, Ok/binary>> -> Ok; + Ok -> Ok + end, Url = case BPath of - <<"http", _/binary>> -> BPath; - _ -> - Base = ejabberd_option:ext_api_url(Server), - case binary:last(Base) of - $/ -> <>; - _ -> <> - end - end, + <<"http", _/binary>> -> BPath; + _ -> + Base = ejabberd_option:ext_api_url(Server), + case binary:last(Base) of + $/ -> <>; + _ -> <> + end + end, case binary:last(Url) of - 47 -> binary_part(Url, 0, size(Url)-1); + 47 -> binary_part(Url, 0, size(Url) - 1); _ -> Url end. + -ifdef(HAVE_URI_STRING). + + uri_hack(Str) -> case uri_string:normalize("%25") of - "%" -> % This hack around bug in httpc >21 <23.2 + "%" -> % 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], + L = [ <<"&", + (iolist_to_binary(Key))/binary, + "=", + (misc:url_encode(Value))/binary>> + || {Key, Value} <- Params ], <<$&, Encoded0/binary>> = iolist_to_binary(L), Encoded = uri_hack(Encoded0), <>. + + url(Server, Path, Params) -> case binary:split(base_url(Server, Path), <<"?">>) of [Url] -> url(Url, Params); [Url, Extra] -> - Custom = [list_to_tuple(binary:split(P, <<"=">>)) - || P <- binary:split(Extra, <<"&">>, [global])], - url(Url, Custom++Params) + Custom = [ list_to_tuple(binary:split(P, <<"=">>)) + || P <- binary:split(Extra, <<"&">>, [global]) ], + url(Url, Custom ++ Params) end. diff --git a/src/str.erl b/src/str.erl index 6dafcfda6..28071c0ae 100644 --- a/src/str.erl +++ b/src/str.erl @@ -36,28 +36,20 @@ span/2, cspan/2, copies/2, - words/1, - words/2, - sub_word/2, - sub_word/3, - strip/1, - strip/2, + words/1, words/2, + sub_word/2, sub_word/3, + strip/1, strip/2, len/1, tokens/2, - left/2, - left/3, - right/2, - right/3, - centre/2, - centre/3, - sub_string/2, - sub_string/3, + left/2, left/3, + right/2, right/3, + centre/2, centre/3, + sub_string/2, sub_string/3, to_upper/1, join/2, substr/2, chr/2, - chars/3, - chars/2, + chars/3, chars/2, substr/3, strip/3, to_lower/1, @@ -70,6 +62,7 @@ to_hexlist/1, translate_and_format/3]). + %%%=================================================================== %%% API %%%=================================================================== @@ -78,119 +71,142 @@ len(B) -> byte_size(B). + -spec equal(binary(), binary()) -> boolean(). equal(B1, B2) -> B1 == B2. + -spec concat(binary(), binary()) -> binary(). concat(B1, B2) -> <>. + -spec rchr(binary(), char()) -> non_neg_integer(). rchr(B, C) -> string:rchr(binary_to_list(B), C). + -spec str(binary(), binary()) -> non_neg_integer(). str(B1, B2) -> case binary:match(B1, B2) of - {R, _Len} -> R+1; - _ -> 0 + {R, _Len} -> R + 1; + _ -> 0 end. + -spec rstr(binary(), binary()) -> non_neg_integer(). rstr(B1, B2) -> string:rstr(binary_to_list(B1), binary_to_list(B2)). + -spec span(binary(), binary()) -> non_neg_integer(). span(B1, B2) -> string:span(binary_to_list(B1), binary_to_list(B2)). + -spec cspan(binary(), binary()) -> non_neg_integer(). cspan(B1, B2) -> string:cspan(binary_to_list(B1), binary_to_list(B2)). + -spec copies(binary(), non_neg_integer()) -> binary(). copies(B, N) -> binary:copy(B, N). + -spec words(binary()) -> pos_integer(). words(B) -> string:words(binary_to_list(B)). + -spec words(binary(), char()) -> pos_integer(). words(B, C) -> string:words(binary_to_list(B), C). + -spec sub_word(binary(), integer()) -> binary(). sub_word(B, N) -> iolist_to_binary(string:sub_word(binary_to_list(B), N)). + -spec sub_word(binary(), integer(), char()) -> binary(). sub_word(B, N, C) -> iolist_to_binary(string:sub_word(binary_to_list(B), N, C)). + -spec strip(binary()) -> binary(). strip(B) -> iolist_to_binary(string:strip(binary_to_list(B))). + -spec strip(binary(), both | left | right) -> binary(). strip(B, D) -> iolist_to_binary(string:strip(binary_to_list(B), D)). + -spec left(binary(), non_neg_integer()) -> binary(). left(B, N) -> iolist_to_binary(string:left(binary_to_list(B), N)). + -spec left(binary(), non_neg_integer(), char()) -> binary(). left(B, N, C) -> iolist_to_binary(string:left(binary_to_list(B), N, C)). + -spec right(binary(), non_neg_integer()) -> binary(). right(B, N) -> iolist_to_binary(string:right(binary_to_list(B), N)). + -spec right(binary(), non_neg_integer(), char()) -> binary(). right(B, N, C) -> iolist_to_binary(string:right(binary_to_list(B), N, C)). + -spec centre(binary(), non_neg_integer()) -> binary(). centre(B, N) -> iolist_to_binary(string:centre(binary_to_list(B), N)). + -spec centre(binary(), non_neg_integer(), char()) -> binary(). centre(B, N, C) -> iolist_to_binary(string:centre(binary_to_list(B), N, C)). + -spec sub_string(binary(), pos_integer()) -> binary(). sub_string(B, N) -> iolist_to_binary(string:sub_string(binary_to_list(B), N)). + -spec sub_string(binary(), pos_integer(), pos_integer()) -> binary(). sub_string(B, S, E) -> iolist_to_binary(string:sub_string(binary_to_list(B), S, E)). + -spec to_upper(binary()) -> binary(); (char()) -> char(). @@ -199,41 +215,49 @@ to_upper(B) when is_binary(B) -> to_upper(C) -> string:to_upper(C). + -spec join([binary()], binary() | char()) -> binary(). join(L, Sep) -> iolist_to_binary(join_s(L, Sep)). + -spec substr(binary(), pos_integer()) -> binary(). substr(B, N) -> - binary_part(B, N-1, byte_size(B)-N+1). + binary_part(B, N - 1, byte_size(B) - N + 1). + -spec chr(binary(), char()) -> non_neg_integer(). chr(B, C) -> string:chr(binary_to_list(B), C). + -spec chars(char(), non_neg_integer(), binary()) -> binary(). chars(C, N, B) -> iolist_to_binary(string:chars(C, N, binary_to_list(B))). + -spec chars(char(), non_neg_integer()) -> binary(). chars(C, N) -> iolist_to_binary(string:chars(C, N)). + -spec substr(binary(), pos_integer(), non_neg_integer()) -> binary(). substr(B, S, E) -> - binary_part(B, S-1, E). + binary_part(B, S - 1, E). + -spec strip(binary(), both | left | right, char()) -> binary(). strip(B, D, C) -> iolist_to_binary(string:strip(binary_to_list(B), D, C)). + -spec to_lower(binary()) -> binary(); (char()) -> char(). @@ -242,11 +266,13 @@ to_lower(B) when is_binary(B) -> to_lower(C) -> string:to_lower(C). + -spec tokens(binary(), binary()) -> [binary()]. tokens(B1, B2) -> - [iolist_to_binary(T) || - T <- string:tokens(binary_to_list(B1), binary_to_list(B2))]. + [ iolist_to_binary(T) + || T <- string:tokens(binary_to_list(B1), binary_to_list(B2)) ]. + -spec to_float(binary()) -> {float(), binary()} | {error, no_float}. @@ -258,6 +284,7 @@ to_float(B) -> {Float, iolist_to_binary(Rest)} end. + -spec to_integer(binary()) -> {integer(), binary()} | {error, no_integer}. to_integer(B) -> @@ -268,6 +295,7 @@ to_integer(B) -> {Int, iolist_to_binary(Rest)} end. + -spec prefix(binary(), binary()) -> boolean(). prefix(Prefix, B) -> @@ -279,16 +307,19 @@ prefix(Prefix, B) -> false end. + -spec suffix(binary(), binary()) -> boolean(). suffix(B1, B2) -> lists:suffix(binary_to_list(B1), binary_to_list(B2)). + -spec format(io:format(), list()) -> binary(). format(Format, Args) -> unicode:characters_to_binary(io_lib:format(Format, Args)). + -spec translate_and_format(binary(), binary(), list()) -> binary(). translate_and_format(Lang, Format, Args) -> @@ -301,6 +332,7 @@ sha(Text) -> Bin = crypto:hash(sha, Text), to_hexlist(Bin). + -spec to_hexlist(binary()) -> binary(). to_hexlist(S) when is_list(S) -> @@ -308,13 +340,15 @@ to_hexlist(S) when is_list(S) -> to_hexlist(Bin) when is_binary(Bin) -> << <<(digit_to_xchar(N div 16)), (digit_to_xchar(N rem 16))>> || <> <= Bin >>. + %%%=================================================================== %%% Internal functions %%%=================================================================== join_s([], _Sep) -> []; -join_s([H|T], Sep) -> - [H, [[Sep, X] || X <- T]]. +join_s([H | T], Sep) -> + [H, [ [Sep, X] || X <- T ]]. + digit_to_xchar(D) when (D >= 0) and (D < 10) -> D + $0; digit_to_xchar(D) -> D + $a - 10. diff --git a/src/translate.erl b/src/translate.erl index 5adde6e24..47bc98cab 100644 --- a/src/translate.erl +++ b/src/translate.erl @@ -31,269 +31,308 @@ -export([start_link/0, reload/0, translate/2]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("logger.hrl"). + -include_lib("kernel/include/file.hrl"). --define(ZERO_DATETIME, {{0,0,0}, {0,0,0}}). +-define(ZERO_DATETIME, {{0, 0, 0}, {0, 0, 0}}). --type error_reason() :: file:posix() | {integer(), module(), term()} | - badarg | terminated | system_limit | bad_file | - bad_encoding. +-type error_reason() :: file:posix() | + {integer(), module(), term()} | + badarg | + terminated | + system_limit | + bad_file | + bad_encoding. -record(state, {}). + start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + init([]) -> process_flag(trap_exit, true), case load() of - ok -> - xmpp:set_tr_callback({?MODULE, translate}), - {ok, #state{}}; - {error, Reason} -> - {stop, Reason} + ok -> + xmpp:set_tr_callback({?MODULE, translate}), + {ok, #state{}}; + {error, Reason} -> + {stop, Reason} end. + handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. + handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. + handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. + terminate(_Reason, _State) -> xmpp:set_tr_callback(undefined). + code_change(_OldVsn, State, _Extra) -> {ok, State}. + -spec reload() -> ok | {error, error_reason()}. reload() -> load(true). + -spec load() -> ok | {error, error_reason()}. load() -> load(false). + -spec load(boolean()) -> ok | {error, error_reason()}. load(ForceCacheRebuild) -> {MsgsDirMTime, MsgsDir} = get_msg_dir(), {CacheMTime, CacheFile} = get_cache_file(), {FilesMTime, MsgFiles} = get_msg_files(MsgsDir), LastModified = lists:max([MsgsDirMTime, FilesMTime]), - if ForceCacheRebuild orelse CacheMTime < LastModified -> - case load(MsgFiles, MsgsDir) of - ok -> dump_to_file(CacheFile); - Err -> Err - end; - true -> - case ets:file2tab(CacheFile) of - {ok, _} -> - ok; - {error, {read_error, {file_error, _, enoent}}} -> - load(MsgFiles, MsgsDir); - {error, {read_error, {file_error, _, Reason}}} -> - ?WARNING_MSG("Failed to read translation cache from ~ts: ~ts", - [CacheFile, format_error(Reason)]), - load(MsgFiles, MsgsDir); - {error, Reason} -> - ?WARNING_MSG("Failed to read translation cache from ~ts: ~p", - [CacheFile, Reason]), - load(MsgFiles, MsgsDir) - end + if + ForceCacheRebuild orelse CacheMTime < LastModified -> + case load(MsgFiles, MsgsDir) of + ok -> dump_to_file(CacheFile); + Err -> Err + end; + true -> + case ets:file2tab(CacheFile) of + {ok, _} -> + ok; + {error, {read_error, {file_error, _, enoent}}} -> + load(MsgFiles, MsgsDir); + {error, {read_error, {file_error, _, Reason}}} -> + ?WARNING_MSG("Failed to read translation cache from ~ts: ~ts", + [CacheFile, format_error(Reason)]), + load(MsgFiles, MsgsDir); + {error, Reason} -> + ?WARNING_MSG("Failed to read translation cache from ~ts: ~p", + [CacheFile, Reason]), + load(MsgFiles, MsgsDir) + end end. + -spec load([file:filename()], file:filename()) -> ok | {error, error_reason()}. load(Files, Dir) -> try ets:new(translations, [named_table, public]) of - _ -> ok - catch _:badarg -> ok + _ -> ok + catch + _:badarg -> ok end, case Files of - [] -> - ?WARNING_MSG("No translation files found in ~ts, " - "check directory access", - [Dir]); - _ -> - ?INFO_MSG("Building language translation cache", []), - Objs = lists:flatten(misc:pmap(fun load_file/1, Files)), - case lists:keyfind(error, 1, Objs) of - false -> - ets:delete_all_objects(translations), - ets:insert(translations, Objs), - ?DEBUG("Language translation cache built successfully", []); - {error, File, Reason} -> - ?ERROR_MSG("Failed to read translation file ~ts: ~ts", - [File, format_error(Reason)]), - {error, Reason} - end + [] -> + ?WARNING_MSG("No translation files found in ~ts, " + "check directory access", + [Dir]); + _ -> + ?INFO_MSG("Building language translation cache", []), + Objs = lists:flatten(misc:pmap(fun load_file/1, Files)), + case lists:keyfind(error, 1, Objs) of + false -> + ets:delete_all_objects(translations), + ets:insert(translations, Objs), + ?DEBUG("Language translation cache built successfully", []); + {error, File, Reason} -> + ?ERROR_MSG("Failed to read translation file ~ts: ~ts", + [File, format_error(Reason)]), + {error, Reason} + end end. + -spec load_file(file:filename()) -> [{{binary(), binary()}, binary()} | - {error, file:filename(), error_reason()}]. + {error, file:filename(), error_reason()}]. load_file(File) -> Lang = lang_of_file(File), try file:consult(File) of - {ok, Lines} -> - lists:map( - fun({In, Out}) -> - try {unicode:characters_to_binary(In), - unicode:characters_to_binary(Out)} of - {InB, OutB} when is_binary(InB), is_binary(OutB) -> - {{Lang, InB}, OutB}; - _ -> - {error, File, bad_encoding} - catch _:badarg -> - {error, File, bad_encoding} - end; - (_) -> - {error, File, bad_file} - end, Lines); - {error, Reason} -> - [{error, File, Reason}] - catch _:{case_clause, {error, _}} -> - %% At the moment of the writing there was a bug in - %% file:consult_stream/3 - it doesn't process {error, term()} - %% result from io:read/3 - [{error, File, bad_file}] + {ok, Lines} -> + lists:map( + fun({In, Out}) -> + try {unicode:characters_to_binary(In), + unicode:characters_to_binary(Out)} of + {InB, OutB} when is_binary(InB), is_binary(OutB) -> + {{Lang, InB}, OutB}; + _ -> + {error, File, bad_encoding} + catch + _:badarg -> + {error, File, bad_encoding} + end; + (_) -> + {error, File, bad_file} + end, + Lines); + {error, Reason} -> + [{error, File, Reason}] + catch + _:{case_clause, {error, _}} -> + %% At the moment of the writing there was a bug in + %% file:consult_stream/3 - it doesn't process {error, term()} + %% result from io:read/3 + [{error, File, bad_file}] end. + -spec translate(binary(), binary()) -> binary(). translate(Lang, Msg) -> LLang = ascii_tolower(Lang), case ets:lookup(translations, {LLang, Msg}) of - [{_, Trans}] -> Trans; - _ -> - ShortLang = case str:tokens(LLang, <<"-">>) of - [] -> LLang; - [SL | _] -> SL - end, - case ShortLang of - <<"en">> -> Msg; - LLang -> translate(Msg); - _ -> - case ets:lookup(translations, {ShortLang, Msg}) of - [{_, Trans}] -> Trans; - _ -> translate(Msg) - end - end + [{_, Trans}] -> Trans; + _ -> + ShortLang = case str:tokens(LLang, <<"-">>) of + [] -> LLang; + [SL | _] -> SL + end, + case ShortLang of + <<"en">> -> Msg; + LLang -> translate(Msg); + _ -> + case ets:lookup(translations, {ShortLang, Msg}) of + [{_, Trans}] -> Trans; + _ -> translate(Msg) + end + end end. + -spec translate(binary()) -> binary(). translate(Msg) -> case ejabberd_option:language() of - <<"en">> -> Msg; - Lang -> - LLang = ascii_tolower(Lang), - case ets:lookup(translations, {LLang, Msg}) of - [{_, Trans}] -> Trans; - _ -> - ShortLang = case str:tokens(LLang, <<"-">>) of - [] -> LLang; - [SL | _] -> SL - end, - case ShortLang of - <<"en">> -> Msg; - Lang -> Msg; - _ -> - case ets:lookup(translations, {ShortLang, Msg}) of - [{_, Trans}] -> Trans; - _ -> Msg - end - end - end + <<"en">> -> Msg; + Lang -> + LLang = ascii_tolower(Lang), + case ets:lookup(translations, {LLang, Msg}) of + [{_, Trans}] -> Trans; + _ -> + ShortLang = case str:tokens(LLang, <<"-">>) of + [] -> LLang; + [SL | _] -> SL + end, + case ShortLang of + <<"en">> -> Msg; + Lang -> Msg; + _ -> + case ets:lookup(translations, {ShortLang, Msg}) of + [{_, Trans}] -> Trans; + _ -> Msg + end + end + end end. + -spec ascii_tolower(list() | binary()) -> binary(). ascii_tolower(B) when is_binary(B) -> - << <<(if X >= $A, X =< $Z -> + << <<(if + X >= $A, X =< $Z -> X + 32; - true -> + true -> X end)>> || <> <= B >>; ascii_tolower(S) -> ascii_tolower(unicode:characters_to_binary(S)). + -spec get_msg_dir() -> {calendar:datetime(), file:filename()}. get_msg_dir() -> Dir = misc:msgs_dir(), case file:read_file_info(Dir) of - {ok, #file_info{mtime = MTime}} -> - {MTime, Dir}; - {error, Reason} -> - ?ERROR_MSG("Failed to read directory ~ts: ~ts", - [Dir, format_error(Reason)]), - {?ZERO_DATETIME, Dir} + {ok, #file_info{mtime = MTime}} -> + {MTime, Dir}; + {error, Reason} -> + ?ERROR_MSG("Failed to read directory ~ts: ~ts", + [Dir, format_error(Reason)]), + {?ZERO_DATETIME, Dir} end. + -spec get_msg_files(file:filename()) -> {calendar:datetime(), [file:filename()]}. get_msg_files(MsgsDir) -> Res = filelib:fold_files( - MsgsDir, ".+\\.msg", false, - fun(File, {MTime, Files} = Acc) -> - case xmpp_lang:is_valid(lang_of_file(File)) of - true -> - case file:read_file_info(File) of - {ok, #file_info{mtime = Time}} -> - {lists:max([MTime, Time]), [File|Files]}; - {error, Reason} -> - ?ERROR_MSG("Failed to read translation file ~ts: ~ts", - [File, format_error(Reason)]), - Acc - end; - false -> - ?WARNING_MSG("Ignoring translation file ~ts: file name " - "must be a valid language tag", - [File]), - Acc - end - end, {?ZERO_DATETIME, []}), + MsgsDir, + ".+\\.msg", + false, + fun(File, {MTime, Files} = Acc) -> + case xmpp_lang:is_valid(lang_of_file(File)) of + true -> + case file:read_file_info(File) of + {ok, #file_info{mtime = Time}} -> + {lists:max([MTime, Time]), [File | Files]}; + {error, Reason} -> + ?ERROR_MSG("Failed to read translation file ~ts: ~ts", + [File, format_error(Reason)]), + Acc + end; + false -> + ?WARNING_MSG("Ignoring translation file ~ts: file name " + "must be a valid language tag", + [File]), + Acc + end + end, + {?ZERO_DATETIME, []}), case Res of - {_, []} -> - case file:list_dir(MsgsDir) of - {ok, _} -> ok; - {error, Reason} -> - ?ERROR_MSG("Failed to read directory ~ts: ~ts", - [MsgsDir, format_error(Reason)]) - end; - _ -> - ok + {_, []} -> + case file:list_dir(MsgsDir) of + {ok, _} -> ok; + {error, Reason} -> + ?ERROR_MSG("Failed to read directory ~ts: ~ts", + [MsgsDir, format_error(Reason)]) + end; + _ -> + ok end, Res. + -spec get_cache_file() -> {calendar:datetime(), file:filename()}. get_cache_file() -> MnesiaDir = mnesia:system_info(directory), CacheFile = filename:join(MnesiaDir, "translations.cache"), CacheMTime = case file:read_file_info(CacheFile) of - {ok, #file_info{mtime = Time}} -> Time; - {error, _} -> ?ZERO_DATETIME - end, + {ok, #file_info{mtime = Time}} -> Time; + {error, _} -> ?ZERO_DATETIME + end, {CacheMTime, CacheFile}. + -spec dump_to_file(file:filename()) -> ok. dump_to_file(CacheFile) -> case ets:tab2file(translations, CacheFile) of - ok -> ok; - {error, Reason} -> - ?WARNING_MSG("Failed to create translation cache in ~ts: ~p", - [CacheFile, Reason]) + ok -> ok; + {error, Reason} -> + ?WARNING_MSG("Failed to create translation cache in ~ts: ~p", + [CacheFile, Reason]) end. + -spec lang_of_file(file:filename()) -> binary(). lang_of_file(FileName) -> BaseName = filename:basename(FileName), ascii_tolower(filename:rootname(BaseName)). + -spec format_error(error_reason()) -> string(). format_error(bad_file) -> "corrupted or invalid translation file"; diff --git a/src/win32_dns.erl b/src/win32_dns.erl index f26f4cd34..0d09036d8 100644 --- a/src/win32_dns.erl +++ b/src/win32_dns.erl @@ -28,91 +28,109 @@ -include("logger.hrl"). --define(IF_KEY, "\\hklm\\system\\CurrentControlSet\\Services\\TcpIp\\Parameters\\Interfaces"). +-define(IF_KEY, "\\hklm\\system\\CurrentControlSet\\Services\\TcpIp\\Parameters\\Interfaces"). -define(TOP_KEY, "\\hklm\\system\\CurrentControlSet\\Services\\TcpIp\\Parameters"). + get_nameservers() -> {_, Config} = pick_config(), IPTs = get_value(["NameServer"], Config), lists:filter(fun(IPTuple) -> is_good_ns(IPTuple) end, IPTs). + is_good_ns(Addr) -> element(1, - inet_res:nnslookup("a.root-servers.net", in, a, [{Addr,53}], - timer:seconds(5))) - =:= ok. + inet_res:nnslookup("a.root-servers.net", + in, + a, + [{Addr, 53}], + timer:seconds(5))) =:= + ok. + reg() -> {ok, R} = win32reg:open([read]), R. + interfaces(R) -> ok = win32reg:change_key(R, ?IF_KEY), {ok, I} = win32reg:sub_keys(R), I. + + config_keys(R, Key) -> ok = win32reg:change_key(R, Key), [ {K, case win32reg:value(R, K) of {ok, V} -> try_translate(K, V); _ -> undefined - end - } || K <- ["Domain", "DhcpDomain", - "NameServer", "DhcpNameServer", "SearchList"]]. + end} || K <- ["Domain", "DhcpDomain", + "NameServer", "DhcpNameServer", "SearchList"] ]. + try_translate(K, V) -> try translate(K, V) of - Res -> - Res + Res -> + Res catch - A:B -> - ?ERROR_MSG("Error '~p' translating Win32 registry~n" - "K: ~p~nV: ~p~nError: ~p", [A, K, V, B]), - undefined + A:B -> + ?ERROR_MSG("Error '~p' translating Win32 registry~n" + "K: ~p~nV: ~p~nError: ~p", + [A, K, V, B]), + undefined end. + translate(NS, V) when NS =:= "NameServer"; NS =:= "DhcpNameServer" -> %% The IPs may be separated by commas ',' or by spaces " " %% The parts of an IP are separated by dots '.' - IPsStrings = [string:tokens(IP, ".") || IP <- string:tokens(V, " ,")], - [ list_to_tuple([list_to_integer(String) || String <- IpStrings]) - || IpStrings <- IPsStrings]; + IPsStrings = [ string:tokens(IP, ".") || IP <- string:tokens(V, " ,") ], + [ list_to_tuple([ list_to_integer(String) || String <- IpStrings ]) + || IpStrings <- IPsStrings ]; translate(_, V) -> V. + interface_configs(R) -> - [{If, config_keys(R, ?IF_KEY ++ "\\" ++ If)} - || If <- interfaces(R)]. + [ {If, config_keys(R, ?IF_KEY ++ "\\" ++ If)} + || If <- interfaces(R) ]. + sort_configs(Configs) -> - lists:sort(fun ({_, A}, {_, B}) -> + lists:sort(fun({_, A}, {_, B}) -> ANS = proplists:get_value("NameServer", A), BNS = proplists:get_value("NameServer", B), - if ANS =/= undefined, BNS =:= undefined -> false; - true -> count_undef(A) < count_undef(B) + if + ANS =/= undefined, BNS =:= undefined -> false; + true -> count_undef(A) < count_undef(B) end end, - Configs). + Configs). + count_undef(L) when is_list(L) -> - lists:foldl(fun ({_K, undefined}, Acc) -> Acc +1; - ({_K, []}, Acc) -> Acc +1; - (_, Acc) -> Acc - end, 0, L). + lists:foldl(fun({_K, undefined}, Acc) -> Acc + 1; + ({_K, []}, Acc) -> Acc + 1; + (_, Acc) -> Acc + end, + 0, + L). + all_configs() -> R = reg(), TopConfig = config_keys(R, ?TOP_KEY), - Configs = [{top, TopConfig} - | interface_configs(R)], + Configs = [{top, TopConfig} | interface_configs(R)], win32reg:close(R), {TopConfig, Configs}. + pick_config() -> {TopConfig, Configs} = all_configs(), - NSConfigs = [{If, C} || {If, C} <- Configs, - get_value(["DhcpNameServer","NameServer"], C) - =/= undefined], - case get_value(["DhcpNameServer","NameServer"], + NSConfigs = [ {If, C} || {If, C} <- Configs, + get_value(["DhcpNameServer", "NameServer"], C) =/= + undefined ], + case get_value(["DhcpNameServer", "NameServer"], TopConfig) of %% No top level nameserver to pick interface with undefined -> @@ -121,14 +139,15 @@ pick_config() -> NS -> Cs = [ {If, C} || {If, C} <- Configs, - lists:member(NS, - [get_value(["NameServer"], C), - get_value(["DhcpNameServer"], C)])], + lists:member(NS, + [get_value(["NameServer"], C), + get_value(["DhcpNameServer"], C)]) ], hd(sort_configs(Cs)) end. + get_value([], _Config) -> undefined; -get_value([K|Keys], Config) -> +get_value([K | Keys], Config) -> case proplists:get_value(K, Config) of undefined -> get_value(Keys, Config); V -> V diff --git a/src/xml_compress.erl b/src/xml_compress.erl index 21b9044a0..a541f893b 100644 --- a/src/xml_compress.erl +++ b/src/xml_compress.erl @@ -67,897 +67,946 @@ % {<<"urn:xmpp:message-correct:0">>,<<"replace">>,[{<<"id">>,[]}],[]}, % {<<"http://jabber.org/protocol/chatstates">>,<<"composing">>,[],[]}] + encode(El, J1, J2) -> - encode_child(El, <<"jabber:client">>, - J1, J2, byte_size(J1), byte_size(J2), <<1:8>>). + encode_child(El, + <<"jabber:client">>, + J1, + J2, + byte_size(J1), + byte_size(J2), + <<1:8>>). + encode_attr({<<"xmlns">>, _}, Acc) -> - Acc; + Acc; encode_attr({N, V}, Acc) -> - <>. + <>. + encode_attrs(Attrs, Acc) -> - lists:foldl(fun encode_attr/2, Acc, Attrs). + lists:foldl(fun encode_attr/2, Acc, Attrs). + encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - E1 = if - PNs == Ns -> encode_attrs(Attrs, <>); - true -> encode_attrs(Attrs, <>) - end, - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>. + E1 = if + PNs == Ns -> encode_attrs(Attrs, <>); + true -> encode_attrs(Attrs, <>) + end, + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>. + encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) -> - case lists:keyfind(<<"xmlns">>, 1, Attrs) of - false -> - encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx); - {_, Ns} -> - encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) - end; + case lists:keyfind(<<"xmlns">>, 1, Attrs) of + false -> + encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx); + {_, Ns} -> + encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) -> - <>. + <>. + encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) -> - lists:foldl( - fun(Child, Acc) -> - encode_child(Child, PNs, J1, J2, J1L, J2L, Acc) - end, Pfx, Children). + lists:foldl( + fun(Child, Acc) -> + encode_child(Child, PNs, J1, J2, J1L, J2L, Acc) + end, + Pfx, + Children). + encode_string(Data) -> - <> = <<(byte_size(Data)):16/unsigned-big-integer>>, - case {V1, V2, V3} of - {0, 0, V3} -> - <>; - {0, V2, V3} -> - <<(V3 bor 64):8, V2:8, Data/binary>>; - _ -> - <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>> - end. + <> = <<(byte_size(Data)):16/unsigned-big-integer>>, + case {V1, V2, V3} of + {0, 0, V3} -> + <>; + {0, V2, V3} -> + <<(V3 bor 64):8, V2:8, Data/binary>>; + _ -> + <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>> + end. + encode(PNs, <<"eu.siacs.conversations.axolotl">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"key">> -> - E = lists:foldl(fun - ({<<"prekey">>, AVal}, Acc) -> - case AVal of - <<"true">> -> <>; - _ -> <> - end; - ({<<"rid">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"encrypted">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"header">> -> - E = lists:foldl(fun - ({<<"sid">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"iv">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"payload">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"key">> -> + E = lists:foldl(fun({<<"prekey">>, AVal}, Acc) -> + case AVal of + <<"true">> -> <>; + _ -> <> + end; + ({<<"rid">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"encrypted">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"header">> -> + E = lists:foldl(fun({<<"sid">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"iv">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"payload">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"jabber:client">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"message">> -> - E = lists:foldl(fun - ({<<"from">>, AVal}, Acc) -> - case AVal of - J2 -> <>; - <> -> <>; - _ -> <> - end; - ({<<"id">>, AVal}, Acc) -> - <>; - ({<<"to">>, AVal}, Acc) -> - case AVal of - J1 -> <>; - J2 -> <>; - <> -> <>; - _ -> <> - end; - ({<<"type">>, AVal}, Acc) -> - case AVal of - <<"chat">> -> <>; - <<"groupchat">> -> <>; - <<"normal">> -> <>; - _ -> <> - end; - ({<<"xml:lang">>, AVal}, Acc) -> - case AVal of - <<"en">> -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"body">> -> - E = encode_attrs(Attrs, <>), - E2 = lists:foldl(fun - ({xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69, - 77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115, - 115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108, - 105,101,110,116,32,100,111,101,115,110,226,128,153,116,32, - 115,101,101,109,32,116,111,32,115,117,112,112,111,114,116,32, - 116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32, - 105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104, - 116,116,112,115,58,47,47,99,111,110,118,101,114,115,97,116, - 105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Acc) -> <>; - (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc) - end, <>, Children), - <>; - <<"subject">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"thread">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"message">> -> + E = lists:foldl(fun({<<"from">>, AVal}, Acc) -> + case AVal of + J2 -> <>; + <> -> <>; + _ -> <> + end; + ({<<"id">>, AVal}, Acc) -> + <>; + ({<<"to">>, AVal}, Acc) -> + case AVal of + J1 -> <>; + J2 -> <>; + <> -> <>; + _ -> <> + end; + ({<<"type">>, AVal}, Acc) -> + case AVal of + <<"chat">> -> <>; + <<"groupchat">> -> <>; + <<"normal">> -> <>; + _ -> <> + end; + ({<<"xml:lang">>, AVal}, Acc) -> + case AVal of + <<"en">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"body">> -> + E = encode_attrs(Attrs, <>), + E2 = lists:foldl(fun({xmlcdata, <<73, 32, 115, 101, 110, 116, 32, 121, 111, 117, 32, 97, 110, 32, 79, 77, 69, + 77, 79, 32, 101, 110, 99, 114, 121, 112, 116, 101, 100, 32, 109, 101, 115, + 115, 97, 103, 101, 32, 98, 117, 116, 32, 121, 111, 117, 114, 32, 99, 108, + 105, 101, 110, 116, 32, 100, 111, 101, 115, 110, 226, 128, 153, 116, 32, + 115, 101, 101, 109, 32, 116, 111, 32, 115, 117, 112, 112, 111, 114, 116, 32, + 116, 104, 97, 116, 46, 32, 70, 105, 110, 100, 32, 109, 111, 114, 101, 32, + 105, 110, 102, 111, 114, 109, 97, 116, 105, 111, 110, 32, 111, 110, 32, 104, + 116, 116, 112, 115, 58, 47, 47, 99, 111, 110, 118, 101, 114, 115, 97, 116, + 105, 111, 110, 115, 46, 105, 109, 47, 111, 109, 101, 109, 111>>}, + Acc) -> <>; + (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc) + end, + <>, + Children), + <>; + <<"subject">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"thread">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:hints">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"store">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"store">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:sid:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"origin-id">> -> - E = lists:foldl(fun - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"stanza-id">> -> - E = lists:foldl(fun - ({<<"by">>, AVal}, Acc) -> - <>; - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"origin-id">> -> + E = lists:foldl(fun({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"stanza-id">> -> + E = lists:foldl(fun({<<"by">>, AVal}, Acc) -> + <>; + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:chat-markers:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"markable">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"displayed">> -> - E = lists:foldl(fun - ({<<"id">>, AVal}, Acc) -> - <>; - ({<<"sender">>, AVal}, Acc) -> - case AVal of - <> -> <>; - <> -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"received">> -> - E = lists:foldl(fun - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"markable">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"displayed">> -> + E = lists:foldl(fun({<<"id">>, AVal}, Acc) -> + <>; + ({<<"sender">>, AVal}, Acc) -> + case AVal of + <> -> <>; + <> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"received">> -> + E = lists:foldl(fun({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:eme:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"encryption">> -> - E = lists:foldl(fun - ({<<"name">>, AVal}, Acc) -> - case AVal of - <<"OMEMO">> -> <>; - _ -> <> - end; - ({<<"namespace">>, AVal}, Acc) -> - case AVal of - <<"eu.siacs.conversations.axolotl">> -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"encryption">> -> + E = lists:foldl(fun({<<"name">>, AVal}, Acc) -> + case AVal of + <<"OMEMO">> -> <>; + _ -> <> + end; + ({<<"namespace">>, AVal}, Acc) -> + case AVal of + <<"eu.siacs.conversations.axolotl">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:delay">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"delay">> -> - E = lists:foldl(fun - ({<<"from">>, AVal}, Acc) -> - case AVal of - J1 -> <>; - _ -> <> - end; - ({<<"stamp">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"delay">> -> + E = lists:foldl(fun({<<"from">>, AVal}, Acc) -> + case AVal of + J1 -> <>; + _ -> <> + end; + ({<<"stamp">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"http://jabber.org/protocol/address">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"address">> -> - E = lists:foldl(fun - ({<<"jid">>, AVal}, Acc) -> - case AVal of - <> -> <>; - _ -> <> - end; - ({<<"type">>, AVal}, Acc) -> - case AVal of - <<"ofrom">> -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"addresses">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"address">> -> + E = lists:foldl(fun({<<"jid">>, AVal}, Acc) -> + case AVal of + <> -> <>; + _ -> <> + end; + ({<<"type">>, AVal}, Acc) -> + case AVal of + <<"ofrom">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"addresses">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:mam:tmp">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"archived">> -> - E = lists:foldl(fun - ({<<"by">>, AVal}, Acc) -> - <>; - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"archived">> -> + E = lists:foldl(fun({<<"by">>, AVal}, Acc) -> + <>; + ({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:receipts">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"request">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"received">> -> - E = lists:foldl(fun - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"request">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"received">> -> + E = lists:foldl(fun({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"http://jabber.org/protocol/chatstates">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"active">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"composing">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"active">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"composing">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"http://jabber.org/protocol/muc#user">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"invite">> -> - E = lists:foldl(fun - ({<<"from">>, AVal}, Acc) -> - case AVal of - <> -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"reason">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"x">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"invite">> -> + E = lists:foldl(fun({<<"from">>, AVal}, Acc) -> + case AVal of + <> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"reason">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"x">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"jabber:x:conference">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"x">> -> - E = lists:foldl(fun - ({<<"jid">>, AVal}, Acc) -> - case AVal of - J2 -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"x">> -> + E = lists:foldl(fun({<<"jid">>, AVal}, Acc) -> + case AVal of + J2 -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"http://jabber.org/protocol/pubsub#event">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"event">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"item">> -> - E = lists:foldl(fun - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - <<"items">> -> - E = lists:foldl(fun - ({<<"node">>, AVal}, Acc) -> - case AVal of - <<"urn:xmpp:mucsub:nodes:messages">> -> <>; - _ -> <> - end; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"event">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"item">> -> + E = lists:foldl(fun({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + <<"items">> -> + E = lists:foldl(fun({<<"node">>, AVal}, Acc) -> + case AVal of + <<"urn:xmpp:mucsub:nodes:messages">> -> <>; + _ -> <> + end; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"p1:push:custom">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"x">> -> - E = lists:foldl(fun - ({<<"key">>, AVal}, Acc) -> - <>; - ({<<"value">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"x">> -> + E = lists:foldl(fun({<<"key">>, AVal}, Acc) -> + <>; + ({<<"value">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"p1:pushed">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"x">> -> - E = encode_attrs(Attrs, <>), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"x">> -> + E = encode_attrs(Attrs, <>), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, <<"urn:xmpp:message-correct:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - case Name of - <<"replace">> -> - E = lists:foldl(fun - ({<<"id">>, AVal}, Acc) -> - <>; - (Attr, Acc) -> encode_attr(Attr, Acc) - end, <>, Attrs), - E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), - <>; - _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -end; + case Name of + <<"replace">> -> + E = lists:foldl(fun({<<"id">>, AVal}, Acc) -> + <>; + (Attr, Acc) -> encode_attr(Attr, Acc) + end, + <>, + Attrs), + E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), + <>; + _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) + end; encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> - encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx). + encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx). + decode(<<$<, _/binary>> = Data, _J1, _J2) -> - fxml_stream:parse_element(Data); + fxml_stream:parse_element(Data); decode(<<1:8, Rest/binary>>, J1, J2) -> - try decode(Rest, <<"jabber:client">>, J1, J2, false) of - {El, _} -> El - catch throw:loop_detected -> - {error, {loop_detected, <<"Compressed data corrupted">>}} - end. + try decode(Rest, <<"jabber:client">>, J1, J2, false) of + {El, _} -> El + catch + throw:loop_detected -> + {error, {loop_detected, <<"Compressed data corrupted">>}} + end. + decode_string(Data) -> - case Data of - <<0:2, L:6, Str:L/binary, Rest/binary>> -> - {Str, Rest}; - <<1:2, L1:6, 0:2, L2:6, Rest/binary>> -> - L = L2*64 + L1, - <> = Rest, - {Str, Rest2}; - <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> -> - L = (L3*64 + L2)*64 + L1, - <> = Rest, - {Str, Rest2} - end. + case Data of + <<0:2, L:6, Str:L/binary, Rest/binary>> -> + {Str, Rest}; + <<1:2, L1:6, 0:2, L2:6, Rest/binary>> -> + L = L2 * 64 + L1, + <> = Rest, + {Str, Rest2}; + <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> -> + L = (L3 * 64 + L2) * 64 + L1, + <> = Rest, + {Str, Rest2} + end. + decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2, _) -> - {Text, Rest2} = decode_string(Rest), - {{xmlcdata, Text}, Rest2}; + {Text, Rest2} = decode_string(Rest), + {{xmlcdata, Text}, Rest2}; decode_child(<<2:8, Rest/binary>>, PNs, J1, J2, _) -> - {Name, Rest2} = decode_string(Rest), - {Attrs, Rest3} = decode_attrs(Rest2), - {Children, Rest4} = decode_children(Rest3, PNs, J1, J2), - {{xmlel, Name, Attrs, Children}, Rest4}; + {Name, Rest2} = decode_string(Rest), + {Attrs, Rest3} = decode_attrs(Rest2), + {Children, Rest4} = decode_children(Rest3, PNs, J1, J2), + {{xmlel, Name, Attrs, Children}, Rest4}; decode_child(<<3:8, Rest/binary>>, PNs, J1, J2, _) -> - {Ns, Rest2} = decode_string(Rest), - {Name, Rest3} = decode_string(Rest2), - {Attrs, Rest4} = decode_attrs(Rest3), - {Children, Rest5} = decode_children(Rest4, Ns, J1, J2), - {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5}; + {Ns, Rest2} = decode_string(Rest), + {Name, Rest3} = decode_string(Rest2), + {Attrs, Rest4} = decode_attrs(Rest3), + {Children, Rest5} = decode_children(Rest4, Ns, J1, J2), + {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5}; decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2, _) -> - {stop, Rest}; + {stop, Rest}; decode_child(_Other, _PNs, _J1, _J2, true) -> - throw(loop_detected); + throw(loop_detected); decode_child(Other, PNs, J1, J2, _) -> - decode(Other, PNs, J1, J2, true). + decode(Other, PNs, J1, J2, true). + decode_children(Data, PNs, J1, J2) -> - prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data). + prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data). + decode_attr(<<1:8, Rest/binary>>) -> - {Name, Rest2} = decode_string(Rest), - {Val, Rest3} = decode_string(Rest2), - {{Name, Val}, Rest3}; + {Name, Rest2} = decode_string(Rest), + {Val, Rest3} = decode_string(Rest2), + {{Name, Val}, Rest3}; decode_attr(<<2:8, Rest/binary>>) -> - {stop, Rest}. + {stop, Rest}. + decode_attrs(Data) -> - prefix_map(fun decode_attr/1, Data). + prefix_map(fun decode_attr/1, Data). + prefix_map(F, Data) -> - prefix_map(F, Data, []). + prefix_map(F, Data, []). + prefix_map(F, Data, Acc) -> - case F(Data) of - {stop, Rest} -> - {lists:reverse(Acc), Rest}; - {Val, Rest} -> - prefix_map(F, Rest, [Val | Acc]) - end. + case F(Data) of + {stop, Rest} -> + {lists:reverse(Acc), Rest}; + {Val, Rest} -> + prefix_map(F, Rest, [Val | Acc]) + end. + add_ns(Ns, Ns, Attrs) -> - Attrs; + Attrs; add_ns(_, Ns, Attrs) -> - [{<<"xmlns">>, Ns} | Attrs]. + [{<<"xmlns">>, Ns} | Attrs]. + decode(<<5:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"eu.siacs.conversations.axolotl">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {{<<"prekey">>, <<"true">>}, Rest3}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"prekey">>, AVal}, Rest4}; - (<<5:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"rid">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"key">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {{<<"prekey">>, <<"true">>}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"prekey">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"rid">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"key">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<12:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"eu.siacs.conversations.axolotl">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"encrypted">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"encrypted">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<13:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"eu.siacs.conversations.axolotl">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"sid">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"header">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sid">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"header">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<14:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"eu.siacs.conversations.axolotl">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"iv">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"iv">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<15:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"eu.siacs.conversations.axolotl">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"payload">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"eu.siacs.conversations.axolotl">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"payload">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<6:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"jabber:client">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {{<<"from">>, J2}, Rest3}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"from">>, <>}, Rest4}; - (<<5:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"from">>, AVal}, Rest4}; - (<<6:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<7:8, Rest3/binary>>) -> - {{<<"to">>, J1}, Rest3}; - (<<8:8, Rest3/binary>>) -> - {{<<"to">>, J2}, Rest3}; - (<<9:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"to">>, <>}, Rest4}; - (<<10:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"to">>, AVal}, Rest4}; - (<<11:8, Rest3/binary>>) -> - {{<<"type">>, <<"chat">>}, Rest3}; - (<<12:8, Rest3/binary>>) -> - {{<<"type">>, <<"groupchat">>}, Rest3}; - (<<13:8, Rest3/binary>>) -> - {{<<"type">>, <<"normal">>}, Rest3}; - (<<14:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"type">>, AVal}, Rest4}; - (<<15:8, Rest3/binary>>) -> - {{<<"xml:lang">>, <<"en">>}, Rest3}; - (<<16:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"xml:lang">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"message">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"jabber:client">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {{<<"from">>, J2}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, <>}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, AVal}, Rest4}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<7:8, Rest3/binary>>) -> + {{<<"to">>, J1}, Rest3}; + (<<8:8, Rest3/binary>>) -> + {{<<"to">>, J2}, Rest3}; + (<<9:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"to">>, <>}, Rest4}; + (<<10:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"to">>, AVal}, Rest4}; + (<<11:8, Rest3/binary>>) -> + {{<<"type">>, <<"chat">>}, Rest3}; + (<<12:8, Rest3/binary>>) -> + {{<<"type">>, <<"groupchat">>}, Rest3}; + (<<13:8, Rest3/binary>>) -> + {{<<"type">>, <<"normal">>}, Rest3}; + (<<14:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"type">>, AVal}, Rest4}; + (<<15:8, Rest3/binary>>) -> + {{<<"xml:lang">>, <<"en">>}, Rest3}; + (<<16:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"xml:lang">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"message">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<8:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"jabber:client">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = prefix_map(fun (<<9:8, Rest5/binary>>) -> - {{xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69, - 77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115, - 115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108, - 105,101,110,116,32,100,111,101,115,110,226,128,153,116,32, - 115,101,101,109,32,116,111,32,115,117,112,112,111,114,116, - 32,116,104,97,116,46,32,70,105,110,100,32,109,111,114,101, - 32,105,110,102,111,114,109,97,116,105,111,110,32,111,110, - 32,104,116,116,112,115,58,47,47,99,111,110,118,101,114,115, - 97,116,105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Rest5}; - (Other) -> - decode_child(Other, Ns, J1, J2, false) - end, Rest2), - {{xmlel, <<"body">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"jabber:client">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = prefix_map(fun(<<9:8, Rest5/binary>>) -> + {{xmlcdata, <<73, 32, 115, 101, 110, 116, 32, 121, 111, 117, 32, 97, 110, 32, 79, 77, 69, + 77, 79, 32, 101, 110, 99, 114, 121, 112, 116, 101, 100, 32, 109, 101, 115, + 115, 97, 103, 101, 32, 98, 117, 116, 32, 121, 111, 117, 114, 32, 99, 108, + 105, 101, 110, 116, 32, 100, 111, 101, 115, 110, 226, 128, 153, 116, 32, + 115, 101, 101, 109, 32, 116, 111, 32, 115, 117, 112, 112, 111, 114, 116, + 32, 116, 104, 97, 116, 46, 32, 70, 105, 110, 100, 32, 109, 111, 114, 101, + 32, 105, 110, 102, 111, 114, 109, 97, 116, 105, 111, 110, 32, 111, 110, + 32, 104, 116, 116, 112, 115, 58, 47, 47, 99, 111, 110, 118, 101, 114, 115, + 97, 116, 105, 111, 110, 115, 46, 105, 109, 47, 111, 109, 101, 109, 111>>}, + Rest5}; + (Other) -> + decode_child(Other, Ns, J1, J2, false) + end, + Rest2), + {{xmlel, <<"body">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<31:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"jabber:client">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"subject">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"jabber:client">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"subject">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<32:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"jabber:client">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"thread">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"jabber:client">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"thread">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<7:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:hints">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"store">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:hints">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"store">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<10:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:sid:0">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"origin-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:sid:0">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"origin-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<22:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:sid:0">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"by">>, AVal}, Rest4}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"stanza-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:sid:0">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"by">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"stanza-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<11:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:chat-markers:0">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"markable">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:chat-markers:0">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"markable">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<20:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:chat-markers:0">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"sender">>, <>}, Rest4}; - (<<5:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"sender">>, <>}, Rest4}; - (<<6:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"sender">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"displayed">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:chat-markers:0">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sender">>, <>}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sender">>, <>}, Rest4}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"sender">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"displayed">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<24:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:chat-markers:0">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:chat-markers:0">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<16:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:eme:0">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {{<<"name">>, <<"OMEMO">>}, Rest3}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"name">>, AVal}, Rest4}; - (<<5:8, Rest3/binary>>) -> - {{<<"namespace">>, <<"eu.siacs.conversations.axolotl">>}, Rest3}; - (<<6:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"namespace">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"encryption">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:eme:0">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {{<<"name">>, <<"OMEMO">>}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"name">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {{<<"namespace">>, <<"eu.siacs.conversations.axolotl">>}, Rest3}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"namespace">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"encryption">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<17:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:delay">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {{<<"from">>, J1}, Rest3}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"from">>, AVal}, Rest4}; - (<<5:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"stamp">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"delay">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:delay">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {{<<"from">>, J1}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"stamp">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"delay">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<18:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/address">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"jid">>, <>}, Rest4}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"jid">>, AVal}, Rest4}; - (<<5:8, Rest3/binary>>) -> - {{<<"type">>, <<"ofrom">>}, Rest3}; - (<<6:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"type">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"address">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/address">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"jid">>, <>}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"jid">>, AVal}, Rest4}; + (<<5:8, Rest3/binary>>) -> + {{<<"type">>, <<"ofrom">>}, Rest3}; + (<<6:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"type">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"address">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<19:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/address">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"addresses">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/address">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"addresses">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<21:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:mam:tmp">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"by">>, AVal}, Rest4}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"archived">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:mam:tmp">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"by">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"archived">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<23:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:receipts">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"request">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:receipts">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"request">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<25:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:receipts">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:receipts">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<26:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/chatstates">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"active">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/chatstates">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"active">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<39:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/chatstates">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"composing">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/chatstates">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"composing">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<27:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/muc#user">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"from">>, <>}, Rest4}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"from">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"invite">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/muc#user">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, <>}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"from">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"invite">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<28:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/muc#user">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"reason">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/muc#user">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"reason">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<29:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/muc#user">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/muc#user">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<30:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"jabber:x:conference">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {{<<"jid">>, J2}, Rest3}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"jid">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"jabber:x:conference">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {{<<"jid">>, J2}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"jid">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<33:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/pubsub#event">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"event">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/pubsub#event">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"event">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<34:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/pubsub#event">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"item">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/pubsub#event">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"item">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<35:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"http://jabber.org/protocol/pubsub#event">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {{<<"node">>, <<"urn:xmpp:mucsub:nodes:messages">>}, Rest3}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"node">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"items">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"http://jabber.org/protocol/pubsub#event">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {{<<"node">>, <<"urn:xmpp:mucsub:nodes:messages">>}, Rest3}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"node">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"items">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<36:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"p1:push:custom">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"key">>, AVal}, Rest4}; - (<<4:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"value">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"p1:push:custom">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"key">>, AVal}, Rest4}; + (<<4:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"value">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<37:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"p1:pushed">>, - {Attrs, Rest2} = decode_attrs(Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"p1:pushed">>, + {Attrs, Rest2} = decode_attrs(Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<38:8, Rest/binary>>, PNs, J1, J2, _) -> - Ns = <<"urn:xmpp:message-correct:0">>, - {Attrs, Rest2} = prefix_map(fun - (<<3:8, Rest3/binary>>) -> - {AVal, Rest4} = decode_string(Rest3), - {{<<"id">>, AVal}, Rest4}; - (<<2:8, Rest3/binary>>) -> - {stop, Rest3}; - (Data) -> - decode_attr(Data) - end, Rest), - {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), - {{xmlel, <<"replace">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; + Ns = <<"urn:xmpp:message-correct:0">>, + {Attrs, Rest2} = prefix_map(fun(<<3:8, Rest3/binary>>) -> + {AVal, Rest4} = decode_string(Rest3), + {{<<"id">>, AVal}, Rest4}; + (<<2:8, Rest3/binary>>) -> + {stop, Rest3}; + (Data) -> + decode_attr(Data) + end, + Rest), + {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), + {{xmlel, <<"replace">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(Other, PNs, J1, J2, Loop) -> - decode_child(Other, PNs, J1, J2, Loop). - + decode_child(Other, PNs, J1, J2, Loop). diff --git a/test/announce_tests.erl b/test/announce_tests.erl index 724baba27..b8577feaf 100644 --- a/test/announce_tests.erl +++ b/test/announce_tests.erl @@ -25,11 +25,18 @@ %% API -compile(export_all). --import(suite, [server_jid/1, send_recv/2, recv_message/1, disconnect/1, - send/2, wait_for_master/1, wait_for_slave/1]). +-import(suite, + [server_jid/1, + send_recv/2, + recv_message/1, + disconnect/1, + send/2, + wait_for_master/1, + wait_for_slave/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,12 +46,14 @@ single_cases() -> {announce_single, [sequence], []}. + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {announce_master_slave, [sequence], - [master_slave_test(set_motd)]}. + [master_slave_test(set_motd)]}. + set_motd_master(Config) -> ServerJID = server_jid(Config), @@ -56,6 +65,7 @@ set_motd_master(Config) -> #message{from = ServerJID, body = Body} = recv_message(Config), disconnect(Config). + set_motd_slave(Config) -> ServerJID = server_jid(Config), Body = xmpp:mk_text(<<"motd">>), @@ -64,13 +74,16 @@ set_motd_slave(Config) -> #message{from = ServerJID, body = Body} = recv_message(Config), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("announce_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("announce_" ++ atom_to_list(T)), [parallel], + {list_to_atom("announce_" ++ atom_to_list(T)), + [parallel], [list_to_atom("announce_" ++ atom_to_list(T) ++ "_master"), list_to_atom("announce_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/antispam_tests.erl b/test/antispam_tests.erl index 8d7bd2472..9033b2091 100644 --- a/test/antispam_tests.erl +++ b/test/antispam_tests.erl @@ -25,11 +25,25 @@ -compile(export_all). --import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1, - send/2, recv_message/1, recv_iq/1, muc_jid/1, - alt_room_jid/1, wait_for_slave/1, wait_for_master/1, - disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, - my_muc_jid/1, get_features/2, set_opt/3]). +-import(suite, + [recv_presence/1, + send_recv/2, + my_jid/1, + muc_room_jid/1, + send/2, + recv_message/1, + recv_iq/1, + muc_jid/1, + alt_room_jid/1, + wait_for_slave/1, + wait_for_master/1, + disconnect/1, + put_event/2, + get_event/1, + peer_muc_jid/1, + my_muc_jid/1, + get_features/2, + set_opt/3]). -include("suite.hrl"). -include("mod_antispam.hrl"). @@ -42,6 +56,7 @@ %%% Single tests %%%=================================================================== + single_cases() -> {antispam_single, [sequence], @@ -63,37 +78,46 @@ single_cases() -> single_test(rtbl_domains_whitelisted), single_test(spam_dump_file)]}. + %%%=================================================================== + block_by_jid(Config) -> is_spam(message_hello(<<"spammer_jid">>, <<"localhost">>, Config)). + block_by_url(Config) -> From = jid:make(<<"spammer">>, <<"localhost">>, <<"spam_client">>), To = my_jid(Config), is_not_spam(message_hello(<<"spammer">>, <<"localhost">>, Config)), is_spam(message(From, To, <<"hello world\nhttps://spam.domain.url">>)). + blocked_jid_is_cached(Config) -> is_spam(message_hello(<<"spammer">>, <<"localhost">>, Config)). + uncache_blocked_jid(Config) -> Host = ?config(server, Config), Spammer = jid:make(<<"spammer">>, <<"localhost">>, <<"">>), mod_antispam:drop_from_spam_filter_cache(Host, jid:to_string(Spammer)), is_not_spam(message_hello(<<"spammer">>, <<"localhost">>, Config)). + check_blocked_domain(Config) -> is_spam(message_hello(<<"other_spammer">>, <<"spam_domain.org">>, Config)). + unblock_domain(Config) -> Host = ?config(server, Config), ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam_domain.org">>)), ?match([], mod_antispam:get_blocked_domains(Host)), is_not_spam(message_hello(<<"spammer">>, <<"spam_domain.org">>, Config)). + %%%=================================================================== + empty_domain_list(Config) -> Host = ?config(server, Config), ?match([], mod_antispam:get_blocked_domains(Host)), @@ -102,17 +126,20 @@ empty_domain_list(Config) -> Msg = message(SpamFrom, To, <<"hello world">>), is_not_spam(Msg). + block_domain_globally(Config) -> ?match({ok, _}, mod_antispam:add_blocked_domain(<<"global">>, <<"spam.domain">>)), SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), To = my_jid(Config), is_spam(message(SpamFrom, To, <<"hello world">>)). + check_domain_blocked_globally(_Config) -> - Vhosts = [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam)], + Vhosts = [ H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam) ], NumVhosts = length(Vhosts), ?match(NumVhosts, length(lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts))). + unblock_domain_in_vhost(Config) -> Host = ?config(server, Config), ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam.domain">>)), @@ -121,22 +148,25 @@ unblock_domain_in_vhost(Config) -> To = my_jid(Config), is_not_spam(message(SpamFrom, To, <<"hello world">>)). + unblock_domain_globally(_Config) -> - Vhosts = [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam)], + Vhosts = [ H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam) ], NumVhosts = length(Vhosts), ?match(NumVhosts, length(lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts)) + 1), ?match({ok, _}, mod_antispam:remove_blocked_domain(<<"global">>, <<"spam.domain">>)), ?match([], lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts)). + block_domain_in_vhost(Config) -> Host = ?config(server, Config), - Vhosts = [H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam)], + Vhosts = [ H || H <- ejabberd_option:hosts(), gen_mod:is_loaded(H, mod_antispam) ], ?match({ok, _}, mod_antispam:add_blocked_domain(Host, <<"spam.domain">>)), ?match([Host], lists:filter(has_spam_domain(<<"spam.domain">>), Vhosts)), SpamFrom = jid:make(<<"spammer">>, <<"spam.domain">>, <<"spam_client">>), To = my_jid(Config), is_spam(message(SpamFrom, To, <<"hello world">>)). + unblock_domain_in_vhost2(Config) -> Host = ?config(server, Config), ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam.domain">>)), @@ -145,8 +175,10 @@ unblock_domain_in_vhost2(Config) -> is_not_spam(message(SpamFrom, To, <<"hello world">>)), disconnect(Config). + %%%=================================================================== + jid_cache(Config) -> Host = ?config(server, Config), SpamFrom = jid:make(<<"spammer">>, Host, <<"spam_client">>), @@ -157,13 +189,15 @@ jid_cache(Config) -> is_not_spam(message_hello(<<"spammer">>, Host, Config)), disconnect(Config). + %%%=================================================================== + rtbl_domains(Config) -> Host = ?config(server, Config), RTBLHost = jid:to_string( - suite:pubsub_jid(Config)), + suite:pubsub_jid(Config)), RTBLDomainsNode = <<"spam_source_domains">>, OldOpts = gen_mod:get_module_opts(Host, mod_antispam), NewOpts = @@ -182,8 +216,10 @@ rtbl_domains(Config) -> RTBLDomainsNode, Owner, <<"spam.source.domain">>, - [xmpp:encode(#ps_item{id = <<"spam.source.domain">>, - sub_els = []})]), + [xmpp:encode(#ps_item{ + id = <<"spam.source.domain">>, + sub_els = [] + })]), mod_antispam:reload(Host, OldOpts, NewOpts), ?match({ok, _}, mod_antispam:remove_blocked_domain(Host, <<"spam_domain.org">>)), ?retry(100, @@ -195,8 +231,10 @@ rtbl_domains(Config) -> RTBLDomainsNode, Owner, <<"spam.source.another">>, - [xmpp:encode(#ps_item{id = <<"spam.source.another">>, - sub_els = []})]), + [xmpp:encode(#ps_item{ + id = <<"spam.source.another">>, + sub_els = [] + })]), ?retry(100, 10, ?match(true, (has_spam_domain(<<"spam.source.another">>))(Host))), {result, _} = mod_pubsub:delete_item(RTBLHost, RTBLDomainsNode, Owner, <<"spam.source.another">>, true), @@ -204,11 +242,12 @@ rtbl_domains(Config) -> {result, _} = mod_pubsub:delete_node(RTBLHost, RTBLDomainsNode, Owner), disconnect(Config). + rtbl_domains_whitelisted(Config) -> Host = ?config(server, Config), RTBLHost = jid:to_string( - suite:pubsub_jid(Config)), + suite:pubsub_jid(Config)), RTBLDomainsNode = <<"spam_source_domains">>, OldOpts = gen_mod:get_module_opts(Host, mod_antispam), NewOpts = @@ -227,8 +266,10 @@ rtbl_domains_whitelisted(Config) -> RTBLDomainsNode, Owner, <<"whitelisted.domain">>, - [xmpp:encode(#ps_item{id = <<"whitelisted.domain">>, - sub_els = []})]), + [xmpp:encode(#ps_item{ + id = <<"whitelisted.domain">>, + sub_els = [] + })]), mod_antispam:reload(Host, OldOpts, NewOpts), {result, _} = mod_pubsub:publish_item(RTBLHost, @@ -236,8 +277,10 @@ rtbl_domains_whitelisted(Config) -> RTBLDomainsNode, Owner, <<"yetanother.domain">>, - [xmpp:encode(#ps_item{id = <<"yetanother.domain">>, - sub_els = []})]), + [xmpp:encode(#ps_item{ + id = <<"yetanother.domain">>, + sub_els = [] + })]), ?retry(100, 10, ?match(true, (has_spam_domain(<<"yetanother.domain">>))(Host))), %% we assume that the previous "whitelisted.domain" pubsub item has been consumed by now, so we %% can check that it doesn't exist @@ -245,8 +288,10 @@ rtbl_domains_whitelisted(Config) -> {result, _} = mod_pubsub:delete_node(RTBLHost, RTBLDomainsNode, Owner), disconnect(Config). + %%%=================================================================== + spam_dump_file(Config) -> {ok, CWD} = file:get_cwd(), Filename = filename:join([CWD, "spam.log"]), @@ -258,32 +303,41 @@ spam_dump_file(Config) -> 100, ?match({match, _}, re:run(get_bytes(Filename), <<"A very specific spam message">>))). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("antispam_" ++ atom_to_list(T)). + has_spam_domain(Domain) -> fun(Host) -> lists:member(Domain, mod_antispam:get_blocked_domains(Host)) end. + is_not_spam(Msg) -> ?match({Msg, undefined}, mod_antispam_filter:s2s_receive_packet({Msg, undefined})). + is_spam(Spam) -> ?match({stop, {drop, undefined}}, mod_antispam_filter:s2s_receive_packet({Spam, undefined})). + message_hello(Username, Host, Config) -> SpamFrom = jid:make(Username, Host, <<"spam_client">>), To = my_jid(Config), message(SpamFrom, To, <<"hello world">>). + message(From, To, BodyText) -> - #message{from = From, - to = To, - type = chat, - body = [#text{data = BodyText}]}. + #message{ + from = From, + to = To, + type = chat, + body = [#text{data = BodyText}] + }. + get_bytes(Filename) -> {ok, Bytes} = file:read_file(Filename), diff --git a/test/carbons_tests.erl b/test/carbons_tests.erl index eabd3af2a..2b6d1f9f8 100644 --- a/test/carbons_tests.erl +++ b/test/carbons_tests.erl @@ -25,13 +25,22 @@ %% API -compile(export_all). --import(suite, [is_feature_advertised/2, disconnect/1, send_recv/2, - recv_presence/1, send/2, get_event/1, recv_message/1, - my_jid/1, wait_for_slave/1, wait_for_master/1, - put_event/2]). +-import(suite, + [is_feature_advertised/2, + disconnect/1, + send_recv/2, + recv_presence/1, + send/2, + get_event/1, + recv_message/1, + my_jid/1, + wait_for_slave/1, + wait_for_master/1, + put_event/2]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -40,33 +49,38 @@ %%%=================================================================== single_cases() -> {carbons_single, [sequence], - [single_test(feature_enabled), - single_test(unsupported_iq)]}. + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_CARBONS_2), disconnect(Config). + unsupported_iq(Config) -> lists:foreach( fun({Type, SubEl}) -> - #iq{type = error} = - send_recv(Config, #iq{type = Type, sub_els = [SubEl]}) - end, [{Type, SubEl} || - Type <- [get, set], - SubEl <- [#carbons_sent{forwarded = #forwarded{}}, - #carbons_received{forwarded = #forwarded{}}, - #carbons_private{}]] ++ - [{get, SubEl} || SubEl <- [#carbons_enable{}, #carbons_disable{}]]), + #iq{type = error} = + send_recv(Config, #iq{type = Type, sub_els = [SubEl]}) + end, + [ {Type, SubEl} + || Type <- [get, set], + SubEl <- [#carbons_sent{forwarded = #forwarded{}}, + #carbons_received{forwarded = #forwarded{}}, + #carbons_private{}] ] ++ + [ {get, SubEl} || SubEl <- [#carbons_enable{}, #carbons_disable{}] ]), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {carbons_master_slave, [sequence], - [master_slave_test(send_recv), - master_slave_test(enable_disable)]}. + [master_slave_test(send_recv), + master_slave_test(enable_disable)]}. + send_recv_master(Config) -> Peer = ?config(peer, Config), @@ -78,6 +92,7 @@ send_recv_master(Config) -> #presence{from = Peer, type = unavailable} = recv_presence(Config), disconnect(Config). + send_recv_slave(Config) -> prepare_slave(Config), ok = enable(Config), @@ -85,6 +100,7 @@ send_recv_slave(Config) -> recv_carbons(Config), disconnect(Config). + enable_disable_master(Config) -> prepare_master(Config), ct:comment("Waiting for the peer to be ready"), @@ -92,6 +108,7 @@ enable_disable_master(Config) -> send_messages(Config), disconnect(Config). + enable_disable_slave(Config) -> Peer = ?config(peer, Config), prepare_slave(Config), @@ -102,17 +119,21 @@ enable_disable_slave(Config) -> #presence{from = Peer, type = unavailable} = recv_presence(Config), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("carbons_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("carbons_" ++ atom_to_list(T)), [parallel], + {list_to_atom("carbons_" ++ atom_to_list(T)), + [parallel], [list_to_atom("carbons_" ++ atom_to_list(T) ++ "_master"), list_to_atom("carbons_" ++ atom_to_list(T) ++ "_slave")]}. + prepare_master(Config) -> MyJID = my_jid(Config), Peer = ?config(peer, Config), @@ -122,6 +143,7 @@ prepare_master(Config) -> #presence{from = Peer} = recv_presence(Config), Config. + prepare_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -132,24 +154,28 @@ prepare_slave(Config) -> #presence{from = Peer} = recv_presence(Config), Config. + send_messages(Config) -> Server = ?config(server, Config), MyJID = my_jid(Config), JID = jid:make(p1_rand:get_string(), Server), lists:foreach( fun({send, #message{type = Type} = Msg}) -> - I = send(Config, Msg#message{to = JID}), - if Type /= error -> - #message{id = I, type = error} = recv_message(Config); - true -> - ok - end; - ({recv, #message{} = Msg}) -> - ejabberd_router:route( - Msg#message{from = JID, to = MyJID}), - ct:comment("Receiving message ~s", [xmpp:pp(Msg)]), - #message{} = recv_message(Config) - end, message_iterator(Config)). + I = send(Config, Msg#message{to = JID}), + if + Type /= error -> + #message{id = I, type = error} = recv_message(Config); + true -> + ok + end; + ({recv, #message{} = Msg}) -> + ejabberd_router:route( + Msg#message{from = JID, to = MyJID}), + ct:comment("Receiving message ~s", [xmpp:pp(Msg)]), + #message{} = recv_message(Config) + end, + message_iterator(Config)). + recv_carbons(Config) -> Peer = ?config(peer, Config), @@ -157,55 +183,65 @@ recv_carbons(Config) -> MyJID = my_jid(Config), lists:foreach( fun({_, #message{sub_els = [#hint{type = 'no-copy'}]}}) -> - ok; - ({_, #message{sub_els = [#carbons_private{}]}}) -> - ok; - ({_, #message{type = T}}) when T /= normal, T /= chat -> - ok; - ({Dir, #message{type = T, body = Body} = M}) - when (T == chat) or (T == normal andalso Body /= []) -> - ct:comment("Receiving carbon ~s", [xmpp:pp(M)]), - #message{from = BarePeer, to = MyJID} = CarbonMsg = - recv_message(Config), - case Dir of - send -> - #carbons_sent{forwarded = #forwarded{sub_els = [El]}} = - xmpp:get_subtag(CarbonMsg, #carbons_sent{}), - #message{body = Body} = xmpp:decode(El); - recv -> - #carbons_received{forwarded = #forwarded{sub_els = [El]}}= - xmpp:get_subtag(CarbonMsg, #carbons_received{}), - #message{body = Body} = xmpp:decode(El) - end; - (_) -> - false - end, message_iterator(Config)). + ok; + ({_, #message{sub_els = [#carbons_private{}]}}) -> + ok; + ({_, #message{type = T}}) when T /= normal, T /= chat -> + ok; + ({Dir, #message{type = T, body = Body} = M}) + when (T == chat) or (T == normal andalso Body /= []) -> + ct:comment("Receiving carbon ~s", [xmpp:pp(M)]), + #message{from = BarePeer, to = MyJID} = CarbonMsg = + recv_message(Config), + case Dir of + send -> + #carbons_sent{forwarded = #forwarded{sub_els = [El]}} = + xmpp:get_subtag(CarbonMsg, #carbons_sent{}), + #message{body = Body} = xmpp:decode(El); + recv -> + #carbons_received{forwarded = #forwarded{sub_els = [El]}} = + xmpp:get_subtag(CarbonMsg, #carbons_received{}), + #message{body = Body} = xmpp:decode(El) + end; + (_) -> + false + end, + message_iterator(Config)). + enable(Config) -> case send_recv( - Config, #iq{type = set, - sub_els = [#carbons_enable{}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#carbons_enable{}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + disable(Config) -> case send_recv( - Config, #iq{type = set, - sub_els = [#carbons_disable{}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#carbons_disable{}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + message_iterator(_Config) -> - [{Dir, #message{type = Type, body = Body, sub_els = Els}} - || Dir <- [send, recv], - Type <- [error, chat, normal, groupchat, headline], - Body <- [[], xmpp:mk_text(<<"body">>)], - Els <- [[], - [#hint{type = 'no-copy'}], - [#carbons_private{}]]]. + [ {Dir, #message{type = Type, body = Body, sub_els = Els}} + || Dir <- [send, recv], + Type <- [error, chat, normal, groupchat, headline], + Body <- [[], xmpp:mk_text(<<"body">>)], + Els <- [[], + [#hint{type = 'no-copy'}], + [#carbons_private{}]] ]. diff --git a/test/commands_tests.erl b/test/commands_tests.erl index 7b0675c3f..f630f5762 100644 --- a/test/commands_tests.erl +++ b/test/commands_tests.erl @@ -34,11 +34,14 @@ -ifdef(OTP_BELOW_24). + single_cases() -> {commands_single, [sequence], []}. + -else. + single_cases() -> {commands_single, [sequence], @@ -70,13 +73,16 @@ single_cases() -> %%single_test(adhoc_all), single_test(clean)]}. + -endif. %% @format-begin + single_test(T) -> list_to_atom("commands_" ++ atom_to_list(T)). + setup(_Config) -> M = <<"mod_example">>, clean(_Config), @@ -90,15 +96,18 @@ setup(_Config) -> Installed = execute(modules_installed, []), ?match(true, lists:keymember(mod_example, 1, Installed)). + clean(_Config) -> M = <<"mod_example">>, execute(module_uninstall, [M]), Installed = execute(modules_installed, []), ?match(false, lists:keymember(mod_example, 1, Installed)). + %%%================================== %%%% ejabberdctl + %% TODO: find a way to read and check the output printed in the command line ejabberdctl(_Config) -> R = ejabberd_ctl:process(["modules_installed"]), @@ -107,26 +116,32 @@ ejabberdctl(_Config) -> Installed = execute(modules_installed, []), ?match(true, lists:keymember(mod_example, 1, Installed)). + execute(Name, Args) -> ejabberd_commands:execute_command2(Name, Args, #{caller_module => ejabberd_ctl}, 1000000). + %%%================================== %%%% mod_http_api + http_integer(Config) -> Integer = 123456789, ?match(Integer, query(Config, "command_test_integer", #{arg_integer => Integer})). + http_string(Config) -> S = "This is a string.", B = iolist_to_binary(S), ?match(B, query(Config, "command_test_string", #{arg_string => S})), ?match(B, query(Config, "command_test_string", #{arg_string => B})). + http_binary(Config) -> B = <<"This is a binary.">>, ?match(B, query(Config, "command_test_binary", #{arg_binary => B})). + %% mod_http_api doesn't handle 'atom' result format %% and formats the result as a binary by default http_atom(Config) -> @@ -135,12 +150,14 @@ http_atom(Config) -> ?match(B, query(Config, "command_test_atom", #{arg_string => S})), ?match(B, query(Config, "command_test_atom", #{arg_string => B})). + http_rescode(Config) -> ?match(0, query(Config, "command_test_rescode", #{code => "true"})), ?match(0, query(Config, "command_test_rescode", #{code => "ok"})), ?match(1, query(Config, "command_test_rescode", #{code => "problem"})), ?match(1, query(Config, "command_test_rescode", #{code => "error"})). + http_restuple(Config) -> ?match(<<"Deleted 0 users: []">>, query(Config, "delete_old_users", #{days => 99})), ?match(<<"Good">>, @@ -148,24 +165,31 @@ http_restuple(Config) -> ?match(<<"OK!!">>, query(Config, "command_test_restuple", #{code => "ok", text => "OK!!"})). + http_list(Config) -> ListS = ["one", "first", "primary"], ListB = lists:sort([<<"one">>, <<"first">>, <<"primary">>]), ?match(ListB, query(Config, "command_test_list", #{arg_list => ListS})), ?match(ListB, query(Config, "command_test_list", #{arg_list => ListB})). + http_tuple(Config) -> MapA = - #{element1 => "one", + #{ + element1 => "one", element2 => "first", - element3 => "primary"}, + element3 => "primary" + }, MapB = - #{<<"element1">> => <<"one">>, + #{ + <<"element1">> => <<"one">>, <<"element2">> => <<"first">>, - <<"element3">> => <<"primary">>}, + <<"element3">> => <<"primary">> + }, ?match(MapB, query(Config, "command_test_tuple", #{arg_tuple => MapA})), ?match(MapB, query(Config, "command_test_tuple", #{arg_tuple => MapB})). + http_list_tuple(Config) -> LTA = [#{element1 => "one", element2 => "uno"}, #{element1 => "two", element2 => "dos"}, @@ -176,22 +200,28 @@ http_list_tuple(Config) -> ?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTA})), ?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTB})). + http_list_tuple_map(Config) -> - LTA = #{<<"one">> => <<"uno">>, + LTA = #{ + <<"one">> => <<"uno">>, <<"two">> => <<"dos">>, - <<"three">> => <<"tres">>}, + <<"three">> => <<"tres">> + }, LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>}, #{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>}, #{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]), ?match(LTB, lists:sort(query(Config, "command_test_list_tuple", #{arg_list => LTA}))). + %%% internal functions + query(Config, Tail, Map) -> BodyQ = misc:json_encode(Map), Body = make_query(Config, Tail, BodyQ), misc:json_decode(Body). + make_query(Config, Tail, BodyQ) -> ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, httpc:request(post, @@ -200,28 +230,33 @@ make_query(Config, Tail, BodyQ) -> [{body_format, binary}]), Body). + page(Config, Tail) -> Server = ?config(server_host, Config), Port = ct:get_config(web_port, 5280), "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/api/" ++ Tail. + %%%================================== %%%% ad-hoc %%% list commands + adhoc_list_commands(Config) -> {ok, Result} = get_items(Config, <<"api-commands">>), {value, #disco_item{name = <<"command_test_binary">>}} = lists:keysearch(<<"command_test_binary">>, #disco_item.name, Result), suite:disconnect(Config). + get_items(Config, Node) -> case suite:send_recv(Config, - #iq{type = get, - to = server_jid(Config), - sub_els = [#disco_items{node = Node}]}) - of + #iq{ + type = get, + to = server_jid(Config), + sub_els = [#disco_items{node = Node}] + }) of #iq{type = result, sub_els = [#disco_items{node = Node, items = Items}]} -> {ok, Items}; #iq{type = result, sub_els = []} -> @@ -230,8 +265,10 @@ get_items(Config, Node) -> xmpp:get_error(Err) end. + %%% apiversion + adhoc_apiversion(Config) -> Node = <<"api-commands/command_test_apiversion">>, ArgFields = make_fields_args([]), @@ -240,8 +277,10 @@ adhoc_apiversion(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% apizero + adhoc_apizero(Config) -> Node = <<"api-commands/command_test_apizero">>, ArgFields = make_fields_args([]), @@ -250,8 +289,10 @@ adhoc_apizero(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% apione + adhoc_apione(Config) -> Node = <<"api-commands/command_test_apione">>, ArgFields = make_fields_args([]), @@ -260,8 +301,10 @@ adhoc_apione(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% integer + adhoc_integer(Config) -> Node = <<"api-commands/command_test_integer">>, ArgFields = make_fields_args([{<<"arg_integer">>, <<"12345">>}]), @@ -270,8 +313,10 @@ adhoc_integer(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% string + adhoc_string(Config) -> Node = <<"api-commands/command_test_string">>, ArgFields = make_fields_args([{<<"arg_string">>, <<"Some string.">>}]), @@ -280,8 +325,10 @@ adhoc_string(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% binary + adhoc_binary(Config) -> Node = <<"api-commands/command_test_binary">>, ArgFields = make_fields_args([{<<"arg_binary">>, <<"Some binary.">>}]), @@ -290,8 +337,10 @@ adhoc_binary(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% tuple + adhoc_tuple(Config) -> Node = <<"api-commands/command_test_tuple">>, ArgFields = make_fields_args([{<<"arg_tuple">>, <<"one:two:three">>}]), @@ -309,8 +358,10 @@ adhoc_tuple(Config) -> set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% list + adhoc_list(Config) -> Node = <<"api-commands/command_test_list">>, ArgFields = make_fields_args([{<<"arg_list">>, [<<"one">>, <<"first">>, <<"primary">>]}]), @@ -320,8 +371,10 @@ adhoc_list(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% list_tuple + adhoc_list_tuple(Config) -> Node = <<"api-commands/command_test_list_tuple">>, ArgFields = @@ -333,8 +386,10 @@ adhoc_list_tuple(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% atom + adhoc_atom(Config) -> Node = <<"api-commands/command_test_atom">>, ArgFields = make_fields_args([{<<"arg_string">>, <<"a_test_atom">>}]), @@ -343,8 +398,10 @@ adhoc_atom(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% rescode + adhoc_rescode(Config) -> Node = <<"api-commands/command_test_rescode">>, ArgFields = make_fields_args([{<<"code">>, <<"ok">>}]), @@ -353,8 +410,10 @@ adhoc_rescode(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% restuple + adhoc_restuple(Config) -> Node = <<"api-commands/command_test_restuple">>, ArgFields = @@ -364,79 +423,105 @@ adhoc_restuple(Config) -> ?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)), suite:disconnect(Config). + %%% internal functions + server_jid(Config) -> jid:make(<<>>, ?config(server, Config), <<>>). + make_fields_args(Fields) -> - lists:map(fun ({Var, Values}) when is_list(Values) -> - #xdata_field{label = Var, - var = Var, - required = true, - type = 'text-multi', - values = Values}; - ({Var, Value}) -> - #xdata_field{label = Var, - var = Var, - required = true, - type = 'text-single', - values = [Value]} + lists:map(fun({Var, Values}) when is_list(Values) -> + #xdata_field{ + label = Var, + var = Var, + required = true, + type = 'text-multi', + values = Values + }; + ({Var, Value}) -> + #xdata_field{ + label = Var, + var = Var, + required = true, + type = 'text-single', + values = [Value] + } end, Fields). + make_fields_res(Fields) -> - lists:map(fun ({Var, Values}) when is_list(Values) -> - #xdata_field{label = Var, - var = Var, - type = 'text-multi', - values = Values}; - ({Var, Value}) -> - #xdata_field{label = Var, - var = Var, - type = 'text-single', - values = [Value]} + lists:map(fun({Var, Values}) when is_list(Values) -> + #xdata_field{ + label = Var, + var = Var, + type = 'text-multi', + values = Values + }; + ({Var, Value}) -> + #xdata_field{ + label = Var, + var = Var, + type = 'text-single', + values = [Value] + } end, Fields). + get_form(Config, Node) -> case suite:send_recv(Config, - #iq{type = set, - to = server_jid(Config), - sub_els = [#adhoc_command{node = Node}]}) - of - #iq{type = result, - sub_els = - [#adhoc_command{node = Node, - action = execute, - status = executing, - sid = Sid, - actions = #adhoc_actions{execute = complete, complete = true}, - xdata = #xdata{fields = Fields}}]} -> - {ok, Sid, [F || F <- Fields, F#xdata_field.type /= fixed]}; + #iq{ + type = set, + to = server_jid(Config), + sub_els = [#adhoc_command{node = Node}] + }) of + #iq{ + type = result, + sub_els = + [#adhoc_command{ + node = Node, + action = execute, + status = executing, + sid = Sid, + actions = #adhoc_actions{execute = complete, complete = true}, + xdata = #xdata{fields = Fields} + }] + } -> + {ok, Sid, [ F || F <- Fields, F#xdata_field.type /= fixed ]}; #iq{type = error} = Err -> xmpp:get_error(Err) end. + set_form(Config, Node, Sid, ArgFields) -> Xdata = #xdata{type = submit, fields = ArgFields}, case suite:send_recv(Config, - #iq{type = set, - to = server_jid(Config), - sub_els = - [#adhoc_command{node = Node, - action = complete, - sid = Sid, - xdata = Xdata}]}) - of - #iq{type = result, - sub_els = - [#adhoc_command{node = Node, - action = execute, - status = completed, - sid = Sid, - xdata = #xdata{fields = ResFields}}]} -> - ResFields2 = [F || F <- ResFields, F#xdata_field.type /= fixed], + #iq{ + type = set, + to = server_jid(Config), + sub_els = + [#adhoc_command{ + node = Node, + action = complete, + sid = Sid, + xdata = Xdata + }] + }) of + #iq{ + type = result, + sub_els = + [#adhoc_command{ + node = Node, + action = execute, + status = completed, + sid = Sid, + xdata = #xdata{fields = ResFields} + }] + } -> + ResFields2 = [ F || F <- ResFields, F#xdata_field.type /= fixed ], {ok, ResFields2 -- ArgFields}; #iq{type = error} = Err -> xmpp:get_error(Err) diff --git a/test/configtest_tests.erl b/test/configtest_tests.erl index 3763b8645..18a6662ec 100644 --- a/test/configtest_tests.erl +++ b/test/configtest_tests.erl @@ -29,6 +29,7 @@ %%%================================== + single_cases() -> {configtest_single, [sequence], @@ -64,99 +65,131 @@ single_cases() -> single_test(toplevel_local_predefined), single_test(module_predefined)]}. + %% Interactions + macro_over_keyword(_) -> toplevel_global(macro, macro_over_keyword). + keyword_inside_macro(_) -> toplevel_global(<<"+macro+/-keyword-">>, keyword_inside_macro). + macro_and_keyword(_) -> toplevel_global(<<"+macro+&-keyword-">>, macro_and_keyword). + macro_double(_) -> toplevel_global(<<"macro--macro">>, macro_double). + keyword_double(_) -> toplevel_global(<<"keyword--keyword">>, keyword_double). + %% Macro Toplevel + macro_toplevel_global_atom(_) -> toplevel_global(mtga, mtga). + macro_toplevel_global_string(_) -> toplevel_global(<<"Mtgs">>, mtgs). + macro_toplevel_global_string_inside(_) -> toplevel_global(<<"Mtgsi">>, mtgsi). + macro_toplevel_local_atom(_) -> toplevel_local(mtla, mtla). + macro_toplevel_local_string(_) -> toplevel_local(<<"Mtls">>, mtls). + macro_toplevel_local_string_inside(_) -> toplevel_local(<<"Mtlsi">>, mtlsi). + %% Keyword Toplevel + keyword_toplevel_global_atom(_) -> toplevel_global(ktga, ktga). + keyword_toplevel_global_string(_) -> toplevel_global(<<"Ktgs">>, ktgs). + keyword_toplevel_global_string_inside(_) -> toplevel_global(<<"Ktgsi">>, ktgsi). + keyword_toplevel_local_atom(_) -> toplevel_local(ktla, ktla). + keyword_toplevel_local_string(_) -> toplevel_local(<<"Ktls">>, ktls). + keyword_toplevel_local_string_inside(_) -> toplevel_local(<<"Ktlsi">>, ktlsi). + %% Macro Module + macro_module_atom(_) -> module(mma, mma). + macro_module_string(_) -> module(<<"Mms">>, mms). + macro_module_string_inside(_) -> module(<<"Mmsi">>, mmsi). + %% Keyword Module + keyword_module_atom(_) -> module(kma, kma). + keyword_module_string(_) -> module(<<"Kms">>, kms). + keyword_module_string_inside(_) -> module(<<"Kmsi">>, kmsi). + %% Predefined + toplevel_global_predefined(_) -> Semver = ejabberd_option:version(), Version = misc:semver_to_xxyy(Semver), String = <<"tgp - semver: ", Semver/binary, ", version: ", Version/binary>>, toplevel_global(String, tgp). + toplevel_local_predefined(_) -> Semver = ejabberd_option:version(), Version = misc:semver_to_xxyy(Semver), String = <<"tlp - semver: ", Semver/binary, ", version: ", Version/binary>>, toplevel_local(String, tlp). + module_predefined(_) -> Host = <<"configtest.localhost">>, Semver = ejabberd_option:version(), @@ -164,19 +197,24 @@ module_predefined(_) -> String = <<"mp - host: ", Host/binary, ", semver: ", Semver/binary, ", version: ", Version/binary>>, module(String, predefined_keywords). + %%%================================== %%%% internal functions + single_test(T) -> list_to_atom("configtest_" ++ atom_to_list(T)). + toplevel_global(Result, Option) -> ?match(Result, ejabberd_config:get_option(Option)). + toplevel_local(Result, Option) -> Host = <<"configtest.localhost">>, ?match(Result, ejabberd_config:get_option({Option, Host})). + module(Result, Option) -> Host = <<"configtest.localhost">>, Module = mod_configtest, diff --git a/test/csi_tests.erl b/test/csi_tests.erl index f2b61abff..0e34c4ca7 100644 --- a/test/csi_tests.erl +++ b/test/csi_tests.erl @@ -25,12 +25,19 @@ %% API -compile(export_all). --import(suite, [disconnect/1, wait_for_slave/1, wait_for_master/1, - send/2, send_recv/2, recv_presence/1, recv_message/1, - server_jid/1]). +-import(suite, + [disconnect/1, + wait_for_slave/1, + wait_for_master/1, + send/2, + send_recv/2, + recv_presence/1, + recv_message/1, + server_jid/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,48 +46,62 @@ %%%=================================================================== single_cases() -> {csi_single, [sequence], - [single_test(feature_enabled)]}. + [single_test(feature_enabled)]}. + feature_enabled(Config) -> true = ?config(csi, Config), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {csi_master_slave, [sequence], - [master_slave_test(all)]}. + [master_slave_test(all)]}. + all_master(Config) -> Peer = ?config(peer, Config), Presence = #presence{to = Peer}, - ChatState = #message{to = Peer, thread = #message_thread{data = <<"1">>}, - sub_els = [#chatstate{type = active}]}, + ChatState = #message{ + to = Peer, + thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = active}] + }, Message = ChatState#message{body = [#text{data = <<"body">>}]}, PepPayload = xmpp:encode(#presence{}), PepOne = #message{ - to = Peer, - sub_els = - [#ps_event{ - items = - #ps_items{ - node = <<"foo-1">>, - items = - [#ps_item{ - id = <<"pep-1">>, - sub_els = [PepPayload]}]}}]}, + to = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-1">>, + items = + [#ps_item{ + id = <<"pep-1">>, + sub_els = [PepPayload] + }] + } + }] + }, PepTwo = #message{ - to = Peer, - sub_els = - [#ps_event{ - items = - #ps_items{ - node = <<"foo-2">>, - items = - [#ps_item{ - id = <<"pep-2">>, - sub_els = [PepPayload]}]}}]}, + to = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-2">>, + items = + [#ps_item{ + id = <<"pep-2">>, + sub_els = [PepPayload] + }] + } + }] + }, %% Wait for the slave to become inactive. wait_for_slave(Config), %% Should be queued (but see below): @@ -105,58 +126,84 @@ all_master(Config) -> send(Config, ChatState), disconnect(Config). + all_slave(Config) -> Peer = ?config(peer, Config), change_client_state(Config, inactive), wait_for_master(Config), #presence{from = Peer, type = unavailable, sub_els = [#delay{}]} = - recv_presence(Config), + recv_presence(Config), #message{ - from = Peer, - sub_els = - [#ps_event{ - items = - #ps_items{ - node = <<"foo-1">>, - items = - [#ps_item{ - id = <<"pep-1">>}]}}, - #delay{}]} = recv_message(Config), + from = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-1">>, + items = + [#ps_item{ + id = <<"pep-1">> + }] + } + }, + #delay{}] + } = recv_message(Config), #message{ - from = Peer, - sub_els = - [#ps_event{ - items = - #ps_items{ - node = <<"foo-2">>, - items = - [#ps_item{ - id = <<"pep-2">>}]}}, - #delay{}]} = recv_message(Config), - #message{from = Peer, thread = #message_thread{data = <<"1">>}, - sub_els = [#chatstate{type = composing}, - #delay{}]} = recv_message(Config), - #message{from = Peer, thread = #message_thread{data = <<"1">>}, - body = [#text{data = <<"body">>}], - sub_els = [#chatstate{type = active}]} = recv_message(Config), + from = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-2">>, + items = + [#ps_item{ + id = <<"pep-2">> + }] + } + }, + #delay{}] + } = recv_message(Config), + #message{ + from = Peer, + thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = composing}, + #delay{}] + } = recv_message(Config), + #message{ + from = Peer, + thread = #message_thread{data = <<"1">>}, + body = [#text{data = <<"body">>}], + sub_els = [#chatstate{type = active}] + } = recv_message(Config), change_client_state(Config, active), wait_for_master(Config), - #message{from = Peer, thread = #message_thread{data = <<"1">>}, - sub_els = [#chatstate{type = active}]} = recv_message(Config), + #message{ + from = Peer, + thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = active}] + } = recv_message(Config), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("csi_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("csi_" ++ atom_to_list(T)), [parallel], + {list_to_atom("csi_" ++ atom_to_list(T)), + [parallel], [list_to_atom("csi_" ++ atom_to_list(T) ++ "_master"), list_to_atom("csi_" ++ atom_to_list(T) ++ "_slave")]}. + change_client_state(Config, NewState) -> send(Config, #csi{type = NewState}), - send_recv(Config, #iq{type = get, to = server_jid(Config), - sub_els = [#ping{}]}). + send_recv(Config, + #iq{ + type = get, + to = server_jid(Config), + sub_els = [#ping{}] + }). diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index ca465689d..985207a83 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -23,26 +23,67 @@ -module(ejabberd_SUITE). -compile(export_all). --import(suite, [init_config/1, connect/1, disconnect/1, recv_message/1, - recv/1, recv_presence/1, send/2, send_recv/2, my_jid/1, - server_jid/1, pubsub_jid/1, proxy_jid/1, muc_jid/1, - muc_room_jid/1, my_muc_jid/1, peer_muc_jid/1, - mix_jid/1, mix_room_jid/1, get_features/2, recv_iq/1, - re_register/1, is_feature_advertised/2, subscribe_to_events/1, - is_feature_advertised/3, set_opt/3, - auth_SASL/2, auth_SASL/3, auth_SASL/4, - wait_for_master/1, wait_for_slave/1, flush/1, - make_iq_result/1, start_event_relay/0, alt_room_jid/1, - stop_event_relay/1, put_event/2, get_event/1, - bind/1, auth/1, auth/2, open_session/1, open_session/2, - zlib/1, starttls/1, starttls/2, close_socket/1, init_stream/1, - auth_legacy/2, auth_legacy/3, tcp_connect/1, send_text/2, - set_roster/3, del_roster/1]). +-import(suite, + [init_config/1, + connect/1, + disconnect/1, + recv_message/1, + recv/1, + recv_presence/1, + send/2, + send_recv/2, + my_jid/1, + server_jid/1, + pubsub_jid/1, + proxy_jid/1, + muc_jid/1, + muc_room_jid/1, + my_muc_jid/1, + peer_muc_jid/1, + mix_jid/1, + mix_room_jid/1, + get_features/2, + recv_iq/1, + re_register/1, + is_feature_advertised/2, + subscribe_to_events/1, + is_feature_advertised/3, + set_opt/3, + auth_SASL/2, + auth_SASL/3, + auth_SASL/4, + wait_for_master/1, + wait_for_slave/1, + flush/1, + make_iq_result/1, + start_event_relay/0, + alt_room_jid/1, + stop_event_relay/1, + put_event/2, + get_event/1, + bind/1, + auth/1, + auth/2, + open_session/1, + open_session/2, + zlib/1, + starttls/1, + starttls/2, + close_socket/1, + init_stream/1, + auth_legacy/2, + auth_legacy/3, + tcp_connect/1, + send_text/2, + set_roster/3, + del_roster/1]). -include("suite.hrl"). + suite() -> [{timetrap, {seconds, 120}}]. + init_per_suite(Config) -> NewConfig = init_config(Config), DataDir = proplists:get_value(data_dir, NewConfig), @@ -51,25 +92,29 @@ init_per_suite(Config) -> LDIFFile = filename:join([DataDir, "ejabberd.ldif"]), {ok, _} = file:copy(ExtAuthScript, filename:join([CWD, "extauth.py"])), {ok, _} = ldap_srv:start(LDIFFile), - inet_db:add_host({127,0,0,1}, [binary_to_list(?S2S_VHOST), - binary_to_list(?MNESIA_VHOST), - binary_to_list(?UPLOAD_VHOST)]), + inet_db:add_host({127, 0, 0, 1}, + [binary_to_list(?S2S_VHOST), + binary_to_list(?MNESIA_VHOST), + binary_to_list(?UPLOAD_VHOST)]), inet_db:set_domain(binary_to_list(p1_rand:get_string())), inet_db:set_lookup([file, native]), start_ejabberd(NewConfig), NewConfig. + start_ejabberd(_) -> TestBeams = case filelib:is_dir("../../test/") of - true -> "../../test/"; - _ -> "../../lib/ejabberd/test/" - end, + true -> "../../test/"; + _ -> "../../lib/ejabberd/test/" + end, application:set_env(ejabberd, external_beams, TestBeams), {ok, _} = application:ensure_all_started(ejabberd, transient). + end_per_suite(_Config) -> application:stop(ejabberd). + init_per_group(Group, Config) -> case lists:member(Group, ?BACKENDS) of false -> @@ -82,15 +127,16 @@ init_per_group(Group, Config) -> do_init_per_group(Group, Config); Backends -> %% Skipped backends that were not explicitly enabled - case lists:member(Group, Backends) of - true -> - do_init_per_group(Group, Config); - false -> - {skip, {disabled_backend, Group}} - end + case lists:member(Group, Backends) of + true -> + do_init_per_group(Group, Config); + false -> + {skip, {disabled_backend, Group}} + end end end. + do_init_per_group(no_db, Config) -> re_register(Config), set_opt(persistent_room, false, Config); @@ -146,32 +192,44 @@ do_init_per_group(s2s, Config) -> ejabberd_config:set_option({s2s_use_starttls, ?COMMON_VHOST}, required), ejabberd_config:set_option(ca_file, "ca.pem"), Port = ?config(s2s_port, Config), - set_opt(server, ?COMMON_VHOST, - set_opt(xmlns, ?NS_SERVER, - set_opt(type, server, - set_opt(server_port, Port, - set_opt(stream_from, ?S2S_VHOST, - set_opt(lang, <<"">>, Config)))))); + set_opt(server, + ?COMMON_VHOST, + set_opt(xmlns, + ?NS_SERVER, + set_opt(type, + server, + set_opt(server_port, + Port, + set_opt(stream_from, + ?S2S_VHOST, + set_opt(lang, <<"">>, Config)))))); do_init_per_group(component, Config) -> Server = ?config(server, Config), Port = ?config(component_port, Config), - set_opt(xmlns, ?NS_COMPONENT, - set_opt(server, <<"component.", Server/binary>>, - set_opt(type, component, - set_opt(server_port, Port, - set_opt(stream_version, undefined, + set_opt(xmlns, + ?NS_COMPONENT, + set_opt(server, + <<"component.", Server/binary>>, + set_opt(type, + component, + set_opt(server_port, + Port, + set_opt(stream_version, + undefined, set_opt(lang, <<"">>, Config)))))); do_init_per_group(GroupName, Config) -> Pid = start_event_relay(), NewConfig = set_opt(event_relay, Pid, Config), case GroupName of - anonymous -> set_opt(anonymous, true, NewConfig); - _ -> NewConfig + anonymous -> set_opt(anonymous, true, NewConfig); + _ -> NewConfig end. + stop_temporary_modules(Host) -> Modules = [mod_shared_roster], - [gen_mod:stop_module(Host, M) || M <- Modules]. + [ gen_mod:stop_module(Host, M) || M <- Modules ]. + end_per_group(mnesia, _Config) -> ok; @@ -202,8 +260,8 @@ end_per_group(pgsql, Config) -> case catch ejabberd_sql:sql_query(?PGSQL_VHOST, [Query]) of {selected, [t]} -> clear_sql_tables(pgsql, Config); - {selected, _, [[<<"t">>]]} -> - clear_sql_tables(pgsql, Config); + {selected, _, [[<<"t">>]]} -> + clear_sql_tables(pgsql, Config); Other -> ct:fail({failed_to_check_table_existence, pgsql, Other}) end, @@ -225,94 +283,110 @@ end_per_group(_GroupName, Config) -> stop_event_relay(Config), set_opt(anonymous, false, Config). + init_per_testcase(stop_ejabberd, Config) -> - NewConfig = set_opt(resource, <<"">>, - set_opt(anonymous, true, Config)), + NewConfig = set_opt(resource, + <<"">>, + set_opt(anonymous, true, Config)), open_session(bind(auth(connect(NewConfig)))); init_per_testcase(TestCase, OrigConfig) -> ct:print(80, "Testcase '~p' starting", [TestCase]), Test = atom_to_list(TestCase), IsMaster = lists:suffix("_master", Test), IsSlave = lists:suffix("_slave", Test), - if IsMaster or IsSlave -> - subscribe_to_events(OrigConfig); - true -> - ok + if + IsMaster or IsSlave -> + subscribe_to_events(OrigConfig); + true -> + ok end, TestGroup = proplists:get_value( - name, ?config(tc_group_properties, OrigConfig)), + name, ?config(tc_group_properties, OrigConfig)), Server = ?config(server, OrigConfig), Resource = case TestGroup of - anonymous -> - <<"">>; - legacy_auth -> - p1_rand:get_string(); - _ -> - ?config(resource, OrigConfig) - end, + anonymous -> + <<"">>; + legacy_auth -> + p1_rand:get_string(); + _ -> + ?config(resource, OrigConfig) + end, MasterResource = ?config(master_resource, OrigConfig), SlaveResource = ?config(slave_resource, OrigConfig), - Mode = if IsSlave -> slave; - IsMaster -> master; - true -> single - end, + Mode = if + IsSlave -> slave; + IsMaster -> master; + true -> single + end, IsCarbons = lists:prefix("carbons_", Test), IsReplaced = lists:prefix("replaced_", Test), - User = if IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; - IsCarbons and not (IsMaster or IsSlave) -> - <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; - IsMaster or IsCarbons -> <<"test_master!#$%^*()`~+-;_=[]{}|\\">>; - IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>; - true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">> + User = if + IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; + IsCarbons and not (IsMaster or IsSlave) -> + <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; + IsMaster or IsCarbons -> <<"test_master!#$%^*()`~+-;_=[]{}|\\">>; + IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>; + true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">> end, - Nick = if IsSlave -> ?config(slave_nick, OrigConfig); - IsMaster -> ?config(master_nick, OrigConfig); - true -> ?config(nick, OrigConfig) - end, - MyResource = if IsMaster and IsCarbons -> MasterResource; - IsSlave and IsCarbons -> SlaveResource; - true -> Resource - end, - Slave = if IsCarbons -> - jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, SlaveResource); - IsReplaced -> - jid:make(User, Server, Resource); - true -> - jid:make(<<"test_slave!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) - end, - Master = if IsCarbons -> - jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, MasterResource); - IsReplaced -> - jid:make(User, Server, Resource); - true -> - jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) - end, - Config1 = set_opt(user, User, - set_opt(slave, Slave, - set_opt(master, Master, - set_opt(resource, MyResource, - set_opt(nick, Nick, - set_opt(mode, Mode, OrigConfig)))))), - Config2 = if IsSlave -> - set_opt(peer_nick, ?config(master_nick, Config1), Config1); - IsMaster -> - set_opt(peer_nick, ?config(slave_nick, Config1), Config1); - true -> - Config1 - end, - Config = if IsSlave -> set_opt(peer, Master, Config2); - IsMaster -> set_opt(peer, Slave, Config2); - true -> Config2 - end, + Nick = if + IsSlave -> ?config(slave_nick, OrigConfig); + IsMaster -> ?config(master_nick, OrigConfig); + true -> ?config(nick, OrigConfig) + end, + MyResource = if + IsMaster and IsCarbons -> MasterResource; + IsSlave and IsCarbons -> SlaveResource; + true -> Resource + end, + Slave = if + IsCarbons -> + jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, SlaveResource); + IsReplaced -> + jid:make(User, Server, Resource); + true -> + jid:make(<<"test_slave!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) + end, + Master = if + IsCarbons -> + jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, MasterResource); + IsReplaced -> + jid:make(User, Server, Resource); + true -> + jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) + end, + Config1 = set_opt(user, + User, + set_opt(slave, + Slave, + set_opt(master, + Master, + set_opt(resource, + MyResource, + set_opt(nick, + Nick, + set_opt(mode, Mode, OrigConfig)))))), + Config2 = if + IsSlave -> + set_opt(peer_nick, ?config(master_nick, Config1), Config1); + IsMaster -> + set_opt(peer_nick, ?config(slave_nick, Config1), Config1); + true -> + Config1 + end, + Config = if + IsSlave -> set_opt(peer, Master, Config2); + IsMaster -> set_opt(peer, Slave, Config2); + true -> Config2 + end, case Test of "test_connect" ++ _ -> Config; "webadmin_" ++ _ -> Config; - "test_legacy_auth_feature" -> - connect(Config); - "test_legacy_auth" ++ _ -> - init_stream(set_opt(stream_version, undefined, Config)); + "test_legacy_auth_feature" -> + connect(Config); + "test_legacy_auth" ++ _ -> + init_stream(set_opt(stream_version, undefined, Config)); "test_auth" ++ _ -> connect(Config); "test_starttls" ++ _ -> @@ -325,20 +399,20 @@ init_per_testcase(TestCase, OrigConfig) -> connect(Config); "auth_plain" -> connect(Config); - "auth_external" ++ _ -> - connect(Config); - "unauthenticated_" ++ _ -> - connect(Config); + "auth_external" ++ _ -> + connect(Config); + "unauthenticated_" ++ _ -> + connect(Config); "test_bind" -> auth(connect(Config)); - "sm_resume" -> - auth(connect(Config)); - "sm_resume_failed" -> - auth(connect(Config)); + "sm_resume" -> + auth(connect(Config)); + "sm_resume_failed" -> + auth(connect(Config)); "test_open_session" -> bind(auth(connect(Config))); - "replaced" ++ _ -> - auth(connect(Config)); + "replaced" ++ _ -> + auth(connect(Config)); "antispam" ++ _ -> Password = ?config(password, Config), ejabberd_auth:try_register(User, Server, Password), @@ -347,58 +421,61 @@ init_per_testcase(TestCase, OrigConfig) -> Password = ?config(password, Config), ejabberd_auth:try_register(User, Server, Password), open_session(bind(auth(connect(Config)))); - _ when TestGroup == s2s_tests -> - auth(connect(starttls(connect(Config)))); + _ when TestGroup == s2s_tests -> + auth(connect(starttls(connect(Config)))); _ -> open_session(bind(auth(connect(Config)))) end. + end_per_testcase(_TestCase, _Config) -> ok. + legacy_auth_tests() -> {legacy_auth, [parallel], - [test_legacy_auth_feature, - test_legacy_auth, - test_legacy_auth_digest, - test_legacy_auth_no_resource, - test_legacy_auth_bad_jid, - test_legacy_auth_fail]}. + [test_legacy_auth_feature, + test_legacy_auth, + test_legacy_auth_digest, + test_legacy_auth_no_resource, + test_legacy_auth_bad_jid, + test_legacy_auth_fail]}. + no_db_tests() -> [{anonymous, [parallel], - [test_connect_bad_xml, - test_connect_unexpected_xml, - test_connect_unknown_ns, - test_connect_bad_xmlns, - test_connect_bad_ns_stream, - test_connect_bad_lang, - test_connect_bad_to, - test_connect_missing_to, - test_connect, - unauthenticated_iq, - unauthenticated_message, - unauthenticated_presence, - test_starttls, - test_auth, - test_zlib, - test_bind, - test_open_session, - codec_failure, - unsupported_query, - bad_nonza, - invalid_from, - ping, - version, - time, - stats, - disco]}, + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect_bad_lang, + test_connect_bad_to, + test_connect_missing_to, + test_connect, + unauthenticated_iq, + unauthenticated_message, + unauthenticated_presence, + test_starttls, + test_auth, + test_zlib, + test_bind, + test_open_session, + codec_failure, + unsupported_query, + bad_nonza, + invalid_from, + ping, + version, + time, + stats, + disco]}, {presence_and_s2s, [sequence], - [test_auth_fail, - presence, - s2s_dialback, - s2s_optional, - s2s_required]}, + [test_auth_fail, + presence, + s2s_dialback, + s2s_optional, + s2s_required]}, auth_external, auth_external_no_jid, auth_external_no_user, @@ -421,28 +498,29 @@ no_db_tests() -> carbons_tests:single_cases(), carbons_tests:master_slave_cases()]. + db_tests(DB) when DB == mnesia; DB == redis -> [{single_user, [sequence], - [test_register, - legacy_auth_tests(), - auth_plain, - auth_md5, - presence_broadcast, - last, - antispam_tests:single_cases(), - webadmin_tests:single_cases(), - roster_tests:single_cases(), - private_tests:single_cases(), - privacy_tests:single_cases(), - vcard_tests:single_cases(), - pubsub_tests:single_cases(), - muc_tests:single_cases(), - offline_tests:single_cases(), - mam_tests:single_cases(), - csi_tests:single_cases(), - push_tests:single_cases(), - test_pass_change, - test_unregister]}, + [test_register, + legacy_auth_tests(), + auth_plain, + auth_md5, + presence_broadcast, + last, + antispam_tests:single_cases(), + webadmin_tests:single_cases(), + roster_tests:single_cases(), + private_tests:single_cases(), + privacy_tests:single_cases(), + vcard_tests:single_cases(), + pubsub_tests:single_cases(), + muc_tests:single_cases(), + offline_tests:single_cases(), + mam_tests:single_cases(), + csi_tests:single_cases(), + push_tests:single_cases(), + test_pass_change, + test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), pubsub_tests:master_slave_cases(), @@ -455,24 +533,24 @@ db_tests(DB) when DB == mnesia; DB == redis -> push_tests:master_slave_cases()]; db_tests(DB) -> [{single_user, [sequence], - [test_register, - legacy_auth_tests(), - auth_plain, - auth_md5, - presence_broadcast, - last, - webadmin_tests:single_cases(), - roster_tests:single_cases(), - private_tests:single_cases(), - privacy_tests:single_cases(), - vcard_tests:single_cases(), - pubsub_tests:single_cases(), - muc_tests:single_cases(), - offline_tests:single_cases(), - mam_tests:single_cases(), - push_tests:single_cases(), - test_pass_change, - test_unregister]}, + [test_register, + legacy_auth_tests(), + auth_plain, + auth_md5, + presence_broadcast, + last, + webadmin_tests:single_cases(), + roster_tests:single_cases(), + private_tests:single_cases(), + privacy_tests:single_cases(), + vcard_tests:single_cases(), + pubsub_tests:single_cases(), + muc_tests:single_cases(), + offline_tests:single_cases(), + mam_tests:single_cases(), + push_tests:single_cases(), + test_pass_change, + test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), pubsub_tests:master_slave_cases(), @@ -483,56 +561,61 @@ db_tests(DB) -> announce_tests:master_slave_cases(), push_tests:master_slave_cases()]. + ldap_tests() -> [{ldap_tests, [sequence], - [test_auth, - test_auth_fail, - vcard_get, - ldap_shared_roster_get]}]. + [test_auth, + test_auth_fail, + vcard_get, + ldap_shared_roster_get]}]. + extauth_tests() -> [{extauth_tests, [sequence], - [test_auth, - test_auth_fail, - test_unregister]}]. + [test_auth, + test_auth_fail, + test_unregister]}]. + component_tests() -> [{component_connect, [parallel], - [test_connect_bad_xml, - test_connect_unexpected_xml, - test_connect_unknown_ns, - test_connect_bad_xmlns, - test_connect_bad_ns_stream, - test_connect_missing_to, - test_connect, - test_auth, - test_auth_fail]}, + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect_missing_to, + test_connect, + test_auth, + test_auth_fail]}, {component_tests, [sequence], - [test_missing_from, - test_missing_to, - test_invalid_from, - test_component_send, - bad_nonza, - codec_failure]}]. + [test_missing_from, + test_missing_to, + test_invalid_from, + test_component_send, + bad_nonza, + codec_failure]}]. + s2s_tests() -> [{s2s_connect, [parallel], - [test_connect_bad_xml, - test_connect_unexpected_xml, - test_connect_unknown_ns, - test_connect_bad_xmlns, - test_connect_bad_ns_stream, - test_connect, - test_connect_s2s_starttls_required, - test_starttls, - test_connect_s2s_unauthenticated_iq, - test_auth_starttls]}, + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect, + test_connect_s2s_starttls_required, + test_starttls, + test_connect_s2s_unauthenticated_iq, + test_auth_starttls]}, {s2s_tests, [sequence], - [test_missing_from, - test_missing_to, - test_invalid_from, - bad_nonza, - codec_failure]}]. + [test_missing_from, + test_missing_to, + test_invalid_from, + bad_nonza, + codec_failure]}]. + groups() -> [{ldap, [sequence], ldap_tests()}, @@ -547,6 +630,7 @@ groups() -> {pgsql, [sequence], db_tests(pgsql)}, {sqlite, [sequence], db_tests(sqlite)}]. + all() -> [{group, ldap}, {group, no_db}, @@ -561,6 +645,7 @@ all() -> {group, s2s}, stop_ejabberd]. + stop_ejabberd(Config) -> ok = application:stop(ejabberd), ?recv1(#stream_error{reason = 'system-shutdown'}), @@ -574,6 +659,7 @@ stop_ejabberd(Config) -> end, Config. + test_connect_bad_xml(Config) -> Config0 = tcp_connect(Config), send_text(Config0, <<"<'/>">>), @@ -583,6 +669,7 @@ test_connect_bad_xml(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_unexpected_xml(Config) -> Config0 = tcp_connect(Config), send(Config0, #caps{}), @@ -592,28 +679,32 @@ test_connect_unexpected_xml(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_unknown_ns(Config) -> Config0 = init_stream(set_opt(xmlns, <<"wrong">>, Config)), ?recv1(#stream_error{reason = 'invalid-xml'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_bad_xmlns(Config) -> NS = case ?config(type, Config) of - client -> ?NS_SERVER; - _ -> ?NS_CLIENT - end, + client -> ?NS_SERVER; + _ -> ?NS_CLIENT + end, Config0 = init_stream(set_opt(xmlns, NS, Config)), ?recv1(#stream_error{reason = 'invalid-namespace'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_bad_ns_stream(Config) -> Config0 = init_stream(set_opt(ns_stream, <<"wrong">>, Config)), ?recv1(#stream_error{reason = 'invalid-namespace'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_bad_lang(Config) -> Lang = iolist_to_binary(lists:duplicate(36, $x)), Config0 = init_stream(set_opt(lang, Lang, Config)), @@ -621,21 +712,25 @@ test_connect_bad_lang(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_bad_to(Config) -> Config0 = init_stream(set_opt(server, <<"wrong.com">>, Config)), ?recv1(#stream_error{reason = 'host-unknown'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect_missing_to(Config) -> Config0 = init_stream(set_opt(server, <<"">>, Config)), ?recv1(#stream_error{reason = 'improper-addressing'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). + test_connect(Config) -> disconnect(connect(Config)). + test_connect_s2s_starttls_required(Config) -> Config1 = connect(Config), send(Config1, #presence{}), @@ -643,10 +738,12 @@ test_connect_s2s_starttls_required(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config1). + test_connect_s2s_unauthenticated_iq(Config) -> Config1 = connect(starttls(connect(Config))), unauthenticated_iq(Config1). + test_starttls(Config) -> case ?config(starttls, Config) of true -> @@ -655,9 +752,10 @@ test_starttls(Config) -> {skipped, 'starttls_not_available'} end. + test_zlib(Config) -> case ?config(compression, Config) of - [_|_] = Ms -> + [_ | _] = Ms -> case lists:member(<<"zlib">>, Ms) of true -> disconnect(zlib(Config)); @@ -668,6 +766,7 @@ test_zlib(Config) -> {skipped, 'compression_not_available'} end. + test_register(Config) -> case ?config(register, Config) of true -> @@ -676,39 +775,62 @@ test_register(Config) -> {skipped, 'registration_not_available'} end. + register(Config) -> - #iq{type = result, - sub_els = [#register{username = <<>>, - password = <<>>}]} = - send_recv(Config, #iq{type = get, to = server_jid(Config), - sub_els = [#register{}]}), + #iq{ + type = result, + sub_els = [#register{ + username = <<>>, + password = <<>> + }] + } = + send_recv(Config, + #iq{ + type = get, + to = server_jid(Config), + sub_els = [#register{}] + }), #iq{type = result, sub_els = []} = send_recv( Config, - #iq{type = set, - sub_els = [#register{username = ?config(user, Config), - password = ?config(password, Config)}]}), + #iq{ + type = set, + sub_els = [#register{ + username = ?config(user, Config), + password = ?config(password, Config) + }] + }), Config. + test_pass_change(Config) -> case ?config(register, Config) of - true -> - #iq{type = result, sub_els = []} = - send_recv( - Config, - #iq{type = set, - sub_els = [#register{username = ?config(user, Config), - password = ?config(password, Config)}]}), - #iq{type = result, sub_els = []} = - send_recv( - Config, - #iq{type = set, - sub_els = [#register{username = str:to_upper(?config(user, Config)), - password = ?config(password, Config)}]}); - _ -> - {skipped, 'registration_not_available'} + true -> + #iq{type = result, sub_els = []} = + send_recv( + Config, + #iq{ + type = set, + sub_els = [#register{ + username = ?config(user, Config), + password = ?config(password, Config) + }] + }), + #iq{type = result, sub_els = []} = + send_recv( + Config, + #iq{ + type = set, + sub_els = [#register{ + username = str:to_upper(?config(user, Config)), + password = ?config(password, Config) + }] + }); + _ -> + {skipped, 'registration_not_available'} end. + test_unregister(Config) -> case ?config(register, Config) of true -> @@ -717,26 +839,33 @@ test_unregister(Config) -> {skipped, 'registration_not_available'} end. + try_unregister(Config) -> true = is_feature_advertised(Config, ?NS_REGISTER), #iq{type = result, sub_els = []} = send_recv( Config, - #iq{type = set, - sub_els = [#register{remove = true}]}), + #iq{ + type = set, + sub_els = [#register{remove = true}] + }), ?recv1(#stream_error{reason = conflict}), Config. + unauthenticated_presence(Config) -> unauthenticated_packet(Config, #presence{}). + unauthenticated_message(Config) -> unauthenticated_packet(Config, #message{}). + unauthenticated_iq(Config) -> IQ = #iq{type = get, sub_els = [#disco_info{}]}, unauthenticated_packet(Config, IQ). + unauthenticated_packet(Config, Pkt) -> From = my_jid(Config), To = server_jid(Config), @@ -745,18 +874,21 @@ unauthenticated_packet(Config, Pkt) -> {xmlstreamend, <<"stream:stream">>} = recv(Config), close_socket(Config). + bad_nonza(Config) -> %% Unsupported and invalid nonza should be silently dropped. send(Config, #caps{}), send(Config, #stanza_error{type = wrong}), disconnect(Config). + invalid_from(Config) -> send(Config, #message{from = jid:make(p1_rand:get_string())}), ?recv1(#stream_error{reason = 'invalid-from'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). + test_missing_from(Config) -> Server = server_jid(Config), send(Config, #message{to = Server}), @@ -764,6 +896,7 @@ test_missing_from(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). + test_missing_to(Config) -> Server = server_jid(Config), send(Config, #message{from = Server}), @@ -771,6 +904,7 @@ test_missing_to(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). + test_invalid_from(Config) -> From = jid:make(p1_rand:get_string()), To = jid:make(p1_rand:get_string()), @@ -779,14 +913,21 @@ test_invalid_from(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). + test_component_send(Config) -> To = jid:make(?COMMON_VHOST), From = server_jid(Config), #iq{type = result, from = To, to = From} = - send_recv(Config, #iq{type = get, to = To, from = From, - sub_els = [#ping{}]}), + send_recv(Config, + #iq{ + type = get, + to = To, + from = From, + sub_els = [#ping{}] + }), disconnect(Config). + s2s_dialback(Config) -> Server = ?config(server, Config), ejabberd_s2s:stop_s2s_connections(), @@ -795,6 +936,7 @@ s2s_dialback(Config) -> ejabberd_config:set_option(ca_file, pkix:get_cafile()), s2s_ping(Config). + s2s_optional(Config) -> Server = ?config(server, Config), ejabberd_s2s:stop_s2s_connections(), @@ -803,6 +945,7 @@ s2s_optional(Config) -> ejabberd_config:set_option(ca_file, pkix:get_cafile()), s2s_ping(Config). + s2s_required(Config) -> Server = ?config(server, Config), ejabberd_s2s:stop_s2s_connections(), @@ -813,15 +956,22 @@ s2s_required(Config) -> ejabberd_config:set_option(ca_file, "ca.pem"), s2s_ping(Config). + s2s_ping(Config) -> From = my_jid(Config), To = jid:make(?MNESIA_VHOST), ID = p1_rand:get_string(), - ejabberd_s2s:route(#iq{from = From, to = To, id = ID, - type = get, sub_els = [#ping{}]}), + ejabberd_s2s:route(#iq{ + from = From, + to = To, + id = ID, + type = get, + sub_els = [#ping{}] + }), #iq{type = result, id = ID, sub_els = []} = recv_iq(Config), disconnect(Config). + auth_md5(Config) -> Mechs = ?config(mechs, Config), case lists:member(<<"DIGEST-MD5">>, Mechs) of @@ -832,6 +982,7 @@ auth_md5(Config) -> {skipped, 'DIGEST-MD5_not_available'} end. + auth_plain(Config) -> Mechs = ?config(mechs, Config), case lists:member(<<"PLAIN">>, Mechs) of @@ -842,113 +993,157 @@ auth_plain(Config) -> {skipped, 'PLAIN_not_available'} end. + auth_external(Config0) -> Config = connect(starttls(Config0)), disconnect(auth_SASL(<<"EXTERNAL">>, Config)). + auth_external_no_jid(Config0) -> Config = connect(starttls(Config0)), - disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShoudFail = false, - {<<"">>, <<"">>, <<"">>})). + disconnect(auth_SASL(<<"EXTERNAL">>, + Config, + _ShoudFail = false, + {<<"">>, <<"">>, <<"">>})). + auth_external_no_user(Config0) -> Config = set_opt(user, <<"">>, connect(starttls(Config0))), disconnect(auth_SASL(<<"EXTERNAL">>, Config)). + auth_external_malformed_jid(Config0) -> Config = connect(starttls(Config0)), - disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, - {<<"">>, <<"@">>, <<"">>})). + disconnect(auth_SASL(<<"EXTERNAL">>, + Config, + _ShouldFail = true, + {<<"">>, <<"@">>, <<"">>})). + auth_external_wrong_jid(Config0) -> - Config = set_opt(user, <<"wrong">>, - connect(starttls(Config0))), + Config = set_opt(user, + <<"wrong">>, + connect(starttls(Config0))), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). + auth_external_wrong_server(Config0) -> Config = connect(starttls(Config0)), - disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, - {<<"">>, <<"wrong.com">>, <<"">>})). + disconnect(auth_SASL(<<"EXTERNAL">>, + Config, + _ShouldFail = true, + {<<"">>, <<"wrong.com">>, <<"">>})). + auth_external_invalid_cert(Config0) -> Config = connect(starttls( - set_opt(certfile, "self-signed-cert.pem", Config0))), + set_opt(certfile, "self-signed-cert.pem", Config0))), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). + test_legacy_auth_feature(Config) -> true = ?config(legacy_auth, Config), disconnect(Config). + test_legacy_auth(Config) -> disconnect(auth_legacy(Config, _Digest = false)). + test_legacy_auth_digest(Config) -> disconnect(auth_legacy(Config, _Digest = true)). + test_legacy_auth_no_resource(Config0) -> Config = set_opt(resource, <<"">>, Config0), disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + test_legacy_auth_bad_jid(Config0) -> Config = set_opt(user, <<"@">>, Config0), disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + test_legacy_auth_fail(Config0) -> Config = set_opt(user, <<"wrong">>, Config0), disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + test_auth(Config) -> disconnect(auth(Config)). + test_auth_starttls(Config) -> disconnect(auth(connect(starttls(Config)))). + test_auth_fail(Config0) -> - Config = set_opt(user, <<"wrong">>, - set_opt(password, <<"wrong">>, Config0)), + Config = set_opt(user, + <<"wrong">>, + set_opt(password, <<"wrong">>, Config0)), disconnect(auth(Config, _ShouldFail = true)). + test_bind(Config) -> disconnect(bind(Config)). + test_open_session(Config) -> disconnect(open_session(Config, true)). + codec_failure(Config) -> JID = my_jid(Config), #iq{type = error} = - send_recv(Config, #iq{type = wrong, from = JID, to = JID}), + send_recv(Config, #iq{type = wrong, from = JID, to = JID}), disconnect(Config). + unsupported_query(Config) -> ServerJID = server_jid(Config), #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID}), - #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, - sub_els = [#caps{}]}), - #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, - sub_els = [#roster_query{}, - #disco_info{}, - #privacy_query{}]}), + #iq{type = error} = send_recv(Config, + #iq{ + type = get, + to = ServerJID, + sub_els = [#caps{}] + }), + #iq{type = error} = send_recv(Config, + #iq{ + type = get, + to = ServerJID, + sub_els = [#roster_query{}, + #disco_info{}, + #privacy_query{}] + }), disconnect(Config). + presence(Config) -> JID = my_jid(Config), #presence{from = JID, to = JID} = send_recv(Config, #presence{}), disconnect(Config). + presence_broadcast(Config) -> Feature = <<"p1:tmp:", (p1_rand:get_string())/binary>>, - Ver = crypto:hash(sha, ["client", $/, "bot", $/, "en", $/, - "ejabberd_ct", $<, Feature, $<]), + Ver = crypto:hash(sha, + ["client", $/, "bot", $/, "en", $/, + "ejabberd_ct", $<, Feature, $<]), B64Ver = base64:encode(Ver), Node = <<(?EJABBERD_CT_URI)/binary, $#, B64Ver/binary>>, Server = ?config(server, Config), - Info = #disco_info{identities = - [#identity{category = <<"client">>, - type = <<"bot">>, - lang = <<"en">>, - name = <<"ejabberd_ct">>}], - node = Node, features = [Feature]}, + Info = #disco_info{ + identities = + [#identity{ + category = <<"client">>, + type = <<"bot">>, + lang = <<"en">>, + name = <<"ejabberd_ct">> + }], + node = Node, + features = [Feature] + }, Caps = #caps{hash = <<"sha-1">>, node = ?EJABBERD_CT_URI, version = B64Ver}, send(Config, #presence{sub_els = [Caps]}), JID = my_jid(Config), @@ -956,26 +1151,38 @@ presence_broadcast(Config) -> %% 1) disco#info iq request for CAPS %% 2) welcome message %% 3) presence broadcast - IQ = #iq{type = get, - from = JID, - sub_els = [#disco_info{node = Node}]} = recv_iq(Config), - #message{type = chat, - subject = [#text{lang = <<"en">>,data = <<"Welcome!">>}]} = recv_message(Config), + IQ = #iq{ + type = get, + from = JID, + sub_els = [#disco_info{node = Node}] + } = recv_iq(Config), + #message{ + type = chat, + subject = [#text{lang = <<"en">>, data = <<"Welcome!">>}] + } = recv_message(Config), #presence{from = JID, to = JID} = recv_presence(Config), - send(Config, #iq{type = result, id = IQ#iq.id, - to = JID, sub_els = [Info]}), + send(Config, + #iq{ + type = result, + id = IQ#iq.id, + to = JID, + sub_els = [Info] + }), %% We're trying to read our feature from ejabberd database %% with exponential back-off as our IQ response may be delayed. [Feature] = - lists:foldl( - fun(Time, []) -> - timer:sleep(Time), - mod_caps:get_features(Server, Caps); - (_, Acc) -> - Acc - end, [], [0, 100, 200, 2000, 5000, 10000]), + lists:foldl( + fun(Time, []) -> + timer:sleep(Time), + mod_caps:get_features(Server, Caps); + (_, Acc) -> + Acc + end, + [], + [0, 100, 200, 2000, 5000, 10000]), disconnect(Config). + ping(Config) -> true = is_feature_advertised(Config, ?NS_PING), #iq{type = result, sub_els = []} = @@ -984,44 +1191,69 @@ ping(Config) -> #iq{type = get, sub_els = [#ping{}], to = server_jid(Config)}), disconnect(Config). + version(Config) -> true = is_feature_advertised(Config, ?NS_VERSION), #iq{type = result, sub_els = [#version{}]} = send_recv( - Config, #iq{type = get, sub_els = [#version{}], - to = server_jid(Config)}), + Config, + #iq{ + type = get, + sub_els = [#version{}], + to = server_jid(Config) + }), disconnect(Config). + time(Config) -> true = is_feature_advertised(Config, ?NS_TIME), #iq{type = result, sub_els = [#time{}]} = - send_recv(Config, #iq{type = get, sub_els = [#time{}], - to = server_jid(Config)}), + send_recv(Config, + #iq{ + type = get, + sub_els = [#time{}], + to = server_jid(Config) + }), disconnect(Config). + disco(Config) -> true = is_feature_advertised(Config, ?NS_DISCO_INFO), true = is_feature_advertised(Config, ?NS_DISCO_ITEMS), #iq{type = result, sub_els = [#disco_items{items = Items}]} = send_recv( - Config, #iq{type = get, sub_els = [#disco_items{}], - to = server_jid(Config)}), + Config, + #iq{ + type = get, + sub_els = [#disco_items{}], + to = server_jid(Config) + }), lists:foreach( fun(#disco_item{jid = JID, node = Node}) -> #iq{type = result} = send_recv(Config, - #iq{type = get, to = JID, - sub_els = [#disco_info{node = Node}]}) - end, Items), + #iq{ + type = get, + to = JID, + sub_els = [#disco_info{node = Node}] + }) + end, + Items), disconnect(Config). + last(Config) -> true = is_feature_advertised(Config, ?NS_LAST), #iq{type = result, sub_els = [#last{}]} = - send_recv(Config, #iq{type = get, sub_els = [#last{}], - to = server_jid(Config)}), + send_recv(Config, + #iq{ + type = get, + sub_els = [#last{}], + to = server_jid(Config) + }), disconnect(Config). + vcard_get(Config) -> true = is_feature_advertised(Config, ?NS_VCARD), %% TODO: check if VCard corresponds to LDIF data from ejabberd.ldif @@ -1029,50 +1261,68 @@ vcard_get(Config) -> send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), disconnect(Config). + ldap_shared_roster_get(Config) -> - Item = #roster_item{jid = jid:decode(<<"user2@ldap.localhost">>), name = <<"Test User 2">>, - groups = [<<"group1">>], subscription = both}, + Item = #roster_item{ + jid = jid:decode(<<"user2@ldap.localhost">>), + name = <<"Test User 2">>, + groups = [<<"group1">>], + subscription = both + }, #iq{type = result, sub_els = [#roster_query{items = [Item]}]} = send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}), disconnect(Config). + stats(Config) -> #iq{type = result, sub_els = [#stats{list = Stats}]} = - send_recv(Config, #iq{type = get, sub_els = [#stats{}], - to = server_jid(Config)}), + send_recv(Config, + #iq{ + type = get, + sub_els = [#stats{}], + to = server_jid(Config) + }), lists:foreach( fun(#stat{} = Stat) -> - #iq{type = result, sub_els = [_|_]} = - send_recv(Config, #iq{type = get, - sub_els = [#stats{list = [Stat]}], - to = server_jid(Config)}) - end, Stats), + #iq{type = result, sub_els = [_ | _]} = + send_recv(Config, + #iq{ + type = get, + sub_els = [#stats{list = [Stat]}], + to = server_jid(Config) + }) + end, + Stats), disconnect(Config). + %%%=================================================================== %%% Aux functions %%%=================================================================== bookmark_conference() -> - #bookmark_conference{name = <<"Some name">>, - autojoin = true, - jid = jid:make( - <<"some">>, - <<"some.conference.org">>, - <<>>)}. + #bookmark_conference{ + name = <<"Some name">>, + autojoin = true, + jid = jid:make( + <<"some">>, + <<"some.conference.org">>, + <<>>) + }. + '$handle_undefined_function'(F, [Config]) when is_list(Config) -> case re:split(atom_to_list(F), "_", [{return, list}, {parts, 2}]) of - [M, T] -> - Module = list_to_atom(M ++ "_tests"), - Function = list_to_atom(T), - case erlang:function_exported(Module, Function, 1) of - true -> - Module:Function(Config); - false -> - erlang:error({undef, F}) - end; - _ -> - erlang:error({undef, F}) + [M, T] -> + Module = list_to_atom(M ++ "_tests"), + Function = list_to_atom(T), + case erlang:function_exported(Module, Function, 1) of + true -> + Module:Function(Config); + false -> + erlang:error({undef, F}) + end; + _ -> + erlang:error({undef, F}) end; '$handle_undefined_function'(_, _) -> erlang:error(undef). @@ -1088,16 +1338,18 @@ update_sql(Host, Config) -> false -> ok end. + schema_suffix(Config) -> case ejabberd_sql:use_new_schema() of true -> case ?config(update_sql, Config) of - true -> ".sql"; + true -> ".sql"; _ -> ".new.sql" end; _ -> ".sql" end. + clear_sql_tables(sqlite, _Config) -> ok; clear_sql_tables(Type, Config) -> @@ -1118,6 +1370,7 @@ clear_sql_tables(Type, Config) -> ct:fail({failed_to_clear_sql_tables, Type, Err}) end. + read_sql_queries(File) -> case file:open(File, [read, binary]) of {ok, Fd} -> @@ -1126,11 +1379,12 @@ read_sql_queries(File) -> ct:fail({open_file_failed, File, Err}) end. + clear_table_queries(Queries) -> lists:foldl( fun(Query, Acc) -> case split(str:to_lower(Query)) of - [<<"create">>, <<"table">>, Table|_] -> + [<<"create">>, <<"table">>, Table | _] -> GlobalRamTables = [<<"bosh">>, <<"oauth_client">>, <<"oauth_token">>, @@ -1140,12 +1394,15 @@ clear_table_queries(Queries) -> true -> Acc; false -> - [<<"DELETE FROM ", Table/binary, ";">>|Acc] + [<<"DELETE FROM ", Table/binary, ";">> | Acc] end; _ -> Acc end - end, [], Queries). + end, + [], + Queries). + read_lines(Fd, File, Acc) -> case file:read_line(Fd) of @@ -1156,7 +1413,7 @@ read_lines(Fd, File, Acc) -> <<>> -> Acc; _ -> - [Line|Acc] + [Line | Acc] end, read_lines(Fd, File, NewAcc); eof -> @@ -1169,15 +1426,18 @@ read_lines(Fd, File, Acc) -> Q -> [<>] end - end, QueryList); + end, + QueryList); {error, _} = Err -> ct:fail({read_file_failed, File, Err}) end. + split(Data) -> lists:filter( fun(<<>>) -> false; (_) -> true - end, re:split(Data, <<"\s">>)). + end, + re:split(Data, <<"\s">>)). diff --git a/test/ejabberd_test_options.erl b/test/ejabberd_test_options.erl index 10cfcab3e..8a733b795 100644 --- a/test/ejabberd_test_options.erl +++ b/test/ejabberd_test_options.erl @@ -21,6 +21,7 @@ -export([opt_type/1, options/0, globals/0, doc/0]). + %%%=================================================================== %%% API %%%=================================================================== @@ -83,6 +84,7 @@ opt_type(tgp) -> opt_type(tlp) -> econf:binary(). + options() -> [{macro_over_keyword, undefined}, {keyword_inside_macro, undefined}, @@ -102,13 +104,13 @@ options() -> {ktls, undefined}, {ktlsi, undefined}, {tgp, undefined}, - {tlp, undefined} - ]. + {tlp, undefined}]. + -spec globals() -> [atom()]. globals() -> []. + doc() -> ejabberd_options_doc:doc(). - diff --git a/test/example_tests.erl b/test/example_tests.erl index 5fd0a86ff..a091f867c 100644 --- a/test/example_tests.erl +++ b/test/example_tests.erl @@ -29,6 +29,7 @@ -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -37,31 +38,38 @@ %%%=================================================================== single_cases() -> {example_single, [sequence], - [single_test(foo)]}. + [single_test(foo)]}. + foo(Config) -> Config. + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {example_master_slave, [sequence], - [master_slave_test(foo)]}. + [master_slave_test(foo)]}. + foo_master(Config) -> Config. + foo_slave(Config) -> Config. + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("example_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("example_" ++ atom_to_list(T)), [parallel], + {list_to_atom("example_" ++ atom_to_list(T)), + [parallel], [list_to_atom("example_" ++ atom_to_list(T) ++ "_master"), list_to_atom("example_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/jidprep_tests.erl b/test/jidprep_tests.erl index f18e150fb..a5b51da59 100644 --- a/test/jidprep_tests.erl +++ b/test/jidprep_tests.erl @@ -25,11 +25,15 @@ %% API -compile(export_all). --import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, - server_jid/1]). +-import(suite, + [send_recv/2, + disconnect/1, + is_feature_advertised/2, + server_jid/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -38,23 +42,30 @@ %%%=================================================================== single_cases() -> {jidprep_single, [sequence], - [single_test(feature_enabled), - single_test(normalize_jid)]}. + [single_test(feature_enabled), + single_test(normalize_jid)]}. + feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_JIDPREP_0), disconnect(Config). + normalize_jid(Config) -> ServerJID = server_jid(Config), OrigJID = jid:decode(<<"Romeo@Example.COM/Orchard">>), NormJID = jid:decode(<<"romeo@example.com/Orchard">>), Request = #jidprep{jid = OrigJID}, #iq{type = result, sub_els = [#jidprep{jid = NormJID}]} = - send_recv(Config, #iq{type = get, to = ServerJID, - sub_els = [Request]}), + send_recv(Config, + #iq{ + type = get, + to = ServerJID, + sub_els = [Request] + }), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/test/json_test.erl b/test/json_test.erl index 7f16cf7a7..393c9ab7e 100644 --- a/test/json_test.erl +++ b/test/json_test.erl @@ -6,71 +6,91 @@ %% @format-begin + encode_binary_test() -> Binary = <<"This is an error text.">>, Encoded = <<"\"This is an error text.\"">>, ?assertMatch(Encoded, misc:json_encode(Binary)). + -ifdef(OTP_BELOW_26). + %% OTP 25 or lower encode_map_test() -> - Map = #{name => <<"room">>, + Map = #{ + name => <<"room">>, service => <<"conference">>, jid => jid:encode({<<"user">>, <<"server">>, <<"">>}), - affiliation => member}, + affiliation => member + }, Encoded = <<"{\"service\":\"conference\",\"name\":\"room\",\"jid\":\"user@server\",\"affiliation\":\"member\"}">>, ?assertMatch(Encoded, misc:json_encode(Map)). + -endif. -ifdef(OTP_BELOW_27). -ifndef(OTP_BELOW_26). + %% OTP 26 encode_map_test() -> - Map = #{name => <<"room">>, + Map = #{ + name => <<"room">>, service => <<"conference">>, jid => jid:encode({<<"user">>, <<"server">>, <<"">>}), - affiliation => member}, + affiliation => member + }, Encoded = <<"{\"affiliation\":\"member\",\"jid\":\"user@server\",\"service\":\"conference\",\"name\":\"room\"}">>, ?assertMatch(Encoded, misc:json_encode(Map)). + -endif. -endif. -ifndef(OTP_BELOW_27). + %% OTP 27 or higher or higher encode_map_test() -> - Map = #{name => <<"room">>, + Map = #{ + name => <<"room">>, service => <<"conference">>, jid => jid:encode({<<"user">>, <<"server">>, <<"">>}), - affiliation => member}, + affiliation => member + }, Encoded27 = <<"{\"name\":\"room\",\"service\":\"conference\",\"jid\":\"user@server\",\"affiliation\":\"member\"}">>, ?assertMatch(Encoded27, misc:json_encode(Map)). + -endif. + decode_test() -> Encoded = <<"{\"affiliation\":\"member\",\"jid\":\"user@server\",\"service\":\"conference\",\"name\":\"room\"}">>, TupleList = - #{<<"affiliation">> => <<"member">>, + #{ + <<"affiliation">> => <<"member">>, <<"jid">> => <<"user@server">>, <<"name">> => <<"room">>, - <<"service">> => <<"conference">>}, + <<"service">> => <<"conference">> + }, ?assertMatch(TupleList, misc:json_decode(Encoded)). + decode_maps_test() -> Encoded = <<"{\"affiliation\":\"member\",\"jid\":\"user@server\",\"service\":\"conference\",\"name\":\"room\"}">>, - Map = #{<<"affiliation">> => misc:atom_to_binary(member), + Map = #{ + <<"affiliation">> => misc:atom_to_binary(member), <<"jid">> => jid:encode({<<"user">>, <<"server">>, <<"">>}), <<"name">> => <<"room">>, - <<"service">> => <<"conference">>}, + <<"service">> => <<"conference">> + }, ?assertMatch(Map, misc:json_decode(Encoded)). diff --git a/test/ldap_srv.erl b/test/ldap_srv.erl index 2d0cea988..46b01063d 100644 --- a/test/ldap_srv.erl +++ b/test/ldap_srv.erl @@ -36,8 +36,12 @@ approxMatch/3]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). -include("ELDAPv3.hrl"). @@ -45,37 +49,40 @@ -define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). -define(TCP_SEND_TIMEOUT, 32000). --define(SERVER, ?MODULE). +-define(SERVER, ?MODULE). -record(state, {listener = make_ref() :: reference()}). + %%%=================================================================== %%% API %%%=================================================================== start(LDIFFile) -> gen_server:start({local, ?SERVER}, ?MODULE, [LDIFFile], []). + %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([LDIFFile]) -> - case gen_tcp:listen(1389, [binary, - {packet, asn1}, - {active, false}, - {reuseaddr, true}, - {nodelay, true}, - {send_timeout, ?TCP_SEND_TIMEOUT}, - {send_timeout_close, true}, - {keepalive, true}]) of + case gen_tcp:listen(1389, + [binary, + {packet, asn1}, + {active, false}, + {reuseaddr, true}, + {nodelay, true}, + {send_timeout, ?TCP_SEND_TIMEOUT}, + {send_timeout_close, true}, + {keepalive, true}]) of {ok, ListenSocket} -> case load_ldif(LDIFFile) of {ok, Tree} -> ?INFO_MSG("LDIF tree loaded, " - "ready to accept connections at ~B", [1389]), + "ready to accept connections at ~B", + [1389]), {_Pid, MRef} = spawn_monitor( - fun() -> accept(ListenSocket, Tree) end - ), + fun() -> accept(ListenSocket, Tree) end), {ok, #state{listener = MRef}}; {error, Reason} -> {stop, Reason} @@ -85,13 +92,16 @@ init([LDIFFile]) -> {stop, Reason} end. + handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. + handle_cast(_Msg, State) -> {noreply, State}. + handle_info({'DOWN', MRef, _Type, _Object, Info}, #state{listener = MRef} = State) -> ?ERROR_MSG("listener died with reason ~p, terminating", @@ -100,25 +110,29 @@ handle_info({'DOWN', MRef, _Type, _Object, Info}, handle_info(_Info, State) -> {noreply, State}. + terminate(_Reason, _State) -> ok. + code_change(_OldVsn, State, _Extra) -> {ok, State}. + %%%=================================================================== %%% Internal functions %%%=================================================================== accept(ListenSocket, Tree) -> case gen_tcp:accept(ListenSocket) of - {ok, Socket} -> + {ok, Socket} -> spawn(fun() -> process(Socket, Tree) end), accept(ListenSocket, Tree); Err -> ?ERROR_MSG("failed to accept: ~p", [Err]), - Err + Err end. + process(Socket, Tree) -> case gen_tcp:recv(Socket, 0) of {ok, B} -> @@ -128,13 +142,16 @@ process(Socket, Tree) -> Id = Msg#'LDAPMessage'.messageID, lists:foreach( fun(ReplyOp) -> - Reply = #'LDAPMessage'{messageID = Id, - protocolOp = ReplyOp}, + Reply = #'LDAPMessage'{ + messageID = Id, + protocolOp = ReplyOp + }, %%?DEBUG("sent:~n~p", [Reply]), {ok, Bytes} = 'ELDAPv3':encode( 'LDAPMessage', Reply), gen_tcp:send(Socket, Bytes) - end, Replies), + end, + Replies), process(Socket, Tree); Err -> ?ERROR_MSG("failed to decode msg: ~p", [Err]), @@ -144,6 +161,7 @@ process(Socket, Tree) -> Err end. + process_msg(#'LDAPMessage'{protocolOp = Op} = _Msg, TopTree) -> %%?DEBUG("got:~n~p", [Msg]), case Op of @@ -157,28 +175,37 @@ process_msg(#'LDAPMessage'{protocolOp = Op} = _Msg, TopTree) -> %%success end, [{bindResponse, - #'BindResponse'{resultCode = ResCode, - matchedDN = <<"">>, - errorMessage = <<"">>}}]; + #'BindResponse'{ + resultCode = ResCode, + matchedDN = <<"">>, + errorMessage = <<"">> + }}]; {searchRequest, - #'SearchRequest'{baseObject = DN, - scope = Scope, - filter = Filter, - attributes = Attrs}} -> + #'SearchRequest'{ + baseObject = DN, + scope = Scope, + filter = Filter, + attributes = Attrs + }} -> DNs = process_dn_filter(DN, Scope, Filter, TopTree), Es = lists:map( fun(D) -> make_entry(D, TopTree, Attrs) - end, DNs), + end, + DNs), Es ++ [{searchResDone, - #'LDAPResult'{resultCode = success, - matchedDN = <<"">>, - errorMessage = <<"">>}}]; + #'LDAPResult'{ + resultCode = success, + matchedDN = <<"">>, + errorMessage = <<"">> + }}]; {extendedReq, _} -> [{extendedResp, - #'ExtendedResponse'{matchedDN = <<"">>, - errorMessage = <<"Not Implemented">>, - resultCode = operationsError}}]; + #'ExtendedResponse'{ + matchedDN = <<"">>, + errorMessage = <<"Not Implemented">>, + resultCode = operationsError + }}]; _ -> RespOp = case Op of {modifyRequest, _} -> modifyResponse; @@ -193,63 +220,80 @@ process_msg(#'LDAPMessage'{protocolOp = Op} = _Msg, TopTree) -> []; _ -> [{RespOp, - #'LDAPResult'{matchedDN = <<"">>, - errorMessage = <<"Not implemented">>, - resultCode = operationsError}}] + #'LDAPResult'{ + matchedDN = <<"">>, + errorMessage = <<"Not implemented">>, + resultCode = operationsError + }}] end end. + make_entry(DN, Tree, Attrs) -> KVs = case ets:lookup(Tree, {dn, DN}) of - [{_, _KVs}|_] -> + [{_, _KVs} | _] -> _KVs; _ -> [] end, - NewKVs = if Attrs /= [], Attrs /= [<<"*">>] -> + NewKVs = if + Attrs /= [], Attrs /= [<<"*">>] -> lists:filter( fun({A, _V}) -> member(A, Attrs) - end, KVs); - true -> + end, + KVs); + true -> KVs end, KVs1 = dict:to_list( lists:foldl( fun({A, V}, D) -> dict:append(A, V, D) - end, dict:new(), NewKVs)), + end, + dict:new(), + NewKVs)), {searchResEntry, #'SearchResultEntry'{ objectName = str:join(DN, <<",">>), - attributes = [#'PartialAttributeList_SEQOF'{type = T, vals = V} - || {T, V} <- KVs1]}}. + attributes = [ #'PartialAttributeList_SEQOF'{type = T, vals = V} + || {T, V} <- KVs1 ] + }}. + process_dn_filter(DN, Level, F, Tree) -> DN1 = str:tokens(DN, <<",">>), Fun = filter_to_fun(F), filter(Fun, DN1, Tree, Level). + filter_to_fun({'and', Fs}) -> fun(KVs) -> lists:all( fun(F) -> (filter_to_fun(F))(KVs) - end, Fs) + end, + Fs) end; filter_to_fun({'or', Fs}) -> fun(KVs) -> lists:any( fun(F) -> (filter_to_fun(F))(KVs) - end, Fs) + end, + Fs) end; filter_to_fun({present, Attr}) -> fun(KVs) -> present(Attr, KVs) end; -filter_to_fun({Tag, #'AttributeValueAssertion'{attributeDesc = Attr, - assertionValue = Val}}) - when Tag == equalityMatch; Tag == greaterOrEqual; - Tag == lessOrEqual; Tag == approxMatch -> +filter_to_fun({Tag, + #'AttributeValueAssertion'{ + attributeDesc = Attr, + assertionValue = Val + }}) + when Tag == equalityMatch; + Tag == greaterOrEqual; + Tag == lessOrEqual; + Tag == approxMatch -> fun(KVs) -> apply(?MODULE, Tag, [Attr, Val, KVs]) end; @@ -260,14 +304,16 @@ filter_to_fun({substrings, filter_to_fun({'not', F}) -> fun(KVs) -> not (filter_to_fun(F))(KVs) end. + find_obj(DN, Tree) -> case ets:lookup(Tree, {dn, str:tokens(DN, <<",">>)}) of - [{_, Obj}|_] -> + [{_, Obj} | _] -> {ok, Obj}; [] -> error end. + present(A, R) -> case keyfind(A, R) of [] -> @@ -276,25 +322,32 @@ present(A, R) -> true end. + equalityMatch(A, V, R) -> Vs = keyfind(A, R), member(V, Vs). + lessOrEqual(A, V, R) -> lists:any( fun(X) -> str:to_lower(X) =< str:to_lower(V) - end, keyfind(A, R)). + end, + keyfind(A, R)). + greaterOrEqual(A, V, R) -> lists:any( fun(X) -> str:to_lower(X) >= str:to_lower(V) - end, keyfind(A, R)). + end, + keyfind(A, R)). + approxMatch(A, V, R) -> equalityMatch(A, V, R). + substrings(A, Re, R) -> lists:any( fun(V) -> @@ -304,7 +357,9 @@ substrings(A, Re, R) -> _ -> false end - end, keyfind(A, R)). + end, + keyfind(A, R)). + substrings_to_regexp(Ss) -> ReS = lists:map( @@ -314,14 +369,16 @@ substrings_to_regexp(Ss) -> [<<".*">>, S, <<".*">>]; ({final, S}) -> [<<".*">>, S] - end, Ss), + end, + Ss), ReS1 = str:to_lower(list_to_binary([$^, ReS, $$])), {ok, Re} = re:compile(ReS1), Re. + filter(F, BaseDN, Tree, Level) -> KVs = case ets:lookup(Tree, {dn, BaseDN}) of - [{_, _KVs}|_] -> + [{_, _KVs} | _] -> _KVs; [] -> [] @@ -330,49 +387,57 @@ filter(F, BaseDN, Tree, Level) -> baseObject -> []; _ -> - NewLevel = if Level /= wholeSubtree -> + NewLevel = if + Level /= wholeSubtree -> baseObject; - true -> + true -> Level end, lists:flatmap( fun({_, D}) -> - NewDN = if BaseDN == [] -> + NewDN = if + BaseDN == [] -> D; - true -> - [D|BaseDN] + true -> + [D | BaseDN] end, filter(F, NewDN, Tree, NewLevel) - end, ets:lookup(Tree, BaseDN)) + end, + ets:lookup(Tree, BaseDN)) end, - if BaseDN == [], Level /= baseObject -> + if + BaseDN == [], Level /= baseObject -> Rest; - true -> + true -> case F(KVs) of true -> - [BaseDN|Rest]; + [BaseDN | Rest]; false -> Rest end end. + keyfind(K, KVs) -> keyfind(str:to_lower(K), KVs, []). -keyfind(K, [{K1, V}|T], Acc) -> + +keyfind(K, [{K1, V} | T], Acc) -> case str:to_lower(K1) of K -> - keyfind(K, T, [V|Acc]); + keyfind(K, T, [V | Acc]); _ -> keyfind(K, T, Acc) end; keyfind(_, [], Acc) -> Acc. + member(E, Es) -> member1(str:to_lower(E), Es). -member1(E, [H|T]) -> + +member1(E, [H | T]) -> case str:to_lower(H) of E -> true; @@ -382,6 +447,7 @@ member1(E, [H|T]) -> member1(_, []) -> false. + load_ldif(Path) -> case file:open(Path, [read, binary]) of {ok, Fd} -> @@ -391,18 +457,20 @@ load_ldif(Path) -> Err end. + read_lines(Fd, Acc) -> case file:read_line(Fd) of {ok, Str} -> Line = process_line(str:strip(Str, right, $\n)), - read_lines(Fd, [Line|Acc]); + read_lines(Fd, [Line | Acc]); eof -> Acc; Err -> Err end. -process_line(<> = L) when C/=$ , C/=$\t, C/=$\n -> + +process_line(<> = L) when C /= $ , C /= $\t, C /= $\n -> case str:chr(L, $:) of 0 -> <<>>; @@ -415,58 +483,65 @@ process_line(<> = L) when C/=$ , C/=$\t, C/=$\n -> {Val, plain, str:strip(Rest, left, $ )} end end; -process_line([_|L]) -> +process_line([_ | L]) -> L; process_line(_) -> <<>>. -format([{Val, Type, L}|T], Ls, Acc) -> - Str1 = iolist_to_binary([L|Ls]), + +format([{Val, Type, L} | T], Ls, Acc) -> + Str1 = iolist_to_binary([L | Ls]), Str2 = case Type of plain -> Str1; base64 -> base64:decode(Str1) end, - format(T, [], [{Val, Str2}|Acc]); -format([<<"-">>|T], Ls, Acc) -> + format(T, [], [{Val, Str2} | Acc]); +format([<<"-">> | T], Ls, Acc) -> format(T, Ls, Acc); -format([L|T], Ls, Acc) -> - format(T, [L|Ls], Acc); +format([L | T], Ls, Acc) -> + format(T, [L | Ls], Acc); format([], _, Acc) -> lists:reverse(Acc). + resort(T) -> resort(T, [], [], ets:new(ldap_tree, [named_table, public, bag])). -resort([{<<"dn">>, S}|T], Ls, DNs, Tree) -> + +resort([{<<"dn">>, S} | T], Ls, DNs, Tree) -> case proplists:get_value(<<"changetype">>, Ls, <<"add">>) of <<"add">> -> - [H|Rest] = DN = str:tokens(S, <<",">>), + [H | Rest] = DN = str:tokens(S, <<",">>), ets:insert(Tree, {{dn, DN}, Ls}), ets:insert(Tree, {Rest, H}), - resort(T, [], [DN|DNs], Tree); + resort(T, [], [DN | DNs], Tree); _ -> resort(T, [], DNs, Tree) end; -resort([AttrVal|T], Ls, DNs, Acc) -> - resort(T, [AttrVal|Ls], DNs, Acc); +resort([AttrVal | T], Ls, DNs, Acc) -> + resort(T, [AttrVal | Ls], DNs, Acc); resort([], _, DNs, Tree) -> {_, TopDNs} = lists:foldl( fun(D, {L, Acc}) -> NewL = length(D), - if NewL < L -> + if + NewL < L -> {NewL, [D]}; - NewL == L -> - {L, [D|Acc]}; - true -> + NewL == L -> + {L, [D | Acc]}; + true -> {L, Acc} end - end, {unlimited, []}, DNs), + end, + {unlimited, []}, + DNs), Attrs = lists:map( fun(TopDN) -> ets:insert(Tree, {[], TopDN}), {<<"namingContexts">>, str:join(TopDN, <<",">>)} - end, TopDNs), + end, + TopDNs), Attrs1 = [{<<"supportedLDAPVersion">>, <<"3">>}, - {<<"objectClass">>, <<"top">>}|Attrs], + {<<"objectClass">>, <<"top">>} | Attrs], ets:insert(Tree, {{dn, []}, Attrs1}), Tree. diff --git a/test/mam_tests.erl b/test/mam_tests.erl index 27988bf5e..b48be4303 100644 --- a/test/mam_tests.erl +++ b/test/mam_tests.erl @@ -25,14 +25,28 @@ %% API -compile(export_all). --import(suite, [get_features/1, disconnect/1, my_jid/1, send_recv/2, - wait_for_slave/1, server_jid/1, send/2, get_features/2, - wait_for_master/1, recv_message/1, recv_iq/1, muc_room_jid/1, - muc_jid/1, is_feature_advertised/3, get_event/1, put_event/2]). +-import(suite, + [get_features/1, + disconnect/1, + my_jid/1, + send_recv/2, + wait_for_slave/1, + server_jid/1, + send/2, + get_features/2, + wait_for_master/1, + recv_message/1, + recv_iq/1, + muc_room_jid/1, + muc_jid/1, + is_feature_advertised/3, + get_event/1, + put_event/2]). -include("suite.hrl"). -define(VERSIONS, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]). + %%%=================================================================== %%% API %%%=================================================================== @@ -41,10 +55,11 @@ %%%=================================================================== single_cases() -> {mam_single, [sequence], - [single_test(feature_enabled), - single_test(get_set_prefs), - single_test(get_form), - single_test(fake_by)]}. + [single_test(feature_enabled), + single_test(get_set_prefs), + single_test(get_form), + single_test(fake_by)]}. + feature_enabled(Config) -> BareMyJID = jid:remove_resource(my_jid(Config)), @@ -61,87 +76,118 @@ feature_enabled(Config) -> true = lists:member(?NS_MAM_2, MUCFeatures), clean(disconnect(Config)). + fake_by(Config) -> BareServerJID = server_jid(Config), FullServerJID = jid:replace_resource(BareServerJID, p1_rand:get_string()), FullMyJID = my_jid(Config), BareMyJID = jid:remove_resource(FullMyJID), Fakes = lists:flatmap( - fun(JID) -> - [#mam_archived{id = p1_rand:get_string(), by = JID}, - #stanza_id{id = p1_rand:get_string(), by = JID}] - end, [BareServerJID, FullServerJID, BareMyJID, FullMyJID]), + fun(JID) -> + [#mam_archived{id = p1_rand:get_string(), by = JID}, + #stanza_id{id = p1_rand:get_string(), by = JID}] + end, + [BareServerJID, FullServerJID, BareMyJID, FullMyJID]), Body = xmpp:mk_text(<<"body">>), ForeignJID = jid:make(p1_rand:get_string()), Archived = #mam_archived{id = p1_rand:get_string(), by = ForeignJID}, StanzaID = #stanza_id{id = p1_rand:get_string(), by = ForeignJID}, #message{body = Body, sub_els = SubEls} = - send_recv(Config, #message{to = FullMyJID, - body = Body, - sub_els = [Archived, StanzaID|Fakes]}), + send_recv(Config, + #message{ + to = FullMyJID, + body = Body, + sub_els = [Archived, StanzaID | Fakes] + }), ct:comment("Checking if only foreign tags present"), [ForeignJID, ForeignJID] = lists:flatmap( - fun(#mam_archived{by = By}) -> [By]; - (#stanza_id{by = By}) -> [By]; - (_) -> [] - end, SubEls), + fun(#mam_archived{by = By}) -> [By]; + (#stanza_id{by = By}) -> [By]; + (_) -> [] + end, + SubEls), clean(disconnect(Config)). + get_set_prefs(Config) -> - Range = [{JID, #mam_prefs{xmlns = NS, - default = Default, - always = Always, - never = Never}} || - JID <- [undefined, server_jid(Config)], - NS <- ?VERSIONS, - Default <- [always, never, roster], - Always <- [[], [jid:decode(<<"foo@bar.baz">>)]], - Never <- [[], [jid:decode(<<"baz@bar.foo">>)]]], + Range = [ {JID, + #mam_prefs{ + xmlns = NS, + default = Default, + always = Always, + never = Never + }} + || JID <- [undefined, server_jid(Config)], + NS <- ?VERSIONS, + Default <- [always, never, roster], + Always <- [[], [jid:decode(<<"foo@bar.baz">>)]], + Never <- [[], [jid:decode(<<"baz@bar.foo">>)]] ], lists:foreach( fun({To, Prefs}) -> - NS = Prefs#mam_prefs.xmlns, - #iq{type = result, sub_els = [Prefs]} = - send_recv(Config, #iq{type = set, to = To, - sub_els = [Prefs]}), - #iq{type = result, sub_els = [Prefs]} = - send_recv(Config, #iq{type = get, to = To, - sub_els = [#mam_prefs{xmlns = NS}]}) - end, Range), + NS = Prefs#mam_prefs.xmlns, + #iq{type = result, sub_els = [Prefs]} = + send_recv(Config, + #iq{ + type = set, + to = To, + sub_els = [Prefs] + }), + #iq{type = result, sub_els = [Prefs]} = + send_recv(Config, + #iq{ + type = get, + to = To, + sub_els = [#mam_prefs{xmlns = NS}] + }) + end, + Range), clean(disconnect(Config)). + get_form(Config) -> ServerJID = server_jid(Config), - Range = [{JID, NS} || JID <- [undefined, ServerJID], - NS <- ?VERSIONS -- [?NS_MAM_TMP]], + Range = [ {JID, NS} || JID <- [undefined, ServerJID], + NS <- ?VERSIONS -- [?NS_MAM_TMP] ], lists:foreach( fun({To, NS}) -> - #iq{type = result, - sub_els = [#mam_query{xmlns = NS, - xdata = #xdata{} = X}]} = - send_recv(Config, #iq{type = get, to = To, - sub_els = [#mam_query{xmlns = NS}]}), - [NS] = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), - true = xmpp_util:has_xdata_var(<<"with">>, X), - true = xmpp_util:has_xdata_var(<<"start">>, X), - true = xmpp_util:has_xdata_var(<<"end">>, X) - end, Range), + #iq{ + type = result, + sub_els = [#mam_query{ + xmlns = NS, + xdata = #xdata{} = X + }] + } = + send_recv(Config, + #iq{ + type = get, + to = To, + sub_els = [#mam_query{xmlns = NS}] + }), + [NS] = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + true = xmpp_util:has_xdata_var(<<"with">>, X), + true = xmpp_util:has_xdata_var(<<"start">>, X), + true = xmpp_util:has_xdata_var(<<"end">>, X) + end, + Range), clean(disconnect(Config)). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {mam_master_slave, [sequence], - [master_slave_test(archived_and_stanza_id), - master_slave_test(query_all), - master_slave_test(query_with), - master_slave_test(query_rsm_max), - master_slave_test(query_rsm_after), - master_slave_test(query_rsm_before), - master_slave_test(muc), - master_slave_test(mucsub), - master_slave_test(mucsub_from_muc), - master_slave_test(mucsub_from_muc_non_persistent)]}. + [master_slave_test(archived_and_stanza_id), + master_slave_test(query_all), + master_slave_test(query_with), + master_slave_test(query_rsm_max), + master_slave_test(query_rsm_after), + master_slave_test(query_rsm_before), + master_slave_test(muc), + master_slave_test(mucsub), + master_slave_test(mucsub_from_muc), + master_slave_test(mucsub_from_muc_non_persistent)]}. + archived_and_stanza_id_master(Config) -> #presence{} = send_recv(Config, #presence{}), @@ -149,6 +195,7 @@ archived_and_stanza_id_master(Config) -> send_messages(Config, lists:seq(1, 5)), clean(disconnect(Config)). + archived_and_stanza_id_slave(Config) -> ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), @@ -156,6 +203,7 @@ archived_and_stanza_id_slave(Config) -> recv_messages(Config, lists:seq(1, 5)), clean(disconnect(Config)). + query_all_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -166,6 +214,7 @@ query_all_master(Config) -> query_all(Config, MyJID, Peer), clean(disconnect(Config)). + query_all_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -176,6 +225,7 @@ query_all_slave(Config) -> query_all(Config, Peer, MyJID), clean(disconnect(Config)). + query_with_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -186,6 +236,7 @@ query_with_master(Config) -> query_with(Config, MyJID, Peer), clean(disconnect(Config)). + query_with_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -196,6 +247,7 @@ query_with_slave(Config) -> query_with(Config, Peer, MyJID), clean(disconnect(Config)). + query_rsm_max_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -206,6 +258,7 @@ query_rsm_max_master(Config) -> query_rsm_max(Config, MyJID, Peer), clean(disconnect(Config)). + query_rsm_max_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -216,6 +269,7 @@ query_rsm_max_slave(Config) -> query_rsm_max(Config, Peer, MyJID), clean(disconnect(Config)). + query_rsm_after_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -226,6 +280,7 @@ query_rsm_after_master(Config) -> query_rsm_after(Config, MyJID, Peer), clean(disconnect(Config)). + query_rsm_after_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -236,6 +291,7 @@ query_rsm_after_slave(Config) -> query_rsm_after(Config, Peer, MyJID), clean(disconnect(Config)). + query_rsm_before_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -246,6 +302,7 @@ query_rsm_before_master(Config) -> query_rsm_before(Config, MyJID, Peer), clean(disconnect(Config)). + query_rsm_before_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), @@ -256,6 +313,7 @@ query_rsm_before_slave(Config) -> query_rsm_before(Config, Peer, MyJID), clean(disconnect(Config)). + muc_master(Config) -> Room = muc_room_jid(Config), %% Joining @@ -287,10 +345,12 @@ muc_master(Config) -> muc_tests:leave(Config), clean(disconnect(Config)). + muc_slave(Config) -> disconnect = get_event(Config), clean(disconnect(Config)). + mucsub_master(Config) -> Room = muc_room_jid(Config), Peer = ?config(peer, Config), @@ -307,10 +367,16 @@ mucsub_master(Config) -> [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), ct:comment("Subscribing peer to room"), - ?send_recv(#iq{to = Room, type = set, sub_els = [ - #muc_subscribe{jid = Peer, nick = <<"peer">>, - events = [?NS_MUCSUB_NODES_MESSAGES]} - ]}, #iq{type = result}), + ?send_recv(#iq{ + to = Room, + type = set, + sub_els = [#muc_subscribe{ + jid = Peer, + nick = <<"peer">>, + events = [?NS_MUCSUB_NODES_MESSAGES] + }] + }, + #iq{type = result}), ct:comment("Sending messages to room"), send_messages_to_room(Config, lists:seq(1, 5)), @@ -324,6 +390,7 @@ mucsub_master(Config) -> muc_tests:leave(Config), clean(disconnect(Config)). + mucsub_slave(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), @@ -334,56 +401,92 @@ mucsub_slave(Config) -> ct:comment("Receiving mucsub events"), lists:foreach( - fun(N) -> - Body = xmpp:mk_text(integer_to_binary(N)), - Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), - PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ - #ps_item{} = PS - ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), - ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) - end, lists:seq(1, 5)), + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), + PS = ?match(#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_MESSAGES, + items = [#ps_item{} = PS] + } + }, + xmpp:get_subtag(Msg, #ps_event{}), + PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, + lists:seq(1, 5)), ct:comment("Retrieving personal mam archive"), QID = p1_rand:get_string(), - I = send(Config, #iq{type = set, - sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), + I = send(Config, + #iq{ + type = set, + sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}] + }), lists:foreach( - fun(N) -> - Body = xmpp:mk_text(integer_to_binary(N)), - Forw = ?match(#message{ - to = MyJID, from = MyJIDBare, - sub_els = [#mam_result{ - xmlns = ?NS_MAM_2, - queryid = QID, - sub_els = [#forwarded{ - delay = #delay{}} = Forw]}]}, - recv_message(Config), Forw), - IMsg = ?match(#message{ - to = MyJIDBare, from = Room} = IMsg, xmpp:get_subtag(Forw, #message{}), IMsg), + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Forw = ?match(#message{ + to = MyJID, + from = MyJIDBare, + sub_els = [#mam_result{ + xmlns = ?NS_MAM_2, + queryid = QID, + sub_els = [#forwarded{ + delay = #delay{} + } = Forw] + }] + }, + recv_message(Config), + Forw), + IMsg = ?match(#message{ + to = MyJIDBare, from = Room + } = IMsg, + xmpp:get_subtag(Forw, #message{}), + IMsg), - PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ - #ps_item{} = PS - ]}}, xmpp:get_subtag(IMsg, #ps_event{}), PS), - ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) - end, lists:seq(1, 5)), - RSM = ?match(#iq{from = MyJIDBare, id = I, type = result, - sub_els = [#mam_fin{xmlns = ?NS_MAM_2, - rsm = RSM, - complete = true}]}, recv_iq(Config), RSM), + PS = ?match(#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_MESSAGES, + items = [#ps_item{} = PS] + } + }, + xmpp:get_subtag(IMsg, #ps_event{}), + PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, + lists:seq(1, 5)), + RSM = ?match(#iq{ + from = MyJIDBare, + id = I, + type = result, + sub_els = [#mam_fin{ + xmlns = ?NS_MAM_2, + rsm = RSM, + complete = true + }] + }, + recv_iq(Config), + RSM), match_rsm_count(RSM, 5), % Wait for master exit ready = get_event(Config), % Unsubscribe yourself - ?send_recv(#iq{to = Room, type = set, sub_els = [ - #muc_unsubscribe{} - ]}, #iq{type = result}), + ?send_recv(#iq{ + to = Room, + type = set, + sub_els = [#muc_unsubscribe{}] + }, + #iq{type = result}), put_event(Config, ready), clean(disconnect(Config)). + mucsub_from_muc_master(Config) -> mucsub_master(Config). + mucsub_from_muc_slave(Config) -> Server = ?config(server, Config), gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), @@ -391,134 +494,179 @@ mucsub_from_muc_slave(Config) -> gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}), Config2. + mucsub_from_muc_non_persistent_master(Config) -> Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), Config2 = mucsub_from_muc_master(Config1), lists:keydelete(persistent_room, 1, Config2). + mucsub_from_muc_non_persistent_slave(Config) -> Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), Config2 = mucsub_from_muc_slave(Config1), lists:keydelete(persistent_room, 1, Config2). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("mam_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("mam_" ++ atom_to_list(T)), [parallel], + {list_to_atom("mam_" ++ atom_to_list(T)), + [parallel], [list_to_atom("mam_" ++ atom_to_list(T) ++ "_master"), list_to_atom("mam_" ++ atom_to_list(T) ++ "_slave")]}. + clean(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), mod_mam:remove_user(U, S), Config. + set_default(Config, Default) -> lists:foreach( fun(NS) -> - ct:comment("Setting default preferences of '~s' to '~s'", - [NS, Default]), - #iq{type = result, - sub_els = [#mam_prefs{xmlns = NS, default = Default}]} = - send_recv(Config, #iq{type = set, - sub_els = [#mam_prefs{xmlns = NS, - default = Default}]}) - end, ?VERSIONS). + ct:comment("Setting default preferences of '~s' to '~s'", + [NS, Default]), + #iq{ + type = result, + sub_els = [#mam_prefs{xmlns = NS, default = Default}] + } = + send_recv(Config, + #iq{ + type = set, + sub_els = [#mam_prefs{ + xmlns = NS, + default = Default + }] + }) + end, + ?VERSIONS). + send_messages(Config, Range) -> Peer = ?config(peer, Config), send_message_extra(Config, 0, <<"to-retract-1">>, []), lists:foreach( - fun - (1) -> - send_message_extra(Config, 1, <<"retraction-1">>, [#message_retract{id = <<"to-retract-1">>}]); - (N) -> - Body = xmpp:mk_text(integer_to_binary(N)), + fun(1) -> + send_message_extra(Config, 1, <<"retraction-1">>, [#message_retract{id = <<"to-retract-1">>}]); + (N) -> + Body = xmpp:mk_text(integer_to_binary(N)), send(Config, #message{to = Peer, body = Body}) - end, Range). + end, + Range). + send_message_extra(Config, N, Id, Sub) -> Peer = ?config(peer, Config), Body = xmpp:mk_text(integer_to_binary(N)), send(Config, #message{id = Id, to = Peer, body = Body, sub_els = Sub}). + recv_messages(Config, Range) -> Peer = ?config(peer, Config), lists:foreach( fun(N) -> - Body = xmpp:mk_text(integer_to_binary(N)), - #message{from = Peer, body = Body} = Msg = - recv_message(Config), - #mam_archived{by = BareMyJID} = - xmpp:get_subtag(Msg, #mam_archived{}), - #stanza_id{by = BareMyJID} = - xmpp:get_subtag(Msg, #stanza_id{}) - end, [0 | Range]). + Body = xmpp:mk_text(integer_to_binary(N)), + #message{from = Peer, body = Body} = Msg = + recv_message(Config), + #mam_archived{by = BareMyJID} = + xmpp:get_subtag(Msg, #mam_archived{}), + #stanza_id{by = BareMyJID} = + xmpp:get_subtag(Msg, #stanza_id{}) + end, + [0 | Range]). + recv_archived_messages(Config, From, To, QID, Range) -> MyJID = my_jid(Config), lists:foreach( fun(N) -> - ct:comment("Retrieving ~pth message in range ~p", - [N, Range]), + ct:comment("Retrieving ~pth message in range ~p", + [N, Range]), Body = xmpp:mk_text(integer_to_binary(N)), - #message{to = MyJID, + #message{ + to = MyJID, + sub_els = + [#mam_result{ + queryid = QID, sub_els = - [#mam_result{ - queryid = QID, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = [El]}]}]} = recv_message(Config), - #message{from = From, to = To, - body = Body} = xmpp:decode(El) - end, Range). + [#forwarded{ + delay = #delay{}, + sub_els = [El] + }] + }] + } = recv_message(Config), + #message{ + from = From, + to = To, + body = Body + } = xmpp:decode(El) + end, + Range). + maybe_recv_iq_result(Config, ?NS_MAM_0, I) -> #iq{type = result, id = I} = recv_iq(Config); maybe_recv_iq_result(_, _, _) -> ok. + query_iq_type(?NS_MAM_TMP) -> get; query_iq_type(_) -> set. + send_query(Config, #mam_query{xmlns = NS} = Query) -> Type = query_iq_type(NS), I = send(Config, #iq{type = Type, sub_els = [Query]}), maybe_recv_iq_result(Config, NS, I), I. + recv_fin(Config, I, _QueryID, NS, IsComplete) when NS == ?NS_MAM_1; NS == ?NS_MAM_2 -> ct:comment("Receiving fin iq for namespace '~s'", [NS]), - #iq{type = result, id = I, - sub_els = [#mam_fin{xmlns = NS, - complete = Complete, - rsm = RSM}]} = recv_iq(Config), + #iq{ + type = result, + id = I, + sub_els = [#mam_fin{ + xmlns = NS, + complete = Complete, + rsm = RSM + }] + } = recv_iq(Config), ct:comment("Checking if complete is ~s", [IsComplete]), ?match(IsComplete, Complete), RSM; recv_fin(Config, I, QueryID, ?NS_MAM_TMP = NS, _IsComplete) -> ct:comment("Receiving fin iq for namespace '~s'", [NS]), - #iq{type = result, id = I, - sub_els = [#mam_query{xmlns = NS, - rsm = RSM, - id = QueryID}]} = recv_iq(Config), + #iq{ + type = result, + id = I, + sub_els = [#mam_query{ + xmlns = NS, + rsm = RSM, + id = QueryID + }] + } = recv_iq(Config), RSM; recv_fin(Config, _, QueryID, ?NS_MAM_0 = NS, IsComplete) -> ct:comment("Receiving fin message for namespace '~s'", [NS]), #message{} = FinMsg = recv_message(Config), - #mam_fin{xmlns = NS, - id = QueryID, - complete = Complete, - rsm = RSM} = xmpp:get_subtag(FinMsg, #mam_fin{xmlns = NS}), + #mam_fin{ + xmlns = NS, + id = QueryID, + complete = Complete, + rsm = RSM + } = xmpp:get_subtag(FinMsg, #mam_fin{xmlns = NS}), ct:comment("Checking if complete is ~s", [IsComplete]), ?match(IsComplete, Complete), RSM. + send_messages_to_room(Config, Range) -> MyNick = ?config(master_nick, Config), Room = muc_room_jid(Config), @@ -526,12 +674,20 @@ send_messages_to_room(Config, Range) -> lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), - #message{from = MyNickJID, - type = groupchat, - body = Body} = - send_recv(Config, #message{to = Room, body = Body, - type = groupchat}) - end, Range). + #message{ + from = MyNickJID, + type = groupchat, + body = Body + } = + send_recv(Config, + #message{ + to = Room, + body = Body, + type = groupchat + }) + end, + Range). + recv_messages_from_room(Config, Range) -> MyNick = ?config(master_nick, Config), @@ -539,36 +695,56 @@ recv_messages_from_room(Config, Range) -> MyNickJID = jid:replace_resource(Room, MyNick), MyJID = my_jid(Config), QID = p1_rand:get_string(), - I = send(Config, #iq{type = set, to = Room, - sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), + I = send(Config, + #iq{ + type = set, + to = Room, + sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}] + }), lists:foreach( fun(N) -> - Body = xmpp:mk_text(integer_to_binary(N)), - #message{ - to = MyJID, from = Room, - sub_els = - [#mam_result{ - xmlns = ?NS_MAM_2, - queryid = QID, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = [El]}]}]} = recv_message(Config), - #message{from = MyNickJID, - type = groupchat, - body = Body} = xmpp:decode(El) - end, Range), - #iq{from = Room, id = I, type = result, - sub_els = [#mam_fin{xmlns = ?NS_MAM_2, - rsm = RSM, - complete = true}]} = recv_iq(Config), + Body = xmpp:mk_text(integer_to_binary(N)), + #message{ + to = MyJID, + from = Room, + sub_els = + [#mam_result{ + xmlns = ?NS_MAM_2, + queryid = QID, + sub_els = + [#forwarded{ + delay = #delay{}, + sub_els = [El] + }] + }] + } = recv_message(Config), + #message{ + from = MyNickJID, + type = groupchat, + body = Body + } = xmpp:decode(El) + end, + Range), + #iq{ + from = Room, + id = I, + type = result, + sub_els = [#mam_fin{ + xmlns = ?NS_MAM_2, + rsm = RSM, + complete = true + }] + } = recv_iq(Config), match_rsm_count(RSM, length(Range)). + query_all(Config, From, To) -> lists:foreach( fun(NS) -> - query_all(Config, From, To, NS) - end, ?VERSIONS). + query_all(Config, From, To, NS) + end, + ?VERSIONS). + query_all(Config, From, To, NS) -> QID = p1_rand:get_string(), @@ -578,11 +754,14 @@ query_all(Config, From, To, NS) -> RSM = recv_fin(Config, ID, QID, NS, _Complete = true), match_rsm_count(RSM, 5). + query_with(Config, From, To) -> lists:foreach( fun(NS) -> - query_with(Config, From, To, NS) - end, ?VERSIONS). + query_with(Config, From, To, NS) + end, + ?VERSIONS). + query_with(Config, From, To, NS) -> Peer = ?config(peer, Config), @@ -591,96 +770,127 @@ query_with(Config, From, To, NS) -> Range = lists:seq(1, 5), lists:foreach( fun(JID) -> - ct:comment("Sending query with jid ~s", [jid:encode(JID)]), - Query = if NS == ?NS_MAM_TMP -> - #mam_query{xmlns = NS, with = JID, id = QID}; - true -> - Fs = mam_query:encode([{with, JID}]), - #mam_query{xmlns = NS, id = QID, - xdata = #xdata{type = submit, - fields = Fs}} - end, - ID = send_query(Config, Query), - recv_archived_messages(Config, From, To, QID, Range), - RSM = recv_fin(Config, ID, QID, NS, true), - match_rsm_count(RSM, 5) - end, [Peer, BarePeer]). + ct:comment("Sending query with jid ~s", [jid:encode(JID)]), + Query = if + NS == ?NS_MAM_TMP -> + #mam_query{xmlns = NS, with = JID, id = QID}; + true -> + Fs = mam_query:encode([{with, JID}]), + #mam_query{ + xmlns = NS, + id = QID, + xdata = #xdata{ + type = submit, + fields = Fs + } + } + end, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5) + end, + [Peer, BarePeer]). + query_rsm_max(Config, From, To) -> lists:foreach( fun(NS) -> - query_rsm_max(Config, From, To, NS) - end, ?VERSIONS). + query_rsm_max(Config, From, To, NS) + end, + ?VERSIONS). + query_rsm_max(Config, From, To, NS) -> lists:foreach( fun(Max) -> - QID = p1_rand:get_string(), - Range = lists:sublist(lists:seq(1, Max), 5), - Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{max = Max}}, - ID = send_query(Config, Query), - recv_archived_messages(Config, From, To, QID, Range), - IsComplete = Max >= 5, - RSM = recv_fin(Config, ID, QID, NS, IsComplete), - match_rsm_count(RSM, 5) - end, lists:seq(0, 6)). + QID = p1_rand:get_string(), + Range = lists:sublist(lists:seq(1, Max), 5), + Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{max = Max}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + IsComplete = Max >= 5, + RSM = recv_fin(Config, ID, QID, NS, IsComplete), + match_rsm_count(RSM, 5) + end, + lists:seq(0, 6)). + query_rsm_after(Config, From, To) -> lists:foreach( fun(NS) -> - query_rsm_after(Config, From, To, NS) - end, ?VERSIONS). + query_rsm_after(Config, From, To, NS) + end, + ?VERSIONS). + query_rsm_after(Config, From, To, NS) -> lists:foldl( fun(Range, #rsm_first{data = After}) -> - ct:comment("Retrieving ~p messages after '~s'", - [length(Range), After]), - QID = p1_rand:get_string(), - Query = #mam_query{xmlns = NS, id = QID, - rsm = #rsm_set{'after' = After}}, - ID = send_query(Config, Query), - recv_archived_messages(Config, From, To, QID, Range), - RSM = #rsm_set{first = First} = - recv_fin(Config, ID, QID, NS, true), - match_rsm_count(RSM, 5), - First - end, #rsm_first{data = undefined}, - [lists:seq(N, 5) || N <- lists:seq(1, 6)]). + ct:comment("Retrieving ~p messages after '~s'", + [length(Range), After]), + QID = p1_rand:get_string(), + Query = #mam_query{ + xmlns = NS, + id = QID, + rsm = #rsm_set{'after' = After} + }, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = #rsm_set{first = First} = + recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5), + First + end, + #rsm_first{data = undefined}, + [ lists:seq(N, 5) || N <- lists:seq(1, 6) ]). + query_rsm_before(Config, From, To) -> lists:foreach( fun(NS) -> - query_rsm_before(Config, From, To, NS), - query_last_message(Config, From, To, NS) - end, ?VERSIONS). + query_rsm_before(Config, From, To, NS), + query_last_message(Config, From, To, NS) + end, + ?VERSIONS). + query_rsm_before(Config, From, To, NS) -> lists:foldl( fun(Range, Before) -> - ct:comment("Retrieving ~p messages before '~s'", - [length(Range), Before]), - QID = p1_rand:get_string(), - Query = #mam_query{xmlns = NS, id = QID, - rsm = #rsm_set{before = Before}}, - ID = send_query(Config, Query), - recv_archived_messages(Config, From, To, QID, Range), - RSM = #rsm_set{last = Last} = - recv_fin(Config, ID, QID, NS, true), - match_rsm_count(RSM, 5), - Last - end, <<"">>, lists:reverse([lists:seq(1, N) || N <- lists:seq(0, 5)])). + ct:comment("Retrieving ~p messages before '~s'", + [length(Range), Before]), + QID = p1_rand:get_string(), + Query = #mam_query{ + xmlns = NS, + id = QID, + rsm = #rsm_set{before = Before} + }, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = #rsm_set{last = Last} = + recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5), + Last + end, + <<"">>, + lists:reverse([ lists:seq(1, N) || N <- lists:seq(0, 5) ])). + query_last_message(Config, From, To, NS) -> ct:comment("Retrieving last message", []), QID = p1_rand:get_string(), - Query = #mam_query{xmlns = NS, id = QID, - rsm = #rsm_set{before = <<>>, max = 1}}, + Query = #mam_query{ + xmlns = NS, + id = QID, + rsm = #rsm_set{before = <<>>, max = 1} + }, ID = send_query(Config, Query), recv_archived_messages(Config, From, To, QID, [5]), RSM = ?match(#rsm_set{} = RSM, recv_fin(Config, ID, QID, NS, false), RSM), match_rsm_count(RSM, 5). + match_rsm_count(#rsm_set{count = undefined}, _) -> %% The backend doesn't support counting ok; diff --git a/test/mod_configtest.erl b/test/mod_configtest.erl index a4ca36f33..4e47d0163 100644 --- a/test/mod_configtest.erl +++ b/test/mod_configtest.erl @@ -3,18 +3,23 @@ -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). + start(_Host, _Opts) -> ok. + stop(_Host) -> ok. + reload(_Host, _NewOpts, _OldOpts) -> ok. + depends(_Host, _Opts) -> []. + mod_opt_type(mma) -> econf:atom(); mod_opt_type(mms) -> @@ -32,6 +37,7 @@ mod_opt_type(kmsi) -> mod_opt_type(predefined_keywords) -> econf:binary(). + mod_options(_) -> [{mma, undefined}, {mms, undefined}, @@ -41,5 +47,6 @@ mod_options(_) -> {kmsi, undefined}, {predefined_keywords, undefined}]. + mod_doc() -> #{}. diff --git a/test/muc_tests.erl b/test/muc_tests.erl index ae249691d..73d108d7a 100644 --- a/test/muc_tests.erl +++ b/test/muc_tests.erl @@ -25,13 +25,28 @@ %% API -compile(export_all). --import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1, - send/2, recv_message/1, recv_iq/1, muc_jid/1, - alt_room_jid/1, wait_for_slave/1, wait_for_master/1, - disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, - my_muc_jid/1, get_features/2, set_opt/3]). +-import(suite, + [recv_presence/1, + send_recv/2, + my_jid/1, + muc_room_jid/1, + send/2, + recv_message/1, + recv_iq/1, + muc_jid/1, + alt_room_jid/1, + wait_for_slave/1, + wait_for_master/1, + disconnect/1, + put_event/2, + get_event/1, + peer_muc_jid/1, + my_muc_jid/1, + get_features/2, + set_opt/3]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -40,273 +55,346 @@ %%%=================================================================== single_cases() -> {muc_single, [sequence], - [single_test(service_presence_error), - single_test(service_message_error), - single_test(service_unknown_ns_iq_error), - single_test(service_iq_set_error), - single_test(service_improper_iq_error), - single_test(service_features), - single_test(service_disco_info_node_error), - single_test(service_disco_items), - single_test(service_unique), - single_test(service_vcard), - single_test(configure_non_existent), - single_test(cancel_configure_non_existent), - single_test(service_subscriptions), - single_test(set_room_affiliation)]}. + [single_test(service_presence_error), + single_test(service_message_error), + single_test(service_unknown_ns_iq_error), + single_test(service_iq_set_error), + single_test(service_improper_iq_error), + single_test(service_features), + single_test(service_disco_info_node_error), + single_test(service_disco_items), + single_test(service_unique), + single_test(service_vcard), + single_test(configure_non_existent), + single_test(cancel_configure_non_existent), + single_test(service_subscriptions), + single_test(set_room_affiliation)]}. + service_presence_error(Config) -> Service = muc_jid(Config), ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), lists:foreach( fun(To) -> - send(Config, #presence{type = error, to = To}), - lists:foreach( - fun(Type) -> - #presence{type = error} = Err = - send_recv(Config, #presence{type = Type, to = To}), - #stanza_error{reason = 'service-unavailable'} = - xmpp:get_error(Err) - end, [available, unavailable]) - end, [Service, ServiceResource]), + send(Config, #presence{type = error, to = To}), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = To}), + #stanza_error{reason = 'service-unavailable'} = + xmpp:get_error(Err) + end, + [available, unavailable]) + end, + [Service, ServiceResource]), disconnect(Config). + service_message_error(Config) -> Service = muc_jid(Config), send(Config, #message{type = error, to = Service}), lists:foreach( fun(Type) -> - #message{type = error} = Err1 = - send_recv(Config, #message{type = Type, to = Service}), - #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1) - end, [chat, normal, headline, groupchat]), + #message{type = error} = Err1 = + send_recv(Config, #message{type = Type, to = Service}), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1) + end, + [chat, normal, headline, groupchat]), ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), send(Config, #message{type = error, to = ServiceResource}), lists:foreach( fun(Type) -> - #message{type = error} = Err2 = - send_recv(Config, #message{type = Type, to = ServiceResource}), - #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2) - end, [chat, normal, headline, groupchat]), + #message{type = error} = Err2 = + send_recv(Config, #message{type = Type, to = ServiceResource}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2) + end, + [chat, normal, headline, groupchat]), disconnect(Config). + service_unknown_ns_iq_error(Config) -> Service = muc_jid(Config), ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), lists:foreach( fun(To) -> - send(Config, #iq{type = result, to = To}), - send(Config, #iq{type = error, to = To}), - lists:foreach( - fun(Type) -> - #iq{type = error} = Err1 = - send_recv(Config, #iq{type = Type, to = To, - sub_els = [#presence{}]}), - #stanza_error{reason = 'service-unavailable'} = - xmpp:get_error(Err1) - end, [set, get]) - end, [Service, ServiceResource]), + send(Config, #iq{type = result, to = To}), + send(Config, #iq{type = error, to = To}), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err1 = + send_recv(Config, + #iq{ + type = Type, + to = To, + sub_els = [#presence{}] + }), + #stanza_error{reason = 'service-unavailable'} = + xmpp:get_error(Err1) + end, + [set, get]) + end, + [Service, ServiceResource]), disconnect(Config). + service_iq_set_error(Config) -> Service = muc_jid(Config), lists:foreach( fun(SubEl) -> - send(Config, #iq{type = result, to = Service, - sub_els = [SubEl]}), - #iq{type = error} = Err2 = - send_recv(Config, #iq{type = set, to = Service, - sub_els = [SubEl]}), - #stanza_error{reason = 'not-allowed'} = - xmpp:get_error(Err2) - end, [#disco_items{}, #disco_info{}, #vcard_temp{}, - #muc_unique{}, #muc_subscriptions{}]), + send(Config, + #iq{ + type = result, + to = Service, + sub_els = [SubEl] + }), + #iq{type = error} = Err2 = + send_recv(Config, + #iq{ + type = set, + to = Service, + sub_els = [SubEl] + }), + #stanza_error{reason = 'not-allowed'} = + xmpp:get_error(Err2) + end, + [#disco_items{}, + #disco_info{}, + #vcard_temp{}, + #muc_unique{}, + #muc_subscriptions{}]), disconnect(Config). + service_improper_iq_error(Config) -> Service = muc_jid(Config), lists:foreach( fun(SubEl) -> - send(Config, #iq{type = result, to = Service, - sub_els = [SubEl]}), - lists:foreach( - fun(Type) -> - #iq{type = error} = Err3 = - send_recv(Config, #iq{type = Type, to = Service, - sub_els = [SubEl]}), - #stanza_error{reason = Reason} = xmpp:get_error(Err3), - true = Reason /= 'internal-server-error' - end, [set, get]) - end, [#disco_item{jid = Service}, - #identity{category = <<"category">>, type = <<"type">>}, - #vcard_email{}, #muc_subscribe{nick = ?config(nick, Config)}]), + send(Config, + #iq{ + type = result, + to = Service, + sub_els = [SubEl] + }), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err3 = + send_recv(Config, + #iq{ + type = Type, + to = Service, + sub_els = [SubEl] + }), + #stanza_error{reason = Reason} = xmpp:get_error(Err3), + true = Reason /= 'internal-server-error' + end, + [set, get]) + end, + [#disco_item{jid = Service}, + #identity{category = <<"category">>, type = <<"type">>}, + #vcard_email{}, + #muc_subscribe{nick = ?config(nick, Config)}]), disconnect(Config). + service_features(Config) -> ServerHost = ?config(server_host, Config), MUC = muc_jid(Config), Features = sets:from_list(get_features(Config, MUC)), MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of - true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; - false -> [] - end, + true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; + false -> [] + end, RequiredFeatures = sets:from_list( - [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, - ?NS_REGISTER, ?NS_MUC, - ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE - | MAMFeatures]), + [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_REGISTER, ?NS_MUC, + ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE | MAMFeatures]), ct:comment("Checking if all needed disco features are set"), true = sets:is_subset(RequiredFeatures, Features), disconnect(Config). + service_disco_info_node_error(Config) -> MUC = muc_jid(Config), Node = p1_rand:get_string(), #iq{type = error} = Err = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#disco_info{node = Node}]}), + send_recv(Config, + #iq{ + type = get, + to = MUC, + sub_els = [#disco_info{node = Node}] + }), #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err), disconnect(Config). + service_disco_items(Config) -> #jid{server = Service} = muc_jid(Config), Rooms = lists:sort( - lists:map( - fun(I) -> - RoomName = integer_to_binary(I), - jid:make(RoomName, Service) - end, lists:seq(1, 5))), + lists:map( + fun(I) -> + RoomName = integer_to_binary(I), + jid:make(RoomName, Service) + end, + lists:seq(1, 5))), lists:foreach( fun(Room) -> - ok = join_new(Config, Room) - end, Rooms), + ok = join_new(Config, Room) + end, + Rooms), Items = disco_items(Config), - Rooms = [J || #disco_item{jid = J} <- Items], + Rooms = [ J || #disco_item{jid = J} <- Items ], lists:foreach( fun(Room) -> - ok = leave(Config, Room) - end, Rooms), + ok = leave(Config, Room) + end, + Rooms), [] = disco_items(Config), disconnect(Config). + service_vcard(Config) -> MUC = muc_jid(Config), ct:comment("Retrieving vCard from ~s", [jid:encode(MUC)]), VCard = mod_muc_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = - send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}), + send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}), disconnect(Config). + service_unique(Config) -> MUC = muc_jid(Config), ct:comment("Requesting muc unique from ~s", [jid:encode(MUC)]), #iq{type = result, sub_els = [#muc_unique{name = Name}]} = - send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}), + send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}), ct:comment("Checking if unique name is set in the response"), <<_, _/binary>> = Name, disconnect(Config). + configure_non_existent(Config) -> - [_|_] = get_config(Config), + [_ | _] = get_config(Config), disconnect(Config). + cancel_configure_non_existent(Config) -> Room = muc_room_jid(Config), #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{to = Room, type = set, - sub_els = [#muc_owner{config = #xdata{type = cancel}}]}), + send_recv(Config, + #iq{ + to = Room, + type = set, + sub_els = [#muc_owner{config = #xdata{type = cancel}}] + }), disconnect(Config). + service_subscriptions(Config) -> MUC = #jid{server = Service} = muc_jid(Config), Rooms = lists:sort( - lists:map( - fun(I) -> - RoomName = integer_to_binary(I), - jid:make(RoomName, Service) - end, lists:seq(1, 5))), + lists:map( + fun(I) -> + RoomName = integer_to_binary(I), + jid:make(RoomName, Service) + end, + lists:seq(1, 5))), lists:foreach( fun(Room) -> - ok = join_new(Config, Room), - [104] = set_config(Config, [{allow_subscription, true}], Room), - [] = subscribe(Config, [], Room) - end, Rooms), + ok = join_new(Config, Room), + [104] = set_config(Config, [{allow_subscription, true}], Room), + [] = subscribe(Config, [], Room) + end, + Rooms), #iq{type = result, sub_els = [#muc_subscriptions{list = JIDs}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#muc_subscriptions{}]}), - Rooms = lists:sort([J || #muc_subscription{jid = J, events = []} <- JIDs]), + send_recv(Config, + #iq{ + type = get, + to = MUC, + sub_els = [#muc_subscriptions{}] + }), + Rooms = lists:sort([ J || #muc_subscription{jid = J, events = []} <- JIDs ]), lists:foreach( fun(Room) -> - ok = unsubscribe(Config, Room), - ok = leave(Config, Room) - end, Rooms), + ok = unsubscribe(Config, Room), + ok = leave(Config, Room) + end, + Rooms), disconnect(Config). + set_room_affiliation(Config) -> - #jid{server = RoomService} = muc_jid(Config), - RoomName = <<"set_room_affiliation">>, - RoomJID = jid:make(RoomName, RoomService), - MyJID = my_jid(Config), - PeerJID = jid:remove_resource(?config(slave, Config)), + #jid{server = RoomService} = muc_jid(Config), + RoomName = <<"set_room_affiliation">>, + RoomJID = jid:make(RoomName, RoomService), + MyJID = my_jid(Config), + PeerJID = jid:remove_resource(?config(slave, Config)), - ct:pal("joining room ~p", [RoomJID]), - ok = join_new(Config, RoomJID), + ct:pal("joining room ~p", [RoomJID]), + ok = join_new(Config, RoomJID), - ct:pal("setting affiliation in room ~p to 'member' for ~p", [RoomJID, PeerJID]), - ServerHost = ?config(server_host, Config), - WebPort = ct:get_config(web_port, 5280), - RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", - Headers = [{"X-Admin", "true"}], - ContentType = "application/json", - Body = misc:json_encode(#{room => RoomName, service => RoomService, - user => PeerJID#jid.luser, host => PeerJID#jid.lserver, - affiliation => member}), - {ok, {{_, 200, _}, _, _}} = httpc:request(post, {RequestURL, Headers, ContentType, Body}, [], []), + ct:pal("setting affiliation in room ~p to 'member' for ~p", [RoomJID, PeerJID]), + ServerHost = ?config(server_host, Config), + WebPort = ct:get_config(web_port, 5280), + RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", + Headers = [{"X-Admin", "true"}], + ContentType = "application/json", + Body = misc:json_encode(#{ + room => RoomName, + service => RoomService, + user => PeerJID#jid.luser, + host => PeerJID#jid.lserver, + affiliation => member + }), + {ok, {{_, 200, _}, _, _}} = httpc:request(post, {RequestURL, Headers, ContentType, Body}, [], []), - #message{id = _, from = RoomJID, to = MyJID, sub_els = [ - #muc_user{items = [ - #muc_item{affiliation = member, role = none, jid = PeerJID}]}]} = recv_message(Config), + #message{ + id = _, + from = RoomJID, + to = MyJID, + sub_els = [#muc_user{ + items = [#muc_item{affiliation = member, role = none, jid = PeerJID}] + }] + } = recv_message(Config), + + ok = leave(Config, RoomJID), + disconnect(Config). - ok = leave(Config, RoomJID), - disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {muc_master_slave, [sequence], - [master_slave_test(register), - master_slave_test(groupchat_msg), - master_slave_test(private_msg), - master_slave_test(set_subject), - master_slave_test(history), - master_slave_test(invite), - master_slave_test(invite_members_only), - master_slave_test(invite_password_protected), - master_slave_test(voice_request), - master_slave_test(change_role), - master_slave_test(kick), - master_slave_test(change_affiliation), - master_slave_test(destroy), - master_slave_test(vcard), - master_slave_test(nick_change), - master_slave_test(config_title_desc), - master_slave_test(config_public_list), - master_slave_test(config_password), - master_slave_test(config_whois), - master_slave_test(config_members_only), - master_slave_test(config_moderated), - master_slave_test(config_private_messages), - master_slave_test(config_query), - master_slave_test(config_allow_invites), - master_slave_test(config_visitor_status), - master_slave_test(config_allow_voice_requests), - master_slave_test(config_voice_request_interval), - master_slave_test(config_visitor_nickchange), - master_slave_test(join_conflict), - master_slave_test(duplicate_occupantid) - ]}. + [master_slave_test(register), + master_slave_test(groupchat_msg), + master_slave_test(private_msg), + master_slave_test(set_subject), + master_slave_test(history), + master_slave_test(invite), + master_slave_test(invite_members_only), + master_slave_test(invite_password_protected), + master_slave_test(voice_request), + master_slave_test(change_role), + master_slave_test(kick), + master_slave_test(change_affiliation), + master_slave_test(destroy), + master_slave_test(vcard), + master_slave_test(nick_change), + master_slave_test(config_title_desc), + master_slave_test(config_public_list), + master_slave_test(config_password), + master_slave_test(config_whois), + master_slave_test(config_members_only), + master_slave_test(config_moderated), + master_slave_test(config_private_messages), + master_slave_test(config_query), + master_slave_test(config_allow_invites), + master_slave_test(config_visitor_status), + master_slave_test(config_allow_voice_requests), + master_slave_test(config_voice_request_interval), + master_slave_test(config_visitor_nickchange), + master_slave_test(join_conflict), + master_slave_test(duplicate_occupantid)]}. + duplicate_occupantid_master(Config) -> Room = muc_room_jid(Config), @@ -316,23 +404,35 @@ duplicate_occupantid_master(Config) -> ok = join_new(Config), wait_for_slave(Config), Pres = ?match(#presence{from = PeerNickJID, type = available} = Pres, - recv_presence(Config), Pres), - ?match(#muc_user{items = [#muc_item{jid = PeerJID, - role = participant, - affiliation = none}]}, - xmpp:get_subtag(Pres, #muc_user{})), + recv_presence(Config), + Pres), + ?match(#muc_user{ + items = [#muc_item{ + jid = PeerJID, + role = participant, + affiliation = none + }] + }, + xmpp:get_subtag(Pres, #muc_user{})), OccupantId = ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres, #occupant_id{}), Id), Pres2 = ?match(#presence{from = PeerNickJID, type = available} = Pres2, - recv_presence(Config), Pres2), + recv_presence(Config), + Pres2), ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Pres2, #occupant_id{})), Body = xmpp:mk_text(<<"test-1">>), - Msg = ?match(#message{type = groupchat, from = PeerNickJID, - body = Body} = Msg, recv_message(Config), Msg), + Msg = ?match(#message{ + type = groupchat, + from = PeerNickJID, + body = Body + } = Msg, + recv_message(Config), + Msg), ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Msg, #occupant_id{})), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + duplicate_occupantid_slave(Config) -> Room = muc_room_jid(Config), MyNick = ?config(slave_nick, Config), @@ -343,30 +443,47 @@ duplicate_occupantid_slave(Config) -> send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), ?match(#presence{from = Room, type = available}, recv_presence(Config)), OccupantId = case recv_presence(Config) of - #presence{from = MyNickJID, type = available} = Pres -> - recv_muc_presence(Config, PeerNickJID, available), - ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres, #occupant_id{}), Id); - #presence{from = PeerNickJID, type = available} -> - Pres2 = ?match(#presence{from = MyNickJID, type = available} = Pres2, - recv_presence(Config), Pres2), - ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres2, #occupant_id{}), Id) - end, + #presence{from = MyNickJID, type = available} = Pres -> + recv_muc_presence(Config, PeerNickJID, available), + ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres, #occupant_id{}), Id); + #presence{from = PeerNickJID, type = available} -> + Pres2 = ?match(#presence{from = MyNickJID, type = available} = Pres2, + recv_presence(Config), + Pres2), + ?match([#occupant_id{id = Id}], xmpp:get_subtags(Pres2, #occupant_id{}), Id) + end, ?match(#message{type = groupchat, from = Room}, recv_message(Config)), - send(Config, #presence{to = Room, sub_els = [#occupant_id{id = <<"fake1">>}, - #occupant_id{id = <<"fake2">>}]}), + send(Config, + #presence{ + to = Room, + sub_els = [#occupant_id{id = <<"fake1">>}, + #occupant_id{id = <<"fake2">>}] + }), Pres3 = ?match(#presence{from = MyNickJID, type = available} = Pres3, - recv_presence(Config), Pres3), + recv_presence(Config), + Pres3), ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Pres3, #occupant_id{})), Body = xmpp:mk_text(<<"test-1">>), - send(Config, #message{to = Room, type = groupchat, body = Body, - sub_els = [#occupant_id{id = <<"fake1">>}, - #occupant_id{id = <<"fake2">>}]}), - Msg = ?match(#message{type = groupchat, from = MyNickJID, - body = Body} = Msg, recv_message(Config), Msg), + send(Config, + #message{ + to = Room, + type = groupchat, + body = Body, + sub_els = [#occupant_id{id = <<"fake1">>}, + #occupant_id{id = <<"fake2">>}] + }), + Msg = ?match(#message{ + type = groupchat, + from = MyNickJID, + body = Body + } = Msg, + recv_message(Config), + Msg), ?match([#occupant_id{id = OccupantId}], xmpp:get_subtags(Msg, #occupant_id{})), ok = leave(Config), disconnect(Config). + join_conflict_master(Config) -> ok = join_new(Config), put_event(Config, join), @@ -375,6 +492,7 @@ join_conflict_master(Config) -> ok = leave(Config), disconnect(Config). + join_conflict_slave(Config) -> NewConfig = set_opt(nick, ?config(peer_nick, Config), Config), ct:comment("Waiting for 'join' command from the master"), @@ -384,6 +502,7 @@ join_conflict_slave(Config) -> put_event(Config, leave), disconnect(NewConfig). + groupchat_msg_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), @@ -394,19 +513,32 @@ groupchat_msg_master(Config) -> ok = master_join(Config), lists:foreach( fun(I) -> - Body = xmpp:mk_text(integer_to_binary(I)), - send(Config, #message{type = groupchat, to = Room, - body = Body}), - #message{type = groupchat, from = MyNickJID, - body = Body} = recv_message(Config) - end, lists:seq(1, 5)), - #muc_user{items = [#muc_item{jid = PeerJID, - role = none, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, + #message{ + type = groupchat, + to = Room, + body = Body + }), + #message{ + type = groupchat, + from = MyNickJID, + body = Body + } = recv_message(Config) + end, + lists:seq(1, 5)), + #muc_user{ + items = [#muc_item{ + jid = PeerJID, + role = none, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + groupchat_msg_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), @@ -414,13 +546,18 @@ groupchat_msg_slave(Config) -> {[], _, _} = slave_join(Config), lists:foreach( fun(I) -> - Body = xmpp:mk_text(integer_to_binary(I)), - #message{type = groupchat, from = PeerNickJID, - body = Body} = recv_message(Config) - end, lists:seq(1, 5)), + Body = xmpp:mk_text(integer_to_binary(I)), + #message{ + type = groupchat, + from = PeerNickJID, + body = Body + } = recv_message(Config) + end, + lists:seq(1, 5)), ok = leave(Config), disconnect(Config). + private_msg_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), @@ -429,14 +566,23 @@ private_msg_master(Config) -> ok = master_join(Config), lists:foreach( fun(I) -> - Body = xmpp:mk_text(integer_to_binary(I)), - send(Config, #message{type = chat, to = PeerNickJID, - body = Body}) - end, lists:seq(1, 5)), - #muc_user{items = [#muc_item{jid = PeerJID, - role = none, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, + #message{ + type = chat, + to = PeerNickJID, + body = Body + }) + end, + lists:seq(1, 5)), + #muc_user{ + items = [#muc_item{ + jid = PeerJID, + role = none, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Fail trying to send a private message to non-existing occupant"), send(Config, #message{type = chat, to = PeerNickJID}), #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), @@ -444,6 +590,7 @@ private_msg_master(Config) -> ok = leave(Config), disconnect(Config). + private_msg_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), @@ -451,13 +598,18 @@ private_msg_slave(Config) -> {[], _, _} = slave_join(Config), lists:foreach( fun(I) -> - Body = xmpp:mk_text(integer_to_binary(I)), - #message{type = chat, from = PeerNickJID, - body = Body} = recv_message(Config) - end, lists:seq(1, 5)), + Body = xmpp:mk_text(integer_to_binary(I)), + #message{ + type = chat, + from = PeerNickJID, + body = Body + } = recv_message(Config) + end, + lists:seq(1, 5)), ok = leave(Config), disconnect(Config). + set_subject_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), @@ -467,33 +619,55 @@ set_subject_master(Config) -> Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), ok = master_join(Config), ct:comment("Setting 1st subject"), - send(Config, #message{type = groupchat, to = Room, - subject = Subject1}), - #message{type = groupchat, from = MyNickJID, - subject = Subject1} = recv_message(Config), + send(Config, + #message{ + type = groupchat, + to = Room, + subject = Subject1 + }), + #message{ + type = groupchat, + from = MyNickJID, + subject = Subject1 + } = recv_message(Config), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Setting 2nd subject"), - send(Config, #message{type = groupchat, to = Room, - subject = Subject2}), - #message{type = groupchat, from = MyNickJID, - subject = Subject2} = recv_message(Config), + send(Config, + #message{ + type = groupchat, + to = Room, + subject = Subject2 + }), + #message{ + type = groupchat, + from = MyNickJID, + subject = Subject2 + } = recv_message(Config), ct:comment("Asking the slave to join"), put_event(Config, join), recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving 1st subject set by the slave"), - #message{type = groupchat, from = PeerNickJID, - subject = Subject1} = recv_message(Config), + #message{ + type = groupchat, + from = PeerNickJID, + subject = Subject1 + } = recv_message(Config), ct:comment("Disallow subject change"), [104] = set_config(Config, [{changesubject, false}]), ct:comment("Waiting for the slave to leave"), - #muc_user{items = [#muc_item{jid = PeerJID, - role = none, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + #muc_user{ + items = [#muc_item{ + jid = PeerJID, + role = none, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + set_subject_slave(Config) -> Room = muc_room_jid(Config), MyNickJID = my_muc_jid(Config), @@ -503,19 +677,28 @@ set_subject_slave(Config) -> Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), {[], _, _} = slave_join(Config), ct:comment("Receiving 1st subject set by the master"), - #message{type = groupchat, from = PeerNickJID, - subject = Subject1} = recv_message(Config), + #message{ + type = groupchat, + from = PeerNickJID, + subject = Subject1 + } = recv_message(Config), ok = leave(Config), ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), {[], SubjMsg2, _} = join(Config), ct:comment("Checking if the master has set 2nd subject during our absence"), - #message{type = groupchat, from = PeerNickJID, - subject = Subject2} = SubjMsg2, + #message{ + type = groupchat, + from = PeerNickJID, + subject = Subject2 + } = SubjMsg2, ct:comment("Setting 1st subject"), send(Config, #message{to = Room, type = groupchat, subject = Subject1}), - #message{type = groupchat, from = MyNickJID, - subject = Subject1} = recv_message(Config), + #message{ + type = groupchat, + from = MyNickJID, + subject = Subject1 + } = recv_message(Config), ct:comment("Waiting for the master to disallow subject change"), [104] = recv_config_change_message(Config), ct:comment("Fail trying to change the subject"), @@ -525,6 +708,7 @@ set_subject_slave(Config) -> ok = leave(Config), disconnect(Config). + history_master(Config) -> Room = muc_room_jid(Config), ServerHost = ?config(server_host, Config), @@ -537,23 +721,33 @@ history_master(Config) -> %% Only Size messages will be stored lists:foreach( fun(I) -> - Body = xmpp:mk_text(integer_to_binary(I)), - send(Config, #message{to = Room, type = groupchat, - body = Body}), - #message{type = groupchat, from = MyNickJID, - body = Body} = recv_message(Config) - end, lists:seq(0, Size)), + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, + #message{ + to = Room, + type = groupchat, + body = Body + }), + #message{ + type = groupchat, + from = MyNickJID, + body = Body + } = recv_message(Config) + end, + lists:seq(0, Size)), put_event(Config, join), lists:foreach( fun(Type) -> - recv_muc_presence(Config, PeerNickJID, Type) - end, [available, unavailable, - available, unavailable, - available, unavailable, - available, unavailable]), + recv_muc_presence(Config, PeerNickJID, Type) + end, + [available, unavailable, + available, unavailable, + available, unavailable, + available, unavailable]), ok = leave(Config), disconnect(Config). + history_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(peer_nick, Config), @@ -564,10 +758,13 @@ history_slave(Config) -> join = get_event(Config), {History, _, _} = join(Config), ct:comment("Checking ordering of history events"), - BodyList = [binary_to_integer(xmpp:get_text(Body)) - || #message{type = groupchat, from = From, - body = Body} <- History, - From == PeerNickJID], + BodyList = [ binary_to_integer(xmpp:get_text(Body)) + || #message{ + type = groupchat, + from = From, + body = Body + } <- History, + From == PeerNickJID ], BodyList = lists:seq(1, Size), ok = leave(Config), %% If the client wishes to receive no history, it MUST set the 'maxchars' @@ -578,75 +775,98 @@ history_slave(Config) -> ok = leave(Config), ct:comment("Receiving only 10 last stanzas"), {History10, _, _} = join(Config, - #muc{history = #muc_history{maxstanzas = 10}}), - BodyList10 = [binary_to_integer(xmpp:get_text(Body)) - || #message{type = groupchat, from = From, - body = Body} <- History10, - From == PeerNickJID], - BodyList10 = lists:nthtail(Size-10, lists:seq(1, Size)), + #muc{history = #muc_history{maxstanzas = 10}}), + BodyList10 = [ binary_to_integer(xmpp:get_text(Body)) + || #message{ + type = groupchat, + from = From, + body = Body + } <- History10, + From == PeerNickJID ], + BodyList10 = lists:nthtail(Size - 10, lists:seq(1, Size)), ok = leave(Config), #delay{stamp = TS} = xmpp:get_subtag(hd(History), #delay{}), ct:comment("Receiving all history without the very first element"), {HistoryWithoutFirst, _, _} = join(Config, - #muc{history = #muc_history{since = TS}}), - BodyListWithoutFirst = [binary_to_integer(xmpp:get_text(Body)) - || #message{type = groupchat, from = From, - body = Body} <- HistoryWithoutFirst, - From == PeerNickJID], + #muc{history = #muc_history{since = TS}}), + BodyListWithoutFirst = [ binary_to_integer(xmpp:get_text(Body)) + || #message{ + type = groupchat, + from = From, + body = Body + } <- HistoryWithoutFirst, + From == PeerNickJID ], BodyListWithoutFirst = lists:nthtail(1, lists:seq(1, Size)), ok = leave(Config), disconnect(Config). + invite_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), ok = join_new(Config), wait_for_slave(Config), %% Inviting the peer - send(Config, #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}), + send(Config, + #message{ + to = Room, + type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}] + }] + }), #message{from = Room} = DeclineMsg = recv_message(Config), #muc_user{decline = #muc_decline{from = PeerJID}} = - xmpp:get_subtag(DeclineMsg, #muc_user{}), + xmpp:get_subtag(DeclineMsg, #muc_user{}), ok = leave(Config), disconnect(Config). + invite_slave(Config) -> Room = muc_room_jid(Config), wait_for_master(Config), PeerJID = ?config(master, Config), #message{from = Room, type = normal} = Msg = recv_message(Config), #muc_user{invites = [#muc_invite{from = PeerJID}]} = - xmpp:get_subtag(Msg, #muc_user{}), + xmpp:get_subtag(Msg, #muc_user{}), %% Decline invitation send(Config, - #message{to = Room, - sub_els = [#muc_user{ - decline = #muc_decline{to = PeerJID}}]}), + #message{ + to = Room, + sub_els = [#muc_user{ + decline = #muc_decline{to = PeerJID} + }] + }), disconnect(Config). + invite_members_only_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), ok = join_new(Config), %% Setting the room to members-only - [_|_] = set_config(Config, [{membersonly, true}]), + [_ | _] = set_config(Config, [{membersonly, true}]), wait_for_slave(Config), %% Inviting the peer - send(Config, #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}), + send(Config, + #message{ + to = Room, + type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}] + }] + }), #message{from = Room, type = normal} = AffMsg = recv_message(Config), #muc_user{items = [#muc_item{jid = PeerJID, affiliation = member}]} = - xmpp:get_subtag(AffMsg, #muc_user{}), + xmpp:get_subtag(AffMsg, #muc_user{}), ok = leave(Config), disconnect(Config). + invite_members_only_slave(Config) -> Room = muc_room_jid(Config), wait_for_master(Config), @@ -654,23 +874,31 @@ invite_members_only_slave(Config) -> #message{from = Room, type = normal} = recv_message(Config), disconnect(Config). + invite_password_protected_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), Password = p1_rand:get_string(), ok = join_new(Config), - [104] = set_config(Config, [{passwordprotectedroom, true}, - {roomsecret, Password}]), + [104] = set_config(Config, + [{passwordprotectedroom, true}, + {roomsecret, Password}]), put_event(Config, Password), %% Inviting the peer - send(Config, #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}), + send(Config, + #message{ + to = Room, + type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}] + }] + }), ok = leave(Config), disconnect(Config). + invite_password_protected_slave(Config) -> Room = muc_room_jid(Config), Password = get_event(Config), @@ -679,6 +907,7 @@ invite_password_protected_slave(Config) -> #muc_user{password = Password} = xmpp:get_subtag(Msg, #muc_user{}), disconnect(Config). + voice_request_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), @@ -688,10 +917,13 @@ voice_request_master(Config) -> [104] = set_config(Config, [{members_by_default, false}]), wait_for_slave(Config), #muc_user{ - items = [#muc_item{role = visitor, - jid = PeerJID, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + items = [#muc_item{ + role = visitor, + jid = PeerJID, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving voice request"), #message{from = Room, type = normal} = VoiceReq = recv_message(Config), #xdata{type = form, fields = Fs} = xmpp:get_subtag(VoiceReq, #xdata{}), @@ -700,20 +932,32 @@ voice_request_master(Config) -> {role, participant}, {roomnick, PeerNick}] = lists:sort(muc_request:decode(Fs)), ct:comment("Approving voice request"), - ApprovalFs = muc_request:encode([{jid, PeerJID}, {role, participant}, - {roomnick, PeerNick}, {request_allow, true}]), - send(Config, #message{to = Room, sub_els = [#xdata{type = submit, - fields = ApprovalFs}]}), + ApprovalFs = muc_request:encode([{jid, PeerJID}, + {role, participant}, + {roomnick, PeerNick}, + {request_allow, true}]), + send(Config, + #message{ + to = Room, + sub_els = [#xdata{ + type = submit, + fields = ApprovalFs + }] + }), #muc_user{ - items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + items = [#muc_item{ + role = participant, + jid = PeerJID, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + voice_request_slave(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), @@ -727,13 +971,17 @@ voice_request_slave(Config) -> send(Config, #message{to = Room, sub_els = [X]}), ct:comment("Waiting to become a participant"), #muc_user{ - items = [#muc_item{role = participant, - jid = MyJID, - affiliation = none}]} = - recv_muc_presence(Config, MyNickJID, available), + items = [#muc_item{ + role = participant, + jid = MyJID, + affiliation = none + }] + } = + recv_muc_presence(Config, MyNickJID, available), ok = leave(Config), disconnect(Config). + change_role_master(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), @@ -744,58 +992,80 @@ change_role_master(Config) -> ok = join_new(Config), ct:comment("Waiting for the slave to join"), wait_for_slave(Config), - #muc_user{items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + #muc_user{ + items = [#muc_item{ + role = participant, + jid = PeerJID, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), lists:foreach( fun(Role) -> - ct:comment("Checking if the slave is not in the roles list"), - case get_role(Config, Role) of - [#muc_item{jid = MyJID, affiliation = owner, - role = moderator, nick = MyNick}] when Role == moderator -> - ok; - [] -> - ok - end, - Reason = p1_rand:get_string(), - put_event(Config, {Role, Reason}), - ok = set_role(Config, Role, Reason), - ct:comment("Receiving role change to ~s", [Role]), - #muc_user{ - items = [#muc_item{role = Role, - affiliation = none, - reason = Reason}]} = - recv_muc_presence(Config, PeerNickJID, available), - [#muc_item{role = Role, affiliation = none, - nick = PeerNick}|_] = get_role(Config, Role) - end, [visitor, participant, moderator]), + ct:comment("Checking if the slave is not in the roles list"), + case get_role(Config, Role) of + [#muc_item{ + jid = MyJID, + affiliation = owner, + role = moderator, + nick = MyNick + }] when Role == moderator -> + ok; + [] -> + ok + end, + Reason = p1_rand:get_string(), + put_event(Config, {Role, Reason}), + ok = set_role(Config, Role, Reason), + ct:comment("Receiving role change to ~s", [Role]), + #muc_user{ + items = [#muc_item{ + role = Role, + affiliation = none, + reason = Reason + }] + } = + recv_muc_presence(Config, PeerNickJID, available), + [#muc_item{ + role = Role, + affiliation = none, + nick = PeerNick + } | _] = get_role(Config, Role) + end, + [visitor, participant, moderator]), put_event(Config, disconnect), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + change_role_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), change_role_slave(Config, get_event(Config)). + change_role_slave(Config, {Role, Reason}) -> Room = muc_room_jid(Config), MyNick = ?config(slave_nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ct:comment("Receiving role change to ~s", [Role]), - #muc_user{status_codes = Codes, - items = [#muc_item{role = Role, - affiliation = none, - reason = Reason}]} = - recv_muc_presence(Config, MyNickJID, available), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + role = Role, + affiliation = none, + reason = Reason + }] + } = + recv_muc_presence(Config, MyNickJID, available), true = lists:member(110, Codes), change_role_slave(Config, get_event(Config)); change_role_slave(Config, disconnect) -> ok = leave(Config), disconnect(Config). + change_affiliation_master(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), @@ -808,83 +1078,105 @@ change_affiliation_master(Config) -> ok = join_new(Config), ct:comment("Waiting for the slave to join"), wait_for_slave(Config), - #muc_user{items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + #muc_user{ + items = [#muc_item{ + role = participant, + jid = PeerJID, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), lists:foreach( fun({Aff, Role, Status}) -> - ct:comment("Checking if slave is not in affiliation list"), - case get_affiliation(Config, Aff) of - [#muc_item{jid = MyBareJID, - affiliation = owner}] when Aff == owner -> - ok; - [] -> - ok - end, - Reason = p1_rand:get_string(), - put_event(Config, {Aff, Role, Status, Reason}), - ok = set_affiliation(Config, Aff, Reason), - ct:comment("Receiving affiliation change to ~s", [Aff]), - #muc_user{ - items = [#muc_item{role = Role, - affiliation = Aff, - actor = Actor, - reason = Reason}]} = - recv_muc_presence(Config, PeerNickJID, Status), - if Aff == outcast -> - ct:comment("Checking if actor is set"), - #muc_actor{nick = MyNick} = Actor; - true -> - ok - end, - Affs = get_affiliation(Config, Aff), - ct:comment("Checking if the affiliation was correctly set"), - case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of - false when Aff == none -> - ok; - #muc_item{affiliation = Aff} -> - ok - end - end, [{member, participant, available}, {none, visitor, available}, - {admin, moderator, available}, {owner, moderator, available}, - {outcast, none, unavailable}]), + ct:comment("Checking if slave is not in affiliation list"), + case get_affiliation(Config, Aff) of + [#muc_item{ + jid = MyBareJID, + affiliation = owner + }] when Aff == owner -> + ok; + [] -> + ok + end, + Reason = p1_rand:get_string(), + put_event(Config, {Aff, Role, Status, Reason}), + ok = set_affiliation(Config, Aff, Reason), + ct:comment("Receiving affiliation change to ~s", [Aff]), + #muc_user{ + items = [#muc_item{ + role = Role, + affiliation = Aff, + actor = Actor, + reason = Reason + }] + } = + recv_muc_presence(Config, PeerNickJID, Status), + if + Aff == outcast -> + ct:comment("Checking if actor is set"), + #muc_actor{nick = MyNick} = Actor; + true -> + ok + end, + Affs = get_affiliation(Config, Aff), + ct:comment("Checking if the affiliation was correctly set"), + case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of + false when Aff == none -> + ok; + #muc_item{affiliation = Aff} -> + ok + end + end, + [{member, participant, available}, + {none, visitor, available}, + {admin, moderator, available}, + {owner, moderator, available}, + {outcast, none, unavailable}]), ok = leave(Config), disconnect(Config). + change_affiliation_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), change_affiliation_slave(Config, get_event(Config)). + change_affiliation_slave(Config, {Aff, Role, Status, Reason}) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ct:comment("Receiving affiliation change to ~s", [Aff]), - if Aff == outcast -> - #presence{from = Room, type = unavailable} = recv_presence(Config); - true -> - ok + if + Aff == outcast -> + #presence{from = Room, type = unavailable} = recv_presence(Config); + true -> + ok end, - #muc_user{status_codes = Codes, - items = [#muc_item{role = Role, - actor = Actor, - affiliation = Aff, - reason = Reason}]} = - recv_muc_presence(Config, MyNickJID, Status), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + role = Role, + actor = Actor, + affiliation = Aff, + reason = Reason + }] + } = + recv_muc_presence(Config, MyNickJID, Status), true = lists:member(110, Codes), - if Aff == outcast -> - ct:comment("Checking for status code '301' (banned)"), - true = lists:member(301, Codes), - ct:comment("Checking if actor is set"), - #muc_actor{nick = PeerNick} = Actor, - disconnect(Config); - true -> - change_affiliation_slave(Config, get_event(Config)) + if + Aff == outcast -> + ct:comment("Checking for status code '301' (banned)"), + true = lists:member(301, Codes), + ct:comment("Checking if actor is set"), + #muc_actor{nick = PeerNick} = Actor, + disconnect(Config); + true -> + change_affiliation_slave(Config, get_event(Config)) end. + kick_master(Config) -> Room = muc_room_jid(Config), MyNick = ?config(nick, Config), @@ -895,28 +1187,39 @@ kick_master(Config) -> ok = join_new(Config), ct:comment("Waiting for the slave to join"), wait_for_slave(Config), - #muc_user{items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), - [#muc_item{role = participant, affiliation = none, - nick = PeerNick}|_] = get_role(Config, participant), + #muc_user{ + items = [#muc_item{ + role = participant, + jid = PeerJID, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), + [#muc_item{ + role = participant, + affiliation = none, + nick = PeerNick + } | _] = get_role(Config, participant), ct:comment("Kicking slave"), ok = set_role(Config, none, Reason), ct:comment("Receiving role change to 'none'"), #muc_user{ - status_codes = Codes, - items = [#muc_item{role = none, - affiliation = none, - actor = #muc_actor{nick = MyNick}, - reason = Reason}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + status_codes = Codes, + items = [#muc_item{ + role = none, + affiliation = none, + actor = #muc_actor{nick = MyNick}, + reason = Reason + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), [] = get_role(Config, participant), ct:comment("Checking if the code is '307' (kicked)"), true = lists:member(307, Codes), ok = leave(Config), disconnect(Config). + kick_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), @@ -927,18 +1230,23 @@ kick_slave(Config) -> {[], _, _} = join(Config), ct:comment("Receiving role change to 'none'"), #presence{from = Room, type = unavailable} = recv_presence(Config), - #muc_user{status_codes = Codes, - items = [#muc_item{role = none, - affiliation = none, - actor = #muc_actor{nick = PeerNick}, - reason = Reason}]} = - recv_muc_presence(Config, MyNickJID, unavailable), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + role = none, + affiliation = none, + actor = #muc_actor{nick = PeerNick}, + reason = Reason + }] + } = + recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if codes '110' (self-presence) " - "and '307' (kicked) are present"), + "and '307' (kicked) are present"), true = lists:member(110, Codes), true = lists:member(307, Codes), disconnect(Config). + destroy_master(Config) -> Reason = <<"Testing">>, Room = muc_room_jid(Config), @@ -951,21 +1259,32 @@ destroy_master(Config) -> ok = join_new(Config), ct:comment("Waiting for slave to join"), wait_for_slave(Config), - #muc_user{items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + #muc_user{ + items = [#muc_item{ + role = participant, + jid = PeerJID, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), wait_for_slave(Config), ok = destroy(Config, Reason), ct:comment("Receiving destruction presence"), #presence{from = Room, type = unavailable} = recv_presence(Config), - #muc_user{items = [#muc_item{role = none, - affiliation = none}], - destroy = #muc_destroy{jid = AltRoom, - reason = Reason}} = - recv_muc_presence(Config, MyNickJID, unavailable), + #muc_user{ + items = [#muc_item{ + role = none, + affiliation = none + }], + destroy = #muc_destroy{ + jid = AltRoom, + reason = Reason + } + } = + recv_muc_presence(Config, MyNickJID, unavailable), disconnect(Config). + destroy_slave(Config) -> Reason = <<"Testing">>, Room = muc_room_jid(Config), @@ -978,13 +1297,20 @@ destroy_slave(Config) -> wait_for_master(Config), ct:comment("Receiving destruction presence"), #presence{from = Room, type = unavailable} = recv_presence(Config), - #muc_user{items = [#muc_item{role = none, - affiliation = none}], - destroy = #muc_destroy{jid = AltRoom, - reason = Reason}} = - recv_muc_presence(Config, MyNickJID, unavailable), + #muc_user{ + items = [#muc_item{ + role = none, + affiliation = none + }], + destroy = #muc_destroy{ + jid = AltRoom, + reason = Reason + } + } = + recv_muc_presence(Config, MyNickJID, unavailable), disconnect(Config). + vcard_master(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), @@ -994,9 +1320,13 @@ vcard_master(Config) -> ok = join_new(Config), ct:comment("Waiting for slave to join"), wait_for_slave(Config), - #muc_user{items = [#muc_item{role = participant, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + #muc_user{ + items = [#muc_item{ + role = participant, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), #stanza_error{reason = 'item-not-found'} = get_vcard(Config), ok = set_vcard(Config, VCard), VCard = get_vcard(Config), @@ -1006,6 +1336,7 @@ vcard_master(Config) -> ok = leave(Config), disconnect(Config). + vcard_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), @@ -1018,6 +1349,7 @@ vcard_slave(Config) -> put_event(Config, leave), disconnect(Config). + nick_change_master(Config) -> NewNick = p1_rand:get_string(), PeerJID = ?config(peer, Config), @@ -1025,10 +1357,14 @@ nick_change_master(Config) -> ok = master_join(Config), put_event(Config, {new_nick, NewNick}), ct:comment("Waiting for nickchange presence from the slave"), - #muc_user{status_codes = Codes, - items = [#muc_item{jid = PeerJID, - nick = NewNick}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + jid = PeerJID, + nick = NewNick + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Checking if code '303' (nick change) is set"), true = lists:member(303, Codes), ct:comment("Waiting for updated presence from the slave"), @@ -1039,6 +1375,7 @@ nick_change_master(Config) -> ok = leave(Config), disconnect(Config). + nick_change_slave(Config) -> MyJID = my_jid(Config), MyNickJID = my_muc_jid(Config), @@ -1048,26 +1385,35 @@ nick_change_slave(Config) -> ct:comment("Sending new presence"), send(Config, #presence{to = MyNewNickJID}), ct:comment("Receiving nickchange self-presence"), - #muc_user{status_codes = Codes1, - items = [#muc_item{role = participant, - jid = MyJID, - nick = NewNick}]} = - recv_muc_presence(Config, MyNickJID, unavailable), + #muc_user{ + status_codes = Codes1, + items = [#muc_item{ + role = participant, + jid = MyJID, + nick = NewNick + }] + } = + recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if codes '110' (self-presence) and " - "'303' (nickchange) are present"), + "'303' (nickchange) are present"), lists:member(110, Codes1), lists:member(303, Codes1), ct:comment("Receiving self-presence update"), - #muc_user{status_codes = Codes2, - items = [#muc_item{jid = MyJID, - role = participant}]} = - recv_muc_presence(Config, MyNewNickJID, available), + #muc_user{ + status_codes = Codes2, + items = [#muc_item{ + jid = MyJID, + role = participant + }] + } = + recv_muc_presence(Config, MyNewNickJID, available), ct:comment("Checking if code '110' (self-presence) is set"), lists:member(110, Codes2), NewConfig = set_opt(nick, NewNick, Config), ok = leave(NewConfig), disconnect(NewConfig). + config_title_desc_master(Config) -> Title = p1_rand:get_string(), Desc = p1_rand:get_string(), @@ -1083,12 +1429,14 @@ config_title_desc_master(Config) -> ok = leave(Config), disconnect(Config). + config_title_desc_slave(Config) -> {[], _, _} = slave_join(Config), [104] = recv_config_change_message(Config), ok = leave(Config), disconnect(Config). + config_public_list_master(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), @@ -1097,22 +1445,26 @@ config_public_list_master(Config) -> wait_for_slave(Config), recv_muc_presence(Config, PeerNickJID, available), lists:member(<<"muc_public">>, get_features(Config, Room)), - [104] = set_config(Config, [{public_list, false}, - {publicroom, false}]), + [104] = set_config(Config, + [{public_list, false}, + {publicroom, false}]), recv_muc_presence(Config, PeerNickJID, unavailable), lists:member(<<"muc_hidden">>, get_features(Config, Room)), wait_for_slave(Config), ok = leave(Config), disconnect(Config). + config_public_list_slave(Config) -> Room = muc_room_jid(Config), wait_for_master(Config), PeerNick = ?config(peer_nick, Config), PeerNickJID = peer_muc_jid(Config), [#disco_item{jid = Room}] = disco_items(Config), - [#disco_item{jid = PeerNickJID, - name = PeerNick}] = disco_room_items(Config), + [#disco_item{ + jid = PeerNickJID, + name = PeerNick + }] = disco_room_items(Config), {[], _, _} = join(Config), [104] = recv_config_change_message(Config), ok = leave(Config), @@ -1121,6 +1473,7 @@ config_public_list_slave(Config) -> wait_for_master(Config), disconnect(Config). + config_password_master(Config) -> Password = p1_rand:get_string(), Room = muc_room_jid(Config), @@ -1128,8 +1481,9 @@ config_password_master(Config) -> PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), lists:member(<<"muc_unsecured">>, get_features(Config, Room)), - [104] = set_config(Config, [{passwordprotectedroom, true}, - {roomsecret, Password}]), + [104] = set_config(Config, + [{passwordprotectedroom, true}, + {roomsecret, Password}]), lists:member(<<"muc_passwordprotected">>, get_features(Config, Room)), put_event(Config, Password), recv_muc_presence(Config, PeerNickJID, available), @@ -1137,15 +1491,17 @@ config_password_master(Config) -> ok = leave(Config), disconnect(Config). + config_password_slave(Config) -> Password = get_event(Config), #stanza_error{reason = 'not-authorized'} = join(Config), #stanza_error{reason = 'not-authorized'} = - join(Config, #muc{password = p1_rand:get_string()}), + join(Config, #muc{password = p1_rand:get_string()}), {[], _, _} = join(Config, #muc{password = Password}), ok = leave(Config), disconnect(Config). + config_whois_master(Config) -> Room = muc_room_jid(Config), PeerNickJID = peer_muc_jid(Config), @@ -1163,6 +1519,7 @@ config_whois_master(Config) -> ok = leave(Config), disconnect(Config). + config_whois_slave(Config) -> PeerJID = ?config(peer, Config), PeerNickJID = peer_muc_jid(Config), @@ -1176,12 +1533,13 @@ config_whois_slave(Config) -> true = lists:member(100, Codes), ct:comment("Receiving presence from peer with JID exposed"), #muc_user{items = [#muc_item{jid = PeerJID}]} = - recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, available), ct:comment("Waiting for the room to become anonymous again (code '173')"), [173] = recv_config_change_message(Config), ok = leave(Config), disconnect(Config). + config_members_only_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), @@ -1190,11 +1548,15 @@ config_members_only_master(Config) -> ok = master_join(Config), lists:member(<<"muc_open">>, get_features(Config, Room)), [104] = set_config(Config, [{membersonly, true}]), - #muc_user{status_codes = Codes, - items = [#muc_item{jid = PeerJID, - affiliation = none, - role = none}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + jid = PeerJID, + affiliation = none, + role = none + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Checking if code '322' (non-member) is set"), true = lists:member(322, Codes), lists:member(<<"muc_membersonly">>, get_features(Config, Room)), @@ -1202,25 +1564,34 @@ config_members_only_master(Config) -> set_member = get_event(Config), ok = set_affiliation(Config, member, p1_rand:get_string()), #message{from = Room, type = normal} = Msg = recv_message(Config), - #muc_user{items = [#muc_item{jid = PeerBareJID, - affiliation = member}]} = - xmpp:get_subtag(Msg, #muc_user{}), + #muc_user{ + items = [#muc_item{ + jid = PeerBareJID, + affiliation = member + }] + } = + xmpp:get_subtag(Msg, #muc_user{}), ct:comment("Asking peer to join"), put_event(Config, join), ct:comment("Waiting for peer to join"), recv_muc_presence(Config, PeerNickJID, available), ok = set_affiliation(Config, none, p1_rand:get_string()), ct:comment("Waiting for peer to be kicked"), - #muc_user{status_codes = NewCodes, - items = [#muc_item{affiliation = none, - role = none}]} = - recv_muc_presence(Config, PeerNickJID, unavailable), + #muc_user{ + status_codes = NewCodes, + items = [#muc_item{ + affiliation = none, + role = none + }] + } = + recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Checking if code '321' (became non-member in " - "members-only room) is set"), + "members-only room) is set"), true = lists:member(321, NewCodes), ok = leave(Config), disconnect(Config). + config_members_only_slave(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), @@ -1229,13 +1600,17 @@ config_members_only_slave(Config) -> [104] = recv_config_change_message(Config), ct:comment("Getting kicked because the room has become members-only"), #presence{from = Room, type = unavailable} = recv_presence(Config), - #muc_user{status_codes = Codes, - items = [#muc_item{jid = MyJID, - role = none, - affiliation = none}]} = - recv_muc_presence(Config, MyNickJID, unavailable), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + jid = MyJID, + role = none, + affiliation = none + }] + } = + recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if the code '110' (self-presence) " - "and '322' (non-member) is set"), + "and '322' (non-member) is set"), true = lists:member(110, Codes), true = lists:member(322, Codes), ct:comment("Fail trying to join members-only room"), @@ -1246,17 +1621,22 @@ config_members_only_slave(Config) -> join = get_event(Config), {[], _, _} = join(Config, participant, member), #presence{from = Room, type = unavailable} = recv_presence(Config), - #muc_user{status_codes = NewCodes, - items = [#muc_item{jid = MyJID, - role = none, - affiliation = none}]} = - recv_muc_presence(Config, MyNickJID, unavailable), + #muc_user{ + status_codes = NewCodes, + items = [#muc_item{ + jid = MyJID, + role = none, + affiliation = none + }] + } = + recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if the code '110' (self-presence) " - "and '321' (became non-member in members-only room) is set"), + "and '321' (became non-member in members-only room) is set"), true = lists:member(110, NewCodes), true = lists:member(321, NewCodes), disconnect(Config). + config_moderated_master(Config) -> Room = muc_room_jid(Config), PeerNickJID = peer_muc_jid(Config), @@ -1264,7 +1644,7 @@ config_moderated_master(Config) -> lists:member(<<"muc_moderated">>, get_features(Config, Room)), ok = set_role(Config, visitor, p1_rand:get_string()), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, available), set_unmoderated = get_event(Config), [104] = set_config(Config, [{moderatedroom, false}]), #message{from = PeerNickJID, type = groupchat} = recv_message(Config), @@ -1273,12 +1653,13 @@ config_moderated_master(Config) -> ok = leave(Config), disconnect(Config). + config_moderated_slave(Config) -> Room = muc_room_jid(Config), MyNickJID = my_muc_jid(Config), {[], _, _} = slave_join(Config), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, MyNickJID, available), + recv_muc_presence(Config, MyNickJID, available), send(Config, #message{to = Room, type = groupchat}), ErrMsg = #message{from = Room, type = error} = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), @@ -1289,6 +1670,7 @@ config_moderated_slave(Config) -> ok = leave(Config), disconnect(Config). + config_private_messages_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = master_join(Config), @@ -1304,8 +1686,9 @@ config_private_messages_master(Config) -> #message{from = PeerNickJID, type = chat} = recv_message(Config), [104] = set_config(Config, [{allow_private_messages_from_visitors, nobody}]), wait_for_slave(Config), - [104] = set_config(Config, [{allow_private_messages_from_visitors, anyone}, - {allowpm, none}]), + [104] = set_config(Config, + [{allow_private_messages_from_visitors, anyone}, + {allowpm, none}]), ct:comment("Fail trying to send a private message"), send(Config, #message{to = PeerNickJID, type = chat}), #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), @@ -1318,6 +1701,7 @@ config_private_messages_master(Config) -> ok = leave(Config), disconnect(Config). + config_private_messages_slave(Config) -> MyNickJID = my_muc_jid(Config), PeerNickJID = peer_muc_jid(Config), @@ -1326,7 +1710,7 @@ config_private_messages_slave(Config) -> send(Config, #message{to = PeerNickJID, type = chat}), ct:comment("Waiting to become a visitor"), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, MyNickJID, available), + recv_muc_presence(Config, MyNickJID, available), ct:comment("Sending a private message"), send(Config, #message{to = PeerNickJID, type = chat}), [104] = recv_config_change_message(Config), @@ -1341,7 +1725,7 @@ config_private_messages_slave(Config) -> [104] = recv_config_change_message(Config), ct:comment("Waiting to become a participant again"), #muc_user{items = [#muc_item{role = participant}]} = - recv_muc_presence(Config, MyNickJID, available), + recv_muc_presence(Config, MyNickJID, available), ct:comment("Fail trying to send a private message"), send(Config, #message{to = PeerNickJID, type = chat}), #message{from = PeerNickJID, type = error} = ErrMsg2 = recv_message(Config), @@ -1349,46 +1733,65 @@ config_private_messages_slave(Config) -> ok = leave(Config), disconnect(Config). + config_query_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), wait_for_slave(Config), recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving IQ query from the slave"), - #iq{type = get, from = PeerNickJID, id = I, - sub_els = [#ping{}]} = recv_iq(Config), + #iq{ + type = get, + from = PeerNickJID, + id = I, + sub_els = [#ping{}] + } = recv_iq(Config), send(Config, #iq{type = result, to = PeerNickJID, id = I}), [104] = set_config(Config, [{allow_query_users, false}]), ct:comment("Fail trying to send IQ"), #iq{type = error, from = PeerNickJID} = Err = - send_recv(Config, #iq{type = get, to = PeerNickJID, - sub_els = [#ping{}]}), + send_recv(Config, + #iq{ + type = get, + to = PeerNickJID, + sub_els = [#ping{}] + }), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + config_query_slave(Config) -> PeerNickJID = peer_muc_jid(Config), wait_for_master(Config), ct:comment("Checking if IQ queries are denied from non-occupants"), #iq{type = error, from = PeerNickJID} = Err1 = - send_recv(Config, #iq{type = get, to = PeerNickJID, - sub_els = [#ping{}]}), + send_recv(Config, + #iq{ + type = get, + to = PeerNickJID, + sub_els = [#ping{}] + }), #stanza_error{reason = 'not-acceptable'} = xmpp:get_error(Err1), {[], _, _} = join(Config), ct:comment("Sending IQ to the master"), #iq{type = result, from = PeerNickJID, sub_els = []} = - send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}), + send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}), [104] = recv_config_change_message(Config), ct:comment("Fail trying to send IQ"), #iq{type = error, from = PeerNickJID} = Err2 = - send_recv(Config, #iq{type = get, to = PeerNickJID, - sub_els = [#ping{}]}), + send_recv(Config, + #iq{ + type = get, + to = PeerNickJID, + sub_els = [#ping{}] + }), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2), ok = leave(Config), disconnect(Config). + config_allow_invites_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), @@ -1400,23 +1803,33 @@ config_allow_invites_master(Config) -> [104] = set_config(Config, [{allowinvites, false}]), send_invitation = get_event(Config), ct:comment("Sending an invitation"), - send(Config, #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}), + send(Config, + #message{ + to = Room, + type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}] + }] + }), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + config_allow_invites_slave(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), - InviteMsg = #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}, + InviteMsg = #message{ + to = Room, + type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}] + }] + }, {[], _, _} = slave_join(Config), [104] = recv_config_change_message(Config), ct:comment("Sending an invitation"), @@ -1432,6 +1845,7 @@ config_allow_invites_slave(Config) -> ok = leave(Config), disconnect(Config). + config_visitor_status_master(Config) -> PeerNickJID = peer_muc_jid(Config), Status = xmpp:mk_text(p1_rand:get_string()), @@ -1440,7 +1854,7 @@ config_visitor_status_master(Config) -> ct:comment("Asking the slave to join as a visitor"), put_event(Config, {join, Status}), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving status change from the visitor"), #presence{from = PeerNickJID, status = Status} = recv_presence(Config), [104] = set_config(Config, [{allow_visitor_status, false}]), @@ -1451,6 +1865,7 @@ config_visitor_status_master(Config) -> ok = leave(Config), disconnect(Config). + config_visitor_status_slave(Config) -> Room = muc_room_jid(Config), MyNickJID = my_muc_jid(Config), @@ -1467,6 +1882,7 @@ config_visitor_status_slave(Config) -> ok = leave(Config), disconnect(Config). + config_allow_voice_requests_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), @@ -1474,13 +1890,14 @@ config_allow_voice_requests_master(Config) -> ct:comment("Asking the slave to join as a visitor"), put_event(Config, join), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, available), [104] = set_config(Config, [{allow_voice_requests, false}]), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + config_allow_voice_requests_slave(Config) -> Room = muc_room_jid(Config), ct:comment("Waiting for 'join' command from the master"), @@ -1496,6 +1913,7 @@ config_allow_voice_requests_slave(Config) -> ok = leave(Config), disconnect(Config). + config_voice_request_interval_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), @@ -1506,15 +1924,23 @@ config_voice_request_interval_master(Config) -> ct:comment("Asking the slave to join as a visitor"), put_event(Config, join), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, available), [104] = set_config(Config, [{voice_request_min_interval, 5}]), ct:comment("Receiving a voice request from slave"), #message{from = Room, type = normal} = recv_message(Config), ct:comment("Deny voice request at first"), - Fs = muc_request:encode([{jid, PeerJID}, {role, participant}, - {roomnick, PeerNick}, {request_allow, false}]), - send(Config, #message{to = Room, sub_els = [#xdata{type = submit, - fields = Fs}]}), + Fs = muc_request:encode([{jid, PeerJID}, + {role, participant}, + {roomnick, PeerNick}, + {request_allow, false}]), + send(Config, + #message{ + to = Room, + sub_els = [#xdata{ + type = submit, + fields = Fs + }] + }), put_event(Config, denied), ct:comment("Waiting for repeated voice request from the slave"), #message{from = Room, type = normal} = recv_message(Config), @@ -1523,6 +1949,7 @@ config_voice_request_interval_master(Config) -> ok = leave(Config), disconnect(Config). + config_voice_request_interval_slave(Config) -> Room = muc_room_jid(Config), Fs = muc_request:encode([{role, participant}]), @@ -1547,6 +1974,7 @@ config_voice_request_interval_slave(Config) -> ok = leave(Config), disconnect(Config). + config_visitor_nickchange_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), @@ -1555,13 +1983,14 @@ config_visitor_nickchange_master(Config) -> put_event(Config, join), ct:comment("Waiting for the slave to join"), #muc_user{items = [#muc_item{role = visitor}]} = - recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, available), [104] = set_config(Config, [{allow_visitor_nickchange, false}]), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). + config_visitor_nickchange_slave(Config) -> NewNick = p1_rand:get_string(), MyNickJID = my_muc_jid(Config), @@ -1577,14 +2006,19 @@ config_visitor_nickchange_slave(Config) -> ok = leave(Config), disconnect(Config). + register_master(Config) -> MUC = muc_jid(Config), %% Register nick "master1" register_nick(Config, MUC, <<"">>, <<"master1">>), %% Unregister nick "master1" via jabber:register #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{remove = true}]}), + send_recv(Config, + #iq{ + type = set, + to = MUC, + sub_els = [#register{remove = true}] + }), %% Register nick "master2" register_nick(Config, MUC, <<"">>, <<"master2">>), %% Now register nick "master" @@ -1596,6 +2030,7 @@ register_master(Config) -> register_nick(Config, MUC, <<"master">>, <<"">>), disconnect(Config). + register_slave(Config) -> MUC = muc_jid(Config), wait_for_master(Config), @@ -1603,29 +2038,39 @@ register_slave(Config) -> Fs = muc_register:encode([{roomnick, <<"master">>}]), X = #xdata{type = submit, fields = Fs}, #iq{type = error} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{xdata = X}]}), + send_recv(Config, + #iq{ + type = set, + to = MUC, + sub_els = [#register{xdata = X}] + }), wait_for_master(Config), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("muc_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("muc_" ++ atom_to_list(T)), [parallel], + {list_to_atom("muc_" ++ atom_to_list(T)), + [parallel], [list_to_atom("muc_" ++ atom_to_list(T) ++ "_master"), list_to_atom("muc_" ++ atom_to_list(T) ++ "_slave")]}. + recv_muc_presence(Config, From, Type) -> Pres = #presence{from = From, type = Type} = recv_presence(Config), xmpp:get_subtag(Pres, #muc_user{}). + join_new(Config) -> join_new(Config, muc_room_jid(Config)). + join_new(Config, Room) -> MyJID = my_jid(Config), MyNick = ?config(nick, Config), @@ -1641,54 +2086,71 @@ join_new(Config, Room) -> %% 5. Live messages, presence updates, new user joins, etc. %% As this is the newly created room, we receive only the 2nd and 4th stanza. #muc_user{ - status_codes = Codes, - items = [#muc_item{role = moderator, - jid = MyJID, - affiliation = owner}]} = - recv_muc_presence(Config, MyNickJID, available), + status_codes = Codes, + items = [#muc_item{ + role = moderator, + jid = MyJID, + affiliation = owner + }] + } = + recv_muc_presence(Config, MyNickJID, available), ct:comment("Checking if codes '110' (self-presence) and " - "'201' (new room) is set"), + "'201' (new room) is set"), true = lists:member(110, Codes), true = lists:member(201, Codes), ct:comment("Receiving empty room subject"), - #message{from = Room, type = groupchat, body = [], - subject = [#text{data = <<>>}]} = recv_message(Config), + #message{ + from = Room, + type = groupchat, + body = [], + subject = [#text{data = <<>>}] + } = recv_message(Config), case ?config(persistent_room, Config) of - true -> - [104] = set_config(Config, [{persistentroom, true}], Room), - ok; - false -> - ok + true -> + [104] = set_config(Config, [{persistentroom, true}], Room), + ok; + false -> + ok end. + recv_history_and_subject(Config) -> ct:comment("Receiving room history and/or subject"), recv_history_and_subject(Config, []). + recv_history_and_subject(Config, History) -> Room = muc_room_jid(Config), - #message{type = groupchat, subject = Subj, - body = Body, thread = Thread} = Msg = recv_message(Config), + #message{ + type = groupchat, + subject = Subj, + body = Body, + thread = Thread + } = Msg = recv_message(Config), case xmpp:get_subtag(Msg, #delay{}) of - #delay{from = Room} -> - recv_history_and_subject(Config, [Msg|History]); - false when Subj /= [], Body == [], Thread == undefined -> - {lists:reverse(History), Msg} + #delay{from = Room} -> + recv_history_and_subject(Config, [Msg | History]); + false when Subj /= [], Body == [], Thread == undefined -> + {lists:reverse(History), Msg} end. + join(Config) -> join(Config, participant, none, #muc{}). + join(Config, Role) when is_atom(Role) -> join(Config, Role, none, #muc{}); join(Config, #muc{} = SubEl) -> join(Config, participant, none, SubEl). + join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) -> join(Config, Role, Aff, #muc{}); join(Config, Role, #muc{} = SubEl) when is_atom(Role) -> join(Config, Role, none, SubEl). + join(Config, Role, Aff, SubEls) when is_list(SubEls) -> ct:comment("Joining existing room as ~s/~s", [Aff, Role]), MyJID = my_jid(Config), @@ -1699,148 +2161,203 @@ join(Config, Role, Aff, SubEls) when is_list(SubEls) -> PeerNickJID = jid:replace_resource(Room, PeerNick), send(Config, #presence{to = MyNickJID, sub_els = SubEls}), case recv_presence(Config) of - #presence{type = error, from = MyNickJID} = Err -> - xmpp:get_subtag(Err, #stanza_error{}); - #presence{from = Room, type = available} -> - case recv_presence(Config) of - #presence{type = available, from = PeerNickJID} = Pres -> - #muc_user{items = [#muc_item{role = moderator, - affiliation = owner}]} = - xmpp:get_subtag(Pres, #muc_user{}), - ct:comment("Receiving initial self-presence"), - #muc_user{status_codes = Codes, - items = [#muc_item{role = Role, - jid = MyJID, - affiliation = Aff}]} = - recv_muc_presence(Config, MyNickJID, available), - ct:comment("Checking if code '110' (self-presence) is set"), - true = lists:member(110, Codes), - {History, Subj} = recv_history_and_subject(Config), - {History, Subj, Codes}; - #presence{type = available, from = MyNickJID} = Pres -> - #muc_user{status_codes = Codes, - items = [#muc_item{role = Role, - jid = MyJID, - affiliation = Aff}]} = - xmpp:get_subtag(Pres, #muc_user{}), - ct:comment("Checking if code '110' (self-presence) is set"), - true = lists:member(110, Codes), - {History, Subj} = recv_history_and_subject(Config), - {empty, History, Subj, Codes} - end + #presence{type = error, from = MyNickJID} = Err -> + xmpp:get_subtag(Err, #stanza_error{}); + #presence{from = Room, type = available} -> + case recv_presence(Config) of + #presence{type = available, from = PeerNickJID} = Pres -> + #muc_user{ + items = [#muc_item{ + role = moderator, + affiliation = owner + }] + } = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Receiving initial self-presence"), + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + role = Role, + jid = MyJID, + affiliation = Aff + }] + } = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {History, Subj, Codes}; + #presence{type = available, from = MyNickJID} = Pres -> + #muc_user{ + status_codes = Codes, + items = [#muc_item{ + role = Role, + jid = MyJID, + affiliation = Aff + }] + } = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {empty, History, Subj, Codes} + end end; join(Config, Role, Aff, SubEl) -> join(Config, Role, Aff, [SubEl]). + leave(Config) -> leave(Config, muc_room_jid(Config)). + leave(Config, Room) -> MyJID = my_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), Mode = ?config(mode, Config), IsPersistent = ?config(persistent_room, Config), - if Mode /= slave, IsPersistent -> - [104] = set_config(Config, [{persistentroom, false}], Room); - true -> - ok + if + Mode /= slave, IsPersistent -> + [104] = set_config(Config, [{persistentroom, false}], Room); + true -> + ok end, ct:comment("Leaving the room"), send(Config, #presence{to = MyNickJID, type = unavailable}), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{ - status_codes = Codes, - items = [#muc_item{role = none, jid = MyJID}]} = - recv_muc_presence(Config, MyNickJID, unavailable), + status_codes = Codes, + items = [#muc_item{role = none, jid = MyJID}] + } = + recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if code '110' (self-presence) is set"), true = lists:member(110, Codes), ok. + get_config(Config) -> ct:comment("Get room config"), Room = muc_room_jid(Config), case send_recv(Config, - #iq{type = get, to = Room, - sub_els = [#muc_owner{}]}) of - #iq{type = result, - sub_els = [#muc_owner{config = #xdata{type = form} = X}]} -> - muc_roomconfig:decode(X#xdata.fields); - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + #iq{ + type = get, + to = Room, + sub_els = [#muc_owner{}] + }) of + #iq{ + type = result, + sub_els = [#muc_owner{config = #xdata{type = form} = X}] + } -> + muc_roomconfig:decode(X#xdata.fields); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + set_config(Config, RoomConfig) -> set_config(Config, RoomConfig, muc_room_jid(Config)). + set_config(Config, RoomConfig, Room) -> ct:comment("Set room config: ~p", [RoomConfig]), Fs = case RoomConfig of - [] -> []; - _ -> muc_roomconfig:encode(RoomConfig) - end, + [] -> []; + _ -> muc_roomconfig:encode(RoomConfig) + end, case send_recv(Config, - #iq{type = set, to = Room, - sub_els = [#muc_owner{config = #xdata{type = submit, - fields = Fs}}]}) of - #iq{type = result, sub_els = []} -> - #presence{from = Room, type = available} = recv_presence(Config), - #message{from = Room, type = groupchat} = Msg = recv_message(Config), - #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), - lists:sort(Codes); - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + #iq{ + type = set, + to = Room, + sub_els = [#muc_owner{ + config = #xdata{ + type = submit, + fields = Fs + } + }] + }) of + #iq{type = result, sub_els = []} -> + #presence{from = Room, type = available} = recv_presence(Config), + #message{from = Room, type = groupchat} = Msg = recv_message(Config), + #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), + lists:sort(Codes); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + create_persistent(Config) -> - [_|_] = get_config(Config), + [_ | _] = get_config(Config), [] = set_config(Config, [{persistentroom, true}], false), ok. + destroy(Config) -> destroy(Config, <<>>). + destroy(Config, Reason) -> Room = muc_room_jid(Config), AltRoom = alt_room_jid(Config), ct:comment("Destroying a room"), case send_recv(Config, - #iq{type = set, to = Room, - sub_els = [#muc_owner{destroy = #muc_destroy{ - reason = Reason, - jid = AltRoom}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + #iq{ + type = set, + to = Room, + sub_els = [#muc_owner{ + destroy = #muc_destroy{ + reason = Reason, + jid = AltRoom + } + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + disco_items(Config) -> MUC = muc_jid(Config), ct:comment("Performing disco#items request to ~s", [jid:encode(MUC)]), #iq{type = result, from = MUC, sub_els = [DiscoItems]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#disco_items{}]}), + send_recv(Config, + #iq{ + type = get, + to = MUC, + sub_els = [#disco_items{}] + }), lists:keysort(#disco_item.jid, DiscoItems#disco_items.items). + disco_room_items(Config) -> Room = muc_room_jid(Config), #iq{type = result, from = Room, sub_els = [DiscoItems]} = - send_recv(Config, #iq{type = get, to = Room, - sub_els = [#disco_items{}]}), + send_recv(Config, + #iq{ + type = get, + to = Room, + sub_els = [#disco_items{}] + }), DiscoItems#disco_items.items. + get_affiliations(Config, Aff) -> Room = muc_room_jid(Config), case send_recv(Config, - #iq{type = get, to = Room, - sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}]}) of - #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> - Items; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + #iq{ + type = get, + to = Room, + sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}] + }) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + Items; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + master_join(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), @@ -1848,104 +2365,144 @@ master_join(Config) -> PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), wait_for_slave(Config), - #muc_user{items = [#muc_item{jid = PeerJID, - role = participant, - affiliation = none}]} = - recv_muc_presence(Config, PeerNickJID, available), + #muc_user{ + items = [#muc_item{ + jid = PeerJID, + role = participant, + affiliation = none + }] + } = + recv_muc_presence(Config, PeerNickJID, available), ok. + slave_join(Config) -> wait_for_master(Config), join(Config). + set_role(Config, Role, Reason) -> ct:comment("Changing role to ~s", [Role]), Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), case send_recv( - Config, - #iq{type = set, to = Room, - sub_els = - [#muc_admin{ - items = [#muc_item{role = Role, - reason = Reason, - nick = PeerNick}]}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + Config, + #iq{ + type = set, + to = Room, + sub_els = + [#muc_admin{ + items = [#muc_item{ + role = Role, + reason = Reason, + nick = PeerNick + }] + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + get_role(Config, Role) -> ct:comment("Requesting list for role '~s'", [Role]), Room = muc_room_jid(Config), case send_recv( - Config, - #iq{type = get, to = Room, - sub_els = [#muc_admin{ - items = [#muc_item{role = Role}]}]}) of - #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> - lists:keysort(#muc_item.affiliation, Items); - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + Config, + #iq{ + type = get, + to = Room, + sub_els = [#muc_admin{ + items = [#muc_item{role = Role}] + }] + }) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + lists:keysort(#muc_item.affiliation, Items); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + set_affiliation(Config, Aff, Reason) -> ct:comment("Changing affiliation to ~s", [Aff]), Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerBareJID = jid:remove_resource(PeerJID), case send_recv( - Config, - #iq{type = set, to = Room, - sub_els = - [#muc_admin{ - items = [#muc_item{affiliation = Aff, - reason = Reason, - jid = PeerBareJID}]}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + Config, + #iq{ + type = set, + to = Room, + sub_els = + [#muc_admin{ + items = [#muc_item{ + affiliation = Aff, + reason = Reason, + jid = PeerBareJID + }] + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + get_affiliation(Config, Aff) -> ct:comment("Requesting list for affiliation '~s'", [Aff]), Room = muc_room_jid(Config), case send_recv( - Config, - #iq{type = get, to = Room, - sub_els = [#muc_admin{ - items = [#muc_item{affiliation = Aff}]}]}) of - #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> - Items; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + Config, + #iq{ + type = get, + to = Room, + sub_els = [#muc_admin{ + items = [#muc_item{affiliation = Aff}] + }] + }) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + Items; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + set_vcard(Config, VCard) -> Room = muc_room_jid(Config), ct:comment("Setting vCard for ~s", [jid:encode(Room)]), - case send_recv(Config, #iq{type = set, to = Room, - sub_els = [VCard]}) of - #iq{type = result, sub_els = []} -> - [104] = recv_config_change_message(Config), - ok; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + case send_recv(Config, + #iq{ + type = set, + to = Room, + sub_els = [VCard] + }) of + #iq{type = result, sub_els = []} -> + [104] = recv_config_change_message(Config), + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + get_vcard(Config) -> Room = muc_room_jid(Config), ct:comment("Retrieving vCard from ~s", [jid:encode(Room)]), - case send_recv(Config, #iq{type = get, to = Room, - sub_els = [#vcard_temp{}]}) of - #iq{type = result, sub_els = [VCard]} -> - VCard; - #iq{type = error} = Err -> - xmpp:get_subtag(Err, #stanza_error{}) + case send_recv(Config, + #iq{ + type = get, + to = Room, + sub_els = [#vcard_temp{}] + }) of + #iq{type = result, sub_els = [VCard]} -> + VCard; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) end. + recv_config_change_message(Config) -> ct:comment("Receiving configuration change notification message"), Room = muc_room_jid(Config), @@ -1954,55 +2511,93 @@ recv_config_change_message(Config) -> #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), lists:sort(Codes). + register_nick(Config, MUC, PrevNick, Nick) -> - PrevRegistered = if PrevNick /= <<"">> -> true; - true -> false - end, - NewRegistered = if Nick /= <<"">> -> true; - true -> false - end, + PrevRegistered = if + PrevNick /= <<"">> -> true; + true -> false + end, + NewRegistered = if + Nick /= <<"">> -> true; + true -> false + end, ct:comment("Requesting registration form"), - #iq{type = result, - sub_els = [#register{registered = PrevRegistered, - xdata = #xdata{type = form, - fields = FsWithoutNick}}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#register{}]}), + #iq{ + type = result, + sub_els = [#register{ + registered = PrevRegistered, + xdata = #xdata{ + type = form, + fields = FsWithoutNick + } + }] + } = + send_recv(Config, + #iq{ + type = get, + to = MUC, + sub_els = [#register{}] + }), ct:comment("Checking if previous nick is registered"), PrevNick = proplists:get_value( - roomnick, muc_register:decode(FsWithoutNick)), + roomnick, muc_register:decode(FsWithoutNick)), X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])}, ct:comment("Submitting registration form"), #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{xdata = X}]}), + send_recv(Config, + #iq{ + type = set, + to = MUC, + sub_els = [#register{xdata = X}] + }), ct:comment("Checking if new nick was registered"), - #iq{type = result, - sub_els = [#register{registered = NewRegistered, - xdata = #xdata{type = form, - fields = FsWithNick}}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#register{}]}), + #iq{ + type = result, + sub_els = [#register{ + registered = NewRegistered, + xdata = #xdata{ + type = form, + fields = FsWithNick + } + }] + } = + send_recv(Config, + #iq{ + type = get, + to = MUC, + sub_els = [#register{}] + }), Nick = proplists:get_value( - roomnick, muc_register:decode(FsWithNick)). + roomnick, muc_register:decode(FsWithNick)). + subscribe(Config, Events, Room) -> MyNick = ?config(nick, Config), case send_recv(Config, - #iq{type = set, to = Room, - sub_els = [#muc_subscribe{nick = MyNick, - events = Events}]}) of - #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} -> - lists:sort(ResEvents); - #iq{type = error} = Err -> - xmpp:get_error(Err) + #iq{ + type = set, + to = Room, + sub_els = [#muc_subscribe{ + nick = MyNick, + events = Events + }] + }) of + #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} -> + lists:sort(ResEvents); + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + unsubscribe(Config, Room) -> - case send_recv(Config, #iq{type = set, to = Room, - sub_els = [#muc_unsubscribe{}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + case send_recv(Config, + #iq{ + type = set, + to = Room, + sub_els = [#muc_unsubscribe{}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. diff --git a/test/offline_tests.erl b/test/offline_tests.erl index d859da622..8d2e2a822 100644 --- a/test/offline_tests.erl +++ b/test/offline_tests.erl @@ -25,27 +25,41 @@ %% API -compile(export_all). --import(suite, [send/2, disconnect/1, my_jid/1, send_recv/2, recv_message/1, - get_features/1, recv/1, get_event/1, server_jid/1, - wait_for_master/1, wait_for_slave/1, - connect/1, open_session/1, bind/1, auth/1]). +-import(suite, + [send/2, + disconnect/1, + my_jid/1, + send_recv/2, + recv_message/1, + get_features/1, + recv/1, + get_event/1, + server_jid/1, + wait_for_master/1, + wait_for_slave/1, + connect/1, + open_session/1, + bind/1, + auth/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== single_cases() -> {offline_single, [sequence], - [single_test(feature_enabled), - single_test(check_identity), - single_test(send_non_existent), - single_test(view_non_existent), - single_test(remove_non_existent), - single_test(view_non_integer), - single_test(remove_non_integer), - single_test(malformed_iq), - single_test(wrong_user), - single_test(unsupported_iq)]}. + [single_test(feature_enabled), + single_test(check_identity), + single_test(send_non_existent), + single_test(view_non_existent), + single_test(remove_non_existent), + single_test(view_non_integer), + single_test(remove_non_integer), + single_test(malformed_iq), + single_test(wrong_user), + single_test(unsupported_iq)]}. + feature_enabled(Config) -> Features = get_features(Config), @@ -54,21 +68,33 @@ feature_enabled(Config) -> true = lists:member(?NS_FLEX_OFFLINE, Features), disconnect(Config). + check_identity(Config) -> - #iq{type = result, - sub_els = [#disco_info{ - node = ?NS_FLEX_OFFLINE, - identities = Ids}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_info{ - node = ?NS_FLEX_OFFLINE}]}), + #iq{ + type = result, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE, + identities = Ids + }] + } = + send_recv(Config, + #iq{ + type = get, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE + }] + }), true = lists:any( - fun(#identity{category = <<"automation">>, - type = <<"message-list">>}) -> true; - (_) -> false - end, Ids), + fun(#identity{ + category = <<"automation">>, + type = <<"message-list">> + }) -> true; + (_) -> false + end, + Ids), disconnect(Config). + send_non_existent(Config) -> Server = ?config(server, Config), To = jid:make(<<"non-existent">>, Server), @@ -76,83 +102,102 @@ send_non_existent(Config) -> #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err), disconnect(Config). + view_non_existent(Config) -> #stanza_error{reason = 'item-not-found'} = view(Config, [rand_string()], false), disconnect(Config). + remove_non_existent(Config) -> ok = remove(Config, [rand_string()]), disconnect(Config). + view_non_integer(Config) -> #stanza_error{reason = 'item-not-found'} = view(Config, [<<"foo">>], false), disconnect(Config). + remove_non_integer(Config) -> #stanza_error{reason = 'item-not-found'} = remove(Config, [<<"foo">>]), disconnect(Config). + malformed_iq(Config) -> Item = #offline_item{node = rand_string()}, - Range = [{Type, SubEl} || Type <- [set, get], - SubEl <- [#offline{items = [], _ = false}, - #offline{items = [Item], _ = true}]] - ++ [{set, #offline{items = [], fetch = true, purge = false}}, - {set, #offline{items = [Item], fetch = true, purge = false}}, - {get, #offline{items = [], fetch = false, purge = true}}, - {get, #offline{items = [Item], fetch = false, purge = true}}], + Range = [ {Type, SubEl} || Type <- [set, get], + SubEl <- [#offline{items = [], _ = false}, + #offline{items = [Item], _ = true}] ] ++ + [{set, #offline{items = [], fetch = true, purge = false}}, + {set, #offline{items = [Item], fetch = true, purge = false}}, + {get, #offline{items = [], fetch = false, purge = true}}, + {get, #offline{items = [Item], fetch = false, purge = true}}], lists:foreach( fun({Type, SubEl}) -> - #iq{type = error} = Err = - send_recv(Config, #iq{type = Type, sub_els = [SubEl]}), - #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err) - end, Range), + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, sub_els = [SubEl]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err) + end, + Range), disconnect(Config). + wrong_user(Config) -> Server = ?config(server, Config), To = jid:make(<<"foo">>, Server), Item = #offline_item{node = rand_string()}, - Range = [{Type, Items, Purge, Fetch} || - Type <- [set, get], - Items <- [[], [Item]], - Purge <- [false, true], - Fetch <- [false, true]], + Range = [ {Type, Items, Purge, Fetch} + || Type <- [set, get], + Items <- [[], [Item]], + Purge <- [false, true], + Fetch <- [false, true] ], lists:foreach( fun({Type, Items, Purge, Fetch}) -> - #iq{type = error} = Err = - send_recv(Config, #iq{type = Type, to = To, - sub_els = [#offline{items = Items, - purge = Purge, - fetch = Fetch}]}), - #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err) - end, Range), + #iq{type = error} = Err = + send_recv(Config, + #iq{ + type = Type, + to = To, + sub_els = [#offline{ + items = Items, + purge = Purge, + fetch = Fetch + }] + }), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err) + end, + Range), disconnect(Config). + unsupported_iq(Config) -> Item = #offline_item{node = rand_string()}, lists:foreach( fun(Type) -> - #iq{type = error} = Err = - send_recv(Config, #iq{type = Type, sub_els = [Item]}), - #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) - end, [set, get]), + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, sub_els = [Item]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, + [set, get]), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases(_DB) -> {offline_master_slave, [sequence], - [master_slave_test(flex), - master_slave_test(send_all), - master_slave_test(from_mam), - master_slave_test(mucsub_mam)]}. + [master_slave_test(flex), + master_slave_test(send_all), + master_slave_test(from_mam), + master_slave_test(mucsub_mam)]}. + flex_master(Config) -> send_messages(Config, 5), disconnect(Config). + flex_slave(Config) -> wait_for_master(Config), peer_down = get_event(Config), @@ -168,20 +213,23 @@ flex_slave(Config) -> ct:comment("Deleting 2nd and 4th message"), ok = remove(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), ct:comment("Checking if messages were deleted"), - [1, 3, 5] = view(Config, [lists:nth(1, Nodes), - lists:nth(3, Nodes), - lists:nth(5, Nodes)]), + [1, 3, 5] = view(Config, + [lists:nth(1, Nodes), + lists:nth(3, Nodes), + lists:nth(5, Nodes)]), ct:comment("Purging everything left"), ok = purge(Config), ct:comment("Checking if there are no offline messages"), 0 = get_number(Config), clean(disconnect(Config)). + from_mam_master(Config) -> C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), C3 = send_all_master(C2), lists:keydelete(mam_enabled, 1, C3). + from_mam_slave(Config) -> Server = ?config(server, Config), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), @@ -192,6 +240,7 @@ from_mam_slave(Config) -> C4 = lists:keydelete(mam_enabled, 1, C3), mam_tests:clean(C4). + mucsub_mam_master(Config) -> Room = suite:muc_room_jid(Config), Peer = ?config(peer, Config), @@ -208,18 +257,29 @@ mucsub_mam_master(Config) -> [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), ct:comment("Subscribing peer to room"), - ?send_recv(#iq{to = Room, type = set, sub_els = [ - #muc_subscribe{jid = Peer, nick = <<"peer">>, - events = [?NS_MUCSUB_NODES_MESSAGES]} - ]}, #iq{type = result}), + ?send_recv(#iq{ + to = Room, + type = set, + sub_els = [#muc_subscribe{ + jid = Peer, + nick = <<"peer">>, + events = [?NS_MUCSUB_NODES_MESSAGES] + }] + }, + #iq{type = result}), ?match(#message{type = groupchat}, - send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"1">>)})), + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"1">>)})), ?match(#message{type = groupchat}, - send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"2">>), - sub_els = [#hint{type = 'no-store'}]})), + send_recv(Config, + #message{ + type = groupchat, + to = Room, + body = xmpp:mk_text(<<"2">>), + sub_els = [#hint{type = 'no-store'}] + })), ?match(#message{type = groupchat}, - send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"3">>)})), + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"3">>)})), ct:comment("Cleaning up"), suite:put_event(Config, ready), @@ -227,6 +287,7 @@ mucsub_mam_master(Config) -> muc_tests:leave(Config), mam_tests:clean(clean(disconnect(Config))). + mucsub_mam_slave(Config) -> Server = ?config(server, Config), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), @@ -246,59 +307,73 @@ mucsub_mam_slave(Config) -> ?match(#presence{}, suite:send_recv(Config, #presence{})), lists:foreach( - fun(N) -> - Body = xmpp:mk_text(integer_to_binary(N)), - Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), - PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ - #ps_item{} = PS - ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), - ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) - end, [1, 3]), + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), + PS = ?match(#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_MESSAGES, + items = [#ps_item{} = PS] + } + }, + xmpp:get_subtag(Msg, #ps_event{}), + PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, + [1, 3]), % Unsubscribe yourself - ?send_recv(#iq{to = Room, type = set, sub_els = [ - #muc_unsubscribe{} - ]}, #iq{type = result}), + ?send_recv(#iq{ + to = Room, + type = set, + sub_els = [#muc_unsubscribe{}] + }, + #iq{type = result}), suite:put_event(Config, ready), mam_tests:clean(clean(disconnect(Config))), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}). + send_all_master(Config) -> wait_for_slave(Config), Peer = ?config(peer, Config), BarePeer = jid:remove_resource(Peer), {Deliver, Errors} = message_iterator(Config), N = lists:foldl( - fun(#message{type = error} = Msg, Acc) -> - send(Config, Msg#message{to = BarePeer}), - Acc; - (Msg, Acc) -> - I = send(Config, Msg#message{to = BarePeer}), - case {xmpp:get_subtag(Msg, #offline{}), xmpp:get_subtag(Msg, #xevent{})} of - {#offline{}, _} -> - ok; - {_, #xevent{offline = true, id = undefined}} -> - ct:comment("Receiving event-reply for:~n~s", - [xmpp:pp(Msg)]), - #message{} = Reply = recv_message(Config), - #xevent{id = I} = xmpp:get_subtag(Reply, #xevent{}); - _ -> - ok - end, - Acc + 1 - end, 0, Deliver), + fun(#message{type = error} = Msg, Acc) -> + send(Config, Msg#message{to = BarePeer}), + Acc; + (Msg, Acc) -> + I = send(Config, Msg#message{to = BarePeer}), + case {xmpp:get_subtag(Msg, #offline{}), xmpp:get_subtag(Msg, #xevent{})} of + {#offline{}, _} -> + ok; + {_, #xevent{offline = true, id = undefined}} -> + ct:comment("Receiving event-reply for:~n~s", + [xmpp:pp(Msg)]), + #message{} = Reply = recv_message(Config), + #xevent{id = I} = xmpp:get_subtag(Reply, #xevent{}); + _ -> + ok + end, + Acc + 1 + end, + 0, + Deliver), lists:foreach( fun(#message{type = headline} = Msg) -> - send(Config, Msg#message{to = BarePeer}); + send(Config, Msg#message{to = BarePeer}); (Msg) -> - #message{type = error} = Err = - send_recv(Config, Msg#message{to = BarePeer}), - #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) - end, Errors), + #message{type = error} = Err = + send_recv(Config, Msg#message{to = BarePeer}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, + Errors), ok = wait_for_complete(Config, N), disconnect(Config). + send_all_slave(Config) -> ServerJID = server_jid(Config), Peer = ?config(peer, Config), @@ -310,54 +385,67 @@ send_all_slave(Config) -> {Deliver, _Errors} = message_iterator(Config), lists:foreach( fun(#message{type = error}) -> - ok; - (#message{type = Type, body = Body, subject = Subject} = Msg) -> - ct:comment("Receiving message:~n~s", [xmpp:pp(Msg)]), - #message{from = Peer, - type = Type, - body = Body, - subject = Subject} = RecvMsg = recv_message(Config), - ct:comment("Checking if delay tag is correctly set"), - #delay{from = ServerJID} = xmpp:get_subtag(RecvMsg, #delay{}) - end, Deliver), + ok; + (#message{type = Type, body = Body, subject = Subject} = Msg) -> + ct:comment("Receiving message:~n~s", [xmpp:pp(Msg)]), + #message{ + from = Peer, + type = Type, + body = Body, + subject = Subject + } = RecvMsg = recv_message(Config), + ct:comment("Checking if delay tag is correctly set"), + #delay{from = ServerJID} = xmpp:get_subtag(RecvMsg, #delay{}) + end, + Deliver), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("offline_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("offline_" ++ atom_to_list(T)), [parallel], + {list_to_atom("offline_" ++ atom_to_list(T)), + [parallel], [list_to_atom("offline_" ++ atom_to_list(T) ++ "_master"), list_to_atom("offline_" ++ atom_to_list(T) ++ "_slave")]}. + clean(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), mod_offline:remove_user(U, S), Config. + send_messages(Config, Num) -> send_messages(Config, Num, normal, []). + send_messages(Config, Num, Type, SubEls) -> wait_for_slave(Config), Peer = ?config(peer, Config), BarePeer = jid:remove_resource(Peer), lists:foreach( fun(I) -> - Body = integer_to_binary(I), - send(Config, - #message{to = BarePeer, - type = Type, - body = [#text{data = Body}], - subject = [#text{data = <<"subject">>}], - sub_els = SubEls}) - end, lists:seq(1, Num)), + Body = integer_to_binary(I), + send(Config, + #message{ + to = BarePeer, + type = Type, + body = [#text{data = Body}], + subject = [#text{data = <<"subject">>}], + sub_els = SubEls + }) + end, + lists:seq(1, Num)), ct:comment("Waiting for all messages to be delivered to offline spool"), ok = wait_for_complete(Config, Num). + recv_messages(Config, Num) -> wait_for_master(Config), peer_down = get_event(Config), @@ -365,127 +453,169 @@ recv_messages(Config, Num) -> #presence{} = send_recv(Config, #presence{}), lists:foreach( fun(I) -> - Text = integer_to_binary(I), - #message{sub_els = SubEls, - from = Peer, - body = [#text{data = Text}], - subject = [#text{data = <<"subject">>}]} = - recv_message(Config), - true = lists:keymember(delay, 1, SubEls) - end, lists:seq(1, Num)), + Text = integer_to_binary(I), + #message{ + sub_els = SubEls, + from = Peer, + body = [#text{data = Text}], + subject = [#text{data = <<"subject">>}] + } = + recv_message(Config), + true = lists:keymember(delay, 1, SubEls) + end, + lists:seq(1, Num)), clean(disconnect(Config)). + get_number(Config) -> ct:comment("Getting offline message number"), - #iq{type = result, - sub_els = [#disco_info{ - node = ?NS_FLEX_OFFLINE, - xdata = [X]}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_info{ - node = ?NS_FLEX_OFFLINE}]}), + #iq{ + type = result, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE, + xdata = [X] + }] + } = + send_recv(Config, + #iq{ + type = get, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE + }] + }), Form = flex_offline:decode(X#xdata.fields), proplists:get_value(number_of_messages, Form). + get_nodes(Config) -> MyJID = my_jid(Config), MyBareJID = jid:remove_resource(MyJID), Peer = ?config(peer, Config), Peer_s = jid:encode(Peer), ct:comment("Getting headers"), - #iq{type = result, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE, - items = DiscoItems}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE}]}), + #iq{ + type = result, + sub_els = [#disco_items{ + node = ?NS_FLEX_OFFLINE, + items = DiscoItems + }] + } = + send_recv(Config, + #iq{ + type = get, + sub_els = [#disco_items{ + node = ?NS_FLEX_OFFLINE + }] + }), ct:comment("Checking if headers are correct"), lists:sort( lists:map( - fun(#disco_item{jid = J, name = P, node = N}) - when (J == MyBareJID) and (P == Peer_s) -> - N - end, DiscoItems)). + fun(#disco_item{jid = J, name = P, node = N}) + when (J == MyBareJID) and (P == Peer_s) -> + N + end, + DiscoItems)). + fetch(Config, Range) -> ID = send(Config, #iq{type = get, sub_els = [#offline{fetch = true}]}), Nodes = lists:map( - fun(I) -> - Text = integer_to_binary(I), - #message{body = Body, sub_els = SubEls} = recv(Config), - [#text{data = Text}] = Body, - #offline{items = [#offline_item{node = Node}]} = - lists:keyfind(offline, 1, SubEls), - #delay{} = lists:keyfind(delay, 1, SubEls), - Node - end, Range), + fun(I) -> + Text = integer_to_binary(I), + #message{body = Body, sub_els = SubEls} = recv(Config), + [#text{data = Text}] = Body, + #offline{items = [#offline_item{node = Node}]} = + lists:keyfind(offline, 1, SubEls), + #delay{} = lists:keyfind(delay, 1, SubEls), + Node + end, + Range), #iq{id = ID, type = result, sub_els = []} = recv(Config), Nodes. + view(Config, Nodes) -> view(Config, Nodes, true). + view(Config, Nodes, NeedReceive) -> Items = lists:map( - fun(Node) -> - #offline_item{action = view, node = Node} - end, Nodes), + fun(Node) -> + #offline_item{action = view, node = Node} + end, + Nodes), I = send(Config, - #iq{type = get, sub_els = [#offline{items = Items}]}), - Range = if NeedReceive -> - lists:map( - fun(Node) -> - #message{body = [#text{data = Text}], - sub_els = SubEls} = recv(Config), - #offline{items = [#offline_item{node = Node}]} = - lists:keyfind(offline, 1, SubEls), - binary_to_integer(Text) - end, Nodes); - true -> - [] - end, + #iq{type = get, sub_els = [#offline{items = Items}]}), + Range = if + NeedReceive -> + lists:map( + fun(Node) -> + #message{ + body = [#text{data = Text}], + sub_els = SubEls + } = recv(Config), + #offline{items = [#offline_item{node = Node}]} = + lists:keyfind(offline, 1, SubEls), + binary_to_integer(Text) + end, + Nodes); + true -> + [] + end, case recv(Config) of - #iq{id = I, type = result, sub_els = []} -> Range; - #iq{id = I, type = error} = Err -> xmpp:get_error(Err) + #iq{id = I, type = result, sub_els = []} -> Range; + #iq{id = I, type = error} = Err -> xmpp:get_error(Err) end. + remove(Config, Nodes) -> Items = lists:map( - fun(Node) -> - #offline_item{action = remove, node = Node} - end, Nodes), - case send_recv(Config, #iq{type = set, - sub_els = [#offline{items = Items}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + fun(Node) -> + #offline_item{action = remove, node = Node} + end, + Nodes), + case send_recv(Config, + #iq{ + type = set, + sub_els = [#offline{items = Items}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + purge(Config) -> - case send_recv(Config, #iq{type = set, - sub_els = [#offline{purge = true}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + case send_recv(Config, + #iq{ + type = set, + sub_els = [#offline{purge = true}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + wait_for_complete(_Config, 0) -> ok; wait_for_complete(Config, N) -> {U, S, _} = jid:tolower(?config(peer, Config)), lists:foldl( fun(_Time, ok) -> - ok; - (Time, Acc) -> - timer:sleep(Time), - case mod_offline:count_offline_messages(U, S) of - N -> ok; - _ -> Acc - end - end, error, [0, 100, 200, 2000, 5000, 10000]). + ok; + (Time, Acc) -> + timer:sleep(Time), + case mod_offline:count_offline_messages(U, S) of + N -> ok; + _ -> Acc + end + end, + error, + [0, 100, 200, 2000, 5000, 10000]). + xevent_stored(#message{body = [], subject = []}, _) -> false; xevent_stored(#message{type = T}, _) when T /= chat, T /= normal -> false; @@ -495,43 +625,46 @@ xevent_stored(_, #xevent{delivered = true}) -> true; xevent_stored(_, #xevent{displayed = true}) -> true; xevent_stored(_, _) -> false. + message_iterator(Config) -> ServerJID = server_jid(Config), ChatStates = [[#chatstate{type = composing}]], Offline = [[#offline{}]], - Hints = [[#hint{type = T}] || T <- [store, 'no-store']], - XEvent = [[#xevent{id = ID, offline = OfflineFlag}] - || ID <- [undefined, rand_string()], - OfflineFlag <- [false, true]], + Hints = [ [#hint{type = T}] || T <- [store, 'no-store'] ], + XEvent = [ [#xevent{id = ID, offline = OfflineFlag}] + || ID <- [undefined, rand_string()], + OfflineFlag <- [false, true] ], Delay = [[#delay{stamp = p1_time_compat:timestamp(), from = ServerJID}]], - AllEls = [Els1 ++ Els2 || Els1 <- [[]] ++ ChatStates ++ Delay ++ Hints ++ Offline, - Els2 <- [[]] ++ XEvent], - All = [#message{type = Type, body = Body, subject = Subject, sub_els = Els} - || %%Type <- [chat], - Type <- [error, chat, normal, groupchat, headline], - Body <- [[], xmpp:mk_text(<<"body">>)], - Subject <- [[], xmpp:mk_text(<<"subject">>)], - Els <- AllEls], + AllEls = [ Els1 ++ Els2 || Els1 <- [[]] ++ ChatStates ++ Delay ++ Hints ++ Offline, + Els2 <- [[]] ++ XEvent ], + All = [ #message{type = Type, body = Body, subject = Subject, sub_els = Els} + || %%Type <- [chat], + Type <- [error, chat, normal, groupchat, headline], + Body <- [[], xmpp:mk_text(<<"body">>)], + Subject <- [[], xmpp:mk_text(<<"subject">>)], + Els <- AllEls ], MamEnabled = ?config(mam_enabled, Config) == true, lists:partition( fun(#message{type = error}) -> true; - (#message{type = groupchat}) -> false; - (#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true; - (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; - (#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false; - (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled -> - xevent_stored(Msg#message{body = body, type = chat}, Event); - (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled -> - xevent_stored(Msg, Event); - (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled -> - xevent_stored(Msg, Event); - (#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; - (#message{sub_els = [#hint{type = store}|_]}) -> true; - (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; - (#message{body = [], subject = []}) -> false; - (#message{type = Type}) -> (Type == chat) or (Type == normal); - (_) -> false - end, All). + (#message{type = groupchat}) -> false; + (#message{sub_els = [#hint{type = store} | _]}) when MamEnabled -> true; + (#message{sub_els = [#hint{type = 'no-store'} | _]}) -> false; + (#message{sub_els = [#offline{} | _]}) when not MamEnabled -> false; + (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled -> + xevent_stored(Msg#message{body = body, type = chat}, Event); + (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled -> + xevent_stored(Msg, Event); + (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled -> + xevent_stored(Msg, Event); + (#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; + (#message{sub_els = [#hint{type = store} | _]}) -> true; + (#message{sub_els = [#hint{type = 'no-store'} | _]}) -> false; + (#message{body = [], subject = []}) -> false; + (#message{type = Type}) -> (Type == chat) or (Type == normal); + (_) -> false + end, + All). + rand_string() -> - integer_to_binary(p1_rand:uniform((1 bsl 31)-1)). + integer_to_binary(p1_rand:uniform((1 bsl 31) - 1)). diff --git a/test/privacy_tests.erl b/test/privacy_tests.erl index 51782dcf4..277b899f5 100644 --- a/test/privacy_tests.erl +++ b/test/privacy_tests.erl @@ -25,13 +25,26 @@ %% API -compile(export_all). --import(suite, [disconnect/1, send_recv/2, get_event/1, put_event/2, - recv_iq/1, recv_presence/1, recv_message/1, recv/1, - send/2, my_jid/1, server_jid/1, get_features/1, - set_roster/3, del_roster/1, get_roster/1]). +-import(suite, + [disconnect/1, + send_recv/2, + get_event/1, + put_event/2, + recv_iq/1, + recv_presence/1, + recv_message/1, + recv/1, + send/2, + my_jid/1, + server_jid/1, + get_features/1, + set_roster/3, + del_roster/1, + get_roster/1]). -include("suite.hrl"). -include("mod_roster.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -40,26 +53,27 @@ %%%=================================================================== single_cases() -> {privacy_single, [sequence], - [single_test(feature_enabled), - single_test(set_get_list), - single_test(get_list_non_existent), - single_test(get_empty_lists), - single_test(set_default), - single_test(del_default), - single_test(set_default_non_existent), - single_test(set_active), - single_test(del_active), - single_test(set_active_non_existent), - single_test(remove_list), - single_test(remove_default_list), - single_test(remove_active_list), - single_test(remove_list_non_existent), - single_test(allow_local_server), - single_test(malformed_iq_query), - single_test(malformed_get), - single_test(malformed_set), - single_test(malformed_type_value), - single_test(set_get_block)]}. + [single_test(feature_enabled), + single_test(set_get_list), + single_test(get_list_non_existent), + single_test(get_empty_lists), + single_test(set_default), + single_test(del_default), + single_test(set_default_non_existent), + single_test(set_active), + single_test(del_active), + single_test(set_active_non_existent), + single_test(remove_list), + single_test(remove_default_list), + single_test(remove_active_list), + single_test(remove_list_non_existent), + single_test(allow_local_server), + single_test(malformed_iq_query), + single_test(malformed_get), + single_test(malformed_set), + single_test(malformed_type_value), + single_test(set_get_block)]}. + feature_enabled(Config) -> Features = get_features(Config), @@ -67,43 +81,74 @@ feature_enabled(Config) -> true = lists:member(?NS_BLOCKING, Features), disconnect(Config). + set_get_list(Config) -> ListName = <<"set-get-list">>, - Items = [#privacy_item{order = 0, action = deny, - type = jid, value = <<"user@jabber.org">>, - iq = true}, - #privacy_item{order = 1, action = allow, - type = group, value = <<"group">>, - message = true}, - #privacy_item{order = 2, action = allow, - type = subscription, value = <<"both">>, - presence_in = true}, - #privacy_item{order = 3, action = deny, - type = subscription, value = <<"from">>, - presence_out = true}, - #privacy_item{order = 4, action = deny, - type = subscription, value = <<"to">>, - iq = true, message = true}, - #privacy_item{order = 5, action = deny, - type = subscription, value = <<"none">>, - _ = true}, - #privacy_item{order = 6, action = deny}], + Items = [#privacy_item{ + order = 0, + action = deny, + type = jid, + value = <<"user@jabber.org">>, + iq = true + }, + #privacy_item{ + order = 1, + action = allow, + type = group, + value = <<"group">>, + message = true + }, + #privacy_item{ + order = 2, + action = allow, + type = subscription, + value = <<"both">>, + presence_in = true + }, + #privacy_item{ + order = 3, + action = deny, + type = subscription, + value = <<"from">>, + presence_out = true + }, + #privacy_item{ + order = 4, + action = deny, + type = subscription, + value = <<"to">>, + iq = true, + message = true + }, + #privacy_item{ + order = 5, + action = deny, + type = subscription, + value = <<"none">>, + _ = true + }, + #privacy_item{order = 6, action = deny}], ok = set_items(Config, ListName, Items), #privacy_list{name = ListName, items = Items1} = get_list(Config, ListName), Items = lists:keysort(#privacy_item.order, Items1), del_privacy(disconnect(Config)). + get_list_non_existent(Config) -> ListName = <<"get-list-non-existent">>, #stanza_error{reason = 'item-not-found'} = get_list(Config, ListName), disconnect(Config). + get_empty_lists(Config) -> - #privacy_query{default = none, - active = none, - lists = []} = get_lists(Config), + #privacy_query{ + default = none, + active = none, + lists = [] + } = get_lists(Config), disconnect(Config). + set_default(Config) -> ListName = <<"set-default">>, Item = #privacy_item{order = 0, action = deny}, @@ -112,6 +157,7 @@ set_default(Config) -> #privacy_query{default = ListName} = get_lists(Config), del_privacy(disconnect(Config)). + del_default(Config) -> ListName = <<"del-default">>, Item = #privacy_item{order = 0, action = deny}, @@ -122,11 +168,13 @@ del_default(Config) -> #privacy_query{default = none} = get_lists(Config), del_privacy(disconnect(Config)). + set_default_non_existent(Config) -> ListName = <<"set-default-non-existent">>, #stanza_error{reason = 'item-not-found'} = set_default(Config, ListName), disconnect(Config). + set_active(Config) -> ListName = <<"set-active">>, Item = #privacy_item{order = 0, action = deny}, @@ -135,6 +183,7 @@ set_active(Config) -> #privacy_query{active = ListName} = get_lists(Config), del_privacy(disconnect(Config)). + del_active(Config) -> ListName = <<"del-active">>, Item = #privacy_item{order = 0, action = deny}, @@ -145,11 +194,13 @@ del_active(Config) -> #privacy_query{active = none} = get_lists(Config), del_privacy(disconnect(Config)). + set_active_non_existent(Config) -> ListName = <<"set-active-non-existent">>, #stanza_error{reason = 'item-not-found'} = set_active(Config, ListName), disconnect(Config). + remove_list(Config) -> ListName = <<"remove-list">>, Item = #privacy_item{order = 0, action = deny}, @@ -158,6 +209,7 @@ remove_list(Config) -> #privacy_query{lists = []} = get_lists(Config), del_privacy(disconnect(Config)). + remove_active_list(Config) -> ListName = <<"remove-active-list">>, Item = #privacy_item{order = 0, action = deny}, @@ -166,6 +218,7 @@ remove_active_list(Config) -> #stanza_error{reason = 'conflict'} = del_list(Config, ListName), del_privacy(disconnect(Config)). + remove_default_list(Config) -> ListName = <<"remove-default-list">>, Item = #privacy_item{order = 0, action = deny}, @@ -174,11 +227,13 @@ remove_default_list(Config) -> #stanza_error{reason = 'conflict'} = del_list(Config, ListName), del_privacy(disconnect(Config)). + remove_list_non_existent(Config) -> ListName = <<"remove-list-non-existent">>, #stanza_error{reason = 'item-not-found'} = del_list(Config, ListName), disconnect(Config). + allow_local_server(Config) -> ListName = <<"allow-local-server">>, Item = #privacy_item{order = 0, action = deny}, @@ -191,59 +246,80 @@ allow_local_server(Config) -> send_stanzas_to_server_resource(Config), del_privacy(disconnect(Config)). + malformed_iq_query(Config) -> lists:foreach( fun(Type) -> - #iq{type = error} = - send_recv(Config, - #iq{type = Type, - sub_els = [#privacy_list{name = <<"foo">>}]}) - end, [get, set]), + #iq{type = error} = + send_recv(Config, + #iq{ + type = Type, + sub_els = [#privacy_list{name = <<"foo">>}] + }) + end, + [get, set]), disconnect(Config). + malformed_get(Config) -> JID = jid:make(p1_rand:get_string()), Item = #block_item{jid = JID}, lists:foreach( fun(SubEl) -> - #iq{type = error} = - send_recv(Config, #iq{type = get, sub_els = [SubEl]}) - end, [#privacy_query{active = none}, - #privacy_query{default = none}, - #privacy_query{lists = [#privacy_list{name = <<"1">>}, - #privacy_list{name = <<"2">>}]}, - #block{items = [Item]}, #unblock{items = [Item]}, - #block{}, #unblock{}]), + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, + [#privacy_query{active = none}, + #privacy_query{default = none}, + #privacy_query{ + lists = [#privacy_list{name = <<"1">>}, + #privacy_list{name = <<"2">>}] + }, + #block{items = [Item]}, + #unblock{items = [Item]}, + #block{}, + #unblock{}]), disconnect(Config). + malformed_set(Config) -> lists:foreach( fun(SubEl) -> - #iq{type = error} = - send_recv(Config, #iq{type = set, sub_els = [SubEl]}) - end, [#privacy_query{active = none, default = none}, - #privacy_query{lists = [#privacy_list{name = <<"1">>}, - #privacy_list{name = <<"2">>}]}, - #block{}, - #block_list{}, - #block_list{ - items = [#block_item{ - jid = jid:make(p1_rand:get_string())}]}]), + #iq{type = error} = + send_recv(Config, #iq{type = set, sub_els = [SubEl]}) + end, + [#privacy_query{active = none, default = none}, + #privacy_query{ + lists = [#privacy_list{name = <<"1">>}, + #privacy_list{name = <<"2">>}] + }, + #block{}, + #block_list{}, + #block_list{ + items = [#block_item{ + jid = jid:make(p1_rand:get_string()) + }] + }]), disconnect(Config). + malformed_type_value(Config) -> Item = #privacy_item{order = 0, action = deny}, #stanza_error{reason = 'bad-request'} = - set_items(Config, <<"malformed-jid">>, - [Item#privacy_item{type = jid, value = <<"@bad">>}]), + set_items(Config, + <<"malformed-jid">>, + [Item#privacy_item{type = jid, value = <<"@bad">>}]), #stanza_error{reason = 'bad-request'} = - set_items(Config, <<"malformed-group">>, - [Item#privacy_item{type = group, value = <<"">>}]), + set_items(Config, + <<"malformed-group">>, + [Item#privacy_item{type = group, value = <<"">>}]), #stanza_error{reason = 'bad-request'} = - set_items(Config, <<"malformed-subscription">>, - [Item#privacy_item{type = subscription, value = <<"bad">>}]), + set_items(Config, + <<"malformed-subscription">>, + [Item#privacy_item{type = subscription, value = <<"bad">>}]), disconnect(Config). + set_get_block(Config) -> J1 = jid:make(p1_rand:get_string(), p1_rand:get_string()), J2 = jid:make(p1_rand:get_string(), p1_rand:get_string()), @@ -254,188 +330,222 @@ set_get_block(Config) -> [] = get_block(Config), del_privacy(disconnect(Config)). + %%%=================================================================== %%% Master-slave cases %%%=================================================================== master_slave_cases() -> {privacy_master_slave, [sequence], - [master_slave_test(deny_bare_jid), - master_slave_test(deny_full_jid), - master_slave_test(deny_server_bare_jid), - master_slave_test(deny_server_full_jid), - master_slave_test(deny_group), - master_slave_test(deny_sub_both), - master_slave_test(deny_sub_from), - master_slave_test(deny_sub_to), - master_slave_test(deny_sub_none), - master_slave_test(deny_all), - master_slave_test(deny_offline), - master_slave_test(block), - master_slave_test(unblock), - master_slave_test(unblock_all)]}. + [master_slave_test(deny_bare_jid), + master_slave_test(deny_full_jid), + master_slave_test(deny_server_bare_jid), + master_slave_test(deny_server_full_jid), + master_slave_test(deny_group), + master_slave_test(deny_sub_both), + master_slave_test(deny_sub_from), + master_slave_test(deny_sub_to), + master_slave_test(deny_sub_none), + master_slave_test(deny_all), + master_slave_test(deny_offline), + master_slave_test(block), + master_slave_test(unblock), + master_slave_test(unblock_all)]}. + deny_bare_jid_master(Config) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), deny_master(Config, {jid, jid:encode(PeerBareJID)}). + deny_bare_jid_slave(Config) -> deny_slave(Config). + deny_full_jid_master(Config) -> PeerJID = ?config(peer, Config), deny_master(Config, {jid, jid:encode(PeerJID)}). + deny_full_jid_slave(Config) -> deny_slave(Config). + deny_server_bare_jid_master(Config) -> {_, Server, _} = jid:tolower(?config(peer, Config)), deny_master(Config, {jid, Server}). + deny_server_bare_jid_slave(Config) -> deny_slave(Config). + deny_server_full_jid_master(Config) -> {_, Server, Resource} = jid:tolower(?config(peer, Config)), deny_master(Config, {jid, jid:encode({<<"">>, Server, Resource})}). + deny_server_full_jid_slave(Config) -> deny_slave(Config). + deny_group_master(Config) -> Group = p1_rand:get_string(), deny_master(Config, {group, Group}). + deny_group_slave(Config) -> deny_slave(Config). + deny_sub_both_master(Config) -> deny_master(Config, {subscription, <<"both">>}). + deny_sub_both_slave(Config) -> deny_slave(Config, 2). + deny_sub_from_master(Config) -> deny_master(Config, {subscription, <<"from">>}). + deny_sub_from_slave(Config) -> deny_slave(Config, 1). + deny_sub_to_master(Config) -> deny_master(Config, {subscription, <<"to">>}). + deny_sub_to_slave(Config) -> deny_slave(Config, 2). + deny_sub_none_master(Config) -> deny_master(Config, {subscription, <<"none">>}). + deny_sub_none_slave(Config) -> deny_slave(Config). + deny_all_master(Config) -> deny_master(Config, {undefined, <<"">>}). + deny_all_slave(Config) -> deny_slave(Config). + deny_master(Config, {Type, Value}) -> - Sub = if Type == subscription -> - erlang:binary_to_atom(Value, utf8); - true -> - both - end, - Groups = if Type == group -> [Value]; - true -> [] - end, + Sub = if + Type == subscription -> + erlang:binary_to_atom(Value, utf8); + true -> + both + end, + Groups = if + Type == group -> [Value]; + true -> [] + end, set_roster(Config, Sub, Groups), lists:foreach( fun(Opts) -> - ct:pal("Set list for ~s, ~s, ~w", [Type, Value, Opts]), - ListName = p1_rand:get_string(), - Item = #privacy_item{order = 0, - action = deny, - iq = proplists:get_bool(iq, Opts), - message = proplists:get_bool(message, Opts), - presence_in = proplists:get_bool(presence_in, Opts), - presence_out = proplists:get_bool(presence_out, Opts), - type = Type, - value = Value}, - ok = set_items(Config, ListName, [Item]), - ok = set_active(Config, ListName), - put_event(Config, Opts), - case is_presence_in_blocked(Opts) of - true -> ok; - false -> recv_presences(Config) - end, - case is_iq_in_blocked(Opts) of - true -> ok; - false -> recv_iqs(Config) - end, - case is_message_in_blocked(Opts) of - true -> ok; - false -> recv_messages(Config) - end, - ct:comment("Waiting for 'send' command from the slave"), - send = get_event(Config), - case is_presence_out_blocked(Opts) of - true -> check_presence_blocked(Config, 'not-acceptable'); - false -> ok - end, - case is_iq_out_blocked(Opts) of - true -> check_iq_blocked(Config, 'not-acceptable'); - false -> send_iqs(Config) - end, - case is_message_out_blocked(Opts) of - true -> check_message_blocked(Config, 'not-acceptable'); - false -> send_messages(Config) - end, - case is_other_blocked(Opts) of - true -> - check_other_blocked(Config, 'not-acceptable', Value); - false -> ok - end, - ct:comment("Waiting for slave to finish processing our stanzas"), - done = get_event(Config) + ct:pal("Set list for ~s, ~s, ~w", [Type, Value, Opts]), + ListName = p1_rand:get_string(), + Item = #privacy_item{ + order = 0, + action = deny, + iq = proplists:get_bool(iq, Opts), + message = proplists:get_bool(message, Opts), + presence_in = proplists:get_bool(presence_in, Opts), + presence_out = proplists:get_bool(presence_out, Opts), + type = Type, + value = Value + }, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + put_event(Config, Opts), + case is_presence_in_blocked(Opts) of + true -> ok; + false -> recv_presences(Config) + end, + case is_iq_in_blocked(Opts) of + true -> ok; + false -> recv_iqs(Config) + end, + case is_message_in_blocked(Opts) of + true -> ok; + false -> recv_messages(Config) + end, + ct:comment("Waiting for 'send' command from the slave"), + send = get_event(Config), + case is_presence_out_blocked(Opts) of + true -> check_presence_blocked(Config, 'not-acceptable'); + false -> ok + end, + case is_iq_out_blocked(Opts) of + true -> check_iq_blocked(Config, 'not-acceptable'); + false -> send_iqs(Config) + end, + case is_message_out_blocked(Opts) of + true -> check_message_blocked(Config, 'not-acceptable'); + false -> send_messages(Config) + end, + case is_other_blocked(Opts) of + true -> + check_other_blocked(Config, 'not-acceptable', Value); + false -> ok + end, + ct:comment("Waiting for slave to finish processing our stanzas"), + done = get_event(Config) end, - [[iq], [message], [presence_in], [presence_out], - [iq, message, presence_in, presence_out], []]), + [[iq], + [message], + [presence_in], + [presence_out], + [iq, message, presence_in, presence_out], + []]), put_event(Config, disconnect), clean_up(disconnect(Config)). + deny_slave(Config) -> deny_slave(Config, 0). + deny_slave(Config, RosterPushesCount) -> set_roster(Config, both, []), deny_slave(Config, RosterPushesCount, get_event(Config)). + deny_slave(Config, RosterPushesCount, disconnect) -> recv_roster_pushes(Config, RosterPushesCount), clean_up(disconnect(Config)); deny_slave(Config, RosterPushesCount, Opts) -> send_presences(Config), case is_iq_in_blocked(Opts) of - true -> check_iq_blocked(Config, 'service-unavailable'); - false -> send_iqs(Config) + true -> check_iq_blocked(Config, 'service-unavailable'); + false -> send_iqs(Config) end, case is_message_in_blocked(Opts) of - true -> check_message_blocked(Config, 'service-unavailable'); - false -> send_messages(Config) + true -> check_message_blocked(Config, 'service-unavailable'); + false -> send_messages(Config) end, put_event(Config, send), case is_iq_out_blocked(Opts) of - true -> ok; - false -> recv_iqs(Config) + true -> ok; + false -> recv_iqs(Config) end, case is_message_out_blocked(Opts) of - true -> ok; - false -> recv_messages(Config) + true -> ok; + false -> recv_messages(Config) end, put_event(Config, done), deny_slave(Config, RosterPushesCount, get_event(Config)). + deny_offline_master(Config) -> set_roster(Config, both, []), ListName = <<"deny-offline">>, @@ -448,6 +558,7 @@ deny_offline_master(Config) -> done = get_event(NewConfig), clean_up(NewConfig). + deny_offline_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from the master"), @@ -458,6 +569,7 @@ deny_offline_slave(Config) -> put_event(Config, done), clean_up(disconnect(Config)). + block_master(Config) -> PeerJID = ?config(peer, Config), set_roster(Config, both, []), @@ -474,6 +586,7 @@ block_master(Config) -> done = get_event(Config), clean_up(disconnect(Config)). + block_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from master"), @@ -484,6 +597,7 @@ block_slave(Config) -> put_event(Config, done), clean_up(disconnect(Config)). + unblock_master(Config) -> PeerJID = ?config(peer, Config), set_roster(Config, both, []), @@ -495,6 +609,7 @@ unblock_master(Config) -> recv_messages(Config), clean_up(disconnect(Config)). + unblock_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from master"), @@ -504,6 +619,7 @@ unblock_slave(Config) -> send_messages(Config), clean_up(disconnect(Config)). + unblock_all_master(Config) -> PeerJID = ?config(peer, Config), set_roster(Config, both, []), @@ -515,6 +631,7 @@ unblock_all_master(Config) -> recv_messages(Config), clean_up(disconnect(Config)). + unblock_all_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from master"), @@ -524,164 +641,211 @@ unblock_all_slave(Config) -> send_messages(Config), clean_up(disconnect(Config)). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("privacy_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("privacy_" ++ atom_to_list(T)), [parallel], + {list_to_atom("privacy_" ++ atom_to_list(T)), + [parallel], [list_to_atom("privacy_" ++ atom_to_list(T) ++ "_master"), list_to_atom("privacy_" ++ atom_to_list(T) ++ "_slave")]}. + set_items(Config, Name, Items) -> ct:comment("Setting privacy list ~s with items = ~p", [Name, Items]), case send_recv( - Config, - #iq{type = set, sub_els = [#privacy_query{ - lists = [#privacy_list{ - name = Name, - items = Items}]}]}) of - #iq{type = result, sub_els = []} -> - ct:comment("Receiving privacy list push"), - #iq{type = set, id = ID, - sub_els = [#privacy_query{lists = [#privacy_list{ - name = Name}]}]} = - recv_iq(Config), - send(Config, #iq{type = result, id = ID}), - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name, + items = Items + }] + }] + }) of + #iq{type = result, sub_els = []} -> + ct:comment("Receiving privacy list push"), + #iq{ + type = set, + id = ID, + sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name + }] + }] + } = + recv_iq(Config), + send(Config, #iq{type = result, id = ID}), + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + get_list(Config, Name) -> ct:comment("Requesting privacy list ~s", [Name]), case send_recv(Config, - #iq{type = get, - sub_els = [#privacy_query{ - lists = [#privacy_list{name = Name}]}]}) of - #iq{type = result, sub_els = [#privacy_query{lists = [List]}]} -> - List; - #iq{type = error} = Err -> - xmpp:get_error(Err) + #iq{ + type = get, + sub_els = [#privacy_query{ + lists = [#privacy_list{name = Name}] + }] + }) of + #iq{type = result, sub_els = [#privacy_query{lists = [List]}]} -> + List; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + get_lists(Config) -> ct:comment("Requesting privacy lists"), case send_recv(Config, #iq{type = get, sub_els = [#privacy_query{}]}) of - #iq{type = result, sub_els = [SubEl]} -> - SubEl; - #iq{type = error} = Err -> - xmpp:get_error(Err) + #iq{type = result, sub_els = [SubEl]} -> + SubEl; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + del_list(Config, Name) -> case send_recv( - Config, - #iq{type = set, sub_els = [#privacy_query{ - lists = [#privacy_list{ - name = Name}]}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name + }] + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + set_active(Config, Name) -> ct:comment("Setting active privacy list ~s", [Name]), case send_recv( - Config, - #iq{type = set, sub_els = [#privacy_query{active = Name}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{type = set, sub_els = [#privacy_query{active = Name}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + set_default(Config, Name) -> ct:comment("Setting default privacy list ~s", [Name]), case send_recv( - Config, - #iq{type = set, sub_els = [#privacy_query{default = Name}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{type = set, sub_els = [#privacy_query{default = Name}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + get_block(Config) -> case send_recv(Config, #iq{type = get, sub_els = [#block_list{}]}) of - #iq{type = result, sub_els = [#block_list{items = Items}]} -> - lists:sort([JID || #block_item{jid = JID} <- Items]); - #iq{type = error} = Err -> - xmpp:get_error(Err) + #iq{type = result, sub_els = [#block_list{items = Items}]} -> + lists:sort([ JID || #block_item{jid = JID} <- Items ]); + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + set_block(Config, JIDs) -> - Items = [#block_item{jid = JID} || JID <- JIDs], - case send_recv(Config, #iq{type = set, - sub_els = [#block{items = Items}]}) of - #iq{type = result, sub_els = []} -> - {#iq{id = I1, sub_els = [#block{items = Items1}]}, - #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = - ?recv2(#iq{type = set, sub_els = [#block{}]}, - #iq{type = set, sub_els = [#privacy_query{}]}), - send(Config, #iq{type = result, id = I1}), - send(Config, #iq{type = result, id = I2}), - ct:comment("Checking if all JIDs present in the push"), - true = lists:sort(Items) == lists:sort(Items1), - ct:comment("Getting name of the corresponding privacy list"), - [#privacy_list{name = Name}] = Lists, - {ok, Name}; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Items = [ #block_item{jid = JID} || JID <- JIDs ], + case send_recv(Config, + #iq{ + type = set, + sub_els = [#block{items = Items}] + }) of + #iq{type = result, sub_els = []} -> + {#iq{id = I1, sub_els = [#block{items = Items1}]}, + #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = + ?recv2(#iq{type = set, sub_els = [#block{}]}, + #iq{type = set, sub_els = [#privacy_query{}]}), + send(Config, #iq{type = result, id = I1}), + send(Config, #iq{type = result, id = I2}), + ct:comment("Checking if all JIDs present in the push"), + true = lists:sort(Items) == lists:sort(Items1), + ct:comment("Getting name of the corresponding privacy list"), + [#privacy_list{name = Name}] = Lists, + {ok, Name}; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + set_unblock(Config, JIDs) -> ct:comment("Unblocking ~p", [JIDs]), - Items = [#block_item{jid = JID} || JID <- JIDs], - case send_recv(Config, #iq{type = set, - sub_els = [#unblock{items = Items}]}) of - #iq{type = result, sub_els = []} -> - {#iq{id = I1, sub_els = [#unblock{items = Items1}]}, - #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = - ?recv2(#iq{type = set, sub_els = [#unblock{}]}, - #iq{type = set, sub_els = [#privacy_query{}]}), - send(Config, #iq{type = result, id = I1}), - send(Config, #iq{type = result, id = I2}), - ct:comment("Checking if all JIDs present in the push"), - true = lists:sort(Items) == lists:sort(Items1), - ct:comment("Getting name of the corresponding privacy list"), - [#privacy_list{name = Name}] = Lists, - {ok, Name}; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Items = [ #block_item{jid = JID} || JID <- JIDs ], + case send_recv(Config, + #iq{ + type = set, + sub_els = [#unblock{items = Items}] + }) of + #iq{type = result, sub_els = []} -> + {#iq{id = I1, sub_els = [#unblock{items = Items1}]}, + #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = + ?recv2(#iq{type = set, sub_els = [#unblock{}]}, + #iq{type = set, sub_els = [#privacy_query{}]}), + send(Config, #iq{type = result, id = I1}), + send(Config, #iq{type = result, id = I2}), + ct:comment("Checking if all JIDs present in the push"), + true = lists:sort(Items) == lists:sort(Items1), + ct:comment("Getting name of the corresponding privacy list"), + [#privacy_list{name = Name}] = Lists, + {ok, Name}; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + del_privacy(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), ct:comment("Removing all privacy data"), mod_privacy:remove_user(U, S), Config. + clean_up(Config) -> del_privacy(del_roster(Config)). + check_iq_blocked(Config, Reason) -> PeerJID = ?config(peer, Config), ct:comment("Checking if all IQs are blocked"), lists:foreach( fun(Type) -> - send(Config, #iq{type = Type, to = PeerJID}) - end, [error, result]), + send(Config, #iq{type = Type, to = PeerJID}) + end, + [error, result]), lists:foreach( fun(Type) -> - #iq{type = error} = Err = - send_recv(Config, #iq{type = Type, to = PeerJID, - sub_els = [#ping{}]}), - #stanza_error{reason = Reason} = xmpp:get_error(Err) - end, [set, get]). + #iq{type = error} = Err = + send_recv(Config, + #iq{ + type = Type, + to = PeerJID, + sub_els = [#ping{}] + }), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, + [set, get]). + check_message_blocked(Config, Reason) -> PeerJID = ?config(peer, Config), @@ -691,116 +855,137 @@ check_message_blocked(Config, Reason) -> %% screws this up. lists:foreach( fun(Type) -> - send(Config, #message{type = Type, to = PeerJID}) - end, [error]), + send(Config, #message{type = Type, to = PeerJID}) + end, + [error]), lists:foreach( fun(Type) -> - #message{type = error} = Err = - send_recv(Config, #message{type = Type, to = PeerJID}), - #stanza_error{reason = Reason} = xmpp:get_error(Err) - end, [chat, normal]). + #message{type = error} = Err = + send_recv(Config, #message{type = Type, to = PeerJID}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, + [chat, normal]). + check_presence_blocked(Config, Reason) -> PeerJID = ?config(peer, Config), ct:comment("Checking if all presences are blocked"), lists:foreach( fun(Type) -> - #presence{type = error} = Err = - send_recv(Config, #presence{type = Type, to = PeerJID}), - #stanza_error{reason = Reason} = xmpp:get_error(Err) - end, [available, unavailable]). + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = PeerJID}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, + [available, unavailable]). + recv_roster_pushes(_Config, 0) -> ok; recv_roster_pushes(Config, Count) -> receive - #iq{type = set, sub_els = [#roster_query{}]} -> - recv_roster_pushes(Config, Count - 1) + #iq{type = set, sub_els = [#roster_query{}]} -> + recv_roster_pushes(Config, Count - 1) end. + recv_err_and_roster_pushes(Config, Count) -> recv_roster_pushes(Config, Count), recv_presence(Config). + check_other_blocked(Config, Reason, Subscription) -> PeerJID = ?config(peer, Config), ct:comment("Checking if subscriptions and presence-errors are blocked"), send(Config, #presence{type = error, to = PeerJID}), {ErrorFor, PushFor} = case Subscription of - <<"both">> -> - {[subscribe, subscribed], - [unsubscribe, unsubscribed]}; - <<"from">> -> - {[subscribe, subscribed, unsubscribe], - [subscribe, unsubscribe, unsubscribed]}; - <<"to">> -> - {[unsubscribe], - [subscribed, unsubscribe, unsubscribed]}; - <<"none">> -> - {[subscribe, subscribed, unsubscribe, unsubscribed], - [subscribe, unsubscribe]}; - _ -> - {[subscribe, subscribed, unsubscribe, unsubscribed], - [unsubscribe, unsubscribed]} - end, + <<"both">> -> + {[subscribe, subscribed], + [unsubscribe, unsubscribed]}; + <<"from">> -> + {[subscribe, subscribed, unsubscribe], + [subscribe, unsubscribe, unsubscribed]}; + <<"to">> -> + {[unsubscribe], + [subscribed, unsubscribe, unsubscribed]}; + <<"none">> -> + {[subscribe, subscribed, unsubscribe, unsubscribed], + [subscribe, unsubscribe]}; + _ -> + {[subscribe, subscribed, unsubscribe, unsubscribed], + [unsubscribe, unsubscribed]} + end, lists:foreach( - fun(Type) -> - send(Config, #presence{type = Type, to = PeerJID}), - Count = case lists:member(Type, PushFor) of true -> 1; _ -> 0 end, - case lists:member(Type, ErrorFor) of - true -> - Err = recv_err_and_roster_pushes(Config, Count), - #stanza_error{reason = Reason} = xmpp:get_error(Err); - _ -> - recv_roster_pushes(Config, Count) - end - end, [subscribe, subscribed, unsubscribe, unsubscribed]). + fun(Type) -> + send(Config, #presence{type = Type, to = PeerJID}), + Count = case lists:member(Type, PushFor) of true -> 1; _ -> 0 end, + case lists:member(Type, ErrorFor) of + true -> + Err = recv_err_and_roster_pushes(Config, Count), + #stanza_error{reason = Reason} = xmpp:get_error(Err); + _ -> + recv_roster_pushes(Config, Count) + end + end, + [subscribe, subscribed, unsubscribe, unsubscribed]). + send_presences(Config) -> PeerJID = ?config(peer, Config), ct:comment("Sending all types of presences to the peer"), lists:foreach( fun(Type) -> - send(Config, #presence{type = Type, to = PeerJID}) - end, [available, unavailable]). + send(Config, #presence{type = Type, to = PeerJID}) + end, + [available, unavailable]). + send_iqs(Config) -> PeerJID = ?config(peer, Config), ct:comment("Sending all types of IQs to the peer"), lists:foreach( fun(Type) -> - send(Config, #iq{type = Type, to = PeerJID}) - end, [set, get, error, result]). + send(Config, #iq{type = Type, to = PeerJID}) + end, + [set, get, error, result]). + send_messages(Config) -> PeerJID = ?config(peer, Config), ct:comment("Sending all types of messages to the peer"), lists:foreach( fun(Type) -> - send(Config, #message{type = Type, to = PeerJID}) - end, [chat, error, groupchat, headline, normal]). + send(Config, #message{type = Type, to = PeerJID}) + end, + [chat, error, groupchat, headline, normal]). + recv_presences(Config) -> PeerJID = ?config(peer, Config), lists:foreach( fun(Type) -> - #presence{type = Type, from = PeerJID} = - recv_presence(Config) - end, [available, unavailable]). + #presence{type = Type, from = PeerJID} = + recv_presence(Config) + end, + [available, unavailable]). + recv_iqs(Config) -> PeerJID = ?config(peer, Config), lists:foreach( fun(Type) -> - #iq{type = Type, from = PeerJID} = recv_iq(Config) - end, [set, get, error, result]). + #iq{type = Type, from = PeerJID} = recv_iq(Config) + end, + [set, get, error, result]). + recv_messages(Config) -> PeerJID = ?config(peer, Config), lists:foreach( fun(Type) -> - #message{type = Type, from = PeerJID} = recv_message(Config) - end, [chat, error, groupchat, headline, normal]). + #message{type = Type, from = PeerJID} = recv_message(Config) + end, + [chat, error, groupchat, headline, normal]). + match_all(Opts) -> IQ = proplists:get_bool(iq, Opts), @@ -809,59 +994,73 @@ match_all(Opts) -> PresenceOut = proplists:get_bool(presence_out, Opts), not (IQ or Message or PresenceIn or PresenceOut). + is_message_in_blocked(Opts) -> proplists:get_bool(message, Opts) or match_all(Opts). + is_message_out_blocked(Opts) -> match_all(Opts). + is_iq_in_blocked(Opts) -> proplists:get_bool(iq, Opts) or match_all(Opts). + is_iq_out_blocked(Opts) -> match_all(Opts). + is_presence_in_blocked(Opts) -> proplists:get_bool(presence_in, Opts) or match_all(Opts). + is_presence_out_blocked(Opts) -> proplists:get_bool(presence_out, Opts) or match_all(Opts). + is_other_blocked(Opts) -> %% 'other' means subscriptions and presence-errors match_all(Opts). + server_send_iqs(Config) -> ServerJID = server_jid(Config), MyJID = my_jid(Config), ct:comment("Sending IQs from ~s to ~s", - [jid:encode(ServerJID), jid:encode(MyJID)]), + [jid:encode(ServerJID), jid:encode(MyJID)]), lists:foreach( fun(Type) -> - ejabberd_router:route( - #iq{from = ServerJID, to = MyJID, type = Type}) - end, [error, result]), + ejabberd_router:route( + #iq{from = ServerJID, to = MyJID, type = Type}) + end, + [error, result]), lists:foreach( fun(Type) -> - ejabberd_local:route_iq( - #iq{from = ServerJID, to = MyJID, type = Type}, - fun(#iq{type = result, sub_els = []}) -> ok; - (IQ) -> ct:fail({unexpected_iq_result, IQ}) - end) - end, [set, get]). + ejabberd_local:route_iq( + #iq{from = ServerJID, to = MyJID, type = Type}, + fun(#iq{type = result, sub_els = []}) -> ok; + (IQ) -> ct:fail({unexpected_iq_result, IQ}) + end) + end, + [set, get]). + server_recv_iqs(Config) -> ServerJID = server_jid(Config), ct:comment("Receiving IQs from ~s", [jid:encode(ServerJID)]), lists:foreach( fun(Type) -> - #iq{type = Type, from = ServerJID} = recv_iq(Config) - end, [error, result]), + #iq{type = Type, from = ServerJID} = recv_iq(Config) + end, + [error, result]), lists:foreach( fun(Type) -> - #iq{type = Type, from = ServerJID, id = I} = recv_iq(Config), - send(Config, #iq{to = ServerJID, type = result, id = I}) - end, [set, get]). + #iq{type = Type, from = ServerJID, id = I} = recv_iq(Config), + send(Config, #iq{to = ServerJID, type = result, id = I}) + end, + [set, get]). + send_stanzas_to_server_resource(Config) -> ServerJID = server_jid(Config), @@ -871,21 +1070,24 @@ send_stanzas_to_server_resource(Config) -> ct:comment("Sending IQs to ~s", [jid:encode(ServerJIDResource)]), lists:foreach( fun(Type) -> - #iq{type = error} = Err = - send_recv(Config, #iq{type = Type, to = ServerJIDResource}), - #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) - end, [set, get]), + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, + [set, get]), ct:comment("Sending messages to ~s", [jid:encode(ServerJIDResource)]), lists:foreach( fun(Type) -> - #message{type = error} = Err = - send_recv(Config, #message{type = Type, to = ServerJIDResource}), - #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) - end, [normal, chat, groupchat, headline]), + #message{type = error} = Err = + send_recv(Config, #message{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, + [normal, chat, groupchat, headline]), ct:comment("Sending presences to ~s", [jid:encode(ServerJIDResource)]), lists:foreach( fun(Type) -> - #presence{type = error} = Err = - send_recv(Config, #presence{type = Type, to = ServerJIDResource}), - #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) - end, [available, unavailable]). + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, + [available, unavailable]). diff --git a/test/private_tests.erl b/test/private_tests.erl index e7077f4ba..c3c04586f 100644 --- a/test/private_tests.erl +++ b/test/private_tests.erl @@ -24,11 +24,16 @@ %% API -compile(export_all). --import(suite, [my_jid/1, server_jid/1, is_feature_advertised/3, - send_recv/2, disconnect/1]). +-import(suite, + [my_jid/1, + server_jid/1, + is_feature_advertised/3, + send_recv/2, + disconnect/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -37,85 +42,114 @@ %%%=================================================================== single_cases() -> {private_single, [sequence], - [single_test(test_features), - single_test(test_no_namespace), - single_test(test_set_get), - single_test(test_published)]}. + [single_test(test_features), + single_test(test_no_namespace), + single_test(test_set_get), + single_test(test_published)]}. + test_features(Config) -> Server = jid:encode(server_jid(Config)), MyJID = my_jid(Config), case gen_mod:is_loaded(Server, mod_pubsub) of - true -> - true = is_feature_advertised(Config, ?NS_BOOKMARKS_CONVERSION_0, - jid:remove_resource(MyJID)); - false -> - ok + true -> + true = is_feature_advertised(Config, + ?NS_BOOKMARKS_CONVERSION_0, + jid:remove_resource(MyJID)); + false -> + ok end, disconnect(Config). + test_no_namespace(Config) -> WrongEl = #xmlel{name = <<"wrong">>}, #iq{type = error} = - send_recv(Config, #iq{type = get, - sub_els = [#private{sub_els = [WrongEl]}]}), + send_recv(Config, + #iq{ + type = get, + sub_els = [#private{sub_els = [WrongEl]}] + }), disconnect(Config). + test_set_get(Config) -> Storage = bookmark_storage(), StorageXMLOut = xmpp:encode(Storage), #iq{type = result, sub_els = []} = send_recv( - Config, #iq{type = set, - sub_els = [#private{sub_els = [StorageXMLOut]}]}), - #iq{type = result, - sub_els = [#private{sub_els = [StorageXMLIn]}]} = + Config, + #iq{ + type = set, + sub_els = [#private{sub_els = [StorageXMLOut]}] + }), + #iq{ + type = result, + sub_els = [#private{sub_els = [StorageXMLIn]}] + } = send_recv( Config, - #iq{type = get, - sub_els = [#private{sub_els = [xmpp:encode( - #bookmark_storage{})]}]}), + #iq{ + type = get, + sub_els = [#private{ + sub_els = [xmpp:encode( + #bookmark_storage{})] + }] + }), Storage = xmpp:decode(StorageXMLIn), disconnect(Config). + test_published(Config) -> Server = jid:encode(server_jid(Config)), case gen_mod:is_loaded(Server, mod_pubsub) of - true -> - Storage = bookmark_storage(), - Node = xmpp:get_ns(Storage), - #iq{type = result, - sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} = - send_recv( - Config, - #iq{type = get, - sub_els = [#pubsub{items = #ps_items{node = Node}}]}), - [#ps_item{sub_els = [StorageXMLIn]}] = Items, - Storage = xmpp:decode(StorageXMLIn), - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#pubsub_owner{delete = {?NS_PEP_BOOKMARKS, <<>>}}]}); - false -> - ok + true -> + Storage = bookmark_storage(), + Node = xmpp:get_ns(Storage), + #iq{ + type = result, + sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}] + } = + send_recv( + Config, + #iq{ + type = get, + sub_els = [#pubsub{items = #ps_items{node = Node}}] + }), + [#ps_item{sub_els = [StorageXMLIn]}] = Items, + Storage = xmpp:decode(StorageXMLIn), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{ + type = set, + sub_els = [#pubsub_owner{delete = {Node, <<>>}}] + }), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{ + type = set, + sub_els = [#pubsub_owner{delete = {?NS_PEP_BOOKMARKS, <<>>}}] + }); + false -> + ok end, disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("private_" ++ atom_to_list(T)). + conference_bookmark() -> #bookmark_conference{ - name = <<"Some name">>, - autojoin = true, - jid = jid:make(<<"some">>, <<"some.conference.org">>)}. + name = <<"Some name">>, + autojoin = true, + jid = jid:make(<<"some">>, <<"some.conference.org">>) + }. + bookmark_storage() -> #bookmark_storage{conference = [conference_bookmark()]}. diff --git a/test/proxy65_tests.erl b/test/proxy65_tests.erl index 612a926fb..f62792ed4 100644 --- a/test/proxy65_tests.erl +++ b/test/proxy65_tests.erl @@ -25,12 +25,20 @@ %% API -compile(export_all). --import(suite, [disconnect/1, is_feature_advertised/3, proxy_jid/1, - my_jid/1, wait_for_slave/1, wait_for_master/1, - send_recv/2, put_event/2, get_event/1]). +-import(suite, + [disconnect/1, + is_feature_advertised/3, + proxy_jid/1, + my_jid/1, + wait_for_slave/1, + wait_for_master/1, + send_recv/2, + put_event/2, + get_event/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,27 +47,31 @@ %%%=================================================================== single_cases() -> {proxy65_single, [sequence], - [single_test(feature_enabled), - single_test(service_vcard)]}. + [single_test(feature_enabled), + single_test(service_vcard)]}. + feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_BYTESTREAMS, proxy_jid(Config)), disconnect(Config). + service_vcard(Config) -> JID = proxy_jid(Config), ct:comment("Retrieving vCard from ~s", [jid:encode(JID)]), VCard = mod_proxy65_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = - send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {proxy65_master_slave, [sequence], - [master_slave_test(all)]}. + [master_slave_test(all)]}. + all_master(Config) -> Proxy = proxy_jid(Config), @@ -78,11 +90,15 @@ all_master(Config) -> wait_for_slave(Config), #iq{type = result, sub_els = []} = send_recv(Config, - #iq{type = set, to = Proxy, - sub_els = [#bytestreams{activate = Peer, sid = SID}]}), + #iq{ + type = set, + to = Proxy, + sub_els = [#bytestreams{activate = Peer, sid = SID}] + }), socks5_send(Socks5, Data), disconnect(Config). + all_slave(Config) -> MyJID = my_jid(Config), Peer = ?config(master, Config), @@ -94,21 +110,26 @@ all_slave(Config) -> socks5_recv(Socks5, Data), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("proxy65_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("proxy65_" ++ atom_to_list(T)), [parallel], + {list_to_atom("proxy65_" ++ atom_to_list(T)), + [parallel], [list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_master"), list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_slave")]}. + socks5_connect(#streamhost{host = Host, port = Port}, {SID, JID1, JID2}) -> Hash = p1_sha:sha([SID, jid:encode(JID1), jid:encode(JID2)]), - {ok, Sock} = gen_tcp:connect(binary_to_list(Host), Port, + {ok, Sock} = gen_tcp:connect(binary_to_list(Host), + Port, [binary, {active, false}]), Init = <>, InitAck = <>, @@ -122,8 +143,10 @@ socks5_connect(#streamhost{host = Host, port = Port}, {ok, Resp} = gen_tcp:recv(Sock, size(Resp)), Sock. + socks5_send(Sock, Data) -> ok = gen_tcp:send(Sock, Data). + socks5_recv(Sock, Data) -> {ok, Data} = gen_tcp:recv(Sock, size(Data)). diff --git a/test/pubsub_tests.erl b/test/pubsub_tests.erl index 1cb02f020..57c8b70a1 100644 --- a/test/pubsub_tests.erl +++ b/test/pubsub_tests.erl @@ -25,13 +25,26 @@ %% API -compile(export_all). --import(suite, [pubsub_jid/1, send_recv/2, get_features/2, disconnect/1, - put_event/2, get_event/1, wait_for_master/1, wait_for_slave/1, - recv_message/1, my_jid/1, send/2, recv_presence/1, recv/1]). +-import(suite, + [pubsub_jid/1, + send_recv/2, + get_features/2, + disconnect/1, + put_event/2, + get_event/1, + wait_for_master/1, + wait_for_slave/1, + recv_message/1, + my_jid/1, + send/2, + recv_presence/1, + recv/1]). -include("suite.hrl"). + -include_lib("stdlib/include/assert.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -40,116 +53,127 @@ %%%=================================================================== single_cases() -> {pubsub_single, [sequence], - [single_test(test_features), - single_test(test_vcard), - single_test(test_create), - single_test(test_configure), - single_test(test_delete), - single_test(test_get_affiliations), - single_test(test_get_subscriptions), - single_test(test_create_instant), - single_test(test_default), - single_test(test_create_configure), - single_test(test_publish), - single_test(test_auto_create), - single_test(test_get_items), - single_test(test_delete_item), - single_test(test_purge), - single_test(test_subscribe), - single_test(test_subscribe_max_item_1), - single_test(test_unsubscribe)]}. + [single_test(test_features), + single_test(test_vcard), + single_test(test_create), + single_test(test_configure), + single_test(test_delete), + single_test(test_get_affiliations), + single_test(test_get_subscriptions), + single_test(test_create_instant), + single_test(test_default), + single_test(test_create_configure), + single_test(test_publish), + single_test(test_auto_create), + single_test(test_get_items), + single_test(test_delete_item), + single_test(test_purge), + single_test(test_subscribe), + single_test(test_subscribe_max_item_1), + single_test(test_unsubscribe)]}. + test_features(Config) -> PJID = pubsub_jid(Config), AllFeatures = sets:from_list(get_features(Config, PJID)), NeededFeatures = sets:from_list( - [?NS_PUBSUB, - ?PUBSUB("access-open"), - ?PUBSUB("access-authorize"), - ?PUBSUB("create-nodes"), - ?PUBSUB("instant-nodes"), - ?PUBSUB("config-node"), - ?PUBSUB("retrieve-default"), - ?PUBSUB("create-and-configure"), - ?PUBSUB("publish"), - ?PUBSUB("auto-create"), - ?PUBSUB("retrieve-items"), - ?PUBSUB("delete-items"), - ?PUBSUB("subscribe"), - ?PUBSUB("retrieve-affiliations"), - ?PUBSUB("modify-affiliations"), - ?PUBSUB("retrieve-subscriptions"), - ?PUBSUB("manage-subscriptions"), - ?PUBSUB("purge-nodes"), - ?PUBSUB("delete-nodes")]), + [?NS_PUBSUB, + ?PUBSUB("access-open"), + ?PUBSUB("access-authorize"), + ?PUBSUB("create-nodes"), + ?PUBSUB("instant-nodes"), + ?PUBSUB("config-node"), + ?PUBSUB("retrieve-default"), + ?PUBSUB("create-and-configure"), + ?PUBSUB("publish"), + ?PUBSUB("auto-create"), + ?PUBSUB("retrieve-items"), + ?PUBSUB("delete-items"), + ?PUBSUB("subscribe"), + ?PUBSUB("retrieve-affiliations"), + ?PUBSUB("modify-affiliations"), + ?PUBSUB("retrieve-subscriptions"), + ?PUBSUB("manage-subscriptions"), + ?PUBSUB("purge-nodes"), + ?PUBSUB("delete-nodes")]), true = sets:is_subset(NeededFeatures, AllFeatures), disconnect(Config). + test_vcard(Config) -> JID = pubsub_jid(Config), ct:comment("Retrieving vCard from ~s", [jid:encode(JID)]), VCard = mod_pubsub_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = - send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), disconnect(Config). + test_create(Config) -> Node = ?config(pubsub_node, Config), Node = create_node(Config, Node), disconnect(Config). + test_create_instant(Config) -> Node = create_node(Config, <<>>), delete_node(Config, Node), disconnect(Config). + test_configure(Config) -> Node = ?config(pubsub_node, Config), NodeTitle = ?config(pubsub_node_title, Config), NodeConfig = get_node_config(Config, Node), MyNodeConfig = set_opts(NodeConfig, - [{title, NodeTitle}]), + [{title, NodeTitle}]), set_node_config(Config, Node, MyNodeConfig), NewNodeConfig = get_node_config(Config, Node), NodeTitle = proplists:get_value(title, NewNodeConfig, <<>>), disconnect(Config). + test_default(Config) -> get_default_node_config(Config), disconnect(Config). + test_create_configure(Config) -> NodeTitle = ?config(pubsub_node_title, Config), DefaultNodeConfig = get_default_node_config(Config), CustomNodeConfig = set_opts(DefaultNodeConfig, - [{title, NodeTitle}]), + [{title, NodeTitle}]), Node = create_node(Config, <<>>, CustomNodeConfig), NodeConfig = get_node_config(Config, Node), NodeTitle = proplists:get_value(title, NodeConfig, <<>>), delete_node(Config, Node), disconnect(Config). + test_publish(Config) -> Node = create_node(Config, <<>>), publish_item(Config, Node), delete_node(Config, Node), disconnect(Config). + test_auto_create(Config) -> Node = p1_rand:get_string(), publish_item(Config, Node), delete_node(Config, Node), disconnect(Config). + test_get_items(Config) -> Node = create_node(Config, <<>>), - ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], + ItemsIn = [ publish_item(Config, Node) || _ <- lists:seq(1, 5) ], ItemsOut = get_items(Config, Node), - true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] - == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], + true = [ I || #ps_item{id = I} <- lists:sort(ItemsIn) ] == + [ I || #ps_item{id = I} <- lists:sort(ItemsOut) ], delete_node(Config, Node), disconnect(Config). + test_delete_item(Config) -> Node = create_node(Config, <<>>), #ps_item{id = I} = publish_item(Config, Node), @@ -159,6 +183,7 @@ test_delete_item(Config) -> delete_node(Config, Node), disconnect(Config). + test_subscribe(Config) -> Node = create_node(Config, <<>>), #ps_subscription{type = subscribed} = subscribe_node(Config, Node), @@ -166,16 +191,18 @@ test_subscribe(Config) -> delete_node(Config, Node), disconnect(Config). + test_subscribe_max_item_1(Config) -> DefaultNodeConfig = get_default_node_config(Config), CustomNodeConfig = set_opts(DefaultNodeConfig, - [{max_items, 1}]), + [{max_items, 1}]), Node = create_node(Config, <<>>, CustomNodeConfig), #ps_subscription{type = subscribed} = subscribe_node(Config, Node), [#ps_subscription{node = Node}] = get_subscriptions(Config), delete_node(Config, Node), disconnect(Config). + test_unsubscribe(Config) -> Node = create_node(Config, <<>>), subscribe_node(Config, Node), @@ -185,47 +212,56 @@ test_unsubscribe(Config) -> delete_node(Config, Node), disconnect(Config). + test_get_affiliations(Config) -> - Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), + Nodes = lists:sort([ create_node(Config, <<>>) || _ <- lists:seq(1, 5) ]), Affs = get_affiliations(Config), - ?assertEqual(Nodes, lists:sort([Node || #ps_affiliation{node = Node, - type = owner} <- Affs])), - [delete_node(Config, Node) || Node <- Nodes], + ?assertEqual(Nodes, + lists:sort([ Node || #ps_affiliation{ + node = Node, + type = owner + } <- Affs ])), + [ delete_node(Config, Node) || Node <- Nodes ], disconnect(Config). + test_get_subscriptions(Config) -> - Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), - [subscribe_node(Config, Node) || Node <- Nodes], + Nodes = lists:sort([ create_node(Config, <<>>) || _ <- lists:seq(1, 5) ]), + [ subscribe_node(Config, Node) || Node <- Nodes ], Subs = get_subscriptions(Config), - ?assertEqual(Nodes, lists:sort([Node || #ps_subscription{node = Node} <- Subs])), - [delete_node(Config, Node) || Node <- Nodes], + ?assertEqual(Nodes, lists:sort([ Node || #ps_subscription{node = Node} <- Subs ])), + [ delete_node(Config, Node) || Node <- Nodes ], disconnect(Config). + test_purge(Config) -> Node = create_node(Config, <<>>), - ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], + ItemsIn = [ publish_item(Config, Node) || _ <- lists:seq(1, 5) ], ItemsOut = get_items(Config, Node), - true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] - == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], + true = [ I || #ps_item{id = I} <- lists:sort(ItemsIn) ] == + [ I || #ps_item{id = I} <- lists:sort(ItemsOut) ], purge_node(Config, Node), [] = get_items(Config, Node), delete_node(Config, Node), disconnect(Config). + test_delete(Config) -> Node = ?config(pubsub_node, Config), delete_node(Config, Node), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {pubsub_master_slave, [sequence], - [master_slave_test(publish), - master_slave_test(subscriptions), - master_slave_test(affiliations), - master_slave_test(authorize)]}. + [master_slave_test(publish), + master_slave_test(subscriptions), + master_slave_test(affiliations), + master_slave_test(authorize)]}. + publish_master(Config) -> Node = create_node(Config, <<>>), @@ -236,18 +272,24 @@ publish_master(Config) -> delete_node(Config, Node), disconnect(Config). + publish_slave(Config) -> Node = get_event(Config), subscribe_node(Config, Node), put_event(Config, ready), #message{ - sub_els = - [#ps_event{ - items = #ps_items{node = Node, - items = [Item]}}]} = recv_message(Config), + sub_els = + [#ps_event{ + items = #ps_items{ + node = Node, + items = [Item] + } + }] + } = recv_message(Config), put_event(Config, Item), disconnect(Config). + subscriptions_master(Config) -> Peer = ?config(slave, Config), Node = ?config(pubsub_node, Config), @@ -256,71 +298,85 @@ subscriptions_master(Config) -> wait_for_slave(Config), lists:foreach( fun(Type) -> - ok = set_subscriptions(Config, Node, [{Peer, Type}]), - #ps_item{} = publish_item(Config, Node), - case get_subscriptions(Config, Node) of - [] when Type == none; Type == pending -> - ok; - [#ps_subscription{jid = Peer, type = Type}] -> - ok - end - end, [subscribed, unconfigured, pending, none]), + ok = set_subscriptions(Config, Node, [{Peer, Type}]), + #ps_item{} = publish_item(Config, Node), + case get_subscriptions(Config, Node) of + [] when Type == none; Type == pending -> + ok; + [#ps_subscription{jid = Peer, type = Type}] -> + ok + end + end, + [subscribed, unconfigured, pending, none]), delete_node(Config, Node), disconnect(Config). + subscriptions_slave(Config) -> wait_for_master(Config), MyJID = my_jid(Config), Node = ?config(pubsub_node, Config), lists:foreach( fun(subscribed = Type) -> - ?recv2(#message{ - sub_els = - [#ps_event{ - subscription = #ps_subscription{ - node = Node, - jid = MyJID, - type = Type}}]}, - #message{sub_els = [#ps_event{}]}); - (Type) -> - #message{ - sub_els = - [#ps_event{ - subscription = #ps_subscription{ - node = Node, - jid = MyJID, - type = Type}}]} = - recv_message(Config) - end, [subscribed, unconfigured, pending, none]), + ?recv2(#message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID, + type = Type + } + }] + }, + #message{sub_els = [#ps_event{}]}); + (Type) -> + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID, + type = Type + } + }] + } = + recv_message(Config) + end, + [subscribed, unconfigured, pending, none]), disconnect(Config). + affiliations_master(Config) -> Peer = ?config(slave, Config), BarePeer = jid:remove_resource(Peer), lists:foreach( fun(Aff) -> - Node = <<(atom_to_binary(Aff, utf8))/binary, - $-, (p1_rand:get_string())/binary>>, - create_node(Config, Node, default_node_config(Config)), - #ps_item{id = I} = publish_item(Config, Node), - ok = set_affiliations(Config, Node, [{Peer, Aff}]), - Affs = get_affiliations(Config, Node), - case lists:keyfind(BarePeer, #ps_affiliation.jid, Affs) of - false when Aff == none -> - ok; - #ps_affiliation{type = Aff} -> - ok - end, - put_event(Config, {Aff, Node, I}), - wait_for_slave(Config), - delete_node(Config, Node) - end, [outcast, none, member, publish_only, publisher, owner]), + Node = <<(atom_to_binary(Aff, utf8))/binary, + $-, + (p1_rand:get_string())/binary>>, + create_node(Config, Node, default_node_config(Config)), + #ps_item{id = I} = publish_item(Config, Node), + ok = set_affiliations(Config, Node, [{Peer, Aff}]), + Affs = get_affiliations(Config, Node), + case lists:keyfind(BarePeer, #ps_affiliation.jid, Affs) of + false when Aff == none -> + ok; + #ps_affiliation{type = Aff} -> + ok + end, + put_event(Config, {Aff, Node, I}), + wait_for_slave(Config), + delete_node(Config, Node) + end, + [outcast, none, member, publish_only, publisher, owner]), put_event(Config, disconnect), disconnect(Config). + affiliations_slave(Config) -> affiliations_slave(Config, get_event(Config)). + affiliations_slave(Config, {outcast, Node, ItemID}) -> #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), #stanza_error{} = unsubscribe_node(Config, Node), @@ -330,14 +386,16 @@ affiliations_slave(Config, {outcast, Node, ItemID}) -> #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = - set_node_config(Config, Node, default_node_config(Config)), + set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = - set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = - set_affiliations(Config, Node, [{?config(master, Config), outcast}, - {my_jid(Config), owner}]), + set_affiliations(Config, + Node, + [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); @@ -345,40 +403,44 @@ affiliations_slave(Config, {none, Node, ItemID}) -> #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), %% This violates the affiliation char from section 4.1 - [_|_] = get_items(Config, Node), + [_ | _] = get_items(Config, Node), #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = - set_node_config(Config, Node, default_node_config(Config)), + set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = - set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = - set_affiliations(Config, Node, [{?config(master, Config), outcast}, - {my_jid(Config), owner}]), + set_affiliations(Config, + Node, + [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {member, Node, ItemID}) -> #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), - [_|_] = get_items(Config, Node), + [_ | _] = get_items(Config, Node), #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = - set_node_config(Config, Node, default_node_config(Config)), + set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = - set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = - set_affiliations(Config, Node, [{?config(master, Config), outcast}, - {my_jid(Config), owner}]), + set_affiliations(Config, + Node, + [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); @@ -393,21 +455,23 @@ affiliations_slave(Config, {publish_only, Node, ItemID}) -> #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = - set_node_config(Config, Node, default_node_config(Config)), + set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = - set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = - set_affiliations(Config, Node, [{?config(master, Config), outcast}, - {my_jid(Config), owner}]), + set_affiliations(Config, + Node, + [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {publisher, Node, _ItemID}) -> #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), - [_|_] = get_items(Config, Node), + [_ | _] = get_items(Config, Node), #ps_item{id = MyItemID} = publish_item(Config, Node), ok = delete_item(Config, Node, MyItemID), %% BUG: this should be fixed @@ -415,14 +479,16 @@ affiliations_slave(Config, {publisher, Node, _ItemID}) -> #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = - set_node_config(Config, Node, default_node_config(Config)), + set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = - set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = - set_affiliations(Config, Node, [{?config(master, Config), outcast}, - {my_jid(Config), owner}]), + set_affiliations(Config, + Node, + [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); @@ -431,12 +497,12 @@ affiliations_slave(Config, {owner, Node, ItemID}) -> Peer = ?config(master, Config), #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), - [_|_] = get_items(Config, Node), + [_ | _] = get_items(Config, Node), #ps_item{id = MyItemID} = publish_item(Config, Node), ok = delete_item(Config, Node, MyItemID), ok = delete_item(Config, Node, ItemID), ok = purge_node(Config, Node), - [_|_] = get_node_config(Config, Node), + [_ | _] = get_node_config(Config, Node), ok = set_node_config(Config, Node, default_node_config(Config)), ok = set_subscriptions(Config, Node, []), [] = get_subscriptions(Config, Node), @@ -448,13 +514,14 @@ affiliations_slave(Config, {owner, Node, ItemID}) -> affiliations_slave(Config, disconnect) -> disconnect(Config). + authorize_master(Config) -> send(Config, #presence{}), #presence{} = recv_presence(Config), Peer = ?config(slave, Config), PJID = pubsub_jid(Config), NodeConfig = set_opts(default_node_config(Config), - [{access_model, authorize}]), + [{access_model, authorize}]), Node = ?config(pubsub_node, Config), Node = create_node(Config, Node, NodeConfig), wait_for_slave(Config), @@ -463,11 +530,13 @@ authorize_master(Config) -> Node = proplists:get_value(node, C1), Peer = proplists:get_value(subscriber_jid, C1), %% Deny it at first - Deny = #xdata{type = submit, - fields = pubsub_subscribe_authorization:encode( - [{node, Node}, - {subscriber_jid, Peer}, - {allow, false}])}, + Deny = #xdata{ + type = submit, + fields = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Peer}, + {allow, false}]) + }, send(Config, #message{to = PJID, sub_els = [Deny]}), %% We should not have any subscriptions [] = get_subscriptions(Config, Node), @@ -477,16 +546,19 @@ authorize_master(Config) -> Node = proplists:get_value(node, C2), Peer = proplists:get_value(subscriber_jid, C2), %% Now we accept is as the peer is very insisting ;) - Approve = #xdata{type = submit, - fields = pubsub_subscribe_authorization:encode( - [{node, Node}, - {subscriber_jid, Peer}, - {allow, true}])}, + Approve = #xdata{ + type = submit, + fields = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Peer}, + {allow, true}]) + }, send(Config, #message{to = PJID, sub_els = [Approve]}), wait_for_slave(Config), delete_node(Config, Node), disconnect(Config). + authorize_slave(Config) -> Node = ?config(pubsub_node, Config), MyJID = my_jid(Config), @@ -494,271 +566,400 @@ authorize_slave(Config) -> #ps_subscription{type = pending} = subscribe_node(Config, Node), %% We're denied at first #message{ - sub_els = - [#ps_event{ - subscription = #ps_subscription{type = none, - jid = MyJID}}]} = - recv_message(Config), + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + type = none, + jid = MyJID + } + }] + } = + recv_message(Config), wait_for_master(Config), #ps_subscription{type = pending} = subscribe_node(Config, Node), %% Now much better! #message{ - sub_els = - [#ps_event{ - subscription = #ps_subscription{type = subscribed, - jid = MyJID}}]} = - recv_message(Config), + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + type = subscribed, + jid = MyJID + } + }] + } = + recv_message(Config), wait_for_master(Config), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("pubsub_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("pubsub_" ++ atom_to_list(T)), [parallel], + {list_to_atom("pubsub_" ++ atom_to_list(T)), + [parallel], [list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_master"), list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_slave")]}. + set_opts(Config, Options) -> lists:foldl( fun({Opt, Val}, Acc) -> - lists:keystore(Opt, 1, Acc, {Opt, Val}) - end, Config, Options). + lists:keystore(Opt, 1, Acc, {Opt, Val}) + end, + Config, + Options). + create_node(Config, Node) -> create_node(Config, Node, undefined). + create_node(Config, Node, Options) -> PJID = pubsub_jid(Config), - NodeConfig = if is_list(Options) -> - #xdata{type = submit, - fields = pubsub_node_config:encode(Options)}; - true -> - undefined - end, + NodeConfig = if + is_list(Options) -> + #xdata{ + type = submit, + fields = pubsub_node_config:encode(Options) + }; + true -> + undefined + end, case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub{create = Node, - configure = {<<>>, NodeConfig}}]}) of - #iq{type = result, sub_els = [#pubsub{create = NewNode}]} -> - NewNode; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub{ + create = Node, + configure = {<<>>, NodeConfig} + }] + }) of + #iq{type = result, sub_els = [#pubsub{create = NewNode}]} -> + NewNode; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + delete_node(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub_owner{delete = {Node, <<>>}}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + purge_node(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub_owner{purge = Node}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub_owner{purge = Node}] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_default_node_config(Config) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub_owner{default = {<<>>, undefined}}]}) of - #iq{type = result, - sub_els = [#pubsub_owner{default = {<<>>, NodeConfig}}]} -> - pubsub_node_config:decode(NodeConfig#xdata.fields); - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub_owner{default = {<<>>, undefined}}] + }) of + #iq{ + type = result, + sub_els = [#pubsub_owner{default = {<<>>, NodeConfig}}] + } -> + pubsub_node_config:decode(NodeConfig#xdata.fields); + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_node_config(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub_owner{configure = {Node, undefined}}]}) of - #iq{type = result, - sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}]} -> - pubsub_node_config:decode(NodeConfig#xdata.fields); - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub_owner{configure = {Node, undefined}}] + }) of + #iq{ + type = result, + sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}] + } -> + pubsub_node_config:decode(NodeConfig#xdata.fields); + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + set_node_config(Config, Node, Options) -> PJID = pubsub_jid(Config), - NodeConfig = #xdata{type = submit, - fields = pubsub_node_config:encode(Options)}, + NodeConfig = #xdata{ + type = submit, + fields = pubsub_node_config:encode(Options) + }, case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub_owner{configure = - {Node, NodeConfig}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub_owner{ + configure = + {Node, NodeConfig} + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + publish_item(Config, Node) -> PJID = pubsub_jid(Config), ItemID = p1_rand:get_string(), Item = #ps_item{id = ItemID, sub_els = [xmpp:encode(#presence{id = ItemID})]}, case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub{publish = #ps_publish{ - node = Node, - items = [Item]}}]}) of - #iq{type = result, - sub_els = [#pubsub{publish = #ps_publish{ - node = Node, - items = [#ps_item{id = ItemID}]}}]} -> - Item; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub{ + publish = #ps_publish{ + node = Node, + items = [Item] + } + }] + }) of + #iq{ + type = result, + sub_els = [#pubsub{ + publish = #ps_publish{ + node = Node, + items = [#ps_item{id = ItemID}] + } + }] + } -> + Item; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_items(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub{items = #ps_items{node = Node}}]}) of - #iq{type = result, - sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} -> - Items; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub{items = #ps_items{node = Node}}] + }) of + #iq{ + type = result, + sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}] + } -> + Items; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + delete_item(Config, Node, I) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub{retract = - #ps_retract{ - node = Node, - items = [#ps_item{id = I}]}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub{ + retract = + #ps_retract{ + node = Node, + items = [#ps_item{id = I}] + } + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + subscribe_node(Config, Node) -> PJID = pubsub_jid(Config), MyJID = my_jid(Config), case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub{subscribe = #ps_subscribe{ - node = Node, - jid = MyJID}}]}) of - #iq{type = result, - sub_els = [#pubsub{ - subscription = #ps_subscription{ - node = Node, - jid = MyJID} = Sub}]} -> - Sub; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub{ + subscribe = #ps_subscribe{ + node = Node, + jid = MyJID + } + }] + }) of + #iq{ + type = result, + sub_els = [#pubsub{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID + } = Sub + }] + } -> + Sub; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + unsubscribe_node(Config, Node) -> PJID = pubsub_jid(Config), MyJID = my_jid(Config), case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub{ - unsubscribe = #ps_unsubscribe{ - node = Node, - jid = MyJID}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub{ + unsubscribe = #ps_unsubscribe{ + node = Node, + jid = MyJID + } + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_affiliations(Config) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub{affiliations = {<<>>, []}}]}) of - #iq{type = result, - sub_els = [#pubsub{affiliations = {<<>>, Affs}}]} -> - Affs; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub{affiliations = {<<>>, []}}] + }) of + #iq{ + type = result, + sub_els = [#pubsub{affiliations = {<<>>, Affs}}] + } -> + Affs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_affiliations(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub_owner{affiliations = {Node, []}}]}) of - #iq{type = result, - sub_els = [#pubsub_owner{affiliations = {Node, Affs}}]} -> - Affs; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub_owner{affiliations = {Node, []}}] + }) of + #iq{ + type = result, + sub_els = [#pubsub_owner{affiliations = {Node, Affs}}] + } -> + Affs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + set_affiliations(Config, Node, JTs) -> PJID = pubsub_jid(Config), - Affs = [#ps_affiliation{jid = J, type = T} || {J, T} <- JTs], + Affs = [ #ps_affiliation{jid = J, type = T} || {J, T} <- JTs ], case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub_owner{affiliations = - {Node, Affs}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub_owner{ + affiliations = + {Node, Affs} + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_subscriptions(Config) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub{subscriptions = {<<>>, []}}]}) of - #iq{type = result, sub_els = [#pubsub{subscriptions = {<<>>, Subs}}]} -> - Subs; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub{subscriptions = {<<>>, []}}] + }) of + #iq{type = result, sub_els = [#pubsub{subscriptions = {<<>>, Subs}}]} -> + Subs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + get_subscriptions(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, - #iq{type = get, to = PJID, - sub_els = [#pubsub_owner{subscriptions = {Node, []}}]}) of - #iq{type = result, - sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}]} -> - Subs; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = get, + to = PJID, + sub_els = [#pubsub_owner{subscriptions = {Node, []}}] + }) of + #iq{ + type = result, + sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}] + } -> + Subs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + set_subscriptions(Config, Node, JTs) -> PJID = pubsub_jid(Config), - Subs = [#ps_subscription{jid = J, type = T} || {J, T} <- JTs], + Subs = [ #ps_subscription{jid = J, type = T} || {J, T} <- JTs ], case send_recv(Config, - #iq{type = set, to = PJID, - sub_els = [#pubsub_owner{subscriptions = - {Node, Subs}}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = IQ -> - xmpp:get_subtag(IQ, #stanza_error{}) + #iq{ + type = set, + to = PJID, + sub_els = [#pubsub_owner{ + subscriptions = + {Node, Subs} + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) end. + default_node_config(Config) -> [{title, ?config(pubsub_node_title, Config)}, {notify_delete, false}, diff --git a/test/push_tests.erl b/test/push_tests.erl index 9e400cccc..859721fd5 100644 --- a/test/push_tests.erl +++ b/test/push_tests.erl @@ -25,19 +25,36 @@ %% API -compile(export_all). --import(suite, [close_socket/1, connect/1, disconnect/1, get_event/1, - get_features/2, make_iq_result/1, my_jid/1, put_event/2, recv/1, - recv_iq/1, recv_message/1, self_presence/2, send/2, send_recv/2, - server_jid/1]). +-import(suite, + [close_socket/1, + connect/1, + disconnect/1, + get_event/1, + get_features/2, + make_iq_result/1, + my_jid/1, + put_event/2, + recv/1, + recv_iq/1, + recv_message/1, + self_presence/2, + send/2, + send_recv/2, + server_jid/1]). -include("suite.hrl"). --define(PUSH_NODE, <<"d3v1c3">>). +-define(PUSH_NODE, <<"d3v1c3">>). -define(PUSH_XDATA_FIELDS, - [#xdata_field{var = <<"FORM_TYPE">>, - values = [?NS_PUBSUB_PUBLISH_OPTIONS]}, - #xdata_field{var = <<"secret">>, - values = [<<"c0nf1d3nt14l">>]}]). + [#xdata_field{ + var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_PUBLISH_OPTIONS] + }, + #xdata_field{ + var = <<"secret">>, + values = [<<"c0nf1d3nt14l">>] + }]). + %%%=================================================================== %%% API @@ -47,8 +64,9 @@ %%%=================================================================== single_cases() -> {push_single, [sequence], - [single_test(feature_enabled), - single_test(unsupported_iq)]}. + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + feature_enabled(Config) -> BareMyJID = jid:remove_resource(my_jid(Config)), @@ -56,29 +74,33 @@ feature_enabled(Config) -> true = lists:member(?NS_PUSH_0, Features), disconnect(Config). + unsupported_iq(Config) -> PushJID = my_jid(Config), lists:foreach( fun(SubEl) -> - #iq{type = error} = - send_recv(Config, #iq{type = get, sub_els = [SubEl]}) - end, [#push_enable{jid = PushJID}, #push_disable{jid = PushJID}]), + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, + [#push_enable{jid = PushJID}, #push_disable{jid = PushJID}]), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {push_master_slave, [sequence], - [master_slave_test(sm), - master_slave_test(offline), - master_slave_test(mam)]}. + [master_slave_test(sm), + master_slave_test(offline), + master_slave_test(mam)]}. + sm_master(Config) -> ct:comment("Waiting for the slave to close the socket"), peer_down = get_event(Config), ct:comment("Waiting a bit in order to test the keepalive feature"), - ct:sleep(5000), % Without mod_push_keepalive, the session would time out. + ct:sleep(5000), % Without mod_push_keepalive, the session would time out. ct:comment("Sending message to the slave"), send_test_message(Config), ct:comment("Handling push notification"), @@ -88,6 +110,7 @@ sm_master(Config) -> ct:comment("Closing the connection"), disconnect(Config). + sm_slave(Config) -> ct:comment("Enabling push notifications"), ok = enable_push(Config), @@ -96,11 +119,12 @@ sm_slave(Config) -> ct:comment("Closing the socket"), close_socket(Config). + offline_master(Config) -> ct:comment("Waiting for the slave to be ready"), ready = get_event(Config), ct:comment("Sending message to the slave"), - send_test_message(Config), % No push notification, slave is online. + send_test_message(Config), % No push notification, slave is online. ct:comment("Waiting for the slave to disconnect"), peer_down = get_event(Config), ct:comment("Sending message to offline storage"), @@ -110,6 +134,7 @@ offline_master(Config) -> ct:comment("Closing the connection"), disconnect(Config). + offline_slave(Config) -> ct:comment("Re-enabling push notifications"), ok = enable_push(Config), @@ -120,6 +145,7 @@ offline_slave(Config) -> ct:comment("Closing the connection"), disconnect(Config). + mam_master(Config) -> ct:comment("Waiting for the slave to be ready"), ready = get_event(Config), @@ -130,6 +156,7 @@ mam_master(Config) -> ct:comment("Closing the connection"), disconnect(Config). + mam_slave(Config) -> self_presence(Config, available), ct:comment("Receiving message from offline storage"), @@ -149,36 +176,48 @@ mam_slave(Config) -> ct:comment("Closing the connection and cleaning up"), clean(disconnect(Config)). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("push_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("push_" ++ atom_to_list(T)), [parallel], + {list_to_atom("push_" ++ atom_to_list(T)), + [parallel], [list_to_atom("push_" ++ atom_to_list(T) ++ "_master"), list_to_atom("push_" ++ atom_to_list(T) ++ "_slave")]}. + enable_sm(Config) -> send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3, resume = true}), case recv(Config) of - #sm_enabled{resume = true} -> - ok; - #sm_failed{reason = Reason} -> - Reason + #sm_enabled{resume = true} -> + ok; + #sm_failed{reason = Reason} -> + Reason end. + enable_mam(Config) -> case send_recv( - Config, #iq{type = set, sub_els = [#mam_prefs{xmlns = ?NS_MAM_1, - default = always}]}) of - #iq{type = result} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#mam_prefs{ + xmlns = ?NS_MAM_1, + default = always + }] + }) of + #iq{type = result} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + enable_push(Config) -> %% Usually, the push JID would be a server JID (such as push.example.com). %% We specify the peer's full user JID instead, so the push notifications @@ -186,37 +225,53 @@ enable_push(Config) -> PushJID = ?config(peer, Config), XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, case send_recv( - Config, #iq{type = set, - sub_els = [#push_enable{jid = PushJID, - node = ?PUSH_NODE, - xdata = XData}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#push_enable{ + jid = PushJID, + node = ?PUSH_NODE, + xdata = XData + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + disable_push(Config) -> PushJID = ?config(peer, Config), case send_recv( - Config, #iq{type = set, - sub_els = [#push_disable{jid = PushJID, - node = ?PUSH_NODE}]}) of - #iq{type = result, sub_els = []} -> - ok; - #iq{type = error} = Err -> - xmpp:get_error(Err) + Config, + #iq{ + type = set, + sub_els = [#push_disable{ + jid = PushJID, + node = ?PUSH_NODE + }] + }) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + send_test_message(Config) -> Peer = ?config(peer, Config), Msg = #message{to = Peer, body = [#text{data = <<"test">>}]}, send(Config, Msg). + recv_test_message(Config) -> Peer = ?config(peer, Config), - #message{from = Peer, - body = [#text{data = <<"test">>}]} = recv_message(Config). + #message{ + from = Peer, + body = [#text{data = <<"test">>}] + } = recv_message(Config). + handle_notification(Config) -> From = server_jid(Config), @@ -227,6 +282,7 @@ handle_notification(Config) -> IQ = #iq{type = set, from = From, sub_els = [PubSub]} = recv_iq(Config), send(Config, make_iq_result(IQ)). + clean(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), mod_push:remove_user(U, S), diff --git a/test/replaced_tests.erl b/test/replaced_tests.erl index 37e22b3ac..2d0a4ba38 100644 --- a/test/replaced_tests.erl +++ b/test/replaced_tests.erl @@ -25,11 +25,17 @@ %% API -compile(export_all). --import(suite, [bind/1, wait_for_slave/1, wait_for_master/1, recv/1, - close_socket/1, disconnect/1]). +-import(suite, + [bind/1, + wait_for_slave/1, + wait_for_master/1, + recv/1, + close_socket/1, + disconnect/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,12 +45,14 @@ single_cases() -> {replaced_single, [sequence], []}. + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {replaced_master_slave, [sequence], - [master_slave_test(conflict)]}. + [master_slave_test(conflict)]}. + conflict_master(Config0) -> Config = bind(Config0), @@ -53,18 +61,22 @@ conflict_master(Config0) -> {xmlstreamend, <<"stream:stream">>} = recv(Config), close_socket(Config). + conflict_slave(Config0) -> wait_for_master(Config0), Config = bind(Config0), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("replaced_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("replaced_" ++ atom_to_list(T)), [parallel], + {list_to_atom("replaced_" ++ atom_to_list(T)), + [parallel], [list_to_atom("replaced_" ++ atom_to_list(T) ++ "_master"), list_to_atom("replaced_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/roster_tests.erl b/test/roster_tests.erl index 8d096eea3..d56a9792d 100644 --- a/test/roster_tests.erl +++ b/test/roster_tests.erl @@ -25,17 +25,32 @@ %% API -compile(export_all). --import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1, - del_roster/2, make_iq_result/1, wait_for_slave/1, - wait_for_master/1, recv_presence/1, self_presence/2, - put_event/2, get_event/1, match_failure/2, get_roster/1]). +-import(suite, + [send_recv/2, + recv_iq/1, + send/2, + disconnect/1, + del_roster/1, + del_roster/2, + make_iq_result/1, + wait_for_slave/1, + wait_for_master/1, + recv_presence/1, + self_presence/2, + put_event/2, + get_event/1, + match_failure/2, + get_roster/1]). -include("suite.hrl"). -include("mod_roster.hrl"). --record(state, {subscription = none :: none | from | to | both, - peer_available = false, - pending_in = false :: boolean(), - pending_out = false :: boolean()}). +-record(state, { + subscription = none :: none | from | to | both, + peer_available = false, + pending_in = false :: boolean(), + pending_out = false :: boolean() + }). + %%%=================================================================== %%% API @@ -43,28 +58,32 @@ init(_TestCase, Config) -> Config. + stop(_TestCase, Config) -> Config. + %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {roster_single, [sequence], - [single_test(feature_enabled), - single_test(iq_set_many_items), - single_test(iq_set_duplicated_groups), - single_test(iq_get_item), - single_test(iq_unexpected_element), - single_test(iq_set_ask), - single_test(set_item), - single_test(version)]}. + [single_test(feature_enabled), + single_test(iq_set_many_items), + single_test(iq_set_duplicated_groups), + single_test(iq_get_item), + single_test(iq_unexpected_element), + single_test(iq_set_ask), + single_test(set_item), + single_test(version)]}. + feature_enabled(Config) -> ct:comment("Checking if roster versioning stream feature is set"), true = ?config(rosterver, Config), disconnect(Config). + set_item(Config) -> JID = jid:decode(<<"nurse@example.com">>), Item = #roster_item{jid = JID}, @@ -83,6 +102,7 @@ set_item(Config) -> {V5, []} = get_items(Config), del_roster(disconnect(Config), JID). + iq_set_many_items(Config) -> J1 = jid:decode(<<"nurse1@example.com">>), J2 = jid:decode(<<"nurse2@example.com">>), @@ -91,6 +111,7 @@ iq_set_many_items(Config) -> #stanza_error{reason = 'bad-request'} = set_items(Config, Items), disconnect(Config). + iq_set_duplicated_groups(Config) -> JID = jid:decode(<<"nurse@example.com">>), G = p1_rand:get_string(), @@ -99,6 +120,7 @@ iq_set_duplicated_groups(Config) -> #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), disconnect(Config). + iq_set_ask(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Trying to send roster-set with 'ask' included"), @@ -106,28 +128,39 @@ iq_set_ask(Config) -> #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), disconnect(Config). + iq_get_item(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Trying to send roster-get with element"), #iq{type = error} = Err3 = - send_recv(Config, #iq{type = get, - sub_els = [#roster_query{ - items = [#roster_item{jid = JID}]}]}), + send_recv(Config, + #iq{ + type = get, + sub_els = [#roster_query{ + items = [#roster_item{jid = JID}] + }] + }), #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3), disconnect(Config). + iq_unexpected_element(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Trying to send IQs with unexpected element"), lists:foreach( fun(Type) -> - #iq{type = error} = Err4 = - send_recv(Config, #iq{type = Type, - sub_els = [#roster_item{jid = JID}]}), - #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) - end, [get, set]), + #iq{type = error} = Err4 = + send_recv(Config, + #iq{ + type = Type, + sub_els = [#roster_item{jid = JID}] + }), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) + end, + [get, set]), disconnect(Config). + version(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Requesting roster"), @@ -142,61 +175,75 @@ version(Config) -> {empty, []} = get_items(Config, NewVersion), del_roster(disconnect(Config), JID). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {roster_master_slave, [sequence], - [master_slave_test(subscribe)]}. + [master_slave_test(subscribe)]}. + subscribe_master(Config) -> Actions = actions(), process_subscriptions_master(Config, Actions), del_roster(disconnect(Config)). + subscribe_slave(Config) -> process_subscriptions_slave(Config), del_roster(disconnect(Config)). + process_subscriptions_master(Config, Actions) -> EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions), self_presence(Config, available), Peer = ?config(peer, Config), lists:foldl( fun({N, {Dir, Type}}, State) -> - if Dir == out -> put_event(Config, {N, in, Type}); - Dir == in -> put_event(Config, {N, out, Type}) - end, - Roster = get_roster(Config), - ct:pal("Performing ~s-~s (#~p) " - "in state:~n~s~nwith roster:~n~s", - [Dir, Type, N, pp(State), pp(Roster)]), - check_roster(Roster, Config, State), - wait_for_slave(Config), - Id = mk_id(N, Dir, Type), - NewState = transition(Id, Config, Dir, Type, State), - wait_for_slave(Config), - send_recv(Config, #iq{type = get, to = Peer, id = Id, - sub_els = [#ping{}]}), - check_roster_item(Config, NewState), - NewState - end, #state{}, EnumeratedActions), + if + Dir == out -> put_event(Config, {N, in, Type}); + Dir == in -> put_event(Config, {N, out, Type}) + end, + Roster = get_roster(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(Roster)]), + check_roster(Roster, Config, State), + wait_for_slave(Config), + Id = mk_id(N, Dir, Type), + NewState = transition(Id, Config, Dir, Type, State), + wait_for_slave(Config), + send_recv(Config, + #iq{ + type = get, + to = Peer, + id = Id, + sub_els = [#ping{}] + }), + check_roster_item(Config, NewState), + NewState + end, + #state{}, + EnumeratedActions), put_event(Config, done), wait_for_slave(Config), Config. + process_subscriptions_slave(Config) -> self_presence(Config, available), process_subscriptions_slave(Config, get_event(Config), #state{}). + process_subscriptions_slave(Config, done, _State) -> wait_for_master(Config), Config; process_subscriptions_slave(Config, {N, Dir, Type}, State) -> Roster = get_roster(Config), ct:pal("Performing ~s-~s (#~p) " - "in state:~n~s~nwith roster:~n~s", - [Dir, Type, N, pp(State), pp(Roster)]), + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(Roster)]), check_roster(Roster, Config, State), wait_for_master(Config), NewState = transition(mk_id(N, Dir, Type), Config, Dir, Type, State), @@ -205,388 +252,467 @@ process_subscriptions_slave(Config, {N, Dir, Type}, State) -> check_roster_item(Config, NewState), process_subscriptions_slave(Config, get_event(Config), NewState). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("roster_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("roster_" ++ atom_to_list(T)), [parallel], + {list_to_atom("roster_" ++ atom_to_list(T)), + [parallel], [list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"), list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}. + get_items(Config) -> get_items(Config, <<"">>). + get_items(Config, Version) -> - case send_recv(Config, #iq{type = get, - sub_els = [#roster_query{ver = Version}]}) of - #iq{type = result, - sub_els = [#roster_query{ver = NewVersion, items = Items}]} -> - {NewVersion, normalize_items(Items)}; - #iq{type = result, sub_els = []} -> - {empty, []}; - #iq{type = error} = Err -> - xmpp:get_error(Err) + case send_recv(Config, + #iq{ + type = get, + sub_els = [#roster_query{ver = Version}] + }) of + #iq{ + type = result, + sub_els = [#roster_query{ver = NewVersion, items = 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), + end, + Items), lists:sort(Items2). + get_item(Config, JID) -> case get_items(Config) of - {_Ver, Items} when is_list(Items) -> - lists:keyfind(JID, #roster_item.jid, Items); - _ -> - false + {_Ver, Items} when is_list(Items) -> + lists:keyfind(JID, #roster_item.jid, Items); + _ -> + false end. + set_items(Config, Items) -> - case send_recv(Config, #iq{type = set, - sub_els = [#roster_query{items = Items}]}) of - #iq{type = result, sub_els = []} -> - recv_push(Config); - #iq{type = error} = Err -> - xmpp:get_error(Err) + case send_recv(Config, + #iq{ + type = set, + sub_els = [#roster_query{items = Items}] + }) of + #iq{type = result, sub_els = []} -> + recv_push(Config); + #iq{type = error} = Err -> + xmpp:get_error(Err) end. + recv_push(Config) -> ct:comment("Receiving roster push"), - Push = #iq{type = set, - sub_els = [#roster_query{ver = Ver, items = [PushItem]}]} - = recv_iq(Config), + Push = #iq{ + type = set, + sub_els = [#roster_query{ver = Ver, items = [PushItem]}] + } = + recv_iq(Config), send(Config, make_iq_result(Push)), {Ver, PushItem}. + recv_push(Config, Subscription, Ask) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), - Match = #roster_item{jid = PeerBareJID, - subscription = Subscription, - ask = Ask, - groups = [], - name = <<"">>}, + Match = #roster_item{ + jid = PeerBareJID, + subscription = Subscription, + ask = Ask, + groups = [], + name = <<"">> + }, ct:comment("Receiving roster push"), Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} = - recv_iq(Config), + recv_iq(Config), case Item of - Match -> send(Config, make_iq_result(Push)); - _ -> match_failure(Item, Match) + Match -> send(Config, make_iq_result(Push)); + _ -> match_failure(Item, Match) end. + recv_presence(Config, Type) -> PeerJID = ?config(peer, Config), case recv_presence(Config) of - #presence{from = PeerJID, type = Type} -> ok; - Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) + #presence{from = PeerJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) end. + recv_subscription(Config, Type) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), case recv_presence(Config) of - #presence{from = PeerBareJID, type = Type} -> ok; - Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) + #presence{from = PeerBareJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) end. + pp(Term) -> io_lib_pretty:print(Term, fun pp/2). + pp(state, N) -> Fs = record_info(fields, state), - try N = length(Fs), Fs - catch _:_ -> no end; + try + N = length(Fs), Fs + catch + _:_ -> no + end; pp(roster, N) -> Fs = record_info(fields, roster), - try N = length(Fs), Fs - catch _:_ -> no end; + try + N = length(Fs), Fs + catch + _:_ -> no + end; pp(_, _) -> no. + mk_id(N, Dir, Type) -> - list_to_binary([integer_to_list(N), $-, atom_to_list(Dir), - $-, atom_to_list(Type)]). + list_to_binary([integer_to_list(N), + $-, + atom_to_list(Dir), + $-, + atom_to_list(Type)]). + check_roster([], _Config, _State) -> ok; check_roster([Roster], _Config, State) -> case {Roster#roster.subscription == State#state.subscription, - Roster#roster.ask, State#state.pending_in, State#state.pending_out} of - {true, both, true, true} -> ok; - {true, in, true, false} -> ok; - {true, out, false, true} -> ok; - {true, none, false, false} -> ok; - _ -> - ct:fail({roster_mismatch, State, Roster}) + Roster#roster.ask, + State#state.pending_in, + State#state.pending_out} of + {true, both, true, true} -> ok; + {true, in, true, false} -> ok; + {true, out, false, true} -> ok; + {true, none, false, false} -> ok; + _ -> + ct:fail({roster_mismatch, State, Roster}) end. + check_roster_item(Config, State) -> Peer = jid:remove_resource(?config(peer, Config)), RosterItem = case get_item(Config, Peer) of - false -> #roster_item{}; - Item -> Item - end, + false -> #roster_item{}; + Item -> Item + end, case {RosterItem#roster_item.subscription == State#state.subscription, - RosterItem#roster_item.ask, State#state.pending_out} of - {true, subscribe, true} -> ok; - {true, undefined, false} -> ok; - _ -> ct:fail({roster_item_mismatch, State, RosterItem}) + RosterItem#roster_item.ask, + State#state.pending_out} of + {true, subscribe, true} -> ok; + {true, undefined, false} -> ok; + _ -> ct:fail({roster_item_mismatch, State, RosterItem}) end. + %% RFC6121, A.2.1 -transition(Id, Config, out, subscribe, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(Id, + Config, + out, + subscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = subscribe}), case {Sub, Out, In} of - {none, false, _} -> - recv_push(Config, none, subscribe), - State#state{pending_out = true}; - {none, true, false} -> - %% BUG: we should not receive roster push here - recv_push(Config, none, subscribe), - State; - {from, false, false} -> - recv_push(Config, from, subscribe), - State#state{pending_out = true}; - _ -> - State + {none, false, _} -> + recv_push(Config, none, subscribe), + State#state{pending_out = true}; + {none, true, false} -> + %% BUG: we should not receive roster push here + recv_push(Config, none, subscribe), + State; + {from, false, false} -> + recv_push(Config, from, subscribe), + State#state{pending_out = true}; + _ -> + State end; %% RFC6121, A.2.2 -transition(Id, Config, out, unsubscribe, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(Id, + Config, + out, + unsubscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribe}), case {Sub, Out, In} of - {none, true, _} -> - recv_push(Config, none, undefined), - State#state{pending_out = false}; - {to, false, _} -> - recv_push(Config, none, undefined), - recv_presence(Config, unavailable), - State#state{subscription = none, peer_available = false}; - {from, true, false} -> - recv_push(Config, from, undefined), - State#state{pending_out = false}; - {both, false, false} -> - recv_push(Config, from, undefined), - recv_presence(Config, unavailable), - State#state{subscription = from, peer_available = false}; - _ -> - State + {none, true, _} -> + recv_push(Config, none, undefined), + State#state{pending_out = false}; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + State#state{pending_out = false}; + {both, false, false} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State end; %% RFC6121, A.2.3 -transition(Id, Config, out, subscribed, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(Id, + Config, + out, + subscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = subscribed}), case {Sub, Out, In} of - {none, false, true} -> - recv_push(Config, from, undefined), - State#state{subscription = from, pending_in = false}; - {none, true, true} -> - recv_push(Config, from, subscribe), - State#state{subscription = from, pending_in = false}; - {to, false, true} -> - recv_push(Config, both, undefined), - State#state{subscription = both, pending_in = false}; - {to, false, _} -> - %% BUG: we should not transition to 'both' state - recv_push(Config, both, undefined), - State#state{subscription = both}; - _ -> - State + {none, false, true} -> + recv_push(Config, from, undefined), + State#state{subscription = from, pending_in = false}; + {none, true, true} -> + recv_push(Config, from, subscribe), + State#state{subscription = from, pending_in = false}; + {to, false, true} -> + recv_push(Config, both, undefined), + State#state{subscription = both, pending_in = false}; + {to, false, _} -> + %% BUG: we should not transition to 'both' state + recv_push(Config, both, undefined), + State#state{subscription = both}; + _ -> + State end; %% RFC6121, A.2.4 -transition(Id, Config, out, unsubscribed, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(Id, + Config, + out, + unsubscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribed}), case {Sub, Out, In} of - {none, false, true} -> - State#state{subscription = none, pending_in = false}; - {none, true, true} -> - recv_push(Config, none, subscribe), - State#state{subscription = none, pending_in = false}; - {to, _, true} -> - State#state{pending_in = false}; - {from, false, _} -> - recv_push(Config, none, undefined), - State#state{subscription = none}; - {from, true, _} -> - recv_push(Config, none, subscribe), - State#state{subscription = none}; - {both, _, _} -> - recv_push(Config, to, undefined), - State#state{subscription = to}; - _ -> - State + {none, false, true} -> + State#state{subscription = none, pending_in = false}; + {none, true, true} -> + recv_push(Config, none, subscribe), + State#state{subscription = none, pending_in = false}; + {to, _, true} -> + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + State#state{subscription = to}; + _ -> + State end; %% RFC6121, A.3.1 -transition(_, Config, in, subscribe = Type, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(_, + Config, + in, + subscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of - {none, false, false} -> - recv_subscription(Config, Type), - State#state{pending_in = true}; - {none, true, false} -> - recv_push(Config, none, subscribe), - recv_subscription(Config, Type), - State#state{pending_in = true}; - {to, false, false} -> - %% BUG: we should not receive roster push in this state! - recv_push(Config, to, undefined), - recv_subscription(Config, Type), - State#state{pending_in = true}; - _ -> - State + {none, false, false} -> + recv_subscription(Config, Type), + State#state{pending_in = true}; + {none, true, false} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{pending_in = true}; + {to, false, false} -> + %% BUG: we should not receive roster push in this state! + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = true}; + _ -> + State end; %% RFC6121, A.3.2 -transition(_, Config, in, unsubscribe = Type, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(_, + Config, + in, + unsubscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of - {none, _, true} -> - State#state{pending_in = false}; - {to, _, true} -> - recv_push(Config, to, undefined), - recv_subscription(Config, Type), - State#state{pending_in = false}; - {from, false, _} -> - recv_push(Config, none, undefined), - recv_subscription(Config, Type), - State#state{subscription = none}; - {from, true, _} -> - recv_push(Config, none, subscribe), - recv_subscription(Config, Type), - State#state{subscription = none}; - {both, _, _} -> - recv_push(Config, to, undefined), - recv_subscription(Config, Type), - State#state{subscription = to}; - _ -> - State + {none, _, true} -> + State#state{pending_in = false}; + {to, _, true} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{subscription = to}; + _ -> + State end; %% RFC6121, A.3.3 -transition(_, Config, in, subscribed = Type, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(_, + Config, + in, + subscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of - {none, true, _} -> - recv_push(Config, to, undefined), - recv_subscription(Config, Type), - recv_presence(Config, available), - State#state{subscription = to, pending_out = false, peer_available = true}; - {from, true, _} -> - recv_push(Config, both, undefined), - recv_subscription(Config, Type), - recv_presence(Config, available), - State#state{subscription = both, pending_out = false, peer_available = true}; - {from, false, _} -> - %% BUG: we should not transition to 'both' in this state - recv_push(Config, both, undefined), - recv_subscription(Config, Type), - recv_presence(Config, available), - State#state{subscription = both, pending_out = false, peer_available = true}; - _ -> - State + {none, true, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = to, pending_out = false, peer_available = true}; + {from, true, _} -> + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + {from, false, _} -> + %% BUG: we should not transition to 'both' in this state + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + _ -> + State end; %% RFC6121, A.3.4 -transition(_, Config, in, unsubscribed = Type, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(_, + Config, + in, + unsubscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of - {none, true, true} -> - %% BUG: we should receive roster push in this state! - recv_subscription(Config, Type), - State#state{subscription = none, pending_out = false}; - {none, true, false} -> - recv_push(Config, none, undefined), - recv_subscription(Config, Type), - State#state{subscription = none, pending_out = false}; - {none, false, false} -> - State; - {to, false, _} -> - recv_push(Config, none, undefined), - recv_presence(Config, unavailable), - recv_subscription(Config, Type), - State#state{subscription = none, peer_available = false}; - {from, true, false} -> - recv_push(Config, from, undefined), - recv_subscription(Config, Type), - State#state{subscription = from, pending_out = false}; - {both, _, _} -> - recv_push(Config, from, undefined), - recv_presence(Config, unavailable), - recv_subscription(Config, Type), - State#state{subscription = from, peer_available = false}; - _ -> - State + {none, true, true} -> + %% BUG: we should receive roster push in this state! + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, true, false} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, false, false} -> + State; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, Type), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + State#state{subscription = from, pending_out = false}; + {both, _, _} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, Type), + State#state{subscription = from, peer_available = false}; + _ -> + State end; %% Outgoing roster remove -transition(Id, Config, out, remove, - #state{subscription = Sub, pending_in = In, pending_out = Out}) -> +transition(Id, + Config, + out, + remove, + #state{subscription = Sub, pending_in = In, pending_out = Out}) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), Item = #roster_item{jid = PeerBareJID, subscription = remove}, #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, id = Id, - sub_els = [#roster_query{items = [Item]}]}), + send_recv(Config, + #iq{ + type = set, + id = Id, + sub_els = [#roster_query{items = [Item]}] + }), recv_push(Config, remove, undefined), case {Sub, Out, In} of - {to, _, _} -> - recv_presence(Config, unavailable); - {both, _, _} -> - recv_presence(Config, unavailable); - _ -> - ok + {to, _, _} -> + recv_presence(Config, unavailable); + {both, _, _} -> + recv_presence(Config, unavailable); + _ -> + ok end, #state{}; %% Incoming roster remove -transition(_, Config, in, remove, - #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> +transition(_, + Config, + in, + remove, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of - {none, true, _} -> - ok; - {from, false, _} -> - recv_push(Config, none, undefined), - recv_subscription(Config, unsubscribe); - {from, true, _} -> - recv_push(Config, none, subscribe), - recv_subscription(Config, unsubscribe); - {to, false, _} -> - %% BUG: we should receive push here - %% recv_push(Config, none, undefined), - recv_presence(Config, unavailable), - recv_subscription(Config, unsubscribed); - {both, _, _} -> - recv_presence(Config, unavailable), - recv_push(Config, to, undefined), - recv_subscription(Config, unsubscribe), - recv_push(Config, none, undefined), - recv_subscription(Config, unsubscribed); - _ -> - ok + {none, true, _} -> + ok; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribe); + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, unsubscribe); + {to, false, _} -> + %% BUG: we should receive push here + %% recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, unsubscribed); + {both, _, _} -> + recv_presence(Config, unavailable), + recv_push(Config, to, undefined), + recv_subscription(Config, unsubscribe), + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribed); + _ -> + ok end, State#state{subscription = none}. + actions() -> - States = [{Dir, Type} || Dir <- [out, in], - Type <- [subscribe, subscribed, - unsubscribe, unsubscribed, - remove]], - Actions = lists:flatten([[X, Y] || X <- States, Y <- States]), + States = [ {Dir, Type} || Dir <- [out, in], + Type <- [subscribe, subscribed, + unsubscribe, unsubscribed, + remove] ], + Actions = lists:flatten([ [X, Y] || X <- States, Y <- States ]), remove_dups(Actions, []). -remove_dups([X|T], [X,X|_] = Acc) -> + +remove_dups([X | T], [X, X | _] = Acc) -> remove_dups(T, Acc); -remove_dups([X|T], Acc) -> - remove_dups(T, [X|Acc]); +remove_dups([X | T], Acc) -> + remove_dups(T, [X | Acc]); remove_dups([], Acc) -> lists:reverse(Acc). diff --git a/test/sm_tests.erl b/test/sm_tests.erl index a55957856..e69e93954 100644 --- a/test/sm_tests.erl +++ b/test/sm_tests.erl @@ -25,12 +25,21 @@ %% API -compile(export_all). --import(suite, [send/2, recv/1, close_socket/1, set_opt/3, my_jid/1, - recv_message/1, disconnect/1, send_recv/2, - put_event/2, get_event/1]). +-import(suite, + [send/2, + recv/1, + close_socket/1, + set_opt/3, + my_jid/1, + recv_message/1, + disconnect/1, + send_recv/2, + put_event/2, + get_event/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,21 +48,26 @@ %%%=================================================================== single_cases() -> {sm_single, [sequence], - [single_test(feature_enabled), - single_test(enable), - single_test(resume), - single_test(resume_failed)]}. + [single_test(feature_enabled), + single_test(enable), + single_test(resume), + single_test(resume_failed)]}. + feature_enabled(Config) -> true = ?config(sm, Config), disconnect(Config). + enable(Config) -> Server = ?config(server, Config), ServerJID = jid:make(<<"">>, Server, <<"">>), ct:comment("Send messages of type 'headline' so the server discards them silently"), - Msg = #message{to = ServerJID, type = headline, - body = [#text{data = <<"body">>}]}, + Msg = #message{ + to = ServerJID, + type = headline, + body = [#text{data = <<"body">>}] + }, ct:comment("Enable the session management with resumption enabled"), send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), #sm_enabled{id = ID, resume = true} = recv(Config), @@ -70,6 +84,7 @@ enable(Config) -> close_socket(Config), {save_config, set_opt(sm_previd, ID, Config)}. + resume(Config) -> {_, SMConfig} = ?config(saved_config, Config), ID = ?config(sm_previd, SMConfig), @@ -96,6 +111,7 @@ resume(Config) -> close_socket(Config), {save_config, set_opt(sm_previd, ID, Config)}. + resume_failed(Config) -> {_, SMConfig} = ?config(saved_config, Config), ID = ?config(sm_previd, SMConfig), @@ -106,13 +122,15 @@ resume_failed(Config) -> #sm_failed{reason = 'item-not-found', h = 4} = recv(Config), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {sm_master_slave, [sequence], - [master_slave_test(queue_limit), - master_slave_test(queue_limit_detached)]}. + [master_slave_test(queue_limit), + master_slave_test(queue_limit_detached)]}. + queue_limit_master(Config) -> ct:comment("Waiting for 'send' command from the peer"), @@ -122,6 +140,7 @@ queue_limit_master(Config) -> peer_down = get_event(Config), disconnect(Config). + queue_limit_slave(Config) -> ct:comment("Enable the session management without resumption"), send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3}), @@ -130,10 +149,11 @@ queue_limit_slave(Config) -> ct:comment("Receiving all messages"), lists:foreach( fun(I) -> - ID = integer_to_binary(I), - Body = xmpp:mk_text(ID), - #message{id = ID, body = Body} = recv_message(Config) - end, lists:seq(1, 11)), + ID = integer_to_binary(I), + Body = xmpp:mk_text(ID), + #message{id = ID, body = Body} = recv_message(Config) + end, + lists:seq(1, 11)), ct:comment("Receiving request ACK"), #sm_r{} = recv(Config), ct:comment("Receiving policy-violation stream error"), @@ -142,12 +162,14 @@ queue_limit_slave(Config) -> ct:comment("Closing socket"), close_socket(Config). + queue_limit_detached_master(Config) -> ct:comment("Waiting for the peer to disconnect"), peer_down = get_event(Config), send_recv_messages(Config), disconnect(Config). + queue_limit_detached_slave(Config) -> #presence{} = send_recv(Config, #presence{}), ct:comment("Enable the session management with resumption enabled"), @@ -156,30 +178,36 @@ queue_limit_detached_slave(Config) -> ct:comment("Closing socket"), close_socket(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("sm_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("sm_" ++ atom_to_list(T)), [parallel], + {list_to_atom("sm_" ++ atom_to_list(T)), + [parallel], [list_to_atom("sm_" ++ atom_to_list(T) ++ "_master"), list_to_atom("sm_" ++ atom_to_list(T) ++ "_slave")]}. + send_recv_messages(Config) -> PeerJID = ?config(peer, Config), Msg = #message{to = PeerJID}, ct:comment("Sending messages to peer"), lists:foreach( fun(I) -> - ID = integer_to_binary(I), - send(Config, Msg#message{id = ID, body = xmpp:mk_text(ID)}) - end, lists:seq(1, 11)), + ID = integer_to_binary(I), + send(Config, Msg#message{id = ID, body = xmpp:mk_text(ID)}) + end, + lists:seq(1, 11)), ct:comment("Receiving bounced messages from the peer"), lists:foreach( fun(I) -> - ID = integer_to_binary(I), - Err = #message{id = ID, type = error} = recv_message(Config), - #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) - end, lists:seq(1, 11)). + ID = integer_to_binary(I), + Err = #message{id = ID, type = error} = recv_message(Config), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, + lists:seq(1, 11)). diff --git a/test/stundisco_tests.erl b/test/stundisco_tests.erl index ca941983f..49d064734 100644 --- a/test/stundisco_tests.erl +++ b/test/stundisco_tests.erl @@ -25,11 +25,15 @@ %% API -compile(export_all). --import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, - server_jid/1]). +-import(suite, + [send_recv/2, + disconnect/1, + is_feature_advertised/2, + server_jid/1]). -include("suite.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -38,17 +42,19 @@ %%%=================================================================== single_cases() -> {stundisco_single, [sequence], - [single_test(feature_enabled), - single_test(stun_service), - single_test(turn_service), - single_test(turns_service), - single_test(turn_credentials), - single_test(turns_credentials)]}. + [single_test(feature_enabled), + single_test(stun_service), + single_test(turn_service), + single_test(turns_service), + single_test(turn_credentials), + single_test(turns_credentials)]}. + feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_EXTDISCO_2), disconnect(Config). + stun_service(Config) -> ServerJID = server_jid(Config), Host = {203, 0, 113, 3}, @@ -56,22 +62,28 @@ stun_service(Config) -> Type = stun, Transport = udp, Request = #services{type = Type}, - #iq{type = result, - sub_els = [#services{ - type = undefined, - list = [#service{host = Host, - port = Port, - type = Type, - transport = Transport, - restricted = false, - username = <<>>, - password = <<>>, - expires = undefined, - action = undefined, - xdata = undefined}]}]} = - send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + #iq{ + type = result, + sub_els = [#services{ + type = undefined, + list = [#service{ + host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = false, + username = <<>>, + password = <<>>, + expires = undefined, + action = undefined, + xdata = undefined + }] + }] + } = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), disconnect(Config). + turn_service(Config) -> ServerJID = server_jid(Config), Host = {203, 0, 113, 3}, @@ -79,24 +91,30 @@ turn_service(Config) -> Type = turn, Transport = udp, Request = #services{type = Type}, - #iq{type = result, - sub_els = [#services{ - type = undefined, - list = [#service{host = Host, - port = Port, - type = Type, - transport = Transport, - restricted = true, - username = Username, - password = Password, - expires = Expires, - action = undefined, - xdata = undefined}]}]} = - send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + #iq{ + type = result, + sub_els = [#services{ + type = undefined, + list = [#service{ + host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined + }] + }] + } = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). + turns_service(Config) -> ServerJID = server_jid(Config), Host = <<"example.com">>, @@ -104,86 +122,114 @@ turns_service(Config) -> Type = turns, Transport = tcp, Request = #services{type = Type}, - #iq{type = result, - sub_els = [#services{ - type = undefined, - list = [#service{host = Host, - port = Port, - type = Type, - transport = Transport, - restricted = true, - username = Username, - password = Password, - expires = Expires, - action = undefined, - xdata = undefined}]}]} = - send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + #iq{ + type = result, + sub_els = [#services{ + type = undefined, + list = [#service{ + host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined + }] + }] + } = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). + turn_credentials(Config) -> ServerJID = server_jid(Config), Host = {203, 0, 113, 3}, Port = ct:get_config(stun_port, 3478), Type = turn, Transport = udp, - Request = #credentials{services = [#service{host = Host, - port = Port, - type = Type}]}, - #iq{type = result, - sub_els = [#credentials{ - services = [#service{host = Host, - port = Port, - type = Type, - transport = Transport, - restricted = true, - username = Username, - password = Password, - expires = Expires, - action = undefined, - xdata = undefined}]}]} = - send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + Request = #credentials{ + services = [#service{ + host = Host, + port = Port, + type = Type + }] + }, + #iq{ + type = result, + sub_els = [#credentials{ + services = [#service{ + host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined + }] + }] + } = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). + turns_credentials(Config) -> ServerJID = server_jid(Config), Host = <<"example.com">>, Port = 5349, Type = turns, Transport = tcp, - Request = #credentials{services = [#service{host = Host, - port = Port, - type = Type}]}, - #iq{type = result, - sub_els = [#credentials{ - services = [#service{host = Host, - port = Port, - type = Type, - transport = Transport, - restricted = true, - username = Username, - password = Password, - expires = Expires, - action = undefined, - xdata = undefined}]}]} = - send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), + Request = #credentials{ + services = [#service{ + host = Host, + port = Port, + type = Type + }] + }, + #iq{ + type = result, + sub_els = [#credentials{ + services = [#service{ + host = Host, + port = Port, + type = Type, + transport = Transport, + restricted = true, + username = Username, + password = Password, + expires = Expires, + action = undefined, + xdata = undefined + }] + }] + } = + send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("stundisco_" ++ atom_to_list(T)). + check_password(Username, Password) -> Secret = <<"cryptic">>, Password == base64:encode(misc:crypto_hmac(sha, Secret, Username)). + check_expires({_, _, _} = Expires) -> Now = {MegaSecs, Secs, MicroSecs} = erlang:timestamp(), Later = {MegaSecs + 1, Secs, MicroSecs}, diff --git a/test/suite.erl b/test/suite.erl index 5fbd70463..40f2ac086 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -27,16 +27,19 @@ -compile(export_all). -include("suite.hrl"). + -include_lib("kernel/include/file.hrl"). + -include("mod_roster.hrl"). + %%%=================================================================== %%% API %%%=================================================================== init_config(Config) -> DataDir = proplists:get_value(data_dir, Config), PrivDir = proplists:get_value(priv_dir, Config), - [_, _|Tail] = lists:reverse(filename:split(DataDir)), + [_, _ | Tail] = lists:reverse(filename:split(DataDir)), BaseDir = filename:join(lists:reverse(Tail)), MacrosPathTpl = filename:join([DataDir, "macros.yml"]), ConfigPath = filename:join([DataDir, "ejabberd.yml"]), @@ -49,7 +52,7 @@ init_config(Config) -> {ok, CWD} = file:get_cwd(), {ok, _} = file:copy(CertFile, filename:join([CWD, "cert.pem"])), {ok, _} = file:copy(SelfSignedCertFile, - filename:join([CWD, "self-signed-cert.pem"])), + filename:join([CWD, "self-signed-cert.pem"])), {ok, _} = file:copy(CAFile, filename:join([CWD, "ca.pem"])), copy_file(Config, "spam_jids.txt"), copy_file(Config, "spam_urls.txt"), @@ -60,49 +63,49 @@ init_config(Config) -> Password = <<"password!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>, Backends = get_config_backends(), MacrosContent = process_config_tpl( - MacrosContentTpl, - [{c2s_port, 5222}, - {loglevel, 4}, - {new_schema, false}, - {update_sql_schema, true}, - {s2s_port, 5269}, - {stun_port, 3478}, - {component_port, 5270}, - {web_port, 5280}, - {proxy_port, 7777}, - {password, Password}, - {mysql_server, <<"localhost">>}, - {mysql_port, 3306}, - {mysql_db, <<"ejabberd_test">>}, - {mysql_user, <<"ejabberd_test">>}, - {mysql_pass, <<"ejabberd_test">>}, - {mssql_server, <<"localhost">>}, - {mssql_port, 1433}, - {mssql_db, <<"ejabberd_test">>}, - {mssql_user, <<"ejabberd_test">>}, - {mssql_pass, <<"ejabberd_Test1">>}, - {pgsql_server, <<"localhost">>}, - {pgsql_port, 5432}, - {pgsql_db, <<"ejabberd_test">>}, - {pgsql_user, <<"ejabberd_test">>}, - {pgsql_pass, <<"ejabberd_test">>}, - {priv_dir, PrivDir}]), + MacrosContentTpl, + [{c2s_port, 5222}, + {loglevel, 4}, + {new_schema, false}, + {update_sql_schema, true}, + {s2s_port, 5269}, + {stun_port, 3478}, + {component_port, 5270}, + {web_port, 5280}, + {proxy_port, 7777}, + {password, Password}, + {mysql_server, <<"localhost">>}, + {mysql_port, 3306}, + {mysql_db, <<"ejabberd_test">>}, + {mysql_user, <<"ejabberd_test">>}, + {mysql_pass, <<"ejabberd_test">>}, + {mssql_server, <<"localhost">>}, + {mssql_port, 1433}, + {mssql_db, <<"ejabberd_test">>}, + {mssql_user, <<"ejabberd_test">>}, + {mssql_pass, <<"ejabberd_Test1">>}, + {pgsql_server, <<"localhost">>}, + {pgsql_port, 5432}, + {pgsql_db, <<"ejabberd_test">>}, + {pgsql_user, <<"ejabberd_test">>}, + {pgsql_pass, <<"ejabberd_test">>}, + {priv_dir, PrivDir}]), MacrosPath = filename:join([CWD, "macros.yml"]), ok = file:write_file(MacrosPath, MacrosContent), copy_configtest_yml(DataDir, CWD), copy_backend_configs(DataDir, CWD, Backends), setup_ejabberd_lib_path(Config), case application:load(sasl) of - ok -> ok; - {error, {already_loaded, _}} -> ok + ok -> ok; + {error, {already_loaded, _}} -> ok end, case application:load(mnesia) of - ok -> ok; - {error, {already_loaded, _}} -> ok + ok -> ok; + {error, {already_loaded, _}} -> ok end, case application:load(ejabberd) of - ok -> ok; - {error, {already_loaded, _}} -> ok + ok -> ok; + {error, {already_loaded, _}} -> ok end, application:set_env(ejabberd, config, ConfigPath), application:set_env(ejabberd, log_path, LogPath), @@ -140,96 +143,105 @@ init_config(Config) -> {slave_resource, <<"slave_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {update_sql, false}, {password, Password}, - {backends, Backends} - |Config]. + {backends, Backends} | Config]. + copy_file(Config, File) -> {ok, CWD} = file:get_cwd(), DataDir = proplists:get_value(data_dir, Config), {ok, _} = file:copy(filename:join([DataDir, File]), filename:join([CWD, File])). + copy_configtest_yml(DataDir, CWD) -> Files = filelib:wildcard(filename:join([DataDir, "configtest.yml"])), lists:foreach( - fun(Src) -> - ct:pal("copying ~p", [Src]), - File = filename:basename(Src), - case string:tokens(File, ".") of - ["configtest", "yml"] -> - Dst = filename:join([CWD, File]), - case true of - true -> - {ok, _} = file:copy(Src, Dst); - false -> - ok - end; - _ -> - ok - end - end, Files). + fun(Src) -> + ct:pal("copying ~p", [Src]), + File = filename:basename(Src), + case string:tokens(File, ".") of + ["configtest", "yml"] -> + Dst = filename:join([CWD, File]), + case true of + true -> + {ok, _} = file:copy(Src, Dst); + false -> + ok + end; + _ -> + ok + end + end, + Files). copy_backend_configs(DataDir, CWD, Backends) -> Files = filelib:wildcard(filename:join([DataDir, "ejabberd.*.yml"])), lists:foreach( - fun(Src) -> - ct:pal("copying ~p", [Src]), - File = filename:basename(Src), - case string:tokens(File, ".") of - ["ejabberd", SBackend, "yml"] -> - Backend = list_to_atom(SBackend), - Macro = list_to_atom(string:to_upper(SBackend) ++ "_CONFIG"), - Dst = filename:join([CWD, File]), - case lists:member(Backend, Backends) of - true -> - {ok, _} = file:copy(Src, Dst); - false -> - ok = file:write_file( - Dst, fast_yaml:encode( - [{define_macro, [{Macro, []}]}])) - end; - _ -> - ok - end - end, Files). + fun(Src) -> + ct:pal("copying ~p", [Src]), + File = filename:basename(Src), + case string:tokens(File, ".") of + ["ejabberd", SBackend, "yml"] -> + Backend = list_to_atom(SBackend), + Macro = list_to_atom(string:to_upper(SBackend) ++ "_CONFIG"), + Dst = filename:join([CWD, File]), + case lists:member(Backend, Backends) of + true -> + {ok, _} = file:copy(Src, Dst); + false -> + ok = file:write_file( + Dst, + fast_yaml:encode( + [{define_macro, [{Macro, []}]}])) + end; + _ -> + ok + end + end, + Files). + find_top_dir(Dir) -> case file:read_file_info(filename:join([Dir, ebin])) of - {ok, #file_info{type = directory}} -> - Dir; - _ -> - find_top_dir(filename:dirname(Dir)) + {ok, #file_info{type = directory}} -> + Dir; + _ -> + find_top_dir(filename:dirname(Dir)) end. + setup_ejabberd_lib_path(Config) -> case code:lib_dir(ejabberd) of - {error, _} -> - DataDir = proplists:get_value(data_dir, Config), - {ok, CWD} = file:get_cwd(), - NewEjPath = filename:join([CWD, "ejabberd-0.0.1"]), - TopDir = find_top_dir(DataDir), - ok = file:make_symlink(TopDir, NewEjPath), - code:replace_path(ejabberd, NewEjPath); - _ -> - ok + {error, _} -> + DataDir = proplists:get_value(data_dir, Config), + {ok, CWD} = file:get_cwd(), + NewEjPath = filename:join([CWD, "ejabberd-0.0.1"]), + TopDir = find_top_dir(DataDir), + ok = file:make_symlink(TopDir, NewEjPath), + code:replace_path(ejabberd, NewEjPath); + _ -> + ok end. + %% Read environment variable CT_DB=mysql to limit the backends to test. %% You can thus limit the backend you want to test with: %% CT_BACKENDS=mysql rebar ct suites=ejabberd get_config_backends() -> EnvBackends = case os:getenv("CT_BACKENDS") of - false -> ?BACKENDS; - String -> - Backends0 = string:tokens(String, ","), - lists:map( - fun(Backend) -> - list_to_atom(string:strip(Backend, both, $ )) - end, Backends0) - end, + false -> ?BACKENDS; + String -> + Backends0 = string:tokens(String, ","), + lists:map( + fun(Backend) -> + list_to_atom(string:strip(Backend, both, $ )) + end, + Backends0) + end, application:load(ejabberd), EnabledBackends = application:get_env(ejabberd, enabled_backends, EnvBackends), - misc:intersection(EnvBackends, [mnesia, ldap, extauth|EnabledBackends]). + misc:intersection(EnvBackends, [mnesia, ldap, extauth | EnabledBackends]). + process_config_tpl(Content, []) -> Content; @@ -243,97 +255,110 @@ process_config_tpl(Content, [{Name, DefaultValue} | Rest]) -> iolist_to_binary(V) end, NewContent = binary:replace(Content, - <<"@@",(atom_to_binary(Name,latin1))/binary, "@@">>, - Val, [global]), + <<"@@", (atom_to_binary(Name, latin1))/binary, "@@">>, + Val, + [global]), process_config_tpl(NewContent, Rest). + stream_header(Config) -> To = case ?config(server, Config) of - <<"">> -> undefined; - Server -> jid:make(Server) - end, + <<"">> -> undefined; + Server -> jid:make(Server) + end, From = case ?config(stream_from, Config) of - <<"">> -> undefined; - Frm -> jid:make(Frm) - end, - #stream_start{to = To, - from = From, - lang = ?config(lang, Config), - version = ?config(stream_version, Config), - xmlns = ?config(xmlns, Config), - db_xmlns = ?config(db_xmlns, Config), - stream_xmlns = ?config(ns_stream, Config)}. + <<"">> -> undefined; + Frm -> jid:make(Frm) + end, + #stream_start{ + to = To, + from = From, + lang = ?config(lang, Config), + version = ?config(stream_version, Config), + xmlns = ?config(xmlns, Config), + db_xmlns = ?config(db_xmlns, Config), + stream_xmlns = ?config(ns_stream, Config) + }. + connect(Config) -> NewConfig = init_stream(Config), case ?config(type, NewConfig) of - client -> process_stream_features(NewConfig); - server -> process_stream_features(NewConfig); - component -> NewConfig + client -> process_stream_features(NewConfig); + server -> process_stream_features(NewConfig); + component -> NewConfig end. + tcp_connect(Config) -> case ?config(receiver, Config) of - undefined -> - Owner = self(), - NS = case ?config(type, Config) of - client -> ?NS_CLIENT; - server -> ?NS_SERVER; - component -> ?NS_COMPONENT - end, - Server = ?config(server_host, Config), - Port = ?config(server_port, Config), - ReceiverPid = spawn(fun() -> - start_receiver(NS, Owner, Server, Port) - end), - set_opt(receiver, ReceiverPid, Config); - _ -> - Config + undefined -> + Owner = self(), + NS = case ?config(type, Config) of + client -> ?NS_CLIENT; + server -> ?NS_SERVER; + component -> ?NS_COMPONENT + end, + Server = ?config(server_host, Config), + Port = ?config(server_port, Config), + ReceiverPid = spawn(fun() -> + start_receiver(NS, Owner, Server, Port) + end), + set_opt(receiver, ReceiverPid, Config); + _ -> + Config end. + init_stream(Config) -> Version = ?config(stream_version, Config), NewConfig = tcp_connect(Config), send(NewConfig, stream_header(NewConfig)), XMLNS = case ?config(type, Config) of - client -> ?NS_CLIENT; - component -> ?NS_COMPONENT; - server -> ?NS_SERVER - end, + client -> ?NS_CLIENT; + component -> ?NS_COMPONENT; + server -> ?NS_SERVER + end, receive - #stream_start{id = ID, xmlns = XMLNS, version = Version} -> - set_opt(stream_id, ID, NewConfig) + #stream_start{id = ID, xmlns = XMLNS, version = Version} -> + set_opt(stream_id, ID, NewConfig) end. + process_stream_features(Config) -> receive - #stream_features{sub_els = Fs} -> - Mechs = lists:flatmap( - fun(#sasl_mechanisms{list = Ms}) -> - Ms; - (_) -> - [] - end, Fs), - lists:foldl( - fun(#feature_register{}, Acc) -> - set_opt(register, true, Acc); - (#starttls{}, Acc) -> - set_opt(starttls, true, Acc); - (#legacy_auth_feature{}, Acc) -> - set_opt(legacy_auth, true, Acc); - (#compression{methods = Ms}, Acc) -> - set_opt(compression, Ms, Acc); - (_, Acc) -> - Acc - end, set_opt(mechs, Mechs, Config), Fs) + #stream_features{sub_els = Fs} -> + Mechs = lists:flatmap( + fun(#sasl_mechanisms{list = Ms}) -> + Ms; + (_) -> + [] + end, + Fs), + lists:foldl( + fun(#feature_register{}, Acc) -> + set_opt(register, true, Acc); + (#starttls{}, Acc) -> + set_opt(starttls, true, Acc); + (#legacy_auth_feature{}, Acc) -> + set_opt(legacy_auth, true, Acc); + (#compression{methods = Ms}, Acc) -> + set_opt(compression, Ms, Acc); + (_, Acc) -> + Acc + end, + set_opt(mechs, Mechs, Config), + Fs) end. + disconnect(Config) -> ct:comment("Disconnecting"), try - send_text(Config, ?STREAM_TRAILER) - catch exit:normal -> - ok + send_text(Config, ?STREAM_TRAILER) + catch + exit:normal -> + ok end, receive {xmlstreamend, <<"stream:stream">>} -> ok end, flush(Config), @@ -341,36 +366,42 @@ disconnect(Config) -> ct:comment("Disconnected"), set_opt(receiver, undefined, Config). + close_socket(Config) -> ok = recv_call(Config, close), Config. + starttls(Config) -> starttls(Config, false). + starttls(Config, ShouldFail) -> send(Config, #starttls{}), receive - #starttls_proceed{} when ShouldFail -> - ct:fail(starttls_should_have_failed); - #starttls_failure{} when ShouldFail -> - Config; - #starttls_failure{} -> - ct:fail(starttls_failed); - #starttls_proceed{} -> - ok = recv_call(Config, {starttls, ?config(certfile, Config)}), - Config + #starttls_proceed{} when ShouldFail -> + ct:fail(starttls_should_have_failed); + #starttls_failure{} when ShouldFail -> + Config; + #starttls_failure{} -> + ct:fail(starttls_failed); + #starttls_proceed{} -> + ok = recv_call(Config, {starttls, ?config(certfile, Config)}), + Config end. + zlib(Config) -> send(Config, #compress{methods = [<<"zlib">>]}), receive #compressed{} -> ok end, ok = recv_call(Config, compress), process_stream_features(init_stream(Config)). + auth(Config) -> auth(Config, false). + auth(Config, ShouldFail) -> Type = ?config(type, Config), IsAnonymous = ?config(anonymous, Config), @@ -379,228 +410,279 @@ auth(Config, ShouldFail) -> HavePLAIN = lists:member(<<"PLAIN">>, Mechs), HaveExternal = lists:member(<<"EXTERNAL">>, Mechs), HaveAnonymous = lists:member(<<"ANONYMOUS">>, Mechs), - if HaveAnonymous and IsAnonymous -> - auth_SASL(<<"ANONYMOUS">>, Config, ShouldFail); - HavePLAIN -> + if + HaveAnonymous and IsAnonymous -> + auth_SASL(<<"ANONYMOUS">>, Config, ShouldFail); + HavePLAIN -> auth_SASL(<<"PLAIN">>, Config, ShouldFail); - HaveMD5 -> + HaveMD5 -> auth_SASL(<<"DIGEST-MD5">>, Config, ShouldFail); - HaveExternal -> - auth_SASL(<<"EXTERNAL">>, Config, ShouldFail); - Type == client -> - auth_legacy(Config, false, ShouldFail); - Type == component -> - auth_component(Config, ShouldFail); - true -> - ct:fail(no_known_sasl_mechanism_available) + HaveExternal -> + auth_SASL(<<"EXTERNAL">>, Config, ShouldFail); + Type == client -> + auth_legacy(Config, false, ShouldFail); + Type == component -> + auth_component(Config, ShouldFail); + true -> + ct:fail(no_known_sasl_mechanism_available) end. + bind(Config) -> U = ?config(user, Config), S = ?config(server, Config), R = ?config(resource, Config), case ?config(type, Config) of - client -> - #iq{type = result, sub_els = [#bind{jid = JID}]} = - send_recv( - Config, #iq{type = set, sub_els = [#bind{resource = R}]}), - case ?config(anonymous, Config) of - false -> - {U, S, R} = jid:tolower(JID), - Config; - true -> - {User, S, Resource} = jid:tolower(JID), - set_opt(user, User, set_opt(resource, Resource, Config)) - end; - component -> - Config + client -> + #iq{type = result, sub_els = [#bind{jid = JID}]} = + send_recv( + Config, #iq{type = set, sub_els = [#bind{resource = R}]}), + case ?config(anonymous, Config) of + false -> + {U, S, R} = jid:tolower(JID), + Config; + true -> + {User, S, Resource} = jid:tolower(JID), + set_opt(user, User, set_opt(resource, Resource, Config)) + end; + component -> + Config end. + open_session(Config) -> open_session(Config, false). + open_session(Config, Force) -> - if Force -> - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#xmpp_session{}]}); - true -> - ok + if + Force -> + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [#xmpp_session{}]}); + true -> + ok end, Config. + auth_legacy(Config, IsDigest) -> auth_legacy(Config, IsDigest, false). + auth_legacy(Config, IsDigest, ShouldFail) -> ServerJID = server_jid(Config), U = ?config(user, Config), R = ?config(resource, Config), P = ?config(password, Config), - #iq{type = result, - from = ServerJID, - sub_els = [#legacy_auth{username = <<"">>, - password = <<"">>, - resource = <<"">>} = Auth]} = - send_recv(Config, - #iq{to = ServerJID, type = get, - sub_els = [#legacy_auth{}]}), + #iq{ + type = result, + from = ServerJID, + sub_els = [#legacy_auth{ + username = <<"">>, + password = <<"">>, + resource = <<"">> + } = Auth] + } = + send_recv(Config, + #iq{ + to = ServerJID, + type = get, + sub_els = [#legacy_auth{}] + }), Res = case Auth#legacy_auth.digest of - <<"">> when IsDigest -> - StreamID = ?config(stream_id, Config), - D = p1_sha:sha(<>), - send_recv(Config, #iq{to = ServerJID, type = set, - sub_els = [#legacy_auth{username = U, - resource = R, - digest = D}]}); - _ when not IsDigest -> - send_recv(Config, #iq{to = ServerJID, type = set, - sub_els = [#legacy_auth{username = U, - resource = R, - password = P}]}) - end, + <<"">> when IsDigest -> + StreamID = ?config(stream_id, Config), + D = p1_sha:sha(<>), + send_recv(Config, + #iq{ + to = ServerJID, + type = set, + sub_els = [#legacy_auth{ + username = U, + resource = R, + digest = D + }] + }); + _ when not IsDigest -> + send_recv(Config, + #iq{ + to = ServerJID, + type = set, + sub_els = [#legacy_auth{ + username = U, + resource = R, + password = P + }] + }) + end, case Res of - #iq{from = ServerJID, type = result, sub_els = []} -> - if ShouldFail -> - ct:fail(legacy_auth_should_have_failed); - true -> - Config - end; - #iq{from = ServerJID, type = error} -> - if ShouldFail -> - Config; - true -> - ct:fail(legacy_auth_failed) - end + #iq{from = ServerJID, type = result, sub_els = []} -> + if + ShouldFail -> + ct:fail(legacy_auth_should_have_failed); + true -> + Config + end; + #iq{from = ServerJID, type = error} -> + if + ShouldFail -> + Config; + true -> + ct:fail(legacy_auth_failed) + end end. + auth_component(Config, ShouldFail) -> StreamID = ?config(stream_id, Config), Password = ?config(password, Config), Digest = p1_sha:sha(<>), send(Config, #handshake{data = Digest}), receive - #handshake{} when ShouldFail -> - ct:fail(component_auth_should_have_failed); - #handshake{} -> - Config; - #stream_error{reason = 'not-authorized'} when ShouldFail -> - Config; - #stream_error{reason = 'not-authorized'} -> - ct:fail(component_auth_failed) + #handshake{} when ShouldFail -> + ct:fail(component_auth_should_have_failed); + #handshake{} -> + Config; + #stream_error{reason = 'not-authorized'} when ShouldFail -> + Config; + #stream_error{reason = 'not-authorized'} -> + ct:fail(component_auth_failed) end. + auth_SASL(Mech, Config) -> auth_SASL(Mech, Config, false). + auth_SASL(Mech, Config, ShouldFail) -> Creds = {?config(user, Config), - ?config(server, Config), - ?config(password, Config)}, + ?config(server, Config), + ?config(password, Config)}, auth_SASL(Mech, Config, ShouldFail, Creds). + auth_SASL(Mech, Config, ShouldFail, Creds) -> {Response, SASL} = sasl_new(Mech, Creds), send(Config, #sasl_auth{mechanism = Mech, text = Response}), wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail). + wait_auth_SASL_result(Config, ShouldFail) -> receive - #sasl_success{} when ShouldFail -> - ct:fail(sasl_auth_should_have_failed); + #sasl_success{} when ShouldFail -> + ct:fail(sasl_auth_should_have_failed); #sasl_success{} -> - ok = recv_call(Config, reset_stream), + ok = recv_call(Config, reset_stream), send(Config, stream_header(Config)), - Type = ?config(type, Config), - NS = if Type == client -> ?NS_CLIENT; - Type == server -> ?NS_SERVER - end, - Config2 = receive #stream_start{id = ID, xmlns = NS, version = {1,0}} -> - set_opt(stream_id, ID, Config) - end, - receive #stream_features{sub_els = Fs} -> - if Type == client -> - #xmpp_session{optional = true} = - lists:keyfind(xmpp_session, 1, Fs); - true -> - ok - end, - lists:foldl( - fun(#feature_sm{}, ConfigAcc) -> - set_opt(sm, true, ConfigAcc); - (#feature_csi{}, ConfigAcc) -> - set_opt(csi, true, ConfigAcc); - (#rosterver_feature{}, ConfigAcc) -> - set_opt(rosterver, true, ConfigAcc); - (#compression{methods = Ms}, ConfigAcc) -> - set_opt(compression, Ms, ConfigAcc); - (_, ConfigAcc) -> - ConfigAcc - end, Config2, Fs) - end; + Type = ?config(type, Config), + NS = if + Type == client -> ?NS_CLIENT; + Type == server -> ?NS_SERVER + end, + Config2 = receive + #stream_start{id = ID, xmlns = NS, version = {1, 0}} -> + set_opt(stream_id, ID, Config) + end, + receive + #stream_features{sub_els = Fs} -> + if + Type == client -> + #xmpp_session{optional = true} = + lists:keyfind(xmpp_session, 1, Fs); + true -> + ok + end, + lists:foldl( + fun(#feature_sm{}, ConfigAcc) -> + set_opt(sm, true, ConfigAcc); + (#feature_csi{}, ConfigAcc) -> + set_opt(csi, true, ConfigAcc); + (#rosterver_feature{}, ConfigAcc) -> + set_opt(rosterver, true, ConfigAcc); + (#compression{methods = Ms}, ConfigAcc) -> + set_opt(compression, Ms, ConfigAcc); + (_, ConfigAcc) -> + ConfigAcc + end, + Config2, + Fs) + end; #sasl_challenge{text = ClientIn} -> {Response, SASL} = (?config(sasl, Config))(ClientIn), send(Config, #sasl_response{text = Response}), wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail); - #sasl_failure{} when ShouldFail -> - Config; + #sasl_failure{} when ShouldFail -> + Config; #sasl_failure{} -> ct:fail(sasl_auth_failed) end. + re_register(Config) -> User = ?config(user, Config), Server = ?config(server, Config), Pass = ?config(password, Config), ok = ejabberd_auth:try_register(User, Server, Pass). -match_failure(Received, [Match]) when is_list(Match)-> + +match_failure(Received, [Match]) when is_list(Match) -> ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~s", [Received, Match]); match_failure(Received, Matches) -> ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~p", [Received, Matches]). + recv(_Config) -> receive - {fail, El, Why} -> - ct:fail("recv failed: ~p->~n~s", - [El, xmpp:format_error(Why)]); - Event -> - Event + {fail, El, Why} -> + ct:fail("recv failed: ~p->~n~s", + [El, xmpp:format_error(Why)]); + Event -> + Event end. + recv_iq(_Config) -> receive #iq{} = IQ -> IQ end. + recv_presence(_Config) -> receive #presence{} = Pres -> Pres end. + recv_message(_Config) -> receive #message{} = Msg -> Msg end. + decode_stream_element(NS, El) -> decode(El, NS, []). + format_element(El) -> Bin = case erlang:function_exported(ct, log, 5) of - true -> ejabberd_web_admin:pretty_print_xml(El); - false -> io_lib:format("~p~n", [El]) - end, + true -> ejabberd_web_admin:pretty_print_xml(El); + false -> io_lib:format("~p~n", [El]) + end, binary:replace(Bin, <<"<">>, <<"<">>, [global]). + decode(El, NS, Opts) -> try - Pkt = xmpp:decode(El, NS, Opts), - ct:pal("RECV:~n~s~n~s", - [format_element(El), xmpp:pp(Pkt)]), - Pkt - catch _:{xmpp_codec, Why} -> - ct:pal("recv failed: ~p->~n~s", - [El, xmpp:format_error(Why)]), - erlang:error({xmpp_codec, Why}) + Pkt = xmpp:decode(El, NS, Opts), + ct:pal("RECV:~n~s~n~s", + [format_element(El), xmpp:pp(Pkt)]), + Pkt + catch + _:{xmpp_codec, Why} -> + ct:pal("recv failed: ~p->~n~s", + [El, xmpp:format_error(Why)]), + erlang:error({xmpp_codec, Why}) end. + send_text(Config, Text) -> recv_call(Config, {send_text, Text}). + send(State, Pkt) -> {NewID, NewPkt} = case Pkt of #message{id = I} -> @@ -617,14 +699,15 @@ send(State, Pkt) -> end, El = xmpp:encode(NewPkt), ct:pal("SENT:~n~s~n~s", - [format_element(El), xmpp:pp(NewPkt)]), + [format_element(El), xmpp:pp(NewPkt)]), Data = case NewPkt of - #stream_start{} -> fxml:element_to_header(El); - _ -> fxml:element_to_binary(El) - end, + #stream_start{} -> fxml:element_to_header(El); + _ -> fxml:element_to_binary(El) + end, ok = send_text(State, Data), NewID. + send_recv(State, #message{} = Msg) -> ID = send(State, Msg), receive #message{id = ID} = Result -> Result end; @@ -635,9 +718,10 @@ send_recv(State, #iq{} = IQ) -> ID = send(State, IQ), receive #iq{id = ID} = Result -> Result end. + sasl_new(<<"PLAIN">>, {User, Server, Password}) -> {<>, - fun (_) -> {error, <<"Invalid SASL challenge">>} end}; + fun(_) -> {error, <<"Invalid SASL challenge">>} end}; sasl_new(<<"EXTERNAL">>, {User, Server, _Password}) -> {jid:encode(jid:make(User, Server)), fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; @@ -646,177 +730,233 @@ sasl_new(<<"ANONYMOUS">>, _) -> fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; sasl_new(<<"DIGEST-MD5">>, {User, Server, Password}) -> {<<"">>, - fun (ServerIn) -> - case xmpp_sasl_digest:parse(ServerIn) of - bad -> {error, <<"Invalid SASL challenge">>}; - KeyVals -> - Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals), - CNonce = id(), - Realm = proplists:get_value(<<"realm">>, KeyVals, Server), - DigestURI = <<"xmpp/", Realm/binary>>, - NC = <<"00000001">>, - QOP = <<"auth">>, - AuthzId = <<"">>, - MyResponse = response(User, Password, Nonce, AuthzId, - Realm, CNonce, DigestURI, NC, QOP, - <<"AUTHENTICATE">>), - SUser = << <<(case Char of - $" -> <<"\\\"">>; - $\\ -> <<"\\\\">>; - _ -> <> - end)/binary>> || <> <= User >>, - Resp = <<"username=\"", SUser/binary, "\",realm=\"", - Realm/binary, "\",nonce=\"", Nonce/binary, - "\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary, - ",qop=", QOP/binary, ",digest-uri=\"", - DigestURI/binary, "\",response=\"", - MyResponse/binary, "\"">>, - {Resp, - fun (ServerIn2) -> - case xmpp_sasl_digest:parse(ServerIn2) of - bad -> {error, <<"Invalid SASL challenge">>}; - _KeyVals2 -> - {<<"">>, - fun (_) -> - {error, - <<"Invalid SASL challenge">>} - end} - end - end} - end + fun(ServerIn) -> + case xmpp_sasl_digest:parse(ServerIn) of + bad -> {error, <<"Invalid SASL challenge">>}; + KeyVals -> + Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals), + CNonce = id(), + Realm = proplists:get_value(<<"realm">>, KeyVals, Server), + DigestURI = <<"xmpp/", Realm/binary>>, + NC = <<"00000001">>, + QOP = <<"auth">>, + AuthzId = <<"">>, + MyResponse = response(User, + Password, + Nonce, + AuthzId, + Realm, + CNonce, + DigestURI, + NC, + QOP, + <<"AUTHENTICATE">>), + SUser = << <<(case Char of + $" -> <<"\\\"">>; + $\\ -> <<"\\\\">>; + _ -> <> + end)/binary>> || <> <= User >>, + Resp = <<"username=\"", SUser/binary, "\",realm=\"", + Realm/binary, "\",nonce=\"", Nonce/binary, + "\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary, + ",qop=", QOP/binary, ",digest-uri=\"", + DigestURI/binary, "\",response=\"", + MyResponse/binary, "\"">>, + {Resp, + fun(ServerIn2) -> + case xmpp_sasl_digest:parse(ServerIn2) of + bad -> {error, <<"Invalid SASL challenge">>}; + _KeyVals2 -> + {<<"">>, + fun(_) -> + {error, + <<"Invalid SASL challenge">>} + end} + end + end} + end end}. + hex(S) -> p1_sha:to_hexlist(S). -response(User, Passwd, Nonce, AuthzId, Realm, CNonce, - DigestURI, NC, QOP, A2Prefix) -> + +response(User, + Passwd, + Nonce, + AuthzId, + Realm, + CNonce, + DigestURI, + NC, + QOP, + A2Prefix) -> A1 = case AuthzId of - <<"">> -> - <<((erlang:md5(<>)))/binary, - ":", Nonce/binary, ":", CNonce/binary>>; - _ -> - <<((erlang:md5(<>)))/binary, - ":", Nonce/binary, ":", CNonce/binary, ":", - AuthzId/binary>> - end, + <<"">> -> + <<((erlang:md5(<>)))/binary, + ":", + Nonce/binary, + ":", + CNonce/binary>>; + _ -> + <<((erlang:md5(<>)))/binary, + ":", + Nonce/binary, + ":", + CNonce/binary, + ":", + AuthzId/binary>> + end, A2 = case QOP of - <<"auth">> -> - <>; - _ -> - <> - end, - T = <<(hex((erlang:md5(A1))))/binary, ":", Nonce/binary, - ":", NC/binary, ":", CNonce/binary, ":", QOP/binary, - ":", (hex((erlang:md5(A2))))/binary>>, + <<"auth">> -> + <>; + _ -> + <> + end, + T = <<(hex((erlang:md5(A1))))/binary, + ":", + Nonce/binary, + ":", + NC/binary, + ":", + CNonce/binary, + ":", + QOP/binary, + ":", + (hex((erlang:md5(A2))))/binary>>, hex((erlang:md5(T))). + my_jid(Config) -> jid:make(?config(user, Config), - ?config(server, Config), - ?config(resource, Config)). + ?config(server, Config), + ?config(resource, Config)). + server_jid(Config) -> jid:make(<<>>, ?config(server, Config), <<>>). + pubsub_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"pubsub.", Server/binary>>, <<>>). + proxy_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"proxy.", Server/binary>>, <<>>). + upload_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"upload.", Server/binary>>, <<>>). + muc_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"conference.", Server/binary>>, <<>>). + muc_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"test">>, <<"conference.", Server/binary>>, <<>>). + my_muc_jid(Config) -> Nick = ?config(nick, Config), RoomJID = muc_room_jid(Config), jid:replace_resource(RoomJID, Nick). + peer_muc_jid(Config) -> PeerNick = ?config(peer_nick, Config), RoomJID = muc_room_jid(Config), jid:replace_resource(RoomJID, PeerNick). + alt_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"alt">>, <<"conference.", Server/binary>>, <<>>). + mix_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"mix.", Server/binary>>, <<>>). + mix_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"test">>, <<"mix.", Server/binary>>, <<>>). + id() -> id(<<>>). + id(<<>>) -> p1_rand:get_string(); id(ID) -> ID. + get_features(Config) -> get_features(Config, server_jid(Config)). + get_features(Config, To) -> ct:comment("Getting features of ~s", [jid:encode(To)]), #iq{type = result, sub_els = [#disco_info{features = Features}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}), Features. + is_feature_advertised(Config, Feature) -> is_feature_advertised(Config, Feature, server_jid(Config)). + is_feature_advertised(Config, Feature, To) -> Features = get_features(Config, To), lists:member(Feature, Features). + set_opt(Opt, Val, Config) -> - [{Opt, Val}|lists:keydelete(Opt, 1, Config)]. + [{Opt, Val} | lists:keydelete(Opt, 1, Config)]. + wait_for_master(Config) -> put_event(Config, peer_ready), case get_event(Config) of - peer_ready -> - ok; - Other -> - suite:match_failure(Other, peer_ready) + peer_ready -> + ok; + Other -> + suite:match_failure(Other, peer_ready) end. + wait_for_slave(Config) -> put_event(Config, peer_ready), case get_event(Config) of - peer_ready -> - ok; - Other -> - suite:match_failure(Other, peer_ready) + peer_ready -> + ok; + Other -> + suite:match_failure(Other, peer_ready) end. + make_iq_result(#iq{from = From} = IQ) -> IQ#iq{type = result, to = From, from = undefined, sub_els = []}. + self_presence(Config, Type) -> MyJID = my_jid(Config), ct:comment("Sending self-presence"), #presence{type = Type, from = MyJID} = - send_recv(Config, #presence{type = Type}). + send_recv(Config, #presence{type = Type}). + set_roster(Config, Subscription, Groups) -> MyJID = my_jid(Config), @@ -825,17 +965,21 @@ set_roster(Config, Subscription, Groups) -> PeerBareJID = jid:remove_resource(PeerJID), PeerLJID = jid:tolower(PeerBareJID), ct:comment("Adding ~s to roster with subscription '~s' in groups ~p", - [jid:encode(PeerBareJID), Subscription, Groups]), - {atomic, _} = mod_roster:set_roster(#roster{usj = {U, S, PeerLJID}, - us = {U, S}, - jid = PeerLJID, - subscription = Subscription, - groups = Groups}), + [jid:encode(PeerBareJID), Subscription, Groups]), + {atomic, _} = mod_roster:set_roster(#roster{ + usj = {U, S, PeerLJID}, + us = {U, S}, + jid = PeerLJID, + subscription = Subscription, + groups = Groups + }), Config. + del_roster(Config) -> del_roster(Config, ?config(peer, Config)). + del_roster(Config, PeerJID) -> MyJID = my_jid(Config), {U, S, _} = jid:tolower(MyJID), @@ -845,77 +989,84 @@ del_roster(Config, PeerJID) -> {atomic, _} = mod_roster:del_roster(U, S, PeerLJID), Config. + get_roster(Config) -> {LUser, LServer, _} = jid:tolower(my_jid(Config)), mod_roster:get_roster(LUser, LServer). + recv_call(Config, Msg) -> Receiver = ?config(receiver, Config), Ref = make_ref(), Receiver ! {Ref, Msg}, receive - {Ref, Reply} -> - Reply + {Ref, Reply} -> + Reply end. + start_receiver(NS, Owner, Server, Port) -> MRef = erlang:monitor(process, Owner), {ok, Socket} = xmpp_socket:connect( - Server, Port, - [binary, {packet, 0}, {active, false}], infinity), + Server, + Port, + [binary, {packet, 0}, {active, false}], + infinity), receiver(NS, Owner, Socket, MRef). + receiver(NS, Owner, Socket, MRef) -> receive - {Ref, reset_stream} -> - Socket1 = xmpp_socket:reset_stream(Socket), - Owner ! {Ref, ok}, - receiver(NS, Owner, Socket1, MRef); - {Ref, {starttls, Certfile}} -> - {ok, TLSSocket} = xmpp_socket:starttls( - Socket, - [{certfile, Certfile}, connect]), - Owner ! {Ref, ok}, - receiver(NS, Owner, TLSSocket, MRef); - {Ref, compress} -> - {ok, ZlibSocket} = xmpp_socket:compress(Socket), - Owner ! {Ref, ok}, - receiver(NS, Owner, ZlibSocket, MRef); - {Ref, {send_text, Text}} -> - Ret = xmpp_socket:send(Socket, Text), - Owner ! {Ref, Ret}, - receiver(NS, Owner, Socket, MRef); - {Ref, close} -> - xmpp_socket:close(Socket), - Owner ! {Ref, ok}, - receiver(NS, Owner, Socket, MRef); + {Ref, reset_stream} -> + Socket1 = xmpp_socket:reset_stream(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, Socket1, MRef); + {Ref, {starttls, Certfile}} -> + {ok, TLSSocket} = xmpp_socket:starttls( + Socket, + [{certfile, Certfile}, connect]), + Owner ! {Ref, ok}, + receiver(NS, Owner, TLSSocket, MRef); + {Ref, compress} -> + {ok, ZlibSocket} = xmpp_socket:compress(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, ZlibSocket, MRef); + {Ref, {send_text, Text}} -> + Ret = xmpp_socket:send(Socket, Text), + Owner ! {Ref, Ret}, + receiver(NS, Owner, Socket, MRef); + {Ref, close} -> + xmpp_socket:close(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, Socket, MRef); {'$gen_event', {xmlstreamelement, El}} -> - Owner ! decode_stream_element(NS, El), - receiver(NS, Owner, Socket, MRef); - {'$gen_event', {xmlstreamstart, Name, Attrs}} -> - Owner ! decode(#xmlel{name = Name, attrs = Attrs}, <<>>, []), - receiver(NS, Owner, Socket, MRef); - {'$gen_event', Event} -> + Owner ! decode_stream_element(NS, El), + receiver(NS, Owner, Socket, MRef); + {'$gen_event', {xmlstreamstart, Name, Attrs}} -> + Owner ! decode(#xmlel{name = Name, attrs = Attrs}, <<>>, []), + receiver(NS, Owner, Socket, MRef); + {'$gen_event', Event} -> Owner ! Event, - receiver(NS, Owner, Socket, MRef); - {'DOWN', MRef, process, Owner, _} -> - ok; - {tcp, _, Data} -> - case xmpp_socket:recv(Socket, Data) of - {ok, Socket1} -> - receiver(NS, Owner, Socket1, MRef); - {error, _} -> - Owner ! closed, - receiver(NS, Owner, Socket, MRef) - end; - {tcp_error, _, _} -> - Owner ! closed, - receiver(NS, Owner, Socket, MRef); - {tcp_closed, _} -> - Owner ! closed, - receiver(NS, Owner, Socket, MRef) + receiver(NS, Owner, Socket, MRef); + {'DOWN', MRef, process, Owner, _} -> + ok; + {tcp, _, Data} -> + case xmpp_socket:recv(Socket, Data) of + {ok, Socket1} -> + receiver(NS, Owner, Socket1, MRef); + {error, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef) + end; + {tcp_error, _, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef); + {tcp_closed, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef) end. + %% @doc Retry an action until success, at max N times with an interval %% `Interval' %% Shamlessly stolen (with slight adaptations) from snabbkaffee. @@ -923,36 +1074,42 @@ receiver(NS, Owner, Socket, MRef) -> retry(_, 0, Fun) -> Fun(); retry(Interval, N, Fun) -> - try Fun() + try + Fun() catch - EC:Err -> + EC:Err -> timer:sleep(Interval), ct:pal("retrying ~p more times, result was ~p:~p", [N, EC, Err]), retry(Interval, N - 1, Fun) end. + %%%=================================================================== %%% Clients puts and gets events via this relay. %%%=================================================================== start_event_relay() -> spawn(fun event_relay/0). + stop_event_relay(Config) -> Pid = ?config(event_relay, Config), exit(Pid, normal). + event_relay() -> event_relay([], []). + event_relay(Events, Subscribers) -> receive {subscribe, From} -> - erlang:monitor(process, From), + erlang:monitor(process, From), From ! {ok, self()}, lists:foreach( fun(Event) -> From ! {event, Event, self()} - end, Events), - event_relay(Events, [From|Subscribers]); + end, + Events), + event_relay(Events, [From | Subscribers]); {put, Event, From} -> From ! {ok, self()}, lists:foreach( @@ -960,22 +1117,25 @@ event_relay(Events, Subscribers) -> Pid ! {event, Event, self()}; (_) -> ok - end, Subscribers), - event_relay([Event|Events], Subscribers); - {'DOWN', _MRef, process, Pid, _Info} -> - case lists:member(Pid, Subscribers) of - true -> - NewSubscribers = lists:delete(Pid, Subscribers), - lists:foreach( - fun(Subscriber) -> - Subscriber ! {event, peer_down, self()} - end, NewSubscribers), - event_relay(Events, NewSubscribers); - false -> - event_relay(Events, Subscribers) - end + end, + Subscribers), + event_relay([Event | Events], Subscribers); + {'DOWN', _MRef, process, Pid, _Info} -> + case lists:member(Pid, Subscribers) of + true -> + NewSubscribers = lists:delete(Pid, Subscribers), + lists:foreach( + fun(Subscriber) -> + Subscriber ! {event, peer_down, self()} + end, + NewSubscribers), + event_relay(Events, NewSubscribers); + false -> + event_relay(Events, Subscribers) + end end. + subscribe_to_events(Config) -> Relay = ?config(event_relay, Config), Relay ! {subscribe, self()}, @@ -984,6 +1144,7 @@ subscribe_to_events(Config) -> ok end. + put_event(Config, Event) -> Relay = ?config(event_relay, Config), Relay ! {put, Event, self()}, @@ -992,6 +1153,7 @@ put_event(Config, Event) -> ok end. + get_event(Config) -> Relay = ?config(event_relay, Config), receive @@ -999,11 +1161,13 @@ get_event(Config) -> Event end. + flush(Config) -> receive - {event, peer_down, _} -> flush(Config); - closed -> flush(Config); - Msg -> ct:fail({unexpected_msg, Msg}) - after 0 -> - ok + {event, peer_down, _} -> flush(Config); + closed -> flush(Config); + Msg -> ct:fail({unexpected_msg, Msg}) + after + 0 -> + ok end. diff --git a/test/suite.hrl b/test/suite.hrl index 00b341cb1..a32ca8128 100644 --- a/test/suite.hrl +++ b/test/suite.hrl @@ -3,6 +3,7 @@ -include_lib("xmpp/include/jid.hrl"). -include_lib("xmpp/include/ns.hrl"). -include_lib("xmpp/include/xmpp_codec.hrl"). + -include("mod_proxy65.hrl"). -define(STREAM_TRAILER, <<"">>). @@ -67,7 +68,7 @@ end)()). -define(match(Pattern, Result), - (fun() -> + (fun() -> case Result of Pattern -> ok; @@ -77,7 +78,7 @@ end)()). -define(match(Pattern, Result, PatternRes), - (fun() -> + (fun() -> case Result of Pattern -> PatternRes; @@ -87,25 +88,26 @@ end)()). -define(send_recv(Send, Recv), - ?match(Recv, suite:send_recv(Config, Send))). + ?match(Recv, suite:send_recv(Config, Send))). -define(retry(TIMEOUT, N, FUN), suite:retry(TIMEOUT, N, fun() -> FUN end)). --define(COMMON_VHOST, <<"localhost">>). --define(MNESIA_VHOST, <<"mnesia.localhost">>). --define(REDIS_VHOST, <<"redis.localhost">>). --define(MYSQL_VHOST, <<"mysql.localhost">>). --define(MSSQL_VHOST, <<"mssql.localhost">>). --define(PGSQL_VHOST, <<"pgsql.localhost">>). --define(SQLITE_VHOST, <<"sqlite.localhost">>). --define(LDAP_VHOST, <<"ldap.localhost">>). +-define(COMMON_VHOST, <<"localhost">>). +-define(MNESIA_VHOST, <<"mnesia.localhost">>). +-define(REDIS_VHOST, <<"redis.localhost">>). +-define(MYSQL_VHOST, <<"mysql.localhost">>). +-define(MSSQL_VHOST, <<"mssql.localhost">>). +-define(PGSQL_VHOST, <<"pgsql.localhost">>). +-define(SQLITE_VHOST, <<"sqlite.localhost">>). +-define(LDAP_VHOST, <<"ldap.localhost">>). -define(EXTAUTH_VHOST, <<"extauth.localhost">>). --define(S2S_VHOST, <<"s2s.localhost">>). --define(UPLOAD_VHOST, <<"upload.localhost">>). +-define(S2S_VHOST, <<"s2s.localhost">>). +-define(UPLOAD_VHOST, <<"upload.localhost">>). -define(BACKENDS, [mnesia, redis, mysql, mssql, odbc, pgsql, sqlite, ldap, extauth]). + insert(Val, N, Tuple) -> L = tuple_to_list(Tuple), - {H, T} = lists:split(N-1, L), - list_to_tuple(H ++ [Val|T]). + {H, T} = lists:split(N - 1, L), + list_to_tuple(H ++ [Val | T]). diff --git a/test/upload_tests.erl b/test/upload_tests.erl index 7e2d89958..534f148d9 100644 --- a/test/upload_tests.erl +++ b/test/upload_tests.erl @@ -24,13 +24,21 @@ %% API -compile(export_all). --import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, - my_jid/1, wait_for_slave/1, wait_for_master/1, - send_recv/2, put_event/2, get_event/1]). +-import(suite, + [disconnect/1, + is_feature_advertised/3, + upload_jid/1, + my_jid/1, + wait_for_slave/1, + wait_for_master/1, + send_recv/2, + put_event/2, + get_event/1]). -include("suite.hrl"). -define(CONTENT_TYPE, "image/png"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,36 +47,42 @@ %%%=================================================================== single_cases() -> {upload_single, [sequence], - [single_test(feature_enabled), - single_test(service_vcard), - single_test(get_max_size), - single_test(slot_request), - single_test(put_get_request), - single_test(max_size_exceed)]}. + [single_test(feature_enabled), + single_test(service_vcard), + single_test(get_max_size), + single_test(slot_request), + single_test(put_get_request), + single_test(max_size_exceed)]}. + feature_enabled(Config) -> lists:foreach( fun(NS) -> - true = is_feature_advertised(Config, NS, upload_jid(Config)) - end, namespaces()), + true = is_feature_advertised(Config, NS, upload_jid(Config)) + end, + namespaces()), disconnect(Config). + service_vcard(Config) -> Upload = upload_jid(Config), ct:comment("Retrieving vCard from ~s", [jid:encode(Upload)]), VCard = mod_http_upload_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = - send_recv(Config, #iq{type = get, to = Upload, sub_els = [#vcard_temp{}]}), + send_recv(Config, #iq{type = get, to = Upload, sub_els = [#vcard_temp{}]}), disconnect(Config). + get_max_size(Config) -> Xs = get_disco_info_xdata(Config), lists:foreach( fun(NS) -> - get_max_size(Config, Xs, NS) - end, namespaces()), + get_max_size(Config, Xs, NS) + end, + namespaces()), disconnect(Config). + get_max_size(_, _, ?NS_HTTP_UPLOAD_OLD) -> %% This old spec didn't specify 'max-file-size' attribute ok; @@ -76,129 +90,167 @@ get_max_size(Config, Xs, NS) -> Xs = get_disco_info_xdata(Config), get_size(NS, Config, Xs). + slot_request(Config) -> lists:foreach( fun(NS) -> - slot_request(Config, NS) - end, namespaces()), + slot_request(Config, NS) + end, + namespaces()), disconnect(Config). + put_get_request(Config) -> lists:foreach( fun(NS) -> - {GetURL, PutURL, _Filename, Size} = slot_request(Config, NS), - Data = p1_rand:bytes(Size), - put_request(Config, PutURL, Data), - get_request(Config, GetURL, Data) - end, namespaces()), + {GetURL, PutURL, _Filename, Size} = slot_request(Config, NS), + Data = p1_rand:bytes(Size), + put_request(Config, PutURL, Data), + get_request(Config, GetURL, Data) + end, + namespaces()), disconnect(Config). + max_size_exceed(Config) -> lists:foreach( fun(NS) -> - max_size_exceed(Config, NS) - end, namespaces()), + max_size_exceed(Config, NS) + end, + namespaces()), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("upload_" ++ atom_to_list(T)). + get_disco_info_xdata(Config) -> To = upload_jid(Config), #iq{type = result, sub_els = [#disco_info{xdata = Xs}]} = - send_recv(Config, - #iq{type = get, sub_els = [#disco_info{}], to = To}), + send_recv(Config, + #iq{type = get, sub_els = [#disco_info{}], to = To}), Xs. -get_size(NS, Config, [X|Xs]) -> + +get_size(NS, Config, [X | Xs]) -> case xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X) of - [NS] -> - [Size] = xmpp_util:get_xdata_values(<<"max-file-size">>, X), - true = erlang:binary_to_integer(Size) > 0, - Size; - _ -> - get_size(NS, Config, Xs) + [NS] -> + [Size] = xmpp_util:get_xdata_values(<<"max-file-size">>, X), + true = erlang:binary_to_integer(Size) > 0, + Size; + _ -> + get_size(NS, Config, Xs) end; get_size(NS, _Config, []) -> ct:fail({disco_info_xdata_failed, NS}). + slot_request(Config, NS) -> To = upload_jid(Config), Filename = filename(), Size = p1_rand:uniform(1, 1024), case NS of - ?NS_HTTP_UPLOAD_0 -> - #iq{type = result, - sub_els = [#upload_slot_0{get = GetURL, - put = PutURL, - xmlns = NS}]} = - send_recv(Config, - #iq{type = get, to = To, - sub_els = [#upload_request_0{ - filename = Filename, - size = Size, - 'content-type' = <>, - xmlns = NS}]}), - {GetURL, PutURL, Filename, Size}; - _ -> - #iq{type = result, - sub_els = [#upload_slot{get = GetURL, - put = PutURL, - xmlns = NS}]} = - send_recv(Config, - #iq{type = get, to = To, - sub_els = [#upload_request{ - filename = Filename, - size = Size, - 'content-type' = <>, - xmlns = NS}]}), - {GetURL, PutURL, Filename, Size} + ?NS_HTTP_UPLOAD_0 -> + #iq{ + type = result, + sub_els = [#upload_slot_0{ + get = GetURL, + put = PutURL, + xmlns = NS + }] + } = + send_recv(Config, + #iq{ + type = get, + to = To, + sub_els = [#upload_request_0{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS + }] + }), + {GetURL, PutURL, Filename, Size}; + _ -> + #iq{ + type = result, + sub_els = [#upload_slot{ + get = GetURL, + put = PutURL, + xmlns = NS + }] + } = + send_recv(Config, + #iq{ + type = get, + to = To, + sub_els = [#upload_request{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS + }] + }), + {GetURL, PutURL, Filename, Size} end. + put_request(_Config, URL0, Data) -> ct:comment("Putting ~B bytes to ~s", [size(Data), URL0]), URL = binary_to_list(URL0), {ok, {{"HTTP/1.1", 201, _}, _, _}} = - httpc:request(put, {URL, [], ?CONTENT_TYPE, Data}, [], []). + httpc:request(put, {URL, [], ?CONTENT_TYPE, Data}, [], []). + get_request(_Config, URL0, Data) -> ct:comment("Getting ~B bytes from ~s", [size(Data), URL0]), URL = binary_to_list(URL0), {ok, {{"HTTP/1.1", 200, _}, _, Body}} = - httpc:request(get, {URL, []}, [], [{body_format, binary}]), + httpc:request(get, {URL, []}, [], [{body_format, binary}]), ct:comment("Checking returned body"), Body = Data. + max_size_exceed(Config, NS) -> To = upload_jid(Config), Filename = filename(), Size = 1000000000, IQErr = - case NS of - ?NS_HTTP_UPLOAD_0 -> - #iq{type = error} = - send_recv(Config, - #iq{type = get, to = To, - sub_els = [#upload_request_0{ - filename = Filename, - size = Size, - 'content-type' = <>, - xmlns = NS}]}); - _ -> - #iq{type = error} = - send_recv(Config, - #iq{type = get, to = To, - sub_els = [#upload_request{ - filename = Filename, - size = Size, - 'content-type' = <>, - xmlns = NS}]}) - end, + case NS of + ?NS_HTTP_UPLOAD_0 -> + #iq{type = error} = + send_recv(Config, + #iq{ + type = get, + to = To, + sub_els = [#upload_request_0{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS + }] + }); + _ -> + #iq{type = error} = + send_recv(Config, + #iq{ + type = get, + to = To, + sub_els = [#upload_request{ + filename = Filename, + size = Size, + 'content-type' = <>, + xmlns = NS + }] + }) + end, check_size_error(IQErr, Size, NS). + check_size_error(IQErr, Size, NS) -> Err = xmpp:get_error(IQErr), FileTooLarge = xmpp:get_subtag(Err, #upload_file_too_large{xmlns = NS}), @@ -206,8 +258,10 @@ check_size_error(IQErr, Size, NS) -> #upload_file_too_large{'max-file-size' = MaxSize} = FileTooLarge, true = Size > MaxSize. + namespaces() -> [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD, ?NS_HTTP_UPLOAD_OLD]. + filename() -> <<(p1_rand:get_string())/binary, ".png">>. diff --git a/test/vcard_tests.erl b/test/vcard_tests.erl index fc3adb611..e20392cec 100644 --- a/test/vcard_tests.erl +++ b/test/vcard_tests.erl @@ -25,14 +25,23 @@ %% API -compile(export_all). --import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, - is_feature_advertised/3, server_jid/1, - my_jid/1, wait_for_slave/1, wait_for_master/1, - recv_presence/1, recv/1]). +-import(suite, + [send_recv/2, + disconnect/1, + is_feature_advertised/2, + is_feature_advertised/3, + server_jid/1, + my_jid/1, + wait_for_slave/1, + wait_for_master/1, + recv_presence/1, + recv/1]). -include("suite.hrl"). + -include_lib("stdlib/include/assert.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -41,9 +50,10 @@ %%%=================================================================== single_cases() -> {vcard_single, [sequence], - [single_test(feature_enabled), - single_test(get_set), - single_test(service_vcard)]}. + [single_test(feature_enabled), + single_test(get_set), + single_test(service_vcard)]}. + feature_enabled(Config) -> BareMyJID = jid:remove_resource(my_jid(Config)), @@ -51,37 +61,56 @@ feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_VCARD, BareMyJID), disconnect(Config). + get_set(Config) -> VCard = - #vcard_temp{fn = <<"Peter Saint-Andre">>, - n = #vcard_name{family = <<"Saint-Andre">>, - given = <<"Peter">>}, - nickname = <<"stpeter">>, - bday = <<"1966-08-06">>, - adr = [#vcard_adr{work = true, - extadd = <<"Suite 600">>, - street = <<"1899 Wynkoop Street">>, - locality = <<"Denver">>, - region = <<"CO">>, - pcode = <<"80202">>, - ctry = <<"USA">>}, - #vcard_adr{home = true, - locality = <<"Denver">>, - region = <<"CO">>, - pcode = <<"80209">>, - ctry = <<"USA">>}], - tel = [#vcard_tel{work = true,voice = true, - number = <<"303-308-3282">>}, - #vcard_tel{home = true,voice = true, - number = <<"303-555-1212">>}], - email = [#vcard_email{internet = true,pref = true, - userid = <<"stpeter@jabber.org">>}], - jabberid = <<"stpeter@jabber.org">>, - title = <<"Executive Director">>,role = <<"Patron Saint">>, - org = #vcard_org{name = <<"XMPP Standards Foundation">>}, - url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, - desc = <<"More information about me is located on my " - "personal website: http://www.saint-andre.com/">>}, + #vcard_temp{ + fn = <<"Peter Saint-Andre">>, + n = #vcard_name{ + family = <<"Saint-Andre">>, + given = <<"Peter">> + }, + nickname = <<"stpeter">>, + bday = <<"1966-08-06">>, + adr = [#vcard_adr{ + work = true, + extadd = <<"Suite 600">>, + street = <<"1899 Wynkoop Street">>, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80202">>, + ctry = <<"USA">> + }, + #vcard_adr{ + home = true, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80209">>, + ctry = <<"USA">> + }], + tel = [#vcard_tel{ + work = true, + voice = true, + number = <<"303-308-3282">> + }, + #vcard_tel{ + home = true, + voice = true, + number = <<"303-555-1212">> + }], + email = [#vcard_email{ + internet = true, + pref = true, + userid = <<"stpeter@jabber.org">> + }], + jabberid = <<"stpeter@jabber.org">>, + title = <<"Executive Director">>, + role = <<"Patron Saint">>, + org = #vcard_org{name = <<"XMPP Standards Foundation">>}, + url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, + desc = <<"More information about me is located on my " + "personal website: http://www.saint-andre.com/">> + }, #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, sub_els = [VCard]}), #iq{type = result, sub_els = [VCard1]} = @@ -89,20 +118,23 @@ get_set(Config) -> ?assertEqual(VCard, VCard1), disconnect(Config). + service_vcard(Config) -> JID = server_jid(Config), ct:comment("Retrieving vCard from ~s", [jid:encode(JID)]), VCard = mod_vcard_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = - send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), disconnect(Config). + %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {vcard_master_slave, [sequence], []}. - %%[master_slave_test(xupdate)]}. +%%[master_slave_test(xupdate)]}. + xupdate_master(Config) -> Img = <<137, "PNG\r\n", 26, $\n>>, @@ -114,16 +146,23 @@ xupdate_master(Config) -> #presence{from = Peer, type = available} = recv_presence(Config), VCard = #vcard_temp{photo = #vcard_photo{type = <<"image/png">>, binval = Img}}, #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [VCard]}), - #presence{from = MyJID, type = available, - sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), + send_recv(Config, #iq{type = set, sub_els = [VCard]}), + #presence{ + from = MyJID, + type = available, + sub_els = [#vcard_xupdate{hash = ImgHash}] + } = recv_presence(Config), #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#vcard_temp{}]}), - {_, _} = ?recv2(#presence{from = MyJID, type = available, - sub_els = [#vcard_xupdate{hash = undefined}]}, - #presence{from = Peer, type = unavailable}), + send_recv(Config, #iq{type = set, sub_els = [#vcard_temp{}]}), + {_, _} = ?recv2(#presence{ + from = MyJID, + type = available, + sub_els = [#vcard_xupdate{hash = undefined}] + }, + #presence{from = Peer, type = unavailable}), disconnect(Config). + xupdate_slave(Config) -> Img = <<137, "PNG\r\n", 26, $\n>>, ImgHash = p1_sha:sha(Img), @@ -132,19 +171,28 @@ xupdate_slave(Config) -> #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), wait_for_master(Config), #presence{from = Peer, type = available} = recv_presence(Config), - #presence{from = Peer, type = available, - sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), - #presence{from = Peer, type = available, - sub_els = [#vcard_xupdate{hash = undefined}]} = recv_presence(Config), + #presence{ + from = Peer, + type = available, + sub_els = [#vcard_xupdate{hash = ImgHash}] + } = recv_presence(Config), + #presence{ + from = Peer, + type = available, + sub_els = [#vcard_xupdate{hash = undefined}] + } = recv_presence(Config), disconnect(Config). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("vcard_" ++ atom_to_list(T)). + master_slave_test(T) -> - {list_to_atom("vcard_" ++ atom_to_list(T)), [parallel], + {list_to_atom("vcard_" ++ atom_to_list(T)), + [parallel], [list_to_atom("vcard_" ++ atom_to_list(T) ++ "_master"), list_to_atom("vcard_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/webadmin_tests.erl b/test/webadmin_tests.erl index fa12fd6f8..7dddf45a9 100644 --- a/test/webadmin_tests.erl +++ b/test/webadmin_tests.erl @@ -24,13 +24,22 @@ %% API -compile(export_all). --import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, -my_jid/1, wait_for_slave/1, wait_for_master/1, -send_recv/2, put_event/2, get_event/1]). +-import(suite, + [disconnect/1, + is_feature_advertised/3, + upload_jid/1, + my_jid/1, + wait_for_slave/1, + wait_for_master/1, + send_recv/2, + put_event/2, + get_event/1]). -include("suite.hrl"). + -include_lib("stdlib/include/assert.hrl"). + %%%=================================================================== %%% API %%%=================================================================== @@ -39,79 +48,99 @@ send_recv/2, put_event/2, get_event/1]). %%%=================================================================== single_cases() -> {webadmin_single, [sequence], - [single_test(login_page), - single_test(welcome_page), - single_test(user_page), - single_test(adduser), - single_test(changepassword), - single_test(removeuser)]}. + [single_test(login_page), + single_test(welcome_page), + single_test(user_page), + single_test(adduser), + single_test(changepassword), + single_test(removeuser)]}. + login_page(Config) -> Headers = ?match({ok, {{"HTTP/1.1", 401, _}, Headers, _}}, - httpc:request(get, {page(Config, ""), []}, [], - [{body_format, binary}]), - Headers), + httpc:request(get, + {page(Config, ""), []}, + [], + [{body_format, binary}]), + Headers), ?match("basic realm=\"ejabberd\"", proplists:get_value("www-authenticate", Headers, none)). + welcome_page(Config) -> Body = ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, - httpc:request(get, {page(Config, ""), [basic_auth_header(Config)]}, [], - [{body_format, binary}]), - Body), + httpc:request(get, + {page(Config, ""), [basic_auth_header(Config)]}, + [], + [{body_format, binary}]), + Body), ?match({_, _}, binary:match(Body, <<"ejabberd Web Admin">>)). + user_page(Config) -> Server = ?config(server, Config), URL = "server/" ++ binary_to_list(Server) ++ "/user/admin/", Body = ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, - httpc:request(get, {page(Config, URL), [basic_auth_header(Config)]}, [], - [{body_format, binary}]), - Body), + httpc:request(get, + {page(Config, URL), [basic_auth_header(Config)]}, + [], + [{body_format, binary}]), + Body), ?match({_, _}, binary:match(Body, <<"ejabberd Web Admin">>)). + adduser(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, Server = ?config(server, Config), Password = ?config(password, Config), Body = make_query( - Config, - "server/" ++ binary_to_list(Server) ++ "/users/", - <<"register/user=", (mue(User))/binary, "®ister/password=", - (mue(Password))/binary, "®ister=Register">>), + Config, + "server/" ++ binary_to_list(Server) ++ "/users/", + <<"register/user=", + (mue(User))/binary, + "®ister/password=", + (mue(Password))/binary, + "®ister=Register">>), Password = ejabberd_auth:get_password_s(User, Server), - ?match({_, _}, binary:match(Body, <<"User ", User/binary, "@", Server/binary, - " successfully registered">>)). + ?match({_, _}, + binary:match(Body, + <<"User ", User/binary, "@", Server/binary, + " successfully registered">>)). + changepassword(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, Server = ?config(server, Config), Password = <<"newpassword-", (?config(password, Config))/binary>>, Body = make_query( - Config, - "server/" ++ binary_to_list(Server) - ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", - <<"change_password/newpass=", (mue(Password))/binary, - "&change_password=Change+Password">>), + Config, + "server/" ++ binary_to_list(Server) ++ + "/user/" ++ binary_to_list(mue(User)) ++ "/", + <<"change_password/newpass=", + (mue(Password))/binary, + "&change_password=Change+Password">>), ?match(Password, ejabberd_auth:get_password_s(User, Server)), ?match({_, _}, binary:match(Body, <<"<div class='result'><code>ok</code></div>">>)). + removeuser(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, Server = ?config(server, Config), Body = make_query( - Config, - "server/" ++ binary_to_list(Server) - ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", - <<"&unregister=Unregister">>), + Config, + "server/" ++ binary_to_list(Server) ++ + "/user/" ++ binary_to_list(mue(User)) ++ "/", + <<"&unregister=Unregister">>), false = ejabberd_auth:user_exists(User, Server), ?match(nomatch, binary:match(Body, <<"<h3>Last Activity</h3>20">>)). + %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("webadmin_" ++ atom_to_list(T)). + basic_auth_header(Config) -> User = <<"admin">>, Server = ?config(server, Config), @@ -119,30 +148,36 @@ basic_auth_header(Config) -> ejabberd_auth:try_register(User, Server, Password), basic_auth_header(User, Server, Password). + basic_auth_header(Username, Server, Password) -> JidBin = <<Username/binary, "@", Server/binary, ":", Password/binary>>, {"authorization", "Basic " ++ base64:encode_to_string(JidBin)}. + page(Config, Tail) -> Server = ?config(server_host, Config), Port = ct:get_config(web_port, 5280), Url = "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/admin/" ++ Tail, %% This bypasses a bug introduced in Erlang OTP R21 and fixed in 23.2: case catch uri_string:normalize("/%2525") of - "/%25" -> - string:replace(Url, "%25", "%2525", all); - _ -> - Url + "/%25" -> + string:replace(Url, "%25", "%2525", all); + _ -> + Url end. + mue(Binary) -> misc:url_encode(Binary). + make_query(Config, URL, BodyQ) -> ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, - httpc:request(post, {page(Config, URL), - [basic_auth_header(Config)], - "application/x-www-form-urlencoded", - BodyQ}, [], - [{body_format, binary}]), - Body). + httpc:request(post, + {page(Config, URL), + [basic_auth_header(Config)], + "application/x-www-form-urlencoded", + BodyQ}, + [], + [{body_format, binary}]), + Body). diff --git a/tools/xml_compress_gen.erl b/tools/xml_compress_gen.erl index d331d7533..a2cafa773 100644 --- a/tools/xml_compress_gen.erl +++ b/tools/xml_compress_gen.erl @@ -32,391 +32,516 @@ -record(el_stats, {count = 0, empty_count = 0, only_text_count = 0, attrs = #{}, text_stats = #{}}). -record(attr_stats, {count = 0, vals = #{}}). + archive_analyze(Host, Table, EHost) -> case ejabberd_sql:sql_query(Host, [<<"select username, peer, kind, xml from ", Table/binary>>]) of - {selected, _, Res} -> - lists:foldl( - fun([U, P, K, X], Stats) -> - M = case K of - <<"groupchat">> -> - U; - _ -> - <<U/binary, "@", EHost/binary>> - end, - El = fxml_stream:parse_element(X), - analyze_element({El, <<"stream">>, <<"jabber:client">>, M, P}, Stats) - end, {0, #{}}, Res); - _ -> - none + {selected, _, Res} -> + lists:foldl( + fun([U, P, K, X], Stats) -> + M = case K of + <<"groupchat">> -> + U; + _ -> + <<U/binary, "@", EHost/binary>> + end, + El = fxml_stream:parse_element(X), + analyze_element({El, <<"stream">>, <<"jabber:client">>, M, P}, Stats) + end, + {0, #{}}, + Res); + _ -> + none end. + encode_id(Num) when Num < 64 -> iolist_to_binary(io_lib:format("~p:8", [Num])). + gen_code(_File, _Rules, $<) -> {error, <<"Invalid version">>}; gen_code(File, Rules, Ver) when Ver < 64 -> {Data, _} = lists:foldl( - fun({Ns, El, Attrs, Text}, {Acc, Id}) -> - NsC = case lists:keyfind(Ns, 1, Acc) of - false -> []; - {_, L} -> L - end, - {AttrsE, _} = lists:mapfoldl( - fun({AName, AVals}, Id2) -> - {AD, Id3} = lists:mapfoldl( - fun(AVal, Id3) -> - {{AVal, encode_id(Id3)}, Id3 + 1} - end, Id2, AVals), - {{AName, AD ++ [encode_id(Id3)]}, Id3 + 1} - end, 3, Attrs), - {TextE, Id5} = lists:mapfoldl( - fun(TextV, Id4) -> - {{TextV, encode_id(Id4)}, Id4 + 1} - end, Id + 1, Text), - {lists:keystore(Ns, 1, Acc, {Ns, NsC ++ [{El, encode_id(Id), AttrsE, TextE}]}), Id5} - end, {[], 5}, Rules), + fun({Ns, El, Attrs, Text}, {Acc, Id}) -> + NsC = case lists:keyfind(Ns, 1, Acc) of + false -> []; + {_, L} -> L + end, + {AttrsE, _} = lists:mapfoldl( + fun({AName, AVals}, Id2) -> + {AD, Id3} = lists:mapfoldl( + fun(AVal, Id3) -> + {{AVal, encode_id(Id3)}, Id3 + 1} + end, + Id2, + AVals), + {{AName, AD ++ [encode_id(Id3)]}, Id3 + 1} + end, + 3, + Attrs), + {TextE, Id5} = lists:mapfoldl( + fun(TextV, Id4) -> + {{TextV, encode_id(Id4)}, Id4 + 1} + end, + Id + 1, + Text), + {lists:keystore(Ns, 1, Acc, {Ns, NsC ++ [{El, encode_id(Id), AttrsE, TextE}]}), Id5} + end, + {[], 5}, + Rules), {ok, Dev} = file:open(File, [write]), Mod = filename:basename(File, ".erl"), io:format(Dev, "-module(~s).~n-export([encode/3, decode/3]).~n~n", [Mod]), RulesS = iolist_to_binary(io_lib:format("~p", [Rules])), RulesS2 = binary:replace(RulesS, <<"\n">>, <<"\n% ">>, [global]), - io:format(Dev, "% This file was generated by xml_compress_gen~n%~n" - "% Rules used:~n%~n% ~s~n~n", [RulesS2]), + io:format(Dev, + "% This file was generated by xml_compress_gen~n%~n" + "% Rules used:~n%~n% ~s~n~n", + [RulesS2]), VerId = iolist_to_binary(io_lib:format("~p:8", [Ver])), gen_encode(Dev, Data, VerId), gen_decode(Dev, Data, VerId), file:close(Dev), Data. -gen_decode(Dev, Data, VerId) -> - io:format(Dev, "decode(<<$<, _/binary>> = Data, _J1, _J2) ->~n" - " fxml_stream:parse_element(Data);~n" - "decode(<<~s, Rest/binary>>, J1, J2) ->~n" - " try decode(Rest, <<\"jabber:client\">>, J1, J2, false) of~n" - " {El, _} -> El~n" - " catch throw:loop_detected ->~n" - " {error, {loop_detected, <<\"Compressed data corrupted\">>}}~n" - " end.~n~n", [VerId]), - io:format(Dev, "decode_string(Data) ->~n" - " case Data of~n" - " <<0:2, L:6, Str:L/binary, Rest/binary>> ->~n" - " {Str, Rest};~n" - " <<1:2, L1:6, 0:2, L2:6, Rest/binary>> ->~n" - " L = L2*64 + L1,~n" - " <<Str:L/binary, Rest2/binary>> = Rest,~n" - " {Str, Rest2};~n" - " <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> ->~n" - " L = (L3*64 + L2)*64 + L1,~n" - " <<Str:L/binary, Rest2/binary>> = Rest,~n" - " {Str, Rest2}~n" - " end.~n~n", []), - io:format(Dev, "decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2, _) ->~n" - " {Text, Rest2} = decode_string(Rest),~n" - " {{xmlcdata, Text}, Rest2};~n", []), - io:format(Dev, "decode_child(<<2:8, Rest/binary>>, PNs, J1, J2, _) ->~n" - " {Name, Rest2} = decode_string(Rest),~n" - " {Attrs, Rest3} = decode_attrs(Rest2),~n" - " {Children, Rest4} = decode_children(Rest3, PNs, J1, J2),~n" - " {{xmlel, Name, Attrs, Children}, Rest4};~n", []), - io:format(Dev, "decode_child(<<3:8, Rest/binary>>, PNs, J1, J2, _) ->~n" - " {Ns, Rest2} = decode_string(Rest),~n" - " {Name, Rest3} = decode_string(Rest2),~n" - " {Attrs, Rest4} = decode_attrs(Rest3),~n" - " {Children, Rest5} = decode_children(Rest4, Ns, J1, J2),~n" - " {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5};~n", []), - io:format(Dev, "decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2, _) ->~n" - " {stop, Rest};~n", []), - io:format(Dev, "decode_child(_Other, _PNs, _J1, _J2, true) ->~n" - " throw(loop_detected);~n", []), - io:format(Dev, "decode_child(Other, PNs, J1, J2, _) ->~n" - " decode(Other, PNs, J1, J2, true).~n~n", []), - io:format(Dev, "decode_children(Data, PNs, J1, J2) ->~n" - " prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data).~n~n", []), - io:format(Dev, "decode_attr(<<1:8, Rest/binary>>) ->~n" - " {Name, Rest2} = decode_string(Rest),~n" - " {Val, Rest3} = decode_string(Rest2),~n" - " {{Name, Val}, Rest3};~n", []), - io:format(Dev, "decode_attr(<<2:8, Rest/binary>>) ->~n" - " {stop, Rest}.~n~n", []), - io:format(Dev, "decode_attrs(Data) ->~n" - " prefix_map(fun decode_attr/1, Data).~n~n", []), - io:format(Dev, "prefix_map(F, Data) ->~n" - " prefix_map(F, Data, []).~n~n", []), - io:format(Dev, "prefix_map(F, Data, Acc) ->~n" - " case F(Data) of~n" - " {stop, Rest} ->~n" - " {lists:reverse(Acc), Rest};~n" - " {Val, Rest} ->~n" - " prefix_map(F, Rest, [Val | Acc])~n" - " end.~n~n", []), - io:format(Dev, "add_ns(Ns, Ns, Attrs) ->~n" - " Attrs;~n" - "add_ns(_, Ns, Attrs) ->~n" - " [{<<\"xmlns\">>, Ns} | Attrs].~n~n", []), - lists:foreach( - fun({Ns, Els}) -> - lists:foreach( - fun({Name, Id, Attrs, Text}) -> - io:format(Dev, "decode(<<~s, Rest/binary>>, PNs, J1, J2, _) ->~n" - " Ns = ~p,~n", [Id, Ns]), - case Attrs of - [] -> - io:format(Dev, " {Attrs, Rest2} = decode_attrs(Rest),~n", []); - _ -> - io:format(Dev, " {Attrs, Rest2} = prefix_map(fun~n", []), - lists:foreach( - fun({AName, AVals}) -> - lists:foreach( - fun({j1, AId}) -> - io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" - " {{~p, J1}, Rest3};~n", [AId, AName]); - ({j2, AId}) -> - io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" - " {{~p, J2}, Rest3};~n", [AId, AName]); - ({{j1}, AId}) -> - io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" - " {AVal, Rest4} = decode_string(Rest3),~n" - " {{~p, <<J1/binary, AVal/binary>>}, Rest4};~n", - [AId, AName]); - ({{j2}, AId}) -> - io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" - " {AVal, Rest4} = decode_string(Rest3),~n" - " {{~p, <<J2/binary, AVal/binary>>}, Rest4};~n", - [AId, AName]); - ({AVal, AId}) -> - io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" - " {{~p, ~p}, Rest3};~n", - [AId, AName, AVal]); - (AId) -> - io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" - " {AVal, Rest4} = decode_string(Rest3),~n" - " {{~p, AVal}, Rest4};~n", - [AId, AName]) - end, AVals) - end, Attrs), - io:format(Dev, " (<<2:8, Rest3/binary>>) ->~n" - " {stop, Rest3};~n" - " (Data) ->~n" - " decode_attr(Data)~n" - " end, Rest),~n", []) - end, - case Text of - [] -> - io:format(Dev, " {Children, Rest6} = decode_children(Rest2, Ns, J1, J2),~n", []); - _ -> - io:format(Dev, " {Children, Rest6} = prefix_map(fun", []), - lists:foreach( - fun({TextS, TId}) -> - io:format(Dev, " (<<~s, Rest5/binary>>) ->~n" - " {{xmlcdata, ~p}, Rest5};~n", - [TId, TextS]) - end, Text), - io:format(Dev, " (Other) ->~n" - " decode_child(Other, Ns, J1, J2, false)~n" - " end, Rest2),~n", []) - end, - io:format(Dev, " {{xmlel, ~p, add_ns(PNs, Ns, Attrs), Children}, Rest6};~n", [Name]) - end, Els) - end, Data), - io:format(Dev, "decode(Other, PNs, J1, J2, Loop) ->~n" - " decode_child(Other, PNs, J1, J2, Loop).~n~n", []). +gen_decode(Dev, Data, VerId) -> + io:format(Dev, + "decode(<<$<, _/binary>> = Data, _J1, _J2) ->~n" + " fxml_stream:parse_element(Data);~n" + "decode(<<~s, Rest/binary>>, J1, J2) ->~n" + " try decode(Rest, <<\"jabber:client\">>, J1, J2, false) of~n" + " {El, _} -> El~n" + " catch throw:loop_detected ->~n" + " {error, {loop_detected, <<\"Compressed data corrupted\">>}}~n" + " end.~n~n", + [VerId]), + io:format(Dev, + "decode_string(Data) ->~n" + " case Data of~n" + " <<0:2, L:6, Str:L/binary, Rest/binary>> ->~n" + " {Str, Rest};~n" + " <<1:2, L1:6, 0:2, L2:6, Rest/binary>> ->~n" + " L = L2*64 + L1,~n" + " <<Str:L/binary, Rest2/binary>> = Rest,~n" + " {Str, Rest2};~n" + " <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> ->~n" + " L = (L3*64 + L2)*64 + L1,~n" + " <<Str:L/binary, Rest2/binary>> = Rest,~n" + " {Str, Rest2}~n" + " end.~n~n", + []), + io:format(Dev, + "decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2, _) ->~n" + " {Text, Rest2} = decode_string(Rest),~n" + " {{xmlcdata, Text}, Rest2};~n", + []), + io:format(Dev, + "decode_child(<<2:8, Rest/binary>>, PNs, J1, J2, _) ->~n" + " {Name, Rest2} = decode_string(Rest),~n" + " {Attrs, Rest3} = decode_attrs(Rest2),~n" + " {Children, Rest4} = decode_children(Rest3, PNs, J1, J2),~n" + " {{xmlel, Name, Attrs, Children}, Rest4};~n", + []), + io:format(Dev, + "decode_child(<<3:8, Rest/binary>>, PNs, J1, J2, _) ->~n" + " {Ns, Rest2} = decode_string(Rest),~n" + " {Name, Rest3} = decode_string(Rest2),~n" + " {Attrs, Rest4} = decode_attrs(Rest3),~n" + " {Children, Rest5} = decode_children(Rest4, Ns, J1, J2),~n" + " {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5};~n", + []), + io:format(Dev, + "decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2, _) ->~n" + " {stop, Rest};~n", + []), + io:format(Dev, + "decode_child(_Other, _PNs, _J1, _J2, true) ->~n" + " throw(loop_detected);~n", + []), + io:format(Dev, + "decode_child(Other, PNs, J1, J2, _) ->~n" + " decode(Other, PNs, J1, J2, true).~n~n", + []), + io:format(Dev, + "decode_children(Data, PNs, J1, J2) ->~n" + " prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2, false) end, Data).~n~n", + []), + io:format(Dev, + "decode_attr(<<1:8, Rest/binary>>) ->~n" + " {Name, Rest2} = decode_string(Rest),~n" + " {Val, Rest3} = decode_string(Rest2),~n" + " {{Name, Val}, Rest3};~n", + []), + io:format(Dev, + "decode_attr(<<2:8, Rest/binary>>) ->~n" + " {stop, Rest}.~n~n", + []), + io:format(Dev, + "decode_attrs(Data) ->~n" + " prefix_map(fun decode_attr/1, Data).~n~n", + []), + io:format(Dev, + "prefix_map(F, Data) ->~n" + " prefix_map(F, Data, []).~n~n", + []), + io:format(Dev, + "prefix_map(F, Data, Acc) ->~n" + " case F(Data) of~n" + " {stop, Rest} ->~n" + " {lists:reverse(Acc), Rest};~n" + " {Val, Rest} ->~n" + " prefix_map(F, Rest, [Val | Acc])~n" + " end.~n~n", + []), + io:format(Dev, + "add_ns(Ns, Ns, Attrs) ->~n" + " Attrs;~n" + "add_ns(_, Ns, Attrs) ->~n" + " [{<<\"xmlns\">>, Ns} | Attrs].~n~n", + []), + lists:foreach( + fun({Ns, Els}) -> + lists:foreach( + fun({Name, Id, Attrs, Text}) -> + io:format(Dev, + "decode(<<~s, Rest/binary>>, PNs, J1, J2, _) ->~n" + " Ns = ~p,~n", + [Id, Ns]), + case Attrs of + [] -> + io:format(Dev, " {Attrs, Rest2} = decode_attrs(Rest),~n", []); + _ -> + io:format(Dev, " {Attrs, Rest2} = prefix_map(fun~n", []), + lists:foreach( + fun({AName, AVals}) -> + lists:foreach( + fun({j1, AId}) -> + io:format(Dev, + " (<<~s, Rest3/binary>>) ->~n" + " {{~p, J1}, Rest3};~n", + [AId, AName]); + ({j2, AId}) -> + io:format(Dev, + " (<<~s, Rest3/binary>>) ->~n" + " {{~p, J2}, Rest3};~n", + [AId, AName]); + ({{j1}, AId}) -> + io:format(Dev, + " (<<~s, Rest3/binary>>) ->~n" + " {AVal, Rest4} = decode_string(Rest3),~n" + " {{~p, <<J1/binary, AVal/binary>>}, Rest4};~n", + [AId, AName]); + ({{j2}, AId}) -> + io:format(Dev, + " (<<~s, Rest3/binary>>) ->~n" + " {AVal, Rest4} = decode_string(Rest3),~n" + " {{~p, <<J2/binary, AVal/binary>>}, Rest4};~n", + [AId, AName]); + ({AVal, AId}) -> + io:format(Dev, + " (<<~s, Rest3/binary>>) ->~n" + " {{~p, ~p}, Rest3};~n", + [AId, AName, AVal]); + (AId) -> + io:format(Dev, + " (<<~s, Rest3/binary>>) ->~n" + " {AVal, Rest4} = decode_string(Rest3),~n" + " {{~p, AVal}, Rest4};~n", + [AId, AName]) + end, + AVals) + end, + Attrs), + io:format(Dev, + " (<<2:8, Rest3/binary>>) ->~n" + " {stop, Rest3};~n" + " (Data) ->~n" + " decode_attr(Data)~n" + " end, Rest),~n", + []) + end, + case Text of + [] -> + io:format(Dev, " {Children, Rest6} = decode_children(Rest2, Ns, J1, J2),~n", []); + _ -> + io:format(Dev, " {Children, Rest6} = prefix_map(fun", []), + lists:foreach( + fun({TextS, TId}) -> + io:format(Dev, + " (<<~s, Rest5/binary>>) ->~n" + " {{xmlcdata, ~p}, Rest5};~n", + [TId, TextS]) + end, + Text), + + io:format(Dev, + " (Other) ->~n" + " decode_child(Other, Ns, J1, J2, false)~n" + " end, Rest2),~n", + []) + end, + io:format(Dev, " {{xmlel, ~p, add_ns(PNs, Ns, Attrs), Children}, Rest6};~n", [Name]) + end, + Els) + end, + Data), + io:format(Dev, + "decode(Other, PNs, J1, J2, Loop) ->~n" + " decode_child(Other, PNs, J1, J2, Loop).~n~n", + []). gen_encode(Dev, Data, VerId) -> - io:format(Dev, "encode(El, J1, J2) ->~n" - " encode_child(El, <<\"jabber:client\">>,~n" - " J1, J2, byte_size(J1), byte_size(J2), <<~s>>).~n~n", [VerId]), - io:format(Dev, "encode_attr({<<\"xmlns\">>, _}, Acc) ->~n" - " Acc;~n" - "encode_attr({N, V}, Acc) ->~n" - " <<Acc/binary, 1:8, (encode_string(N))/binary,~n" - " (encode_string(V))/binary>>.~n~n", []), - io:format(Dev, "encode_attrs(Attrs, Acc) ->~n" - " lists:foldl(fun encode_attr/2, Acc, Attrs).~n~n", []), - io:format(Dev, "encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" - " E1 = if~n" - " PNs == Ns -> encode_attrs(Attrs, <<Pfx/binary, 2:8, (encode_string(Name))/binary>>);~n" - " true -> encode_attrs(Attrs, <<Pfx/binary, 3:8, " - "(encode_string(Ns))/binary, (encode_string(Name))/binary>>)~n" - " end,~n" - " E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <<E1/binary, 2:8>>),~n" - " <<E2/binary, 4:8>>.~n~n", []), - io:format(Dev, "encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) ->~n" - " case lists:keyfind(<<\"xmlns\">>, 1, Attrs) of~n" - " false ->~n" - " encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx);~n" - " {_, Ns} ->~n" - " encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)~n" - " end;~n" - "encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) ->~n" - " <<Pfx/binary, 1:8, (encode_string(Data))/binary>>.~n~n", []), - io:format(Dev, "encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) ->~n" - " lists:foldl(~n" - " fun(Child, Acc) ->~n" - " encode_child(Child, PNs, J1, J2, J1L, J2L, Acc)~n" - " end, Pfx, Children).~n~n", []), - io:format(Dev, "encode_string(Data) ->~n" - " <<V1:4, V2:6, V3:6>> = <<(byte_size(Data)):16/unsigned-big-integer>>,~n" - " case {V1, V2, V3} of~n" - " {0, 0, V3} ->~n" - " <<V3:8, Data/binary>>;~n" - " {0, V2, V3} ->~n" - " <<(V3 bor 64):8, V2:8, Data/binary>>;~n" - " _ ->~n" - " <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>>~n" - " end.~n~n", []), + io:format(Dev, + "encode(El, J1, J2) ->~n" + " encode_child(El, <<\"jabber:client\">>,~n" + " J1, J2, byte_size(J1), byte_size(J2), <<~s>>).~n~n", + [VerId]), + io:format(Dev, + "encode_attr({<<\"xmlns\">>, _}, Acc) ->~n" + " Acc;~n" + "encode_attr({N, V}, Acc) ->~n" + " <<Acc/binary, 1:8, (encode_string(N))/binary,~n" + " (encode_string(V))/binary>>.~n~n", + []), + io:format(Dev, + "encode_attrs(Attrs, Acc) ->~n" + " lists:foldl(fun encode_attr/2, Acc, Attrs).~n~n", + []), + io:format(Dev, + "encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" + " E1 = if~n" + " PNs == Ns -> encode_attrs(Attrs, <<Pfx/binary, 2:8, (encode_string(Name))/binary>>);~n" + " true -> encode_attrs(Attrs, <<Pfx/binary, 3:8, " + "(encode_string(Ns))/binary, (encode_string(Name))/binary>>)~n" + " end,~n" + " E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <<E1/binary, 2:8>>),~n" + " <<E2/binary, 4:8>>.~n~n", + []), + io:format(Dev, + "encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) ->~n" + " case lists:keyfind(<<\"xmlns\">>, 1, Attrs) of~n" + " false ->~n" + " encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx);~n" + " {_, Ns} ->~n" + " encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)~n" + " end;~n" + "encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) ->~n" + " <<Pfx/binary, 1:8, (encode_string(Data))/binary>>.~n~n", + []), + io:format(Dev, + "encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) ->~n" + " lists:foldl(~n" + " fun(Child, Acc) ->~n" + " encode_child(Child, PNs, J1, J2, J1L, J2L, Acc)~n" + " end, Pfx, Children).~n~n", + []), + io:format(Dev, + "encode_string(Data) ->~n" + " <<V1:4, V2:6, V3:6>> = <<(byte_size(Data)):16/unsigned-big-integer>>,~n" + " case {V1, V2, V3} of~n" + " {0, 0, V3} ->~n" + " <<V3:8, Data/binary>>;~n" + " {0, V2, V3} ->~n" + " <<(V3 bor 64):8, V2:8, Data/binary>>;~n" + " _ ->~n" + " <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>>~n" + " end.~n~n", + []), lists:foreach( - fun({Ns, Els}) -> - io:format(Dev, "encode(PNs, ~p = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" - " case Name of~n", [Ns]), - lists:foreach( - fun({ElN, Id, Attrs, Text}) -> - io:format(Dev, " ~p ->~n", [ElN]), - case Attrs of - [] -> - io:format(Dev, " E = encode_attrs(Attrs, <<Pfx/binary, ~s>>),~n", [Id]); - _ -> - io:format(Dev, " E = lists:foldl(fun~n", []), - lists:foreach( - fun({AName, AVals}) -> - case AVals of - [AIdS] when is_binary(AIdS) -> - io:format(Dev, " ({~p, AVal}, Acc) ->~n" - " <<Acc/binary, ~s, (encode_string(AVal))/binary>>;~n", - [AName, AIdS]); - _ -> - io:format(Dev, " ({~p, AVal}, Acc) ->~n" - " case AVal of~n", [AName]), - lists:foreach( - fun({j1, AId}) -> - io:format(Dev, " J1 -> <<Acc/binary, ~s>>;~n", - [AId]); - ({j2, AId}) -> - io:format(Dev, " J2 -> <<Acc/binary, ~s>>;~n", - [AId]); - ({{j1}, AId}) -> - io:format(Dev, " <<J1:J1L/binary, Rest/binary>> -> " - "<<Acc/binary, ~s, (encode_string(Rest))/binary>>;~n", - [AId]); - ({{j2}, AId}) -> - io:format(Dev, " <<J2:J2L/binary, Rest/binary>> -> " - "<<Acc/binary, ~s, (encode_string(Rest))/binary>>;~n", - [AId]); - ({AVal, AId}) -> - io:format(Dev, " ~p -> <<Acc/binary, ~s>>;~n", - [AVal, AId]); - (AId) -> - io:format(Dev, " _ -> <<Acc/binary, ~s, " - "(encode_string(AVal))/binary>>~n", - [AId]) - end, AVals), - io:format(Dev, " end;~n", []) - end - end, Attrs), - io:format(Dev, " (Attr, Acc) -> encode_attr(Attr, Acc)~n", []), - io:format(Dev, " end, <<Pfx/binary, ~s>>, Attrs),~n", [Id]) - end, - case Text of - [] -> - io:format(Dev, " E2 = encode_children(Children, Ns, " - "J1, J2, J1L, J2L, <<E/binary, 2:8>>),~n", []); - _ -> - io:format(Dev, " E2 = lists:foldl(fun~n", []), - lists:foreach( - fun({TextV, TId}) -> - io:format(Dev, " ({xmlcdata, ~p}, Acc) -> <<Acc/binary, ~s>>;~n", [TextV, TId]) - end, Text), - io:format(Dev, " (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc)~n", []), - io:format(Dev, " end, <<E/binary, 2:8>>, Children),~n", []) - end, - io:format(Dev, " <<E2/binary, 4:8>>;~n", []) - end, Els), - io:format(Dev, " _ -> encode_el(PNs, Ns, Name, Attrs, Children, " - "J1, J2, J1L, J2L, Pfx)~nend;~n", []) - end, Data), - io:format(Dev, "encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" - " encode_el(PNs, Ns, Name, Attrs, Children, " - "J1, J2, J1L, J2L, Pfx).~n~n", []). + fun({Ns, Els}) -> + io:format(Dev, + "encode(PNs, ~p = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" + " case Name of~n", + [Ns]), + lists:foreach( + fun({ElN, Id, Attrs, Text}) -> + io:format(Dev, " ~p ->~n", [ElN]), + case Attrs of + [] -> + io:format(Dev, " E = encode_attrs(Attrs, <<Pfx/binary, ~s>>),~n", [Id]); + _ -> + io:format(Dev, " E = lists:foldl(fun~n", []), + lists:foreach( + fun({AName, AVals}) -> + case AVals of + [AIdS] when is_binary(AIdS) -> + io:format(Dev, + " ({~p, AVal}, Acc) ->~n" + " <<Acc/binary, ~s, (encode_string(AVal))/binary>>;~n", + [AName, AIdS]); + _ -> + io:format(Dev, + " ({~p, AVal}, Acc) ->~n" + " case AVal of~n", + [AName]), + lists:foreach( + fun({j1, AId}) -> + io:format(Dev, + " J1 -> <<Acc/binary, ~s>>;~n", + [AId]); + ({j2, AId}) -> + io:format(Dev, + " J2 -> <<Acc/binary, ~s>>;~n", + [AId]); + ({{j1}, AId}) -> + io:format(Dev, + " <<J1:J1L/binary, Rest/binary>> -> " + "<<Acc/binary, ~s, (encode_string(Rest))/binary>>;~n", + [AId]); + ({{j2}, AId}) -> + io:format(Dev, + " <<J2:J2L/binary, Rest/binary>> -> " + "<<Acc/binary, ~s, (encode_string(Rest))/binary>>;~n", + [AId]); + ({AVal, AId}) -> + io:format(Dev, + " ~p -> <<Acc/binary, ~s>>;~n", + [AVal, AId]); + (AId) -> + io:format(Dev, + " _ -> <<Acc/binary, ~s, " + "(encode_string(AVal))/binary>>~n", + [AId]) + end, + AVals), + io:format(Dev, " end;~n", []) + end + end, + Attrs), + io:format(Dev, " (Attr, Acc) -> encode_attr(Attr, Acc)~n", []), + io:format(Dev, " end, <<Pfx/binary, ~s>>, Attrs),~n", [Id]) + end, + case Text of + [] -> + io:format(Dev, + " E2 = encode_children(Children, Ns, " + "J1, J2, J1L, J2L, <<E/binary, 2:8>>),~n", + []); + _ -> + io:format(Dev, " E2 = lists:foldl(fun~n", []), + lists:foreach( + fun({TextV, TId}) -> + io:format(Dev, " ({xmlcdata, ~p}, Acc) -> <<Acc/binary, ~s>>;~n", [TextV, TId]) + end, + Text), + io:format(Dev, " (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc)~n", []), + io:format(Dev, " end, <<E/binary, 2:8>>, Children),~n", []) + end, + io:format(Dev, " <<E2/binary, 4:8>>;~n", []) + end, + Els), + io:format(Dev, + " _ -> encode_el(PNs, Ns, Name, Attrs, Children, " + "J1, J2, J1L, J2L, Pfx)~nend;~n", + []) + end, + Data), + io:format(Dev, + "encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" + " encode_el(PNs, Ns, Name, Attrs, Children, " + "J1, J2, J1L, J2L, Pfx).~n~n", + []). + process_stats({_Counts, Stats}) -> SStats = lists:sort( - fun({_, #el_stats{count = C1}}, {_, #el_stats{count = C2}}) -> - C1 >= C2 - end, maps:to_list(Stats)), + fun({_, #el_stats{count = C1}}, {_, #el_stats{count = C2}}) -> + C1 >= C2 + end, + maps:to_list(Stats)), lists:map( - fun({Name, #el_stats{count = C, attrs = A, text_stats = T}}) -> - [Ns, El] = binary:split(Name, <<"<">>), - Attrs = lists:filtermap( - fun({AN, #attr_stats{count = AC, vals = AV}}) -> - if - AC*5 < C -> - false; - true -> - AVC = AC div min(maps:size(AV)*2, 10), - AVA = [N || {N, C2} <- maps:to_list(AV), C2 > AVC], - {true, {AN, AVA}} - end - end, maps:to_list(A)), - Text = [TE || {TE, TC} <- maps:to_list(T), TC > C/2], - {Ns, El, Attrs, Text} - end, SStats). + fun({Name, #el_stats{count = C, attrs = A, text_stats = T}}) -> + [Ns, El] = binary:split(Name, <<"<">>), + Attrs = lists:filtermap( + fun({AN, #attr_stats{count = AC, vals = AV}}) -> + if + AC * 5 < C -> + false; + true -> + AVC = AC div min(maps:size(AV) * 2, 10), + AVA = [ N || {N, C2} <- maps:to_list(AV), C2 > AVC ], + {true, {AN, AVA}} + end + end, + maps:to_list(A)), + Text = [ TE || {TE, TC} <- maps:to_list(T), TC > C / 2 ], + {Ns, El, Attrs, Text} + end, + SStats). + analyze_elements(Elements, Stats, PName, PNS, J1, J2) -> lists:foldl(fun analyze_element/2, Stats, lists:map(fun(V) -> {V, PName, PNS, J1, J2} end, Elements)). + maps_update(Key, F, InitVal, Map) -> case maps:is_key(Key, Map) of - true -> - maps:update_with(Key, F, Map); - _ -> - maps:put(Key, F(InitVal), Map) + true -> + maps:update_with(Key, F, Map); + _ -> + maps:put(Key, F(InitVal), Map) end. + analyze_element({{xmlcdata, Data}, PName, PNS, _J1, _J2}, {ElCount, Stats}) -> Stats2 = maps_update(<<PNS/binary, "<", PName/binary>>, - fun(#el_stats{text_stats = TS} = E) -> - TS2 = maps_update(Data, fun(C) -> C + 1 end, 0, TS), - E#el_stats{text_stats = TS2} - end, #el_stats{}, Stats), + fun(#el_stats{text_stats = TS} = E) -> + TS2 = maps_update(Data, fun(C) -> C + 1 end, 0, TS), + E#el_stats{text_stats = TS2} + end, + #el_stats{}, + Stats), {ElCount, Stats2}; analyze_element({#xmlel{name = Name, attrs = Attrs, children = Children}, _PName, PNS, J1, J2}, {ElCount, Stats}) -> XMLNS = case lists:keyfind(<<"xmlns">>, 1, Attrs) of - {_, NS} -> - NS; - false -> - PNS - end, + {_, NS} -> + NS; + false -> + PNS + end, NStats = maps_update(<<XMLNS/binary, "<", Name/binary>>, - fun(#el_stats{count = C, empty_count = EC, only_text_count = TC, attrs = A} = ES) -> - A2 = lists:foldl( - fun({<<"xmlns">>, _}, AMap) -> - AMap; - ({AName, AVal}, AMap) -> - J1S = size(J1), - J2S = size(J2), - AVal2 = case AVal of - J1 -> - j1; - J2 -> - j2; - <<J1:J1S/binary, _Rest/binary>> -> - {j1}; - <<J2:J2S/binary, _Rest/binary>> -> - {j2}; - Other -> - Other - end, - maps_update(AName, fun(#attr_stats{count = AC, vals = AV}) -> - AV2 = maps_update(AVal2, fun(C2) -> C2 + 1 end, 0, AV), - #attr_stats{count = AC + 1, vals = AV2} - end, #attr_stats{}, AMap) - end, A, Attrs), - ES#el_stats{count = C + 1, - empty_count = if Children == [] -> EC + 1; true -> - EC end, - only_text_count = case Children of [{xmlcdata, _}] -> TC + 1; _ -> TC end, - attrs = A2} - end, #el_stats{}, Stats), + fun(#el_stats{count = C, empty_count = EC, only_text_count = TC, attrs = A} = ES) -> + A2 = lists:foldl( + fun({<<"xmlns">>, _}, AMap) -> + AMap; + ({AName, AVal}, AMap) -> + J1S = size(J1), + J2S = size(J2), + AVal2 = case AVal of + J1 -> + j1; + J2 -> + j2; + <<J1:J1S/binary, _Rest/binary>> -> + {j1}; + <<J2:J2S/binary, _Rest/binary>> -> + {j2}; + Other -> + Other + end, + maps_update(AName, + fun(#attr_stats{count = AC, vals = AV}) -> + AV2 = maps_update(AVal2, fun(C2) -> C2 + 1 end, 0, AV), + #attr_stats{count = AC + 1, vals = AV2} + end, + #attr_stats{}, + AMap) + end, + A, + Attrs), + ES#el_stats{ + count = C + 1, + empty_count = if + Children == [] -> EC + 1; + true -> + EC + end, + only_text_count = case Children of [{xmlcdata, _}] -> TC + 1; _ -> TC end, + attrs = A2 + } + end, + #el_stats{}, + Stats), analyze_elements(Children, {ElCount + 1, NStats}, Name, XMLNS, J1, J2). From 24f614b580985ca318982726052d537480264ff7 Mon Sep 17 00:00:00 2001 From: Badlop <badlop@process-one.net> Date: Fri, 29 Aug 2025 22:41:37 +0200 Subject: [PATCH 5/5] Add file that tells what cosmetic git commits to ignore --- .git-blame-ignore-revs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..bca40a29e --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,11 @@ +# .git-blame-ignore-revs + +# Use this file when viewing blame: +# git blame --ignore-revs-file .git-blame-ignore-revs +# Or configure git to use always this file: +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Accumulated patch to binarize and indent code +9deb294328bb3f9eb6bd2c0e7cd500732e9b5830 +# Result of running "make format indent" for the first time +6b7d15f0271686b6902a0edc82f407e529b35a90