diff --git a/.dockerignore b/.dockerignore index 94b549649..b825bcd1f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,46 @@ .git .win32 .examples +*.swp +*~ +\#*# +.#* +.edts +*.dump +/Makefile +/config.log +/config.status +/config/releases.exs +/configure +/aclocal.m4 +/*.cache +/deps/ +/.deps-update/ +/ebin/ +/ejabberd.init +/ejabberd.service +/ejabberdctl +/ejabberdctl.example +/rel/ejabberd/ +/rel/overlays/ +/src/eldap_filter_yecc.erl +/vars.config +/dialyzer/ +/test/*.beam +/test/*.ctc +/logs/ +/priv/bin/captcha*sh +/priv/sql +/rel/ejabberd +/_build +/database/ +/.rebar +/rebar.lock +/log/ +Mnesia.nonode@nohost/ +# Binaries created with tools/make-{binaries,installers,packages}: +/ejabberd_*.deb +/ejabberd-*.rpm +/ejabberd-*.run +/ejabberd-*.tar.gz + diff --git a/.github/container/Dockerfile b/.github/container/Dockerfile new file mode 100644 index 000000000..a60446c32 --- /dev/null +++ b/.github/container/Dockerfile @@ -0,0 +1,119 @@ +FROM alpine:3.15.4 AS build +ARG VERSION=master + +RUN apk upgrade --update musl \ + && apk add \ + autoconf \ + automake \ + bash \ + build-base \ + curl \ + elixir \ + erlang-odbc \ + erlang-reltool \ + expat-dev \ + file \ + gd-dev \ + git \ + jpeg-dev \ + libpng-dev \ + libwebp-dev \ + linux-pam-dev \ + openssl \ + openssl-dev \ + sqlite-dev \ + yaml-dev \ + zlib-dev + +RUN mix local.hex --force \ + && mix local.rebar --force + +COPY . ./ejabberd + +WORKDIR ejabberd + +RUN mv .github/container/ejabberdctl.template . \ + && ./autogen.sh \ + && ./configure --with-rebar=mix --enable-all \ + && make deps \ + && make rel + +RUN cp -r _build/prod/rel/ejabberd/ /opt/ejabberd-$VERSION \ + && mkdir -p /opt/ejabberd \ + && mv /opt/ejabberd-$VERSION/conf /opt/ejabberd/conf + +RUN BINPATH=$(dirname $(find /opt -name msgs))/bin/ \ + && mkdir -p $BINPATH \ + && cp tools/captcha*.sh $BINPATH + +RUN [ ! -d .ejabberd-modules ] || cp -r .ejabberd-modules /opt/ejabberd/ + +RUN export PEM=/opt/ejabberd/conf/server.pem \ + && curl -o "/opt/ejabberd/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' \ + && openssl req -x509 \ + -batch \ + -nodes \ + -newkey rsa:4096 \ + -keyout $PEM \ + -out $PEM \ + -days 3650 \ + -subj "/CN=localhost" \ + && sed -i '/^loglevel:/a \ \ + \nca_file: /opt/ejabberd/conf/cacert.pem \ + \ncertfiles: \ + \n - /opt/ejabberd/conf/server.pem' "/opt/ejabberd/conf/ejabberd.yml" + +FROM alpine:3.15.4 +ENV HOME=/opt/ejabberd +ARG VERSION=master + +RUN apk upgrade --update musl \ + && apk add \ + expat \ + freetds \ + gd \ + jpeg \ + libgd \ + libpng \ + libstdc++ \ + libwebp \ + linux-pam \ + ncurses-libs \ + openssl \ + sqlite \ + sqlite-libs \ + unixodbc \ + yaml \ + zlib \ + && ln -fs /usr/lib/libtdsodbc.so.0 /usr/lib/libtdsodbc.so \ + && rm -rf /var/cache/apk/* + +COPY --from=build /opt /opt +RUN echo -e \ + "#!/bin/sh \ + \n[ -z \$ERLANG_NODE_ARG ] && export ERLANG_NODE_ARG=ejabberd@localhost \ + \nexport CONFIG_DIR=/opt/ejabberd/conf \ + \nexport LOGS_DIR=/opt/ejabberd/logs \ + \nexport SPOOL_DIR=/opt/ejabberd/database \ + \nexec /opt/ejabberd-$VERSION/bin/ejabberdctl \"\$@\"" > /usr/local/bin/ejabberdctl \ + && chmod +x /usr/local/bin/ejabberdctl + +RUN addgroup ejabberd -g 9000 \ + && adduser -s /bin/sh -D -G ejabberd ejabberd -u 9000 \ + && mkdir -p $HOME/conf $HOME/database $HOME/logs $HOME/upload \ + && chown -R ejabberd:ejabberd $HOME + +HEALTHCHECK \ + --interval=1m \ + --timeout=5s \ + --start-period=5s \ + --retries=10 \ + CMD /usr/local/bin/ejabberdctl status + +WORKDIR $HOME +USER ejabberd +VOLUME ["$HOME/conf", "$HOME/database", "$HOME/logs", "$HOME/upload"] +EXPOSE 1883 4369-4399 5210 5222 5269 5280 5443 + +ENTRYPOINT ["/usr/local/bin/ejabberdctl"] +CMD ["foreground"] diff --git a/.github/container/ejabberd-container-install.bat b/.github/container/ejabberd-container-install.bat new file mode 100755 index 000000000..704011a6d --- /dev/null +++ b/.github/container/ejabberd-container-install.bat @@ -0,0 +1,294 @@ +@echo off + +:: +:: ejabberd container installer for Windows +:: ------------------------------------- +:: v0.4 +:: +:: This batch script downloads an ejabberd container image +:: and setups a docker container to run ejabberd. + +:: +:: 1. Download and install Docker: +:: +:: If you use Windows 10, download Docker Desktop from: +:: https://www.docker.com/ +:: +:: If you use Windows 7 or 8, download Docker Toolbox from: +:: https://github.com/docker/toolbox/releases +:: After installation, run Docker Quickstart Installer +:: + +:: +:: 2. Edit those options: + +:: Directory where your ejabberd deployment files will be installed +:: (configuration, database, logs, ...) +:: +:: In Windows 10 you can configure the path: + +set INSTALL_DIR_WINDOWS10=C:\ejabberd + +:: In older Windows, not configurable, it will be installed in: +:: C:\Users\%USERNAME%\ejabberd + +:: Please enter the desired ejabberd domain name. +:: The domain is the visible attribute that is added to the username +:: to form the Jabber Identifier (for example: user@example.net). +:: This computer must be known on the network with this address name. +:: You can later add more in conf/ejabberd.yml + +set HOST=localhost + +:: Please enter the administrator username for the current +:: ejabberd installation. A Jabber account with this username +:: will be created and granted administrative privileges. +:: Don't use blankspaces in the username. + +set USER=admin + +:: Please provide a password for that new administrator account + +set PASSWORD= + +:: By default this downloads 'latest' ejabberd version, +:: but you can set a specific version, for example '22.05' +:: or the bleeding edge 'master'. See available tags in +:: https://github.com/processone/ejabberd/pkgs/container/ejabberd + +set VERSION=latest + +:: This tells docker what ports ejabberd will use. +:: You can later configure them in conf/ejabberd.yml + +set PORTS=5180 5222 5269 5443 + +:: +:: 3. Now save this script and run it. +:: + +:: +:: 4. When installation is completed: +:: +:: If using Windows 10, open Docker Desktop and you can: +:: +:: - (>) START the ejabberd container +:: - Enter WebAdmin: click the ([->]) OPEN IN BROWSER button +:: - To try ejabberdctl, click the (>_) CLI button, then: ejabberdctl +:: - ([]) STOP the ejabberd container +:: +:: If using an old Windows, open Kitematic and you can: +:: +:: - START the ejabberd container +:: - Open your configuration, logs, ... in Settings > Volumes +:: - Enter WebAdmin in Settings > Hostname/Ports > click on the 5180 port +:: - Try ejabberdctl in EXEC, then: ejabberdctl +:: - STOP the ejabberd container +:: +:: You can delete the container and create it again running this script, +:: the configuration and database are maintained. +:: + +::=============================================================== +:: Check Windows version +:: +::=============================================================== + +set INSTALL_DIR_DOCKER=c/Users/%USERNAME%/ejabberd + +for /f "tokens=4-5 delims=. " %%i in ('ver') do set WVERSION=%%i.%%j +if "%wversion%" == "10.0" ( + echo === Preparing paths to install in Windows 10... + set INSTALL_DIR=%INSTALL_DIR_WINDOWS10% + set VC=-v %INSTALL_DIR_WINDOWS10%\conf:/opt/ejabberd/conf + set VD=-v %INSTALL_DIR_WINDOWS10%\database:/opt/ejabberd/database + set VL=-v %INSTALL_DIR_WINDOWS10%\logs:/opt/ejabberd/logs + set VM=-v %INSTALL_DIR_WINDOWS10%\ejabberd-modules:/opt/ejabberd/.ejabberd-modules + set DOCKERDOWNLOAD="First download and install Docker Desktop from https://www.docker.com/" +) else ( + echo === Preparing paths to install in Windows older than 10... + set INSTALL_DIR=C:\Users\%USERNAME%\ejabberd + set VC=-v "/%INSTALL_DIR_DOCKER%/conf:/opt/ejabberd/conf" + set VD=-v "/%INSTALL_DIR_DOCKER%/database:/opt/ejabberd/database" + set VL=-v "/%INSTALL_DIR_DOCKER%/logs:/opt/ejabberd/logs" + set VM=-v "/%INSTALL_DIR_DOCKER%/ejabberd-modules:/opt/ejabberd/.ejabberd-modules" + set DOCKERDOWNLOAD="First download and install Docker Toolbox from https://github.com/docker/toolbox/releases" +) +set VOLUMES=%VC% %VD% %VL% %VM% + +::=============================================================== +:: Check docker is installed +:: +::=============================================================== + +docker version >NUL +if %ERRORLEVEL% NEQ 0 ( + echo. + echo === ERROR: It seems docker is not installed!!! + echo. + echo %DOCKERDOWNLOAD% + echo === Then try to run this script again. + echo. + pause + exit 1 +) + +::=============================================================== +:: Check install options are correctly set +:: +::=============================================================== + +if [%PASSWORD%]==[] ( + echo. + echo === ERROR: PASSWORD not set!!! + echo. + echo === Please edit this script and set the PASSWORD. + echo === Then try to run this script again. + echo. + pause + exit 1 +) + +::=============================================================== +:: Download Docker image +:: +::=============================================================== + +set IMAGE=ghcr.io/processone/ejabberd:%VERSION% + +echo. +echo === Checking if the '%IMAGE%' container image was already downloaded... +docker image history %IMAGE% >NUL +if %ERRORLEVEL% NEQ 0 ( + echo === The '%IMAGE%' container image was not downloaded yet. + echo. + echo === Downloading the '%IMAGE%' container image, please wait... + docker pull %IMAGE% +) else ( + echo === The '%IMAGE%' container image was already downloaded. +) + +::=============================================================== +:: Create preliminary container +:: +::=============================================================== + +echo. +echo === Checking if the 'ejabberd' container already exists... +docker container logs ejabberd +if %ERRORLEVEL% EQU 0 ( + echo. + echo === The 'ejabberd' container already exists. + echo === Nothing to do, so installation finishes now. + echo === You can go to Docker Desktop and start the 'ejabberd' container. + echo. + pause + exit 1 +) else ( + echo === The 'ejabberd' container doesn't yet exist, + echo === so let's continue the installation process. +) + +echo. +if exist %INSTALL_DIR% ( + echo === The INSTALL_DIR %INSTALL_DIR% already exists. + echo === No need to create the preliminary 'ejabberd-pre' image. +) else ( + echo === The INSTALL_DIR %INSTALL_DIR% doesn't exist. + echo === Let's create the preliminary 'ejabberd-pre' image. + CALL :create-ejabberd-pre +) + +::=============================================================== +:: Create final container +:: +::=============================================================== + +echo. +echo === Creating the final 'ejabberd' container using %IMAGE% image... + +setlocal EnableDelayedExpansion +set PS= +for %%a in (%PORTS%) do ( + set PS=!PS! -p %%a:%%a +) + +docker create --name ejabberd --hostname localhost %PS% %VOLUMES% %IMAGE% + +echo. +echo === Installation completed. +echo. +pause + +EXIT /B %ERRORLEVEL% + +::=============================================================== +:: Function to create preliminary container +:: +::=============================================================== + +:create-ejabberd-pre + +echo. +echo === Creating a preliminary 'ejabberd-pre' container using %IMAGE% image... +docker create --name ejabberd-pre --hostname localhost %IMAGE% + +echo. +echo === Now 'ejabberd-pre' will be started. +docker container start ejabberd-pre + +echo. +echo === Waiting ejabberd to be running... +set /A timeout = 10 +set status=4 +goto :while + +:statusstart +docker exec -it ejabberd-pre ejabberdctl status +goto :statusend + +:while +if %status% GTR 0 ( + echo. + timeout /t 1 /nobreak >NUL + set /A timeout = timeout - 1 + if %timeout% EQU 0 ( + set status=-1 + ) else ( + goto :statusstart + :statusend + set status=%ERRORLEVEL% + ) + goto :while +) + +echo. +echo === Setting a few options... +docker exec -it ejabberd-pre sed -i "s!- localhost!- %HOST%!g" conf/ejabberd.yml +docker exec -it ejabberd-pre sed -i "s!^acl:!acl:\n admin:\n user:\n - \"%USER%@%HOST%\"!g" conf/ejabberd.yml +docker exec -it ejabberd-pre sed -i "s!5280!5180!g" conf/ejabberd.yml +docker exec -it ejabberd-pre sed -i "s!/admin!/!g" conf/ejabberd.yml +docker exec -it ejabberd-pre ejabberdctl reload_config + +echo. +echo === Registering the administrator account... +docker exec -it ejabberd-pre ejabberdctl register %USER% %HOST% %PASSWORD% +docker exec -it ejabberd-pre ejabberdctl stop + +echo. +echo === Copying conf, database, logs... +mkdir %INSTALL_DIR% +mkdir %INSTALL_DIR%\conf +mkdir %INSTALL_DIR%\database +mkdir %INSTALL_DIR%\logs +mkdir %INSTALL_DIR%\ejabberd-modules +docker cp ejabberd-pre:/opt/ejabberd/conf/ %INSTALL_DIR% +docker cp ejabberd-pre:/opt/ejabberd/database/ %INSTALL_DIR% +docker cp ejabberd-pre:/opt/ejabberd/logs/ %INSTALL_DIR% + +echo. +echo === Deleting the preliminary 'ejabberd-pre' container... +docker stop ejabberd-pre +docker rm ejabberd-pre + +EXIT /B 0 diff --git a/.github/container/ejabberdctl.template b/.github/container/ejabberdctl.template new file mode 100755 index 000000000..de3826a46 --- /dev/null +++ b/.github/container/ejabberdctl.template @@ -0,0 +1,387 @@ +#!/bin/sh + +# define default configuration +POLL=true +ERL_MAX_PORTS=32000 +ERL_PROCESSES=250000 +ERL_MAX_ETS_TABLES=1400 +FIREWALL_WINDOW="" +INET_DIST_INTERFACE="" +ERLANG_NODE=ejabberd@localhost + +# define default environment variables +[ -z "$SCRIPT" ] && SCRIPT=$0 +SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" +# shellcheck disable=SC2034 +ERTS_VSN="{{erts_vsn}}" +ERL="{{erl}}" +IEX="{{bindir}}/iex" +EPMD="{{epmd}}" +[ -z "$ERLANG_COOKIE" ] && ERL_OPTIONS="-setcookie $(cat "${SCRIPT_DIR%/*}/releases/COOKIE")" +[ -n "$ERLANG_COOKIE" ] && [ ! -f "$HOME"/.erlang.cookie ] && echo "$ERLANG_COOKIE" > "$HOME"/.erlang.cookie && chmod 400 "$HOME"/.erlang.cookie + +# check the proper system user is used +case $(id -un) in + "$INSTALLUSER") + EXEC_CMD="as_current_user" + ;; + root) + if [ -n "$INSTALLUSER" ] ; then + EXEC_CMD="as_install_user" + else + EXEC_CMD="as_current_user" + echo "WARNING: It is not recommended to run ejabberd as root" >&2 + fi + ;; + *) + if [ -n "$INSTALLUSER" ] ; then + echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2 + exit 7 + else + EXEC_CMD="as_current_user" + fi + ;; +esac + +# parse command line parameters +while [ $# -gt 0 ]; do + case $1 in + -n|--node) ERLANG_NODE_ARG=$2; shift 2;; + -s|--spool) SPOOL_DIR=$2; shift 2;; + -l|--logs) LOGS_DIR=$2; shift 2;; + -f|--config) EJABBERD_CONFIG_PATH=$2; shift 2;; + -c|--ctl-config) EJABBERDCTL_CONFIG_PATH=$2; shift 2;; + -d|--config-dir) CONFIG_DIR=$2; shift 2;; + -t|--no-timeout) NO_TIMEOUT="--no-timeout"; shift;; + *) break;; + esac +done + +# define ejabberd variables if not already defined from the command line +: "${CONFIG_DIR:="{{config_dir}}"}" +: "${LOGS_DIR:="{{logs_dir}}"}" +: "${SPOOL_DIR:="{{spool_dir}}"}" +: "${EJABBERD_CONFIG_PATH:="$CONFIG_DIR/ejabberd.yml"}" +: "${EJABBERDCTL_CONFIG_PATH:="$CONFIG_DIR/ejabberdctl.cfg"}" +# Allows passing extra Erlang command-line arguments in vm.args file +: "${VMARGS:="$CONFIG_DIR/vm.args"}" +# shellcheck source=ejabberdctl.cfg.example +[ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH" +[ -n "$ERLANG_NODE_ARG" ] && ERLANG_NODE="$ERLANG_NODE_ARG" +[ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s" +: "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}" + +# define erl parameters +ERLANG_OPTS="+K $POLL +P $ERL_PROCESSES $ERL_OPTIONS" +if [ -n "$FIREWALL_WINDOW" ] ; then + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}" +fi +if [ -n "$INET_DIST_INTERFACE" ] ; then + INET_DIST_INTERFACE2=$("$ERL" -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) + if [ -n "$INET_DIST_INTERFACE2" ] ; then + ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2" + fi +fi +[ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -erl_epmd_port $ERL_DIST_PORT -start_epmd false" +# if vm.args file exists in config directory, pass it to Erlang VM +[ -f "$VMARGS" ] && ERLANG_OPTS="$ERLANG_OPTS -args_file $VMARGS" +ERL_LIBS='{{libdir}}' +ERL_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump +ERL_INETRC="$CONFIG_DIR"/inetrc + +# define ejabberd parameters +EJABBERD_OPTS="$EJABBERD_OPTS\ +$(sed '/^log_rotate_size/!d;s/:[ \t]*\([0-9]\{1,\}\).*/ \1/;s/:[ \t]*\(infinity\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_count/!d;s/:[ \t]*\([0-9]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_window_time/!d;s/:[ \t]*\([0-9]*[a-z]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")" +[ -n "$EJABBERD_OPTS" ] && EJABBERD_OPTS="-ejabberd $EJABBERD_OPTS" +EJABBERD_OPTS="-mnesia dir \"$SPOOL_DIR\" $MNESIA_OPTIONS $EJABBERD_OPTS -s ejabberd" + +# export global variables +export EJABBERD_CONFIG_PATH +export EJABBERD_LOG_PATH +export EJABBERD_PID_PATH +export ERL_CRASH_DUMP +export ERL_EPMD_ADDRESS +export ERL_DIST_PORT +export ERL_INETRC +export ERL_MAX_PORTS +export ERL_MAX_ETS_TABLES +export CONTRIB_MODULES_PATH +export CONTRIB_MODULES_CONF_DIR +export ERL_LIBS +export SCRIPT_DIR + +set_dist_client() +{ + [ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -dist_listen false" +} + +# run command either directly or via su $INSTALLUSER +run_cmd() +{ + case $EXEC_CMD in + as_install_user) su -s /bin/sh -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; + as_current_user) "$@" ;; + esac +} +exec_cmd() +{ + case $EXEC_CMD in + as_install_user) su -s /bin/sh -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; + as_current_user) exec "$@" ;; + esac +} +run_erl() +{ + NODE=$1; shift + run_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@" +} +exec_erl() +{ + NODE=$1; shift + exec_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@" +} +exec_iex() +{ + NODE=$1; shift + exec_cmd "$IEX" -${S:--}name "$NODE" --erl "$ERLANG_OPTS" "$@" +} + +# usage +debugwarning() +{ + if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then + echo "--------------------------------------------------------------------" + echo "" + echo "IMPORTANT: we will attempt to attach an INTERACTIVE shell" + echo "to an already running ejabberd node." + echo "If an ERROR is printed, it means the connection was not successful." + echo "You can interact with the ejabberd node if you know how to use it." + echo "Please be extremely cautious with your actions," + echo "and exit immediately if you are not completely sure." + echo "" + echo "To detach this shell from ejabberd, press:" + echo " control+c, control+c" + echo "" + echo "--------------------------------------------------------------------" + echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" + echo " EJABBERD_BYPASS_WARNINGS=true" + echo "Press return to continue" + read -r _ + echo "" + fi +} + +livewarning() +{ + if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then + echo "--------------------------------------------------------------------" + echo "" + echo "IMPORTANT: ejabberd is going to start in LIVE (interactive) mode." + echo "All log messages will be shown in the command shell." + echo "You can interact with the ejabberd node if you know how to use it." + echo "Please be extremely cautious with your actions," + echo "and exit immediately if you are not completely sure." + echo "" + echo "To exit this LIVE mode and stop ejabberd, press:" + echo " q(). and press the Enter key" + echo "" + echo "--------------------------------------------------------------------" + echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" + echo " EJABBERD_BYPASS_WARNINGS=true" + echo "Press return to continue" + read -r _ + echo "" + fi +} + +help() +{ + echo "" + echo "Commands to start an ejabberd node:" + echo " start Start in server mode" + echo " foreground Start in server mode (attached)" + echo " foreground-quiet Start in server mode (attached), show only critical messages" + echo " live Start in interactive mode, with Erlang shell" + echo " iexlive Start in interactive mode, with Elixir shell" + echo "" + echo "Commands to interact with a running ejabberd node:" + echo " debug Attach an interactive Erlang shell to a running node" + echo " iexdebug Attach an interactive Elixir shell to a running node" + echo " etop Attach to a running node and start Erlang Top" + echo " ping Send ping to the node, returns pong or pang" + echo " started|stopped Wait for the node to fully start|stop" + echo "" + echo "Optional parameters when starting an ejabberd node:" + echo " --config-dir dir Config ejabberd: $CONFIG_DIR" + echo " --config file Config ejabberd: $EJABBERD_CONFIG_PATH" + echo " --ctl-config file Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH" + echo " --logs dir Directory for logs: $LOGS_DIR" + echo " --spool dir Database spool dir: $SPOOL_DIR" + echo " --node nodename ejabberd node name: $ERLANG_NODE" + echo "" +} + +# dynamic node name helper +uid() +{ + uuid=$(uuidgen 2>/dev/null) + random=$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null) + [ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid) + [ -z "$uuid" ] && uuid=$(printf "%X" "${random:-$$}$(date +%M%S)") + uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/') + [ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}" + [ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}" + [ $# -eq 2 ] && echo "${uuid}-${1}@${2}" +} + +# stop epmd if there is no other running node +stop_epmd() +{ + [ -n "$ERL_DIST_PORT" ] && return + "$EPMD" -names 2>/dev/null | grep -q name || "$EPMD" -kill >/dev/null +} + +# make sure node not already running and node name unregistered +# if all ok, ensure runtime directory exists and make it current directory +check_start() +{ + [ -n "$ERL_DIST_PORT" ] && return + "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { + pgrep -f "$ERLANG_NODE" >/dev/null && { + echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running." + exit 4 + } + pgrep beam >/dev/null && { + echo "ERROR: The ejabberd node '$ERLANG_NODE' is registered," + echo " but no related beam process has been found." + echo "Shutdown all other erlang nodes, and call 'epmd -kill'." + exit 5 + } + "$EPMD" -kill >/dev/null + } +} + +post_waiter_fork() +{ + (FIRST_RUN=$FIRST_RUN "$0" post_waiter)& +} + +post_waiter_waiting() +{ + $0 started + [ -n "$FIRST_RUN" ] && [ -n "$CTL_ON_CREATE" ] && (post_waiter_loop $CTL_ON_CREATE) + [ -n "$CTL_ON_START" ] && post_waiter_loop $CTL_ON_START +} + +post_waiter_loop() +{ + LIST=$@ + HEAD=${LIST%% ; *} + TAIL=${LIST#* ; } + echo ":> ejabberdctl $HEAD" + $0 $HEAD + [ "$HEAD" = "$TAIL" ] || post_waiter_loop $TAIL +} + +# allow sync calls +wait_status() +{ + # args: status try delay + # return: 0 OK, 1 KO + timeout="$2" + status=4 + while [ "$status" -ne "$1" ] ; do + sleep "$3" + timeout=$((timeout - 1)) + if [ $timeout -eq 0 ] ; then + status="$1" + else + run_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ + -extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null + status="$?" + fi + done + [ $timeout -gt 0 ] +} + +# ensure we can change current directory to SPOOL_DIR +[ -f "$SPOOL_DIR/schema.DAT" ] || FIRST_RUN=true +[ -d "$SPOOL_DIR" ] || run_cmd mkdir -p "$SPOOL_DIR" +cd "$SPOOL_DIR" || { + echo "ERROR: can not access directory $SPOOL_DIR" + exit 6 +} + +# main +case $1 in + start) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached + ;; + foreground) + check_start + post_waiter_fork + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput + ;; + foreground-quiet) + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput -ejabberd quiet true + ;; + live) + livewarning + check_start + exec_erl "$ERLANG_NODE" $EJABBERD_OPTS + ;; + debug) + debugwarning + set_dist_client + exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE" + ;; + etop) + set_dist_client + exec_erl "$(uid top)" -hidden -node "$ERLANG_NODE" -s etop \ + -s erlang halt -output text + ;; + iexdebug) + debugwarning + set_dist_client + exec_iex "$(uid debug)" --remsh "$ERLANG_NODE" + ;; + iexlive) + livewarning + exec_iex "$ERLANG_NODE" --erl "$EJABBERD_OPTS" --app ejabberd + ;; + ping) + PEER=${2:-$ERLANG_NODE} + [ "$PEER" = "${PEER%.*}" ] && PS="-s" + set_dist_client + exec_cmd "$ERL" ${PS:--}name "$(uid ping "$(hostname $PS)")" $ERLANG_OPTS \ + -noinput -hidden -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ + -s erlang halt -output text + ;; + started) + set_dist_client + wait_status 0 30 2 # wait 30x2s before timeout + ;; + stopped) + set_dist_client + wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout + ;; + post_waiter) + post_waiter_waiting + ;; + *) + set_dist_client + run_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ + -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" + result=$? + case $result in + 2|3) help;; + *) :;; + esac + exit $result + ;; +esac diff --git a/.github/workflows/ci-19.3.yml b/.github/workflows/ci-19.3.yml new file mode 100644 index 000000000..c65a26050 --- /dev/null +++ b/.github/workflows/ci-19.3.yml @@ -0,0 +1,229 @@ +name: CI (19.3) + +on: + push: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + pull_request: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + +jobs: + + tests: + name: Tests + strategy: + fail-fast: false + matrix: + otp: ['19.3'] + runs-on: ubuntu-18.04 + services: + redis: + image: redis + ports: + - 6379:6379 + + steps: + + - uses: actions/checkout@v3 + + - name: Get specific Erlang/OTP + uses: erlef/setup-beam@v1 + with: + otp-version: ${{ matrix.otp }} + + - name: Get a compatible Rebar3 + run: | + rm rebar3 + wget https://github.com/processone/ejabberd/raw/21.12/rebar3 + chmod +x rebar3 + + - name: Prepare databases + run: | + sudo systemctl start mysql.service + sudo systemctl start postgresql.service + mysql -u root -proot -e "CREATE USER 'ejabberd_test'@'localhost' + IDENTIFIED BY 'ejabberd_test';" + mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" + mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* + TO 'ejabberd_test'@'localhost';" + mysql -u root -proot ejabberd_test < sql/mysql.sql + pg_isready + sudo -u postgres psql -c "CREATE USER ejabberd_test + WITH PASSWORD 'ejabberd_test';" + sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" + sudo -u postgres psql ejabberd_test -f sql/pg.sql + sudo -u postgres psql -c "GRANT ALL PRIVILEGES + ON DATABASE ejabberd_test TO ejabberd_test;" + sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL + TABLES IN SCHEMA public + TO ejabberd_test;" + sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL + SEQUENCES IN SCHEMA public + TO ejabberd_test;" + + - name: Prepare libraries + run: | + sudo apt-get -qq update + sudo apt-get -y purge libgd3 nginx + sudo apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Prepare rebar + run: | + echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} + ]}.' >>rebar.config + echo '{xref_checks, [deprecated_function_calls, deprecated_functions, + locals_not_used, undefined_function_calls, undefined_functions]}. + % Disabled: exports_not_used,' >>rebar.config + echo '{dialyzer, [{get_warnings, true}, {plt_extra_apps, [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]} ]}.' >>rebar.config + echo '{ct_extra_params, "-verbosity 20"}.' >>rebar.config + echo "{ct_opts, [{verbosity, 20}, {keep_logs, 20}]}." >>rebar.config + + - name: Remove syntax_tools from release + run: sed -i 's|, syntax_tools||g' src/ejabberd.app.src.script + + - name: Cache rebar + uses: actions/cache@v3 + with: + path: | + ~/.cache/rebar3/ + key: ${{matrix.otp}}-${{hashFiles('rebar.config')}} + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=./rebar3 \ + --prefix=/tmp/ejabberd \ + --enable-all \ + --disable-elixir \ + --disable-mssql \ + --disable-odbc + make update + make + + - run: make install -s + - run: make hooks + - run: make options + - run: make xref + - run: make dialyzer + + - name: Check Production Release + run: | + make rel + RE=_build/prod/rel/ejabberd + $RE/bin/ejabberdctl start + $RE/bin/ejabberdctl started + $RE/bin/ejabberdctl stop + $RE/bin/ejabberdctl stopped + cat $RE/logs/ejabberd.log + grep -q "is stopped in" $RE/logs/ejabberd.log + + - name: Check Development Release + run: | + make dev + RE=_build/dev/rel/ejabberd + $RE/bin/ejabberdctl start + $RE/bin/ejabberdctl started + $RE/bin/ejabberdctl stop + $RE/bin/ejabberdctl stopped + cat $RE/logs/ejabberd.log + grep -q "is stopped in" $RE/logs/ejabberd.log + + - name: Run tests + id: ct + run: | + (cd priv && ln -sf ../sql) + COMMIT=`echo $GITHUB_SHA | cut -c 1-7` + DATE=`date +%s` + REF_NAME=`echo $GITHUB_REF_NAME | tr "/" "_"` + NODENAME=$DATE@$GITHUB_RUN_NUMBER-$GITHUB_ACTOR-$REF_NAME-$COMMIT + LABEL=`git show -s --format=%s | cut -c 1-30` + ./rebar3 ct --name $NODENAME --label "$LABEL" + ./rebar3 cover + + - name: Check results + if: always() && (steps.ct.outcome != 'skipped' || steps.ct2.outcome != 'skipped') + id: ctresults + run: | + [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true + ln `find logs/ -name suite.log` logs/suite.log + grep 'TEST COMPLETE' logs/suite.log + grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log + test $(find logs/ -empty -name error.log) + + - name: View logs failures + if: failure() && steps.ctresults.outcome == 'failure' + run: | + cat logs/suite.log | awk \ + 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' + find logs/ -name error.log -exec cat '{}' ';' + find logs/ -name exunit.log -exec cat '{}' ';' + + - name: Upload test logs + if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' + uses: peaceiris/actions-gh-pages@v3 + with: + publish_dir: _build/test + exclude_assets: '.github,lib,plugins' + external_repository: processone/ecil + deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} + keep_files: true + + - name: View ECIL address + if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' + run: | + CTRUN=`ls -la _build/test/logs/last | sed 's|.*-> ||'` + echo "::notice::View CT results: https://processone.github.io/ecil/logs/$CTRUN/" + + - name: Prepare new schema + run: | + [[ -d logs ]] && rm -rf logs + [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true + mysql -u root -proot -e "DROP DATABASE ejabberd_test;" + sudo -u postgres psql -c "DROP DATABASE ejabberd_test;" + mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" + mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* + TO 'ejabberd_test'@'localhost';" + mysql -u root -proot ejabberd_test < sql/mysql.new.sql + sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" + sudo -u postgres psql ejabberd_test -f sql/pg.new.sql + sudo -u postgres psql -c "GRANT ALL PRIVILEGES + ON DATABASE ejabberd_test TO ejabberd_test;" + sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL + TABLES IN SCHEMA public + TO ejabberd_test;" + sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL + SEQUENCES IN SCHEMA public + TO ejabberd_test;" + sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl + - run: CT_BACKENDS=mysql,pgsql make test + id: ctnewschema + - name: Check results + if: always() && steps.ctnewschema.outcome != 'skipped' + run: | + [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true + ln `find logs/ -name suite.log` logs/suite.log + grep 'TEST COMPLETE' logs/suite.log + grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log + test $(find logs/ -empty -name error.log) + - name: View logs failures + if: failure() && steps.ctnewschema.outcome != 'skipped' + run: | + cat logs/suite.log | awk \ + 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' + find logs/ -name error.log -exec cat '{}' ';' + find logs/ -name exunit.log -exec cat '{}' ';' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d498760be..ba236f9ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,15 +25,8 @@ jobs: strategy: fail-fast: false matrix: - otp: ['19.3', '24.0'] - include: - - otp: '19.3' - rebar: 2 - os: ubuntu-18.04 - - otp: '24.0' - rebar: 3 - os: ubuntu-20.04 - runs-on: ${{ matrix.os }} + otp: ['20.0', '21.3', '24.3', '25'] + runs-on: ubuntu-20.04 services: redis: image: redis @@ -42,14 +35,29 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Get previous Erlang/OTP - uses: ErlGang/setup-erlang@master - if: matrix.otp != 24.0 + - name: Test shell scripts + if: matrix.otp == 25 + run: | + shellcheck test/ejabberd_SUITE_data/gencerts.sh + shellcheck tools/captcha.sh + shellcheck ejabberd.init.template + shellcheck -x ejabberdctl.template + + - name: Get specific Erlang/OTP + if: matrix.otp != 25 + uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.otp }} + - name: Get a compatible Rebar3 + if: matrix.otp <= '21.3' + run: | + rm rebar3 + wget https://github.com/processone/ejabberd/raw/21.12/rebar3 + chmod +x rebar3 + - name: Prepare databases run: | sudo systemctl start mysql.service @@ -77,11 +85,11 @@ jobs: - name: Prepare libraries run: | sudo apt-get -qq update + sudo apt-get -y purge libgd3 nginx sudo apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ libsqlite3-dev libwebp-dev libyaml-dev - name: Prepare rebar - id: rebar run: | echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} ]}.' >>rebar.config @@ -93,81 +101,103 @@ jobs: mqtree, p1_acme, p1_mysql, p1_oauth2, p1_pgsql, p1_utils, pkix, sqlite3, stringprep, stun, xmpp, yconf]} ]}.' >>rebar.config echo '{ct_extra_params, "-verbosity 20"}.' >>rebar.config + echo "{ct_opts, [{verbosity, 20}, {keep_logs, 20}]}." >>rebar.config - - name: Cache rebar2 - if: matrix.rebar == 2 - uses: actions/cache@v2 + - name: Remove syntax_tools from release + run: sed -i 's|, syntax_tools||g' src/ejabberd.app.src.script + + - name: Cache rebar + uses: actions/cache@v3 with: path: | - deps/ - dialyzer/ - ebin/ - key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}} + ~/.cache/rebar3/ + key: ${{matrix.otp}}-${{hashFiles('rebar.config')}} - - name: Cache rebar3 - if: matrix.rebar == 3 - uses: actions/cache@v2 - with: - path: ~/.cache/rebar3/ - key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}} + - name: Download test logs + if: matrix.otp == 25 && github.repository == 'processone/ejabberd' + continue-on-error: true + run: | + mkdir -p _build/test + curl -sSL https://github.com/processone/ecil/tarball/gh-pages | + tar -C _build/test --strip-components=1 --wildcards -xzf - + rm -rf _build/test/logs/last/ - name: Compile run: | ./autogen.sh - [[ ${{ matrix.rebar }} = 2 ]] && REBAR=rebar || REBAR=`which rebar3` - ./configure --with-rebar=$REBAR \ + ./configure --with-rebar=./rebar3 \ --prefix=/tmp/ejabberd \ --enable-all \ --disable-elixir \ + --disable-mssql \ --disable-odbc make update make - - run: make rel - run: make install -s - run: make hooks - run: make options - run: make xref - run: make dialyzer - - run: make test + + - name: Check Production Release + run: | + make rel + RE=_build/prod/rel/ejabberd + $RE/bin/ejabberdctl start + $RE/bin/ejabberdctl started + $RE/bin/ejabberdctl stop + $RE/bin/ejabberdctl stopped + cat $RE/logs/ejabberd.log + grep -q "is stopped in" $RE/logs/ejabberd.log + + - name: Check Development Release + run: | + make dev + RE=_build/dev/rel/ejabberd + $RE/bin/ejabberdctl start + $RE/bin/ejabberdctl started + $RE/bin/ejabberdctl stop + $RE/bin/ejabberdctl stopped + cat $RE/logs/ejabberd.log + grep -q "is stopped in" $RE/logs/ejabberd.log + + - name: Run tests + id: ct + run: | + (cd priv && ln -sf ../sql) + COMMIT=`echo $GITHUB_SHA | cut -c 1-7` + DATE=`date +%s` + REF_NAME=`echo $GITHUB_REF_NAME | tr "/" "_"` + NODENAME=$DATE@$GITHUB_RUN_NUMBER-$GITHUB_ACTOR-$REF_NAME-$COMMIT + LABEL=`git show -s --format=%s | cut -c 1-30` + ./rebar3 ct --name $NODENAME --label "$LABEL" + ./rebar3 cover - name: Check results - if: always() + if: always() && (steps.ct.outcome != 'skipped' || steps.ct2.outcome != 'skipped') + id: ctresults run: | - [[ -d _build ]] && ln -s _build/test/logs/ logs \ - && ln `find _build/ -name "*dialyzer_warnings"` \ - logs/dialyzer.log \ - || ln dialyzer/error.log logs/dialyzer.log + [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true ln `find logs/ -name suite.log` logs/suite.log grep 'TEST COMPLETE' logs/suite.log grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log test $(find logs/ -empty -name error.log) - - name: View dialyzer report - run: cat logs/dialyzer.log - - - name: View full suite.log - run: cat logs/suite.log - - - name: View suite.log failures - if: failure() - run: cat logs/suite.log | awk - 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' - - - name: View full ejabberd.log - if: failure() - run: find logs/ -name ejabberd.log -exec cat '{}' ';' - - - name: View exunit.log - if: failure() - run: find logs/ -name exunit.log -exec cat '{}' ';' + - name: View logs failures + if: failure() && steps.ctresults.outcome == 'failure' + run: | + cat logs/suite.log | awk \ + 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' + find logs/ -name error.log -exec cat '{}' ';' + find logs/ -name exunit.log -exec cat '{}' ';' - name: Send to coveralls - if: matrix.otp == 24.0 + if: matrix.otp == 25 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - rebar3 as test coveralls send + DIAGNOSTIC=1 ./rebar3 as test coveralls send curl -v -k https://coveralls.io/webhook \ --header "Content-Type: application/json" \ --data '{"repo_name":"$GITHUB_REPOSITORY", @@ -175,64 +205,57 @@ jobs: "payload":{"build_num":$GITHUB_RUN_ID, "status":"done"}}' - binaries: - name: Binaries - needs: [tests] - strategy: - fail-fast: false - matrix: - otp: ['21.3'] - include: - - otp: '21.3' - rebar: 3 - os: ubuntu-20.04 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 + - name: Upload test logs + if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' + uses: peaceiris/actions-gh-pages@v3 with: - fetch-depth: 150 - - name: Get last git tag + publish_dir: _build/test + exclude_assets: '.github,lib,plugins' + external_repository: processone/ecil + deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} + keep_files: true + + - name: View ECIL address + if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' run: | - export TAGLAST=`git ls-remote --tags --refs origin 'refs/tags/2*' \ - | tail -1 | awk '{print $2}'` - git fetch origin "$TAGLAST:$TAGLAST" - git describe - - name: Get previous Erlang/OTP - uses: ErlGang/setup-erlang@master - with: - otp-version: ${{ matrix.otp }} - - name: Get a compatible Rebar3 + CTRUN=`ls -la _build/test/logs/last | sed 's|.*-> ||'` + echo "::notice::View CT results: https://processone.github.io/ecil/logs/$CTRUN/" + + - name: Prepare new schema run: | - wget https://github.com/erlang/rebar3/releases/download/3.15.2/rebar3 \ - && chmod +x rebar3 - - name: Cache Rebar3 - uses: actions/cache@v2 - with: - path: ~/.cache/rebar3/ - key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}} - - name: Prepare libraries + [[ -d logs ]] && rm -rf logs + [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true + mysql -u root -proot -e "DROP DATABASE ejabberd_test;" + sudo -u postgres psql -c "DROP DATABASE ejabberd_test;" + mysql -u root -proot -e "CREATE DATABASE ejabberd_test;" + mysql -u root -proot -e "GRANT ALL ON ejabberd_test.* + TO 'ejabberd_test'@'localhost';" + mysql -u root -proot ejabberd_test < sql/mysql.new.sql + sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;" + sudo -u postgres psql ejabberd_test -f sql/pg.new.sql + sudo -u postgres psql -c "GRANT ALL PRIVILEGES + ON DATABASE ejabberd_test TO ejabberd_test;" + sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL + TABLES IN SCHEMA public + TO ejabberd_test;" + sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL + SEQUENCES IN SCHEMA public + TO ejabberd_test;" + sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl + - run: CT_BACKENDS=mysql,pgsql make test + id: ctnewschema + - name: Check results + if: always() && steps.ctnewschema.outcome != 'skipped' run: | - sudo apt-get -qq update - sudo apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ - libsqlite3-dev libwebp-dev libyaml-dev - - name: Compile + [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true + ln `find logs/ -name suite.log` logs/suite.log + grep 'TEST COMPLETE' logs/suite.log + grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log + test $(find logs/ -empty -name error.log) + - name: View logs failures + if: failure() && steps.ctnewschema.outcome != 'skipped' run: | - ./autogen.sh - ./configure --with-rebar=./rebar3 \ - --prefix=/tmp/ejabberd \ - --disable-debug \ - --enable-all \ - --disable-elixir - make update - make - - run: make install -s - - name: Strip binaries - run: echo 'beam_lib:strip_files(filelib:wildcard( - "/tmp/ejabberd/lib/*/ebin/*beam")), init:stop().' \ - | erl -boot start_clean - - name: Upload binaries - uses: actions/upload-artifact@v2 - with: - name: ejabberd-binaries - path: /tmp/ejabberd/lib - retention-days: 7 + cat logs/suite.log | awk \ + 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' + find logs/ -name error.log -exec cat '{}' ';' + find logs/ -name exunit.log -exec cat '{}' ';' diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 000000000..d36965d97 --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,75 @@ +name: Container + +on: + push: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + container: + name: Container + runs-on: ubuntu-latest + permissions: + packages: write + steps: + + - name: Check out repository code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Checkout ejabberd-contrib + uses: actions/checkout@v3 + with: + repository: processone/ejabberd-contrib + path: .ejabberd-modules/sources/ejabberd-contrib + + - name: Log in to the Container registry + uses: docker/login-action@v1.14.1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get git describe + id: gitdescribe + run: echo "::set-output name=ver::$(git describe --tags)" + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3.8.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.revision=${{ steps.gitdescribe.outputs.ver }} + org.opencontainers.image.licenses=GPL-2.0 + org.opencontainers.image.vendor=ProcessOne + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push Docker image + uses: docker/build-push-action@v2.10.0 + with: + build-args: | + VERSION=${{ steps.gitdescribe.outputs.ver }} + cache-from: type=gha + cache-to: type=gha,mode=max + context: . + file: .github/container/Dockerfile + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml new file mode 100644 index 000000000..5fe622411 --- /dev/null +++ b/.github/workflows/installers.yml @@ -0,0 +1,84 @@ +name: Installers + +on: + push: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + pull_request: + paths-ignore: + - '.devcontainer/**' + - 'examples/**' + - 'lib/**' + - 'man/**' + - 'priv/**' + - '**.md' + +jobs: + binaries: + name: Binaries + runs-on: ubuntu-latest + steps: + - name: Cache build directory + uses: actions/cache@v3 + with: + path: ~/build/ + key: ${{runner.os}}-ct-ng-1.25.0 + - name: Install prerequisites + run: | + sudo apt-get -qq update + sudo apt-get -qq install makeself + # https://github.com/crosstool-ng/crosstool-ng/blob/master/testing/docker/ubuntu21.10/Dockerfile + sudo apt-get -qq install build-essential autoconf bison flex gawk + sudo apt-get -qq install help2man libncurses5-dev libtool libtool-bin + sudo apt-get -qq install python3-dev texinfo unzip + - name: Install FPM + run: | + gem install --no-document --user-install fpm + echo $HOME/.gem/ruby/*/bin >> $GITHUB_PATH + - name: Check out repository code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Build binary archives + run: CHECK_DEPS=false tools/make-binaries + - name: Build DEB and RPM packages + run: tools/make-packages + - name: Build installers + run: tools/make-installers + - name: Collect packages + run: | + mkdir ejabberd-packages + mv ejabberd_*.deb ejabberd-*.rpm ejabberd-*.run ejabberd-packages + - name: Upload packages + uses: actions/upload-artifact@v3 + with: + name: ejabberd-packages + # + # Appending the wildcard character ("*") is a trick to make + # "ejabberd-packages" the root directory of the uploaded ZIP file: + # + # https://github.com/actions/upload-artifact#upload-using-multiple-paths-and-exclusions + # + path: ejabberd-packages* + retention-days: 14 + + release: + name: Release + needs: [binaries] + runs-on: ubuntu-latest + if: github.ref_type == 'tag' + steps: + - name: Download packages + uses: actions/download-artifact@v3 + with: + name: ejabberd-packages + - name: Draft Release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: ejabberd-packages/* diff --git a/.github/workflows/runtime.yml b/.github/workflows/runtime.yml new file mode 100644 index 000000000..b991e8b1a --- /dev/null +++ b/.github/workflows/runtime.yml @@ -0,0 +1,202 @@ +name: Runtime + +on: + push: + paths: + - '*' + - '!*.md' + - '.github/workflows/runtime.yml' + - 'checkouts/**' + - 'config/**' + - 'lib/**' + - 'm4/**' + - 'plugins/**' + - 'rel/**' + pull_request: + paths: + - '*' + - '!*.md' + - '.github/workflows/runtime.yml' + - 'checkouts/**' + - 'config/**' + - 'lib/**' + - 'm4/**' + - 'plugins/**' + - 'rel/**' + +jobs: + + rebars: + name: Rebars + strategy: + fail-fast: false + matrix: + otp: ['19.3', '20.3', '24.3', '25'] + rebar: ['rebar', 'rebar3'] + runs-on: ubuntu-latest + container: + image: erlang:${{ matrix.otp }} + + steps: + + - uses: actions/checkout@v3 + + - name: Prepare libraries + run: | + apt-get -qq update + apt-get purge -y libgd3 + apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=`which ${{ matrix.rebar }}` \ + --prefix=/tmp/ejabberd \ + --enable-all \ + --disable-elixir \ + --disable-odbc + make update + make + + - name: Prepare rebar + run: | + echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} + ]}.' >>rebar.config + echo '{xref_checks, [deprecated_function_calls, deprecated_functions, + locals_not_used, undefined_function_calls, undefined_functions]}. + % Disabled: exports_not_used,' >>rebar.config + + - run: make xref + + - name: Test rel (rebar2) + if: matrix.rebar == 'rebar' + run: | + make rel + rel/ejabberd/bin/ejabberdctl start \ + && rel/ejabberd/bin/ejabberdctl started + rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + rel/ejabberd/bin/ejabberdctl registered_users localhost + cat rel/ejabberd/logs/* + + - name: Test rel + if: matrix.rebar != 'rebar' + run: | + make rel + _build/prod/rel/ejabberd/bin/ejabberdctl start \ + && _build/prod/rel/ejabberd/bin/ejabberdctl started + _build/prod/rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + _build/prod/rel/ejabberd/bin/ejabberdctl registered_users localhost + _build/prod/rel/ejabberd/bin/ejabberdctl stop \ + && _build/prod/rel/ejabberd/bin/ejabberdctl stopped + cat _build/prod/rel/ejabberd/logs/* + + - name: Test dev + if: matrix.rebar != 'rebar' + run: | + make dev + _build/dev/rel/ejabberd/bin/ejabberdctl start \ + && _build/dev/rel/ejabberd/bin/ejabberdctl started + _build/dev/rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + _build/dev/rel/ejabberd/bin/ejabberdctl registered_users localhost + _build/dev/rel/ejabberd/bin/ejabberdctl stop \ + && _build/dev/rel/ejabberd/bin/ejabberdctl stopped + cat _build/dev/rel/ejabberd/logs/* + + mix: + name: Mix + strategy: + fail-fast: false + matrix: + otp: ['21.3', '22.0', '25.0'] + elixir: ['1.10.3', '1.11.4', '1.12.3', '1.13.0'] + exclude: + - otp: '21.3' + elixir: '1.12.3' + - otp: '21.3' + elixir: '1.13.0' + - otp: '25.0' + elixir: '1.10.3' + - otp: '25.0' + elixir: '1.11.4' + - otp: '25.0' + elixir: '1.12.3' + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v3 + + - name: Get specific Erlang/OTP + uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + + - name: Prepare libraries + run: | + sudo apt-get -qq update + sudo apt-get -y purge libgd3 nginx + sudo apt-get -qq install libexpat1-dev libgd-dev libpam0g-dev \ + libsqlite3-dev libwebp-dev libyaml-dev + + - name: Remove Elixir Matchers + run: | + echo "::remove-matcher owner=elixir-mixCompileWarning::" + echo "::remove-matcher owner=elixir-credoOutputDefault::" + echo "::remove-matcher owner=elixir-mixCompileError::" + echo "::remove-matcher owner=elixir-mixTestFailure::" + echo "::remove-matcher owner=elixir-dialyzerOutputDefault::" + + - name: Compile + run: | + ./autogen.sh + ./configure --with-rebar=mix \ + --prefix=/tmp/ejabberd \ + --enable-all \ + --disable-elixir \ + --disable-odbc + mix deps.get + make + + - run: make xref + + - name: Run rel + run: | + make rel + _build/prod/rel/ejabberd/bin/ejabberdctl start \ + && _build/prod/rel/ejabberd/bin/ejabberdctl started + _build/prod/rel/ejabberd/bin/ejabberdctl register user1 localhost s0mePass + _build/prod/rel/ejabberd/bin/ejabberdctl registered_users localhost > registered.log + _build/prod/rel/ejabberd/bin/ejabberdctl stop \ + && _build/prod/rel/ejabberd/bin/ejabberdctl stopped + + - name: Run dev + run: | + make dev + _build/dev/rel/ejabberd/bin/ejabberdctl start \ + && _build/dev/rel/ejabberd/bin/ejabberdctl started + _build/dev/rel/ejabberd/bin/ejabberdctl register user2 localhost s0mePass + _build/dev/rel/ejabberd/bin/ejabberdctl registered_users localhost >> registered.log + _build/dev/rel/ejabberd/bin/ejabberdctl stop \ + && _build/dev/rel/ejabberd/bin/ejabberdctl stopped + + - name: Check rel + if: always() + run: | + grep -q '^user1$' registered.log + grep -q '^user2$' registered.log + grep -q 'is started' _build/prod/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/prod/rel/ejabberd/logs/ejabberd.log + test $(find _build/prod/ -empty -name error.log) + grep -q 'is started' _build/dev/rel/ejabberd/logs/ejabberd.log + grep -q 'is stopped' _build/dev/rel/ejabberd/logs/ejabberd.log + test $(find _build/dev/ -empty -name error.log) + + - name: View logs failures + if: failure() + run: | + cat _build/prod/rel/ejabberd/logs/ejabberd.log + cat _build/prod/rel/ejabberd/logs/error.log + cat _build/dev/rel/ejabberd/logs/ejabberd.log + cat _build/dev/rel/ejabberd/logs/error.log diff --git a/.gitignore b/.gitignore index e233877df..6bfafc069 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /*.cache /deps/ /.deps-update/ +/.ejabberd-modules/ /ebin/ /ejabberd.init /ejabberd.service @@ -37,3 +38,8 @@ /rebar.lock /log/ Mnesia.nonode@nohost/ +# Binaries created with tools/make-{binaries,installers,packages}: +/ejabberd_*.deb +/ejabberd-*.rpm +/ejabberd-*.run +/ejabberd-*.tar.gz diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..0b7131a2e --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,4 @@ +disable=SC2016,SC2086,SC2089,SC2090 +external-sources=true +source=ejabberdctl.cfg.example +shell=sh diff --git a/CHANGELOG.md b/CHANGELOG.md index f55ce7127..758bddc0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,160 @@ +# Version 22.05 + +Core +- C2S: Don't expect that socket will be available in `c2s_terminated` hook +- Event handling process hook tracing +- Guard against `erlang:system_info(logical_processors)` not always returning a number +- `domain_balancing`: Allow for specifying `type` only, without specifying `component_number` + +MQTT +- Add TLS certificate authentication for MQTT connections +- Fix login when generating client id, keep connection record (#3593) +- Pass property name as expected in mqtt_codec (fixes login using MQTT 5) +- Support MQTT subscriptions spread over the cluster (#3750) + +MUC +- Attach meta field with real jid to mucsub subscription events +- Handle user removal +- Stop empty MUC rooms 30 seconds after creation +- `default_room_options`: Update options configurable +- `subscribe_room_many_max_users`: New option in `mod_muc_admin` + +mod_conversejs +- Improved options to support `@HOST@` and `auto` values +- Set `auth` and `register` options based on ejabberd configuration +- `conversejs_options`: New option +- `conversejs_resources`: New option + +PubSub +- `mod_pubsub`: Allow for limiting `item_expire` value +- `mod_pubsub`: Unsubscribe JID on whitelist removal +- `node_pep`: Add config-node and multi-items features (#3714) + +SQL +- Improve compatibility with various db engine versions +- Sync old-to-new schema script with reality (#3790) +- Slight improvement in MSSQL testing support, but not yet complete + +Other Modules +- `auth_jwt`: Checking if an user is active in SM for a JWT authenticated user (#3795) +- `mod_configure`: Implement Get List of Registered/Online Users from XEP-0133 +- `mod_host_meta`: New module to serve host-meta files, see XEP-0156 +- `mod_mam`: Store all mucsub notifications not only message notifications +- `mod_ping`: Delete ping timer if resource is gone after the ping has been sent +- `mod_ping`: Don't send ping if resource is gone +- `mod_push`: Fix notifications for pending sessions (XEP-0198) +- `mod_push`: Keep push session ID on session resume +- `mod_shared_roster`: Adjust special group cache size +- `mod_shared_roster`: Normalize JID on unset_presence (#3752) +- `mod_stun_disco`: Fix parsing of IPv6 listeners + +Dependencies +- autoconf: Supported from 2.59 to the new 2.71 +- fast_tls: Update to 1.1.14 to support OpenSSL 3 +- jiffy: Update to 1.1.1 to support Erlang/OTP 25.0-rc1 +- luerl: Update to 1.0.0, now available in hex.pm +- lager: This dependency is used only when Erlang is older than 22 +- rebar2: Updated binary to work from Erlang/OTP 22 to 25 +- rebar3: Updated binary to work from Erlang/OTP 22 to 25 +- `make update`: Fix when used with rebar 3.18 + +Compile +- `mix release`: Copy `include/` files for ejabberd, deps and otp, in `mix.exs` +- `rebar3 release`: Fix ERTS path in `ejabberdctl` +- `configure.ac`: Set default ejabberd version number when not using git +- `mix.exs`: Move some dependencies as optional +- `mix.exs`: No need to use Distillery, Elixir has built-in support for OTP releases (#3788) +- `tools/make-binaries`: New script for building Linux binaries +- `tools/make-installers`: New script for building command line installers + +Start +- New `make relive` similar to `ejabberdctl live` without installing +- `ejabberdctl`: Fix some warnings detected by ShellCheck +- `ejabberdctl`: Mention in the help: `etop`, `ping` and `started`/`stopped` +- `make rel`: Switch to paths: `conf/`, `database/`, `logs/` +- `mix.exs`: Add `-boot` and `-boot_var` in `ejabberdctl` instead of adding `vm.args` +- `tools/captcha.sh`: Fix some warnings detected by ShellCheck + +Commands +- Accept more types of ejabberdctl commands arguments as JSON-encoded +- `delete_old_mam_messages_batch`: New command with rate limit +- `delete_old_messages_batch`: New command with rate limit +- `get_room_occupants_number`: Don't request the whole MUC room state (#3684, #1964) +- `get_vcard`: Add support for MUC room vCard +- `oauth_revoke_token`: Add support to work with all backends +- `room_unused_*`: Optimize commands in SQL by reusing `created_at` +- `rooms_unused_...`: Let `get_all_rooms` handle `global` argument (#3726) +- `stop|restart`: Terminate ejabberd_sm before everything else to ensure sessions closing (#3641) +- `subscribe_room_many`: New command + +Translations +- Updated Catalan +- Updated French +- Updated German +- Updated Portuguese +- Updated Portuguese (Brazil) +- Updated Spanish + +Workflows +- CI: Publish CT logs and Cover on failure to an external GH Pages repo +- CI: Test shell scripts using ShellCheck (#3738) +- Container: New workflow to build and publish containers +- Installers: Add job to create draft release +- Installers: New workflow to build binary packages +- Runtime: New workflow to test compilation, rel, starting and ejabberdctl + +# Version 21.12 + +Commands +- `create_room_with_opts`: Fixed when using SQL storage +- `change_room_option`: Add missing fields from config inside `mod_muc_admin:change_options` +- piefxis: Fixed arguments of all commands + +Modules +- mod_caps: Don't forget caps on XEP-0198 resumption +- mod_conversejs: New module to serve a simple page for Converse.js +- mod_http_upload_quota: Avoid `max_days` race +- mod_muc: Support MUC hats (XEP-0317, conversejs/prosody compatible) +- mod_muc: Optimize MucSub processing +- mod_muc: Fix exception in mucsub {un}subscription events multicast handler +- mod_multicast: Improve and optimize multicast routing code +- mod_offline: Allow storing non-composing x:events in offline +- mod_ping: Send ping from server, not bare user JID +- mod_push: Fix handling of MUC/Sub messages +- mod_register: New allow_modules option to restrict registration modules +- mod_register_web: Handle unknown host gracefully +- mod_register_web: Use mod_register configured restrictions + +PubSub +- Add `delete_expired_pubsub_items` command +- Add `delete_old_pubsub_items` command +- Optimize publishing on large nodes (SQL) +- Support unlimited number of items +- Support `max_items=max` node configuration +- Bump default value for `max_items` limit from 10 to 1000 +- Use configured `max_items` by default +- node_flat: Avoid catch-all clauses for RSM +- node_flat_sql: Avoid catch-all clauses for RSM + +SQL +- Use `INSERT ... ON CONFLICT` in SQL_UPSERT for PostgreSQL >= 9.5 +- mod_mam export: assign MUC entries to the MUC service +- MySQL: Fix typo when creating index +- PgSQL: Add SASL auth support, PostgreSQL 14 +- PgSQL: Add missing SQL migration for table `push_session` +- PgSQL: Fix `vcard_search` definition in pgsql new schema + +Other +- `captcha-ng.sh`: "sort -R" command not POSIX, added "shuf" and "cat" as fallback +- Make s2s connection table cleanup more robust +- Update export/import of scram password to XEP-0227 1.1 +- Update Jose to 1.11.1 (the last in hex.pm correctly versioned) + # Version 21.07 Compilation - Add rebar3 3.15.2 binary - Add support for mix to: `./configure --enable-rebar=mix` -- Add workaround so rebar2 can use Elixir 1.12.0 - Improved `make rel` to work with rebar3 and mix - Add `make dev` to build a development release with rebar3 or mix - Hex: Add `sql/` and `vars.config` to Hex package files @@ -167,7 +318,7 @@ Translations: - Fix problem with leaving old data when updating shared rosters - Fix edge case that caused failure of resuming old sessions with stream management. -- Fix crash when room that was started with loging enabled was later +- Fix crash when room that was started with logging enabled was later changed to logging disabled - Increase default shaper limits (this should help with delays for clients that are using jingle) @@ -232,7 +383,7 @@ Translations: for all backends not only SQL - Add infrastructure for having module documentation directly in individual module source code -- Generate man page automaticaly +- Generate man page automatically - Implement copy feature in mod_carboncopy * Fixes @@ -240,7 +391,7 @@ Translations: - Fix handling of result in xmlrpc module - Make webadmin work even when accessed through not declared domain - Better error reporting in xmlrpc -- Limit ammount of results returned by disco queries to pubsub nodes +- Limit amount of results returned by disco queries to pubsub nodes - Improve validation of configured JWT keys - Fix race condition in Redis/SQL startup - Fix loading order of third party modules @@ -341,7 +492,7 @@ Translations: - Improve handling of unexpected iq in mod_muc_room - Attach mod_muc_room processes to a supervisor - Restore room when receiving message or generic iq for not started room -- Distribute routing of MUC messages accross all CPU cores +- Distribute routing of MUC messages across all CPU cores * PubSub - Fix pending nodes retrieval for SQL backend @@ -375,7 +526,7 @@ Translations: - Make static hooks analyzer working again * MUC -- Service admins are allowed to recreate room even if archiv is nonempty +- Service admins are allowed to recreate room even if archive is nonempty - New option user_mucsub_from_muc_archive - Avoid late arrival of get_disco_item response - Handle get_subscribed_rooms call from mod_muc_room pid diff --git a/COMPILE.md b/COMPILE.md new file mode 100644 index 000000000..5bcfaa581 --- /dev/null +++ b/COMPILE.md @@ -0,0 +1,126 @@ +Compile and Install ejabberd +============================ + +This document explains how to compile and install ejabberd +from source code. + +For a more detailed explanation, please check the +ejabberd Docs: [Source Code Installation][docs-source]. + +[docs-source]: https://docs.ejabberd.im/admin/installation/#source-code + + +Requirements +------------ + +To compile ejabberd you need: + + - GNU Make + - GCC + - Libexpat ≥ 1.95 + - Libyaml ≥ 0.1.4 + - Erlang/OTP ≥ 19.3 + - OpenSSL ≥ 1.0.0 + +Other optional libraries are: + + - Zlib ≥ 1.2.3, for Stream Compression support (XEP-0138) + - PAM library, for Pluggable Authentication Modules (PAM) + - ImageMagick's Convert program and Ghostscript fonts, for CAPTCHA + challenges + - Elixir ≥ 1.10.3, to support Elixir, and alternative to rebar/rebar3 + +If your system splits packages in libraries and development headers, +install the development packages too. + + +Download Source Code +-------------------- + +There are several ways to obtain the ejabberd source code: + +- Source code archive from [ProcessOne Downloads][p1dl] +- Source code package from [ejabberd GitHub Releases][ghr] +- Latest development code from [ejabberd Git repository][gitrepo] + +[p1dl]: https://www.process-one.net/en/ejabberd/downloads/ +[ghr]: https://github.com/processone/ejabberd/releases +[gitrepo]: https://github.com/processone/ejabberd + + +Compile +------- + +The general instructions to compile ejabberd are: + + ./configure + make + +If the source code doesn't contain a `configure` script, +first of all install `autoconf` and run this to generate it: + + ./autogen.sh + +To configure the compilation, features, install paths... + + ./configure --help + + +Install in the System +--------------------- + +To install ejabberd in the system, run this with system administrator rights (root user): + + sudo make install + +This will: + +- Install the configuration files in `/etc/ejabberd/` +- Install ejabberd binary, header and runtime files in `/lib/ejabberd/` +- Install the administration script: `/sbin/ejabberdctl` +- Install ejabberd documentation in `/share/doc/ejabberd/` +- Create a spool directory: `/var/lib/ejabberd/` +- Create a directory for log files: `/var/log/ejabberd/` + + +Build an OTP Release +-------------------- + +Instead of installing ejabberd in the system, you can build an OTP release +that includes all necessary to run ejabberd in a subdirectory: + + ./configure --with-rebar=rebar3 + make rel + +Or, if you have Elixir available and plan to develop Elixir code: + + ./configure --with-rebar=mix + make dev + +Check the full list of targets: + + make help + + +Start ejabberd +-------------- + +You can use the `ejabberdctl` command line administration script to +start and stop ejabberd. Some examples, depending on your installation method: + +- When installed in the system: + ``` + ejabberdctl start + /sbin/ejabberdctl start + ``` + +- When built an OTP production release: + ``` + _build/prod/rel/ejabberd/bin/ejabberdctl start + _build/prod/rel/ejabberd/bin/ejabberdctl live + ``` + +- Start interactively without installing or building OTP release: + ``` + make relive + ``` diff --git a/CONTAINER.md b/CONTAINER.md new file mode 100644 index 000000000..d42a0ae6e --- /dev/null +++ b/CONTAINER.md @@ -0,0 +1,313 @@ + +[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/processone/ejabberd?sort=semver&logo=embarcadero&label=&color=49c0c4)](https://github.com/processone/ejabberd/tags) +[![GitHub Container](https://img.shields.io/github/v/tag/processone/ejabberd?label=container&sort=semver)](https://github.com/processone/ejabberd/pkgs/container/ejabberd) +[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/ejabberd/ecs?label=docker)](https://hub.docker.com/r/ejabberd/ecs/) + + +ejabberd Container +================== + +[ejabberd][home] is an open-source, +robust, scalable and extensible realtime platform built using [Erlang/OTP][erlang], +that includes [XMPP][xmpp] Server, [MQTT][mqtt] Broker and [SIP][sip] Service. + +[home]: https://ejabberd.im/ +[erlang]: https://www.erlang.org/ +[xmpp]: https://xmpp.org/ +[mqtt]: https://mqtt.org/ +[sip]: https://en.wikipedia.org/wiki/Session_Initiation_Protocol + +This document explains how to use the +[ejabberd container images](https://github.com/processone/ejabberd/pkgs/container/ejabberd) +available in the GitHub Container Registry, +built using the files in `.github/container/`. + +Alternatively, there are also +[ejabberd-ecs Docker images](https://hub.docker.com/r/ejabberd/ecs/) +available in Docker Hub, +built using the +[docker-ejabberd/ecs](https://github.com/processone/docker-ejabberd/tree/master/ecs) +repository. + +If you are using a Windows operating system, check the tutorials mentioned in +[ejabberd Docs > Docker Image](https://docs.ejabberd.im/admin/installation/#docker-image). + + +Start ejabberd +-------------- + +### With default configuration + +Start ejabberd in a new container: + +```bash +docker run --name ejabberd -d -p 5222:5222 ghcr.io/processone/ejabberd +``` + +That runs the container as a daemon, +using ejabberd default configuration file and XMPP domain "localhost". + +Stop the running container: + +```bash +docker stop ejabberd +``` + +Restart the stopped ejabberd container: + +```bash +docker restart ejabberd +``` + + +### Start with Erlang console attached + +Start ejabberd with an Erlang console attached using the `live` command: + +```bash +docker run --name ejabberd -it -p 5222:5222 ghcr.io/processone/ejabberd live +``` + +That uses the default configuration file and XMPP domain "localhost". + + +### Start with your configuration and database + +Pass a configuration file as a volume +and share the local directory to store database: + +```bash +mkdir database +chown ejabberd database + +cp ejabberd.yml.example ejabberd.yml + +docker run --name ejabberd -it \ + -v $(pwd)/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml \ + -v $(pwd)/database:/opt/ejabberd/database \ + -p 5222:5222 ghcr.io/processone/ejabberd live +``` + +Notice that ejabberd runs in the container with an account named `ejabberd`, +and the volumes you mount must grant proper rights to that account. + + +Next steps +---------- + +### Register the administrator account + +The default ejabberd configuration does not grant admin privileges +to any account, +you may want to register a new account in ejabberd +and grant it admin rights. + +Register an account using the `ejabberdctl` script: + +```bash +docker exec -it ejabberd ejabberdctl register admin localhost passw0rd +``` + +Then edit conf/ejabberd.yml and add the ACL as explained in +[ejabberd Docs: Administration Account](https://docs.ejabberd.im/admin/installation/#administration-account) + + +### Check ejabberd log files + +Check the content of the log files inside the container, +even if you do not put it on a shared persistent drive: + +```bash +docker exec -it ejabberd tail -f logs/ejabberd.log +``` + + +### Inspect the container files + +The container uses Alpine Linux. Start a shell inside the container: + +```bash +docker exec -it ejabberd sh +``` + + +### Open ejabberd debug console + +Open an interactive debug Erlang console attached to a running ejabberd in a running container: + +```bash +docker exec -it ejabberd ejabberdctl debug +``` + + +### CAPTCHA + +ejabberd includes two example CAPTCHA scripts. +If you want to use any of them, first install some additional required libraries: + +```bash +docker exec --user root ejabberd apk add imagemagick ghostscript-fonts bash +``` + +Now update your ejabberd configuration file, for example: +```bash +docker exec -it ejabberd vi conf/ejabberd.yml +``` + +and add the required options: +``` +captcha_cmd: /opt/ejabberd-22.04/lib/ejabberd-22.04/priv/bin/captcha.sh +captcha_url: https://localhost:5443/captcha +``` + +Finally, reload the configuration file or restart the container: +```bash +docker exec ejabberd ejabberdctl reload_config +``` + + +Advanced Container Configuration +-------------------------------- + +### Ports + +This container image exposes the ports: + +- `5222`: The default port for XMPP clients. +- `5269`: For XMPP federation. Only needed if you want to communicate with users on other servers. +- `5280`: For admin interface. +- `5443`: With encryption, used for admin interface, API, CAPTCHA, OAuth, Websockets and XMPP BOSH. +- `1883`: Used for MQTT +- `4369-4399`: EPMD and Erlang connectivity, used for `ejabberdctl` and clustering +- `5210`: Erlang connectivity when `ERL_DIST_PORT` is set, alternative to EPMD + + +### Volumes + +ejabberd produces two types of data: log files and database spool files (Mnesia). +This is the kind of data you probably want to store on a persistent or local drive (at least the database). + +The volumes you may want to map: + +- `/opt/ejabberd/conf/`: Directory containing configuration and certificates +- `/opt/ejabberd/database/`: Directory containing Mnesia database. +You should back up or export the content of the directory to persistent storage +(host storage, local storage, any storage plugin) +- `/opt/ejabberd/logs/`: Directory containing log files +- `/opt/ejabberd/upload/`: Directory containing uploaded files. This should also be backed up. + +All these files are owned by `ejabberd` user inside the container. + +It's possible to install additional ejabberd modules using volumes, +[this comment](https://github.com/processone/docker-ejabberd/issues/81#issuecomment-1036115146) +explains how to install an additional module using docker-compose. + + +### Commands on start + +The ejabberdctl script reads the `CTL_ON_CREATE` environment variable +the first time the docker container is started, +and reads `CTL_ON_START` every time the container is started. +Those variables can contain one ejabberdctl command, +or several commands separated with the blankspace and `;` characters. + +Example usage (see full example [docker-compose.yml](https://github.com/processone/docker-ejabberd/issues/64#issuecomment-887741332)): +```yaml + environment: + - CTL_ON_CREATE=register admin localhost asd + - CTL_ON_START=stats registeredusers ; + check_password admin localhost asd ; + status +``` + + +### Clustering + +When setting several containers to form a +[cluster of ejabberd nodes](https://docs.ejabberd.im/admin/guide/clustering/), +each one must have a different +[Erlang Node Name](https://docs.ejabberd.im/admin/guide/security/#erlang-node-name) +and the same +[Erlang Cookie](https://docs.ejabberd.im/admin/guide/security/#erlang-cookie). + +For this you can either: +- edit `conf/ejabberdctl.cfg` and set variables `ERLANG_NODE` and `ERLANG_COOKIE` +- set the environment variables `ERLANG_NODE_ARG` and `ERLANG_COOKIE` + +Example to connect a local `ejabberdctl` to a containerized ejabberd: +1. When creating the container, export port 5210, and set `ERLANG_COOKIE`: +``` +docker run --name ejabberd -it \ + -e ERLANG_COOKIE=`cat $HOME/.erlang.cookie` \ + -p 5210:5210 -p 5222:5222 \ + ghcr.io/processone/ejabberd +``` +2. Set `ERL_DIST_PORT=5210` in ejabberdctl.cfg of container and local ejabberd +3. Restart the container +4. Now use `ejabberdctl` in your local ejabberd deployment + +To connect using a local `ejabberd` script: +``` +ERL_DIST_PORT=5210 _build/dev/rel/ejabberd/bin/ejabberd ping +``` + +Example using environment variables (see full example [docker-compose.yml](https://github.com/processone/docker-ejabberd/issues/64#issuecomment-887741332)): +```yaml + environment: + - ERLANG_NODE_ARG=ejabberd@node7 + - ERLANG_COOKIE=dummycookie123 +``` + + +Generating a Container Image +---------------------------- + +This container image includes ejabberd as a standalone OTP release built using Elixir. + +That OTP release is configured with: + +- `mix.exs`: Customize ejabberd release +- `vars.config`: ejabberd compilation configuration options +- `config/runtime.exs`: Customize ejabberd paths +- `ejabberd.yml.template`: ejabberd default config file + +Build ejabberd Community Server base image from ejabberd master on GitHub: + +```bash +docker build \ + -t personal/ejabberd \ + -f .github/container/Dockerfile \ + . +``` + +Build ejabberd Community Server base image for a given ejabberd version, +both for amd64 and arm64 architectures: + +```bash +VERSION=22.05 +git checkout $VERSION +docker buildx build \ + --platform=linux/amd64,linux/arm64 + -t personal/ejabberd:$VERSION \ + -f .github/container/Dockerfile \ + . +``` + +It's also possible to use podman instead of docker, just notice: +- `EXPOSE 4369-4399` port range is not supported, remove that in Dockerfile +- It mentions that `healthcheck` is not supported by the Open Container Initiative image format +- If you want to start with command `live`, add environment variable `EJABBERD_BYPASS_WARNINGS=true` +```bash +podman build \ + -t ejabberd \ + -f .github/container/Dockerfile \ + . + +podman run --name eja1 -d -p 5222:5222 localhost/ejabberd + +podman exec eja1 ejabberdctl status + +podman exec -it eja1 sh + +podman stop eja1 +``` diff --git a/Makefile.in b/Makefile.in index c39195ac7..42b799c71 100644 --- a/Makefile.in +++ b/Makefile.in @@ -10,27 +10,27 @@ exec_prefix = @exec_prefix@ DESTDIR = # /etc/ejabberd/ -ETCDIR = $(DESTDIR)@sysconfdir@/ejabberd +ETCDIR = @sysconfdir@/ejabberd # /bin/ -BINDIR = $(DESTDIR)@bindir@ +BINDIR = @bindir@ # /sbin/ -SBINDIR = $(DESTDIR)@sbindir@ +SBINDIR = @sbindir@ # /lib/ -LIBDIR = $(DESTDIR)@libdir@ +LIBDIR = @libdir@ # /lib/ejabberd/ -EJABBERDDIR = $(DESTDIR)@libdir@/ejabberd +EJABBERDDIR = @libdir@/ejabberd # /share/doc/ejabberd PACKAGE_TARNAME = @PACKAGE_TARNAME@ datarootdir = @datarootdir@ -DOCDIR = $(DESTDIR)@docdir@ +DOCDIR = @docdir@ # /share/doc/man/man5 -MANDIR = $(DESTDIR)@mandir@/man5 +MANDIR = @mandir@/man5 # /usr/lib/ejabberd/ebin/ BEAMDIR = $(EJABBERDDIR)/ebin @@ -66,16 +66,10 @@ SQLDIR = $(PRIVDIR)/sql LUADIR = $(PRIVDIR)/lua # /var/lib/ejabberd/ -SPOOLDIR = $(DESTDIR)@localstatedir@/lib/ejabberd - -# /var/lock/ejabberdctl -CTLLOCKDIR = $(DESTDIR)@localstatedir@/lock/ejabberdctl - -# /var/lib/ejabberd/.erlang.cookie -COOKIEFILE = $(SPOOLDIR)/.erlang.cookie +SPOOLDIR = @localstatedir@/lib/ejabberd # /var/log/ejabberd/ -LOGDIR = $(DESTDIR)@localstatedir@/log/ejabberd +LOGDIR = @localstatedir@/log/ejabberd INSTALLUSER=@INSTALLUSER@ # if no user was enabled, don't set privileges or ownership @@ -99,12 +93,14 @@ ifneq ($(INSTALLGROUP),) endif ifeq "$(MIX)" "mix" -IS_REBAR:=6 +REBAR_VER:=6 +REBAR_VER_318:=0 else -IS_REBAR:=$(shell expr `$(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}'`) +REBAR_VER:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}') +REBAR_VER_318:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print ($$2 == 3 && $$3 >= 18 ? 1 : 0)}') endif -ifeq "$(IS_REBAR)" "6" +ifeq "$(REBAR_VER)" "6" REBAR=$(MIX) SKIPDEPS= LISTDEPS=deps.tree @@ -113,23 +109,33 @@ ifeq "$(IS_REBAR)" "6" DEPSBASE=_build DEPSDIR=$(DEPSBASE)/dev/lib GET_DEPS= deps.get - CONFIGURE_DEPS= + CONFIGURE_DEPS=(cd deps/eimp; ./configure) EBINDIR=$(DEPSDIR)/ejabberd/ebin + XREFOPTIONS=graph + CLEANARG=--deps REBARREL=MIX_ENV=prod $(REBAR) release --overwrite REBARDEV=MIX_ENV=dev $(REBAR) release --overwrite + RELIVECMD=escript rel/relive.escript && MIX_ENV=dev RELIVE=true iex --name ejabberd@localhost -S mix run else -ifeq "$(IS_REBAR)" "3" +ifeq "$(REBAR_VER)" "3" SKIPDEPS= LISTDEPS=tree +ifeq "$(REBAR_VER_318)" "1" + UPDATEDEPS=upgrade --all +else UPDATEDEPS=upgrade +endif DEPSPATTERN="s/ (.*//; /^ / s/.* \([a-z0-9_]*\).*/\1/p;" DEPSBASE=_build DEPSDIR=$(DEPSBASE)/default/lib GET_DEPS= get-deps CONFIGURE_DEPS=$(REBAR) configure-deps EBINDIR=$(DEPSDIR)/ejabberd/ebin + XREFOPTIONS= + CLEANARG=--all REBARREL=$(REBAR) as prod tar REBARDEV=REBAR_PROFILE=dev $(REBAR) release + RELIVECMD=$(REBAR) relive else SKIPDEPS=skip_deps=true LISTDEPS=-q list-deps @@ -140,12 +146,16 @@ else GET_DEPS= get-deps CONFIGURE_DEPS=$(REBAR) configure-deps EBINDIR=ebin + XREFOPTIONS= + CLEANARG= REBARREL=$(REBAR) generate REBARDEV= + RELIVECMD=@echo "Rebar2 detected... relive not supported.\ + \nTry: ./configure --with-rebar=./rebar3 ; make relive" endif endif -all: deps src +all: scripts deps src deps: $(DEPSDIR)/.got @@ -154,9 +164,9 @@ $(DEPSDIR)/.got: rm -rf $(DEPSDIR)/.built mkdir -p $(DEPSDIR) $(REBAR) $(GET_DEPS) && :> $(DEPSDIR)/.got + $(CONFIGURE_DEPS) $(DEPSDIR)/.built: $(DEPSDIR)/.got - $(CONFIGURE_DEPS) $(REBAR) compile && :> $(DEPSDIR)/.built src: $(DEPSDIR)/.built @@ -166,9 +176,10 @@ update: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built $(REBAR) $(UPDATEDEPS) && :> $(DEPSDIR)/.got + $(CONFIGURE_DEPS) xref: all - $(REBAR) $(SKIPDEPS) xref + $(REBAR) $(SKIPDEPS) xref $(XREFOPTIONS) hooks: all tools/hook_deps.sh $(EBINDIR) @@ -194,7 +205,7 @@ ELIXIR_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,$(word 2,$(1))) $(wordlist 5,1000, DEPS_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,$(word 2,$(1))) $(wordlist 3,1000,$(1)) MAIN_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,ejabberd) $(1) TO_DEST_SINGLE=$(if $(subst X$(DEPSBASE)X,,X$(word 1,$(1))X),$(call MAIN_TO_DEST,$(1)),$(if $(subst XlibX,,X$(word $(LIBIX),$(1))X),$(call DEPS_TO_DEST,$(wordlist $(DEPIX),1000,$(1))),$(call ELIXIR_TO_DEST,$(wordlist $(DEPIX),1000,$(1))))) -TO_DEST=$(foreach path,$(1),$(call JOIN_PATHS,$(call TO_DEST_SINGLE,$(subst /, ,$(path))))) +TO_DEST=$(foreach path,$(1),$(call JOIN_PATHS,$(DESTDIR)$(call TO_DEST_SINGLE,$(subst /, ,$(path))))) FILTER_DIRS=$(foreach path,$(1),$(if $(wildcard $(path)/*),,$(path))) FILES_WILDCARD=$(call FILTER_DIRS,$(foreach w,$(1),$(wildcard $(w)))) @@ -235,7 +246,7 @@ $(foreach file,$(DEPS_FILES_FILTERED) $(MAIN_FILES),$(eval $(call COPY_template, $(foreach file,$(BINARIES),$(eval $(call COPY_BINARY_template,$(file)))) $(sort $(call TO_DEST,$(MAIN_DIRS) $(DEPS_DIRS))): - $(INSTALL) -d $@ + $(INSTALL) -d $@ $(call TO_DEST,priv/sql/lite.sql): sql/lite.sql $(call TO_DEST,priv/sql) $(INSTALL) -m 644 $< $@ @@ -249,7 +260,15 @@ $(call TO_DEST,priv/bin/captcha.sh): tools/captcha.sh $(call TO_DEST,priv/bin) $(call TO_DEST,priv/lua/redis_sm.lua): priv/lua/redis_sm.lua $(call TO_DEST,priv/lua) $(INSTALL) -m 644 $< $@ -copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh priv/sql/lite.sql priv/sql/lite.new.sql priv/lua/redis_sm.lua) +ifeq (@sqlite@,true) +SQLITE_FILES = priv/sql/lite.sql priv/sql/lite.new.sql +endif + +ifeq (@redis@,true) +REDIS_FILES = priv/lua/redis_sm.lua +endif + +copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh $(SQLITE_FILES) $(REDIS_FILES)) .PHONY: $(call TO_DEST,$(DEPS_FILES) $(MAIN_DIRS) $(DEPS_DIRS)) @@ -260,135 +279,155 @@ copy-files: copy-files-sub: copy-files-sub2 -install: copy-files - # - # Configuration files - $(INSTALL) -d -m 750 $(G_USER) $(ETCDIR) - [ -f $(ETCDIR)/ejabberd.yml ] \ - && $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(ETCDIR)/ejabberd.yml-new \ - || $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(ETCDIR)/ejabberd.yml - $(SED) -e "s*{{rootdir}}*@prefix@*g" \ - -e "s*{{installuser}}*@INSTALLUSER@*g" \ +relive: + $(RELIVECMD) + +relivelibdir=$(shell pwd)/$(DEPSDIR) +relivedir=$(shell pwd)/_build/relive +iexpath=$(shell which iex) +CONFIG_DIR = ${relivedir}/conf +SPOOL_DIR = ${relivedir}/database +LOGS_DIR = ${relivedir}/logs + +ejabberdctl.relive: + $(SED) -e "s*{{installuser}}*@INSTALLUSER@*g" \ + -e "s*{{config_dir}}*${CONFIG_DIR}*g" \ + -e "s*{{logs_dir}}*${LOGS_DIR}*g" \ + -e "s*{{spool_dir}}*${SPOOL_DIR}*g" \ + -e "s*{{bindir}}/iex*$(iexpath)*g" \ -e "s*{{bindir}}*@bindir@*g" \ - -e "s*{{libdir}}*@libdir@*g" \ - -e "s*{{sysconfdir}}*@sysconfdir@*g" \ - -e "s*{{localstatedir}}*@localstatedir@*g" \ - -e "s*{{docdir}}*@docdir@*g" \ + -e "s*{{libdir}}*${relivelibdir}*g" \ -e "s*{{erl}}*@ERL@*g" \ -e "s*{{epmd}}*@EPMD@*g" ejabberdctl.template \ - > ejabberdctl.example - [ -f $(ETCDIR)/ejabberdctl.cfg ] \ - && $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(ETCDIR)/ejabberdctl.cfg-new \ - || $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(ETCDIR)/ejabberdctl.cfg - $(INSTALL) -b -m 644 $(G_USER) inetrc $(ETCDIR)/inetrc - # - # Administration script - [ -d $(SBINDIR) ] || $(INSTALL) -d -m 755 $(SBINDIR) - $(INSTALL) -m 550 $(G_USER) ejabberdctl.example $(SBINDIR)/ejabberdctl - # Elixir binaries - [ -d $(BINDIR) ] || $(INSTALL) -d -m 755 $(BINDIR) - [ -f $(DEPSDIR)/elixir/bin/iex ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/iex $(BINDIR)/iex || true - [ -f $(DEPSDIR)/elixir/bin/elixir ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/elixir $(BINDIR)/elixir || true - [ -f $(DEPSDIR)/elixir/bin/mix ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/mix $(BINDIR)/mix || true - # - # Init script + > ejabberdctl.relive + +ejabberd.init: $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \ -e "s*@installuser@*$(INIT_USER)*g" ejabberd.init.template \ > ejabberd.init chmod 755 ejabberd.init - # - # Service script - $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" ejabberd.service.template \ + +ejabberd.service: + $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \ + -e "s*@installuser@*$(INIT_USER)*g" ejabberd.service.template \ > ejabberd.service chmod 644 ejabberd.service + +ejabberdctl.example: vars.config + $(SED) -e "s*{{installuser}}*@INSTALLUSER@*g" \ + -e "s*{{config_dir}}*${ETCDIR}*g" \ + -e "s*{{logs_dir}}*${LOGDIR}*g" \ + -e "s*{{spool_dir}}*${SPOOLDIR}*g" \ + -e "s*{{bindir}}*@bindir@*g" \ + -e "s*{{libdir}}*@libdir@*g" \ + -e "s*{{erl}}*@ERL@*g" \ + -e "s*{{epmd}}*@EPMD@*g" ejabberdctl.template \ + > ejabberdctl.example + +scripts: ejabberd.init ejabberd.service ejabberdctl.example + +install: copy-files + # + # Configuration files + $(INSTALL) -d -m 750 $(G_USER) $(DESTDIR)$(ETCDIR) + [ -f $(DESTDIR)$(ETCDIR)/ejabberd.yml ] \ + && $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(DESTDIR)$(ETCDIR)/ejabberd.yml-new \ + || $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(DESTDIR)$(ETCDIR)/ejabberd.yml + [ -f $(DESTDIR)$(ETCDIR)/ejabberdctl.cfg ] \ + && $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(DESTDIR)$(ETCDIR)/ejabberdctl.cfg-new \ + || $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(DESTDIR)$(ETCDIR)/ejabberdctl.cfg + $(INSTALL) -b -m 644 $(G_USER) inetrc $(DESTDIR)$(ETCDIR)/inetrc + # + # Administration script + [ -d $(DESTDIR)$(SBINDIR) ] || $(INSTALL) -d -m 755 $(DESTDIR)$(SBINDIR) + $(INSTALL) -m 550 $(G_USER) ejabberdctl.example $(DESTDIR)$(SBINDIR)/ejabberdctl + # Elixir binaries + [ -d $(DESTDIR)$(BINDIR) ] || $(INSTALL) -d -m 755 $(DESTDIR)$(BINDIR) + [ -f $(DEPSDIR)/elixir/bin/iex ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/iex $(DESTDIR)$(BINDIR)/iex || true + [ -f $(DEPSDIR)/elixir/bin/elixir ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/elixir $(DESTDIR)$(BINDIR)/elixir || true + [ -f $(DEPSDIR)/elixir/bin/mix ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/mix $(DESTDIR)$(BINDIR)/mix || true # # Spool directory - $(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT) - chmod -R 750 $(SPOOLDIR) - [ ! -f $(COOKIEFILE) ] || { $(CHOWN_COMMAND) @INSTALLUSER@ $(COOKIEFILE) >$(CHOWN_OUTPUT) ; chmod 400 $(COOKIEFILE) ; } - # - # ejabberdctl lock directory - $(INSTALL) -d -m 750 $(O_USER) $(CTLLOCKDIR) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(CTLLOCKDIR) >$(CHOWN_OUTPUT) - chmod -R 750 $(CTLLOCKDIR) + $(INSTALL) -d -m 750 $(O_USER) $(DESTDIR)$(SPOOLDIR) + $(CHOWN_COMMAND) -R @INSTALLUSER@ $(DESTDIR)$(SPOOLDIR) >$(CHOWN_OUTPUT) + chmod -R 750 $(DESTDIR)$(SPOOLDIR) # # Log directory - $(INSTALL) -d -m 750 $(O_USER) $(LOGDIR) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(LOGDIR) >$(CHOWN_OUTPUT) - chmod -R 750 $(LOGDIR) + $(INSTALL) -d -m 750 $(O_USER) $(DESTDIR)$(LOGDIR) + $(CHOWN_COMMAND) -R @INSTALLUSER@ $(DESTDIR)$(LOGDIR) >$(CHOWN_OUTPUT) + chmod -R 750 $(DESTDIR)$(LOGDIR) # # Documentation - $(INSTALL) -d $(MANDIR) - $(INSTALL) -d $(DOCDIR) + $(INSTALL) -d $(DESTDIR)$(MANDIR) + $(INSTALL) -d $(DESTDIR)$(DOCDIR) [ -f man/ejabberd.yml.5 ] \ - && $(INSTALL) -m 644 man/ejabberd.yml.5 $(MANDIR) \ + && $(INSTALL) -m 644 man/ejabberd.yml.5 $(DESTDIR)$(MANDIR) \ || echo "Man page not included in sources" - $(INSTALL) -m 644 COPYING $(DOCDIR) + $(INSTALL) -m 644 COPYING $(DESTDIR)$(DOCDIR) uninstall: uninstall-binary uninstall-binary: - rm -f $(SBINDIR)/ejabberdctl - rm -f $(BINDIR)/iex - rm -f $(BINDIR)/elixir - rm -f $(BINDIR)/mix - rm -fr $(DOCDIR) - rm -f $(BEAMDIR)/*.beam - rm -f $(BEAMDIR)/*.app - rm -fr $(BEAMDIR) - rm -f $(INCLUDEDIR)/*.hrl - rm -fr $(INCLUDEDIR) - rm -fr $(PBINDIR) - rm -f $(SODIR)/*.so - rm -fr $(SODIR) - rm -f $(MSGSDIR)/*.msg - rm -fr $(MSGSDIR) - rm -f $(CSSDIR)/*.css - rm -fr $(CSSDIR) - rm -f $(IMGDIR)/*.png - rm -fr $(IMGDIR) - rm -f $(JSDIR)/*.js - rm -fr $(JSDIR) - rm -f $(SQLDIR)/*.sql - rm -fr $(SQLDIR) - rm -fr $(LUADIR)/*.lua - rm -fr $(LUADIR) - rm -fr $(PRIVDIR) - rm -fr $(EJABBERDDIR) + rm -f $(DESTDIR)$(SBINDIR)/ejabberdctl + rm -f $(DESTDIR)$(BINDIR)/iex + rm -f $(DESTDIR)$(BINDIR)/elixir + rm -f $(DESTDIR)$(BINDIR)/mix + rm -fr $(DESTDIR)$(DOCDIR) + rm -f $(DESTDIR)$(BEAMDIR)/*.beam + rm -f $(DESTDIR)$(BEAMDIR)/*.app + rm -fr $(DESTDIR)$(BEAMDIR) + rm -f $(DESTDIR)$(INCLUDEDIR)/*.hrl + rm -fr $(DESTDIR)$(INCLUDEDIR) + rm -fr $(DESTDIR)$(PBINDIR) + rm -f $(DESTDIR)$(SODIR)/*.so + rm -fr $(DESTDIR)$(SODIR) + rm -f $(DESTDIR)$(MSGSDIR)/*.msg + rm -fr $(DESTDIR)$(MSGSDIR) + rm -f $(DESTDIR)$(CSSDIR)/*.css + rm -fr $(DESTDIR)$(CSSDIR) + rm -f $(DESTDIR)$(IMGDIR)/*.png + rm -fr $(DESTDIR)$(IMGDIR) + rm -f $(DESTDIR)$(JSDIR)/*.js + rm -fr $(DESTDIR)$(JSDIR) + rm -f $(DESTDIR)$(SQLDIR)/*.sql + rm -fr $(DESTDIR)$(SQLDIR) + rm -fr $(DESTDIR)$(LUADIR)/*.lua + rm -fr $(DESTDIR)$(LUADIR) + rm -fr $(DESTDIR)$(PRIVDIR) + rm -fr $(DESTDIR)$(EJABBERDDIR) uninstall-all: uninstall-binary - rm -rf $(ETCDIR) - rm -rf $(EJABBERDDIR) - rm -rf $(SPOOLDIR) - rm -rf $(CTLLOCKDIR) - rm -rf $(LOGDIR) + rm -rf $(DESTDIR)$(ETCDIR) + rm -rf $(DESTDIR)$(EJABBERDDIR) + rm -rf $(DESTDIR)$(SPOOLDIR) + rm -rf $(DESTDIR)$(LOGDIR) clean: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built rm -rf test/*.beam - $(REBAR) clean + rm -f rebar.lock + rm -f ejabberdctl.example ejabberd.init ejabberd.service + $(REBAR) clean $(CLEANARG) clean-rel: rm -rf rel/ejabberd distclean: clean clean-rel + rm -f aclocal.m4 rm -f config.status rm -f config.log rm -rf autom4te.cache rm -rf $(EBINDIR) rm -rf $(DEPSBASE) + rm -rf deps rm -f Makefile rm -f vars.config - rm -f src/ejabberd.app.src - rm -f ejabberdctl.example ejabberd.init ejabberd.service - [ ! -f ../ChangeLog ] || rm -f ../ChangeLog rel: $(REBARREL) -DEV_CONFIG = _build/dev/rel/ejabberd/etc/ejabberd/ejabberd.yml +DEV_CONFIG = _build/dev/rel/ejabberd/conf/ejabberd.yml dev $(DEV_CONFIG): $(REBARDEV) @@ -398,7 +437,7 @@ TAGS: Makefile: Makefile.in -ifeq "$(IS_REBAR)" "3" +ifeq "$(REBAR_VER)" "3" dialyzer: $(REBAR) dialyzer else @@ -455,9 +494,10 @@ test: help: @echo "" - @echo " [all] Compile dependencies and ejabberd" - @echo " src Compile ejabberd" - @echo " deps Get dependencies" + @echo " [all] " + @echo " scripts Prepare ejabberd start scripts" + @echo " deps Get and configure dependencies" + @echo " src Compile dependencies and ejabberd" @echo " update Update dependencies' source code" @echo " clean Clean binary files" @echo " distclean Clean completely the development files" @@ -468,6 +508,7 @@ help: @echo "" @echo " rel Build a production release" @echo " dev Build a development release" + @echo " relive Start a live ejabberd in _build/relive/" @echo "" @echo " edoc Generate edoc documentation (unused)" @echo " options Generate ejabberd_option.erl" diff --git a/README.md b/README.md index 163ead480..7d526ee55 100644 --- a/README.md +++ b/README.md @@ -1,208 +1,126 @@ -ejabberd Community Edition -========================== -[![CI](https://github.com/processone/ejabberd/actions/workflows/ci.yml/badge.svg)](https://github.com/processone/ejabberd/actions/workflows/ci.yml) -[![Coverage Status](https://coveralls.io/repos/github/processone/ejabberd/badge.svg?branch=master "Coverage in coveralls.io")](https://coveralls.io/github/processone/ejabberd?branch=master) -[![Translation status](https://hosted.weblate.org/widgets/ejabberd/-/ejabberd-po/svg-badge.svg "Translation status in Weblate")](https://hosted.weblate.org/projects/ejabberd/ejabberd-po/) -[![Hex version](https://img.shields.io/hexpm/v/ejabberd.svg "Hex version")](https://hex.pm/packages/ejabberd) - -ejabberd is a distributed, fault-tolerant technology that allows the creation -of large-scale instant messaging applications. The server can reliably support -thousands of simultaneous users on a single node and has been designed to -provide exceptional standards of fault tolerance. As an open source -technology, based on industry-standards, ejabberd can be used to build bespoke -solutions very cost effectively. +

+ +

+

+ + + + + + + + + + +
+ + + + + + +

-Key Features +[ejabberd][im] is an open-source, +robust, scalable and extensible realtime platform built using [Erlang/OTP][erlang], +that includes [XMPP][xmpp] Server, [MQTT][mqtt] Broker and [SIP][sip] Service. + +Check the features in [ejabberd.im][im], [ejabberd Docs][features], +[ejabberd at ProcessOne][p1home], and a list of [supported protocols and XEPs][xeps]. + + +Installation ------------ -- **Cross-platform** - ejabberd runs under Microsoft Windows and Unix-derived systems such as - Linux, FreeBSD and NetBSD. +There are several ways to install ejabberd: -- **Distributed** - You can run ejabberd on a cluster of machines and all of them will serve the - same XMPP domain(s). When you need more capacity you can simply add a new - cheap node to your cluster. Accordingly, you do not need to buy an expensive - high-end machine to support tens of thousands concurrent users. - -- **Fault-tolerant** - You can deploy an ejabberd cluster so that all the information required for - a properly working service will be replicated permanently on all nodes. This - means that if one of the nodes crashes, the others will continue working - without disruption. In addition, nodes also can be added or replaced ‘on - the fly’. - -- **Administrator-friendly** - ejabberd is built on top of the Open Source Erlang. As a result you do not - need to install an external database, an external web server, amongst others - because everything is already included, and ready to run out of the box. - Other administrator benefits include: - - Comprehensive documentation. - - Straightforward installers for Linux. - - Docker packaging to help with deploy / development on Linux, Windows or MacOS. - - Deb and RPM packaging to support most Linux distributions. - - Web administration. - - Shared roster groups. - - Command line administration tool. - - Can integrate with existing authentication mechanisms. - - Capability to send announce messages. - -- **Internationalized** - ejabberd leads in internationalization. Hence it is very well suited in a - globalized world. Related features are: - - Translated to 25 languages. - - Support for IDNA. - -- **Open Standards** - ejabberd is the first Open Source XMPP server claiming to fully comply to - the XMPP standard. - - Fully XMPP-compliant. - - XML-based protocol. - - Many protocols supported. +- Source code: compile yourself, see [COMPILE](COMPILE.md) +- Installers from [ejabberd GitHub Releases][releases] (run/deb/rpm for x64 and arm64) +- Container image from [ejabberd Docker Hub][hubecs], see [ecs README][docker-ecs-readme] (for x64) +- Container image from [ejabberd Github Packages][packages], see [CONTAINER](CONTAINER.md) (for x64 and arm64) +- Using your [Operating System package][osp] +- Using the [Homebrew][homebrew] package manager -Additional Features -------------------- +Documentation +------------- -Moreover, ejabberd comes with a wide range of other state-of-the-art features: +Please check the [ejabberd Docs][docs] website. -- **Modularity** - - Load only the modules you want. - - Extend ejabberd with your own custom modules. - -- **Security** - - SASL and STARTTLS for c2s and s2s connections. - - STARTTLS and Dialback s2s connections. - - Web Admin accessible via HTTPS secure access. - -- **Databases** - - Internal database for fast deployment (Mnesia). - - Native MySQL support. - - Native PostgreSQL support. - - ODBC data storage support. - - Microsoft SQL Server support. - -- **Authentication** - - Internal authentication. - - PAM, LDAP and ODBC. - - External authentication script. - -- **Others** - - Support for virtual hosting. - - Compressing XML streams with Stream Compression (XEP-0138). - - Statistics via Statistics Gathering (XEP-0039). - - IPv6 support both for c2s and s2s connections. - - Multi-User Chat module with support for clustering and HTML logging. - - Users Directory based on users vCards. - - Publish-Subscribe component with support for Personal Eventing. - - Support for web clients: HTTP Polling and HTTP Binding (BOSH). - - Component support: interface with networks such as AIM, ICQ and MSN. - - -Quickstart guide ----------------- - -### 0. Requirements - -To compile ejabberd you need: - - - GNU Make. - - GCC. - - Libexpat ≥ 1.95. - - Libyaml ≥ 0.1.4. - - Erlang/OTP ≥ 19.3. - - OpenSSL ≥ 1.0.0. - - Zlib ≥ 1.2.3, for Stream Compression support (XEP-0138). Optional. - - PAM library. Optional. For Pluggable Authentication Modules (PAM). - - ImageMagick's Convert program and Ghostscript fonts. Optional. For CAPTCHA - challenges. - - Elixir ≥ 1.10.3. Optional. Alternative to build ejabberd - -If your system splits packages in libraries and development headers, you must -install the development packages also. - -### 1. Compile and install on *nix systems - -To compile ejabberd, execute the following commands. The first one is only -necessary if your source tree didn't come with a `configure` script (In this -case you need autoconf installed). - - ./autogen.sh - ./configure - make - -To install ejabberd, run this command with system administrator rights (root -user): - - sudo make install - -These commands will: - -- Install the configuration files in `/etc/ejabberd/` -- Install ejabberd binary, header and runtime files in `/lib/ejabberd/` -- Install the administration script: `/sbin/ejabberdctl` -- Install ejabberd documentation in `/share/doc/ejabberd/` -- Create a spool directory: `/var/lib/ejabberd/` -- Create a directory for log files: `/var/log/ejabberd/` - - -### 2. Start ejabberd - -You can use the `ejabberdctl` command line administration script to -start and stop ejabberd. For example: - - ejabberdctl start - - -For detailed information please refer to the -[ejabberd Documentation](https://docs.ejabberd.im) - - -### 3. Use ejabberd locally - -Alternatively, you can setup ejabberd without installing in your system: - - ./configure --with-rebar=rebar3 - make dev - -Or, if you have Elixir available and plan to develop Elixir code: - - ./configure --with-rebar=mix - make dev - -Check the full list of targets: +When compiling from source code, you can get some help with: + ./configure --help make help +Once ejabberd is installed, try: + + ejabberdctl help + man ejabberd.yml + Development ----------- -In order to assist in the development of ejabberd, and particularly the -execution of the test suite, a Vagrant environment is available at -https://github.com/processone/ejabberd-vagrant-dev. +Bug reports and features are tracked using [GitHub Issues][issues], +please check [CONTRIBUTING](CONTRIBUTING.md) for details. -To start ejabberd in development mode from the repository directory, you can -type a command like: +Translations can be improved online [using Weblate][weblate] +or in your local machine as explained in [Localization][localization]. - EJABBERD_CONFIG_PATH=ejabberd.yml erl -pa ebin -pa deps/*/ebin -pa test -pa deps/elixir/lib/*/ebin/ -s ejabberd +Documentation for developers is available in [ejabberd docs: Developers][docs-dev]. + +Security reports or concerns should preferably be reported privately, +please send an email to the address: contact [at] process-one [dot] net +or some other method from [ProcessOne Contact][p1contact]. + +For commercial offering and support, including [ejabberd Business Edition][p1home] +and [Fluux (ejabberd in the Cloud)][fluux], please check [ProcessOne ejabberd page][p1home]. -Translation ------------ +Community +--------- -Using any gettext editor, you can improve the translation files found in -`priv/msgs/*.po`, and then submit your changes. +There are several places to get in touch with other ejabberd developers and administrators: -Alternatively, a simple way to improve translations is using our Weblate project: -https://hosted.weblate.org/projects/ejabberd/ejabberd-po/ +- [ejabberd XMPP chatroom][muc]: ejabberd@conference.process-one.net +- [Mailing list][list] +- [GitHub Discussions][discussions] +- [Stack Overflow][stackoverflow] -Links ------ +License +------- -- Documentation: https://docs.ejabberd.im -- Community site: https://www.ejabberd.im -- ejabberd commercial offering and support: https://www.process-one.net/en/ejabberd +ejabberd is released under the GNU General Public License v2 (see [COPYING](COPYING.md)), +and [ejabberd translations](https://github.com/processone/ejabberd-po/) under MIT License. + + +[discussions]: https://github.com/processone/ejabberd/discussions +[docker-ecs-readme]: https://github.com/processone/docker-ejabberd/tree/master/ecs#readme +[docs-dev]: https://docs.ejabberd.im/developer/ +[docs]: https://docs.ejabberd.im +[erlang]: https://www.erlang.org/ +[features]: https://docs.ejabberd.im/admin/introduction/ +[fluux]: https://fluux.io/ +[github]: https://github.com/processone/ejabberd +[homebrew]: https://docs.ejabberd.im/admin/installation/#homebrew +[hubecs]: https://hub.docker.com/r/ejabberd/ecs/ +[im]: https://ejabberd.im/ +[issues]: https://github.com/processone/ejabberd/issues +[list]: https://lists.jabber.ru/mailman/listinfo/ejabberd +[localization]: https://docs.ejabberd.im/developer/extending-ejabberd/localization/ +[mqtt]: https://mqtt.org/ +[muc]: xmpp:ejabberd@conference.process-one.net +[osp]: https://docs.ejabberd.im/admin/installation/#operating-system-packages +[p1contact]: https://www.process-one.net/en/company/contact/ +[p1home]: https://www.process-one.net/en/ejabberd/ +[packages]: https://github.com/processone/ejabberd/pkgs/container/ejabberd +[releases]: https://github.com/processone/ejabberd/releases +[sip]: https://en.wikipedia.org/wiki/Session_Initiation_Protocol +[stackoverflow]: https://stackoverflow.com/questions/tagged/ejabberd?sort=newest +[weblate]: https://hosted.weblate.org/projects/ejabberd/ejabberd-po/ +[xeps]: https://www.process-one.net/en/ejabberd/protocols/ +[xmpp]: https://xmpp.org/ diff --git a/_checkouts/configure_deps/src/configure_deps_prv.erl b/_checkouts/configure_deps/src/configure_deps_prv.erl index f4333e4f0..91f2a3adc 100644 --- a/_checkouts/configure_deps/src/configure_deps_prv.erl +++ b/_checkouts/configure_deps/src/configure_deps_prv.erl @@ -19,7 +19,7 @@ init(State) -> {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 depdendencies. Useful if dependencies might change prior to compilation when configure is run."} + {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)}. diff --git a/config/runtime.exs b/config/runtime.exs index 050eae8b0..b4e6dc5f1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,12 +1,13 @@ import Config -rootpath = System.get_env("RELEASE_ROOT", "") +rootdefault = case System.get_env("RELIVE", "false") do + "true" -> "_build/relive" + "false" -> "" +end -# This is standard path in the context of ejabberd release +rootpath = System.get_env("RELEASE_ROOT", rootdefault) config :ejabberd, - file: Path.join(rootpath, "etc/ejabberd/ejabberd.yml"), - log_path: Path.join(rootpath, 'var/log/ejabberd/ejabberd.log') - -# Customize Mnesia directory: + file: Path.join(rootpath, "conf/ejabberd.yml"), + log_path: Path.join(rootpath, 'logs/ejabberd.log') config :mnesia, - dir: Path.join(rootpath, 'var/lib/ejabberd/') + dir: Path.join(rootpath, 'database/') diff --git a/configure.ac b/configure.ac index 1b0500e0d..68fb1f4d2 100644 --- a/configure.ac +++ b/configure.ac @@ -1,8 +1,8 @@ # -*- Autoconf -*- # Process this file with autoconf to produce a configure script. -AC_PREREQ(2.53) -AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 0.0` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) +AC_PREREQ(2.59) +AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 22.05` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) REQUIRE_ERLANG_MIN="8.3 (Erlang/OTP 19.3)" REQUIRE_ERLANG_MAX="100.0.0 (No Max)" @@ -19,8 +19,7 @@ fi # Checks Erlang runtime and compiler AC_ARG_WITH(erlang, - AC_HELP_STRING([--with-erlang=dir], - [search for erlang in dir]), + AS_HELP_STRING([--with-erlang=dir],[search for erlang in dir]), [if test "$withval" = "yes" -o "$withval" = "no" -o "X$with_erlang" = "X"; then extra_erl_path="" else @@ -29,8 +28,7 @@ fi ]) AC_ARG_WITH(rebar, - AC_HELP_STRING([--with-rebar=bin], - [use the rebar/rebar3/mix binary specified]), + AS_HELP_STRING([--with-rebar=bin],[use the rebar/rebar3/mix binary specified]), [if test "$withval" = "yes" -o "$withval" = "no" -o "X$with_rebar" = "X"; then rebar="rebar" else @@ -70,15 +68,15 @@ AC_CONFIG_FILES([Makefile vars.config]) AC_ARG_ENABLE(all, -[AC_HELP_STRING([--enable-all], [same as --enable-odbc --enable-mysql --enable-pgsql --enable-sqlite --enable-pam --enable-zlib --enable-redis --enable-elixir --enable-stun --enable-sip --enable-debug --enable-lua --enable-tools (useful for Dialyzer checks, default: no)])], +[AS_HELP_STRING([--enable-all],[same as --enable-odbc --enable-mssql --enable-mysql --enable-pgsql --enable-sqlite --enable-pam --enable-zlib --enable-redis --enable-elixir --enable-stun --enable-sip --enable-debug --enable-lua --enable-tools (useful for Dialyzer checks, default: no)])], [case "${enableval}" in - yes) odbc=true mysql=true pgsql=true sqlite=true pam=true zlib=true redis=true elixir=true stun=true sip=true debug=true lua=true tools=true ;; - no) odbc=false mysql=false pgsql=false sqlite=false pam=false zlib=false redis=false elixir=false stun=false sip=false debug=false lua=false tools=false ;; + yes) odbc=true mssql=true mysql=true pgsql=true sqlite=true pam=true zlib=true redis=true elixir=true stun=true sip=true debug=true lua=true tools=true ;; + no) odbc=false mssql=false mysql=false pgsql=false sqlite=false pam=false zlib=false redis=false elixir=false stun=false sip=false debug=false lua=false tools=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-all) ;; esac],[]) AC_ARG_ENABLE(debug, -[AC_HELP_STRING([--enable-debug], [enable debug information (default: yes)])], +[AS_HELP_STRING([--enable-debug],[enable debug information (default: yes)])], [case "${enableval}" in yes) debug=true ;; no) debug=false ;; @@ -86,7 +84,7 @@ AC_ARG_ENABLE(debug, esac],[if test "x$debug" = "x"; then debug=true; fi]) AC_ARG_ENABLE(elixir, -[AC_HELP_STRING([--enable-elixir], [enable Elixir support (default: no)])], +[AS_HELP_STRING([--enable-elixir],[enable Elixir support (default: no)])], [case "${enableval}" in yes) elixir=true ;; no) elixir=false ;; @@ -94,8 +92,7 @@ AC_ARG_ENABLE(elixir, esac],[if test "x$elixir" = "x"; then elixir=false; fi]) AC_ARG_ENABLE(erlang-version-check, -[AC_HELP_STRING([--enable-erlang-version-check], - [Check Erlang/OTP version (default: yes)])]) +[AS_HELP_STRING([--enable-erlang-version-check],[Check Erlang/OTP version (default: yes)])]) case "$enable_erlang_version_check" in yes|'') ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX]) @@ -106,7 +103,7 @@ case "$enable_erlang_version_check" in esac AC_ARG_ENABLE(full_xml, -[AC_HELP_STRING([--enable-full-xml], [use XML features in XMPP stream (ex: CDATA) (default: no, requires XML compliant clients)])], +[AS_HELP_STRING([--enable-full-xml],[use XML features in XMPP stream (ex: CDATA) (default: no, requires XML compliant clients)])], [case "${enableval}" in yes) full_xml=true ;; no) full_xml=false ;; @@ -128,7 +125,7 @@ if test "$ENABLEGROUP" != ""; then fi AC_ARG_ENABLE(latest_deps, -[AC_HELP_STRING([--enable-latest-deps], [makes rebar use latest commits for dependencies instead of tagged versions (default: no)])], +[AS_HELP_STRING([--enable-latest-deps],[makes rebar use latest commits for dependencies instead of tagged versions (default: no)])], [case "${enableval}" in yes) latest_deps=true ;; no) latest_deps=false ;; @@ -136,7 +133,7 @@ AC_ARG_ENABLE(latest_deps, esac],[if test "x$latest_deps" = "x"; then latest_deps=false; fi]) AC_ARG_ENABLE(lua, -[AC_HELP_STRING([--enable-lua], [enable Lua support, to import from Prosody (default: no)])], +[AS_HELP_STRING([--enable-lua],[enable Lua support, to import from Prosody (default: no)])], [case "${enableval}" in yes) lua=true ;; no) lua=false ;; @@ -144,15 +141,15 @@ AC_ARG_ENABLE(lua, esac],[if test "x$lua" = "x"; then lua=false; fi]) AC_ARG_ENABLE(mssql, -[AC_HELP_STRING([--enable-mssql], [use Microsoft SQL Server database (default: no, requires --enable-odbc)])], +[AS_HELP_STRING([--enable-mssql],[use Microsoft SQL Server database (default: no, requires --enable-odbc)])], [case "${enableval}" in - yes) db_type=mssql; mssql=true ;; - no) db_type=generic; mssql=false ;; + yes) mssql=true ;; + no) mssql=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-mssql) ;; -esac],[db_type=generic]) +esac],[if test "x$mssql" = "x"; then mssql=false; fi]) AC_ARG_ENABLE(mysql, -[AC_HELP_STRING([--enable-mysql], [enable MySQL support (default: no)])], +[AS_HELP_STRING([--enable-mysql],[enable MySQL support (default: no)])], [case "${enableval}" in yes) mysql=true ;; no) mysql=false ;; @@ -160,7 +157,7 @@ AC_ARG_ENABLE(mysql, esac],[if test "x$mysql" = "x"; then mysql=false; fi]) AC_ARG_ENABLE(new_sql_schema, -[AC_HELP_STRING([--enable-new-sql-schema], [use new SQL schema (default: no)])], +[AS_HELP_STRING([--enable-new-sql-schema],[use new SQL schema by default (default: no)])], [case "${enableval}" in yes) new_sql_schema=true ;; no) new_sql_schema=false ;; @@ -168,7 +165,7 @@ AC_ARG_ENABLE(new_sql_schema, esac],[new_sql_schema=false]) AC_ARG_ENABLE(odbc, -[AC_HELP_STRING([--enable-odbc], [enable pure ODBC support (default: no)])], +[AS_HELP_STRING([--enable-odbc],[enable pure ODBC support (default: no)])], [case "${enableval}" in yes) odbc=true ;; no) odbc=false ;; @@ -176,7 +173,7 @@ AC_ARG_ENABLE(odbc, esac],[if test "x$odbc" = "x"; then odbc=false; fi]) AC_ARG_ENABLE(pam, -[AC_HELP_STRING([--enable-pam], [enable PAM support (default: no)])], +[AS_HELP_STRING([--enable-pam],[enable PAM support (default: no)])], [case "${enableval}" in yes) pam=true ;; no) pam=false ;; @@ -184,7 +181,7 @@ AC_ARG_ENABLE(pam, esac],[if test "x$pam" = "x"; then pam=false; fi]) AC_ARG_ENABLE(pgsql, -[AC_HELP_STRING([--enable-pgsql], [enable PostgreSQL support (default: no)])], +[AS_HELP_STRING([--enable-pgsql],[enable PostgreSQL support (default: no)])], [case "${enableval}" in yes) pgsql=true ;; no) pgsql=false ;; @@ -192,7 +189,7 @@ AC_ARG_ENABLE(pgsql, esac],[if test "x$pgsql" = "x"; then pgsql=false; fi]) AC_ARG_ENABLE(redis, -[AC_HELP_STRING([--enable-redis], [enable Redis support (default: no)])], +[AS_HELP_STRING([--enable-redis],[enable Redis support (default: no)])], [case "${enableval}" in yes) redis=true ;; no) redis=false ;; @@ -200,7 +197,7 @@ AC_ARG_ENABLE(redis, esac],[if test "x$redis" = "x"; then redis=false; fi]) AC_ARG_ENABLE(roster_gateway_workaround, -[AC_HELP_STRING([--enable-roster-gateway-workaround], [turn on workaround for processing gateway subscriptions (default: no)])], +[AS_HELP_STRING([--enable-roster-gateway-workaround],[turn on workaround for processing gateway subscriptions (default: no)])], [case "${enableval}" in yes) roster_gateway_workaround=true ;; no) roster_gateway_workaround=false ;; @@ -208,7 +205,7 @@ AC_ARG_ENABLE(roster_gateway_workaround, esac],[roster_gateway_workaround=false]) AC_ARG_ENABLE(sip, -[AC_HELP_STRING([--enable-sip], [enable SIP support (default: no)])], +[AS_HELP_STRING([--enable-sip],[enable SIP support (default: no)])], [case "${enableval}" in yes) sip=true ;; no) sip=false ;; @@ -216,7 +213,7 @@ AC_ARG_ENABLE(sip, esac],[if test "x$sip" = "x"; then sip=false; fi]) AC_ARG_ENABLE(sqlite, -[AC_HELP_STRING([--enable-sqlite], [enable SQLite support (default: no)])], +[AS_HELP_STRING([--enable-sqlite],[enable SQLite support (default: no)])], [case "${enableval}" in yes) sqlite=true ;; no) sqlite=false ;; @@ -224,7 +221,7 @@ AC_ARG_ENABLE(sqlite, esac],[if test "x$sqlite" = "x"; then sqlite=false; fi]) AC_ARG_ENABLE(stun, -[AC_HELP_STRING([--enable-stun], [enable STUN/TURN support (default: yes)])], +[AS_HELP_STRING([--enable-stun],[enable STUN/TURN support (default: yes)])], [case "${enableval}" in yes) stun=true ;; no) stun=false ;; @@ -232,7 +229,7 @@ AC_ARG_ENABLE(stun, esac],[if test "x$stun" = "x"; then stun=true; fi]) AC_ARG_ENABLE(system_deps, -[AC_HELP_STRING([--enable-system-deps], [makes rebar use locally installed dependencies instead of downloading them (default: no)])], +[AS_HELP_STRING([--enable-system-deps],[makes rebar use locally installed dependencies instead of downloading them (default: no)])], [case "${enableval}" in yes) system_deps=true ;; no) system_deps=false ;; @@ -240,7 +237,7 @@ AC_ARG_ENABLE(system_deps, esac],[if test "x$system_deps" = "x"; then system_deps=false; fi]) AC_ARG_ENABLE(tools, -[AC_HELP_STRING([--enable-tools], [build development tools (default: no)])], +[AS_HELP_STRING([--enable-tools],[build development tools (default: no)])], [case "${enableval}" in yes) tools=true ;; no) tools=false ;; @@ -262,7 +259,7 @@ if test "$ENABLEUSER" != ""; then fi AC_ARG_ENABLE(zlib, -[AC_HELP_STRING([--enable-zlib], [enable Stream Compression (XEP-0138) using zlib (default: yes)])], +[AS_HELP_STRING([--enable-zlib],[enable Stream Compression (XEP-0138) using zlib (default: yes)])], [case "${enableval}" in yes) zlib=true ;; no) zlib=false ;; @@ -286,8 +283,8 @@ esac AC_SUBST(roster_gateway_workaround) AC_SUBST(new_sql_schema) AC_SUBST(full_xml) -AC_SUBST(db_type) AC_SUBST(odbc) +AC_SUBST(mssql) AC_SUBST(mysql) AC_SUBST(pgsql) AC_SUBST(sqlite) diff --git a/ejabberd.service.template b/ejabberd.service.template index c779ea031..685a104d0 100644 --- a/ejabberd.service.template +++ b/ejabberd.service.template @@ -4,8 +4,8 @@ After=network.target [Service] Type=notify -User=ejabberd -Group=ejabberd +User=@installuser@ +Group=@installuser@ LimitNOFILE=65536 Restart=on-failure RestartSec=5 diff --git a/ejabberdctl.cfg.example b/ejabberdctl.cfg.example index 2300378e0..8b15933db 100644 --- a/ejabberdctl.cfg.example +++ b/ejabberdctl.cfg.example @@ -47,10 +47,28 @@ #INET_DIST_INTERFACE=127.0.0.1 #. -#' ERL_EPMD_ADDRESS: IP addresses where epmd listens for connections +#' ERL_DIST_PORT: Port number for Erlang distribution +# +# For Erlang distribution, clustering and ejabberdctl usage, the +# Erlang VM listens in a random TCP port number, and the Erlang Port +# Mapper Daemon (EPMD) is spawned and used to determine this port +# number. +# +# ERL_DIST_PORT can define this port number. In that case, EPMD is +# not spawned during ejabberd startup, and ERL_EPMD_ADDRESS is +# ignored. ERL_DIST_PORT must be set to the same port number during +# ejabberd startup and when calling ejabberdctl. This feature +# requires at least Erlang/OTP 23.1. +# +# Default: not defined +# +#ERL_DIST_PORT=5210 + +#. +#' ERL_EPMD_ADDRESS: IP addresses where EPMD listens for connections # # This environment variable may be set to a comma-separated -# list of IP addresses, in which case the epmd daemon +# list of IP addresses, in which case the EPMD daemon # will listen only on the specified address(es) and on the # loopback address (which is implicitly added to the list if it # has not been specified). The default behaviour is to listen on diff --git a/ejabberdctl.template b/ejabberdctl.template index 4cab19977..3b6ee7c64 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -12,6 +12,8 @@ ERLANG_NODE=ejabberd@localhost # define default environment variables [ -z "$SCRIPT" ] && SCRIPT=$0 SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" +# shellcheck disable=SC2034 +ERTS_VSN="{{erts_vsn}}" ERL="{{erl}}" IEX="{{bindir}}/iex" EPMD="{{epmd}}" @@ -48,24 +50,24 @@ while [ $# -gt 0 ]; do -l|--logs) LOGS_DIR=$2; shift 2;; -f|--config) EJABBERD_CONFIG_PATH=$2; shift 2;; -c|--ctl-config) EJABBERDCTL_CONFIG_PATH=$2; shift 2;; - -d|--config-dir) ETC_DIR=$2; shift 2;; + -d|--config-dir) CONFIG_DIR=$2; shift 2;; -t|--no-timeout) NO_TIMEOUT="--no-timeout"; shift;; *) break;; esac done # define ejabberd variables if not already defined from the command line -: "${ETC_DIR:="{{sysconfdir}}/ejabberd"}" -: "${LOGS_DIR:="{{localstatedir}}/log/ejabberd"}" -: "${SPOOL_DIR:="{{localstatedir}}/lib/ejabberd"}" -: "${EJABBERD_CONFIG_PATH:="$ETC_DIR/ejabberd.yml"}" -: "${EJABBERDCTL_CONFIG_PATH:="$ETC_DIR/ejabberdctl.cfg"}" +: "${CONFIG_DIR:="{{config_dir}}"}" +: "${LOGS_DIR:="{{logs_dir}}"}" +: "${SPOOL_DIR:="{{spool_dir}}"}" +: "${EJABBERD_CONFIG_PATH:="$CONFIG_DIR/ejabberd.yml"}" +: "${EJABBERDCTL_CONFIG_PATH:="$CONFIG_DIR/ejabberdctl.cfg"}" # Allows passing extra Erlang command-line arguments in vm.args file -: "${VMARGS:="$ETC_DIR/vm.args"}" +: "${VMARGS:="$CONFIG_DIR/vm.args"}" +# shellcheck source=ejabberdctl.cfg.example [ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH" [ -n "$ERLANG_NODE_ARG" ] && ERLANG_NODE="$ERLANG_NODE_ARG" [ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s" -: "${EJABBERD_DOC_PATH:="{{docdir}}"}" : "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}" # define erl parameters @@ -79,32 +81,41 @@ if [ -n "$INET_DIST_INTERFACE" ] ; then ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2" fi fi +[ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -erl_epmd_port $ERL_DIST_PORT -start_epmd false" # if vm.args file exists in config directory, pass it to Erlang VM [ -f "$VMARGS" ] && ERLANG_OPTS="$ERLANG_OPTS -args_file $VMARGS" -ERL_LIBS={{libdir}} +ERL_LIBS='{{libdir}}' ERL_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump -ERL_INETRC="$ETC_DIR"/inetrc +ERL_INETRC="$CONFIG_DIR"/inetrc # define ejabberd parameters EJABBERD_OPTS="$EJABBERD_OPTS\ $(sed '/^log_rotate_size/!d;s/:[ \t]*\([0-9]\{1,\}\).*/ \1/;s/:[ \t]*\(infinity\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ -$(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")" +$(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_count/!d;s/:[ \t]*\([0-9]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ +$(sed '/^log_burst_limit_window_time/!d;s/:[ \t]*\([0-9]*[a-z]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")" [ -n "$EJABBERD_OPTS" ] && EJABBERD_OPTS="-ejabberd $EJABBERD_OPTS" EJABBERD_OPTS="-mnesia dir \"$SPOOL_DIR\" $MNESIA_OPTIONS $EJABBERD_OPTS -s ejabberd" # export global variables export EJABBERD_CONFIG_PATH export EJABBERD_LOG_PATH -export EJABBERD_DOC_PATH export EJABBERD_PID_PATH export ERL_CRASH_DUMP export ERL_EPMD_ADDRESS +export ERL_DIST_PORT export ERL_INETRC export ERL_MAX_PORTS export ERL_MAX_ETS_TABLES export CONTRIB_MODULES_PATH export CONTRIB_MODULES_CONF_DIR export ERL_LIBS +export SCRIPT_DIR + +set_dist_client() +{ + [ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -dist_listen false" +} # run command either directly or via su $INSTALLUSER exec_cmd() @@ -128,14 +139,6 @@ exec_iex() # usage debugwarning() { - if [ "$OSTYPE" != "cygwin" ] && [ "$OSTYPE" != "win32" ] ; then - if [ "a$TERM" = "a" ] || [ "$TERM" = "dumb" ] ; then - echo "Terminal type not supported." - echo "You may have to set the TERM environment variable to fix this." - exit 8 - fi - fi - if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then echo "--------------------------------------------------------------------" echo "" @@ -153,7 +156,7 @@ debugwarning() echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" - read -r input + read -r _ echo "" fi } @@ -176,7 +179,7 @@ livewarning() echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" - read -r input + read -r _ echo "" fi } @@ -185,15 +188,21 @@ help() { echo "" echo "Commands to start an ejabberd node:" - echo " start Start an ejabberd node in server mode" - echo " debug Attach an interactive Erlang shell to a running ejabberd node" - echo " iexdebug Attach an interactive Elixir shell to a running ejabberd node" - echo " live Start an ejabberd node in live (interactive) mode" - echo " iexlive Start an ejabberd node in live (interactive) mode, within an Elixir shell" - echo " foreground Start an ejabberd node in server mode (attached)" + echo " start Start in server mode" + echo " foreground Start in server mode (attached)" + echo " foreground-quiet Start in server mode (attached), show only critical messages" + echo " live Start in interactive mode, with Erlang shell" + echo " iexlive Start in interactive mode, with Elixir shell" + echo "" + echo "Commands to interact with a running ejabberd node:" + echo " debug Attach an interactive Erlang shell to a running node" + echo " iexdebug Attach an interactive Elixir shell to a running node" + echo " etop Attach to a running node and start Erlang Top" + echo " ping Send ping to the node, returns pong or pang" + echo " started|stopped Wait for the node to fully start|stop" echo "" echo "Optional parameters when starting an ejabberd node:" - echo " --config-dir dir Config ejabberd: $ETC_DIR" + echo " --config-dir dir Config ejabberd: $CONFIG_DIR" echo " --config file Config ejabberd: $EJABBERD_CONFIG_PATH" echo " --ctl-config file Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH" echo " --logs dir Directory for logs: $LOGS_DIR" @@ -206,8 +215,9 @@ help() uid() { uuid=$(uuidgen 2>/dev/null) + random=$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null) [ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid) - [ -z "$uuid" ] && uuid=$(printf "%X" "${RANDOM:-$$}$(date +%M%S)") + [ -z "$uuid" ] && uuid=$(printf "%X" "${random:-$$}$(date +%M%S)") uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/') [ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}" [ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}" @@ -217,6 +227,7 @@ uid() # stop epmd if there is no other running node stop_epmd() { + [ -n "$ERL_DIST_PORT" ] && return "$EPMD" -names 2>/dev/null | grep -q name || "$EPMD" -kill >/dev/null } @@ -224,6 +235,7 @@ stop_epmd() # if all ok, ensure runtime directory exists and make it current directory check_start() { + [ -n "$ERL_DIST_PORT" ] && return "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { pgrep -f "$ERLANG_NODE" >/dev/null && { echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running." @@ -288,14 +300,17 @@ case $1 in ;; debug) debugwarning + set_dist_client exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE" ;; etop) + set_dist_client exec_erl "$(uid top)" -hidden -node "$ERLANG_NODE" -s etop \ -s erlang halt -output text ;; iexdebug) debugwarning + set_dist_client exec_iex "$(uid debug)" --remsh "$ERLANG_NODE" ;; iexlive) @@ -305,17 +320,21 @@ case $1 in ping) PEER=${2:-$ERLANG_NODE} [ "$PEER" = "${PEER%.*}" ] && PS="-s" + set_dist_client exec_cmd "$ERL" ${PS:--}name "$(uid ping "$(hostname $PS)")" $ERLANG_OPTS \ -noinput -hidden -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ -s erlang halt -output text ;; started) + set_dist_client wait_status 0 30 2 # wait 30x2s before timeout ;; stopped) + set_dist_client wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout ;; *) + set_dist_client exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" result=$? diff --git a/include/adhoc.hrl b/include/adhoc.hrl deleted file mode 100644 index 2047db874..000000000 --- a/include/adhoc.hrl +++ /dev/null @@ -1,44 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License along -%%% with this program; if not, write to the Free Software Foundation, Inc., -%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -%%% -%%%---------------------------------------------------------------------- - --record(adhoc_request, -{ - lang = <<"">> :: binary(), - node = <<"">> :: binary(), - sessionid = <<"">> :: binary(), - action = <<"">> :: binary(), - xdata = false :: false | xmlel(), - others = [] :: [xmlel()] -}). - --record(adhoc_response, -{ - lang = <<"">> :: binary(), - node = <<"">> :: binary(), - sessionid = <<"">> :: binary(), - status :: atom(), - defaultaction = <<"">> :: binary(), - actions = [] :: [binary()], - notes = [] :: [{binary(), binary()}], - elements = [] :: [xmlel()] -}). - --type adhoc_request() :: #adhoc_request{}. --type adhoc_response() :: #adhoc_response{}. diff --git a/include/bosh.hrl b/include/bosh.hrl index ca43a569b..e02675ed0 100644 --- a/include/bosh.hrl +++ b/include/bosh.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_auth.hrl b/include/ejabberd_auth.hrl index 00f31cfd3..a29b2cc29 100644 --- a/include/ejabberd_auth.hrl +++ b/include/ejabberd_auth.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index bfef47080..9009f7bb2 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -59,6 +59,7 @@ policy = restricted :: open | restricted | admin | user, %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}] access = [] :: [{atom(),atom(),atom()}|atom()], + definer = unknown :: atom(), result = {res, rescode} :: rterm() | '_' | '$2', args_rename = [] :: [{atom(),atom()}], args_desc = none :: none | [string()] | '_', diff --git a/include/ejabberd_config.hrl b/include/ejabberd_config.hrl deleted file mode 100644 index 555d998b7..000000000 --- a/include/ejabberd_config.hrl +++ /dev/null @@ -1,30 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License along -%%% with this program; if not, write to the Free Software Foundation, Inc., -%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -%%% -%%%---------------------------------------------------------------------- - --record(local_config, {key :: any(), value :: any()}). - --type local_config() :: #local_config{}. - --record(state, - {opts = [] :: [acl:acl() | local_config()], - hosts = [] :: [binary()], - override_local = false :: boolean(), - override_global = false :: boolean(), - override_acls = false :: boolean()}). diff --git a/include/ejabberd_ctl.hrl b/include/ejabberd_ctl.hrl index e962e85be..37935ebe2 100644 --- a/include/ejabberd_ctl.hrl +++ b/include/ejabberd_ctl.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_http.hrl b/include/ejabberd_http.hrl index 62e19a8f0..92dfd870e 100644 --- a/include/ejabberd_http.hrl +++ b/include/ejabberd_http.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl index d9a5a09de..7e7509454 100644 --- a/include/ejabberd_oauth.hrl +++ b/include/ejabberd_oauth.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_sm.hrl b/include/ejabberd_sm.hrl index 92e85ae03..d5286d793 100644 --- a/include/ejabberd_sm.hrl +++ b/include/ejabberd_sm.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_sql.hrl b/include/ejabberd_sql.hrl index 2d9ac03ec..cfa238b48 100644 --- a/include/ejabberd_sql.hrl +++ b/include/ejabberd_sql.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_sql_pt.hrl b/include/ejabberd_sql_pt.hrl index 07479d440..fd61bdd03 100644 --- a/include/ejabberd_sql_pt.hrl +++ b/include/ejabberd_sql_pt.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_stacktrace.hrl b/include/ejabberd_stacktrace.hrl index cc07c6eab..2d8fc6e43 100644 --- a/include/ejabberd_stacktrace.hrl +++ b/include/ejabberd_stacktrace.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/ejabberd_web_admin.hrl b/include/ejabberd_web_admin.hrl index 25df7f0c0..99f27f8de 100644 --- a/include/ejabberd_web_admin.hrl +++ b/include/ejabberd_web_admin.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/eldap.hrl b/include/eldap.hrl index db4f5428a..0787d1f98 100644 --- a/include/eldap.hrl +++ b/include/eldap.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/http_bind.hrl b/include/http_bind.hrl index a94379ee0..8a5ceb8ab 100644 --- a/include/http_bind.hrl +++ b/include/http_bind.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/logger.hrl b/include/logger.hrl index d69ff0d07..ed62f3607 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_announce.hrl b/include/mod_announce.hrl index 9c7647272..498dce8d0 100644 --- a/include/mod_announce.hrl +++ b/include/mod_announce.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_caps.hrl b/include/mod_caps.hrl index aa5287069..4bf404837 100644 --- a/include/mod_caps.hrl +++ b/include/mod_caps.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_last.hrl b/include/mod_last.hrl index 6b2ae7d78..aed9bdc6f 100644 --- a/include/mod_last.hrl +++ b/include/mod_last.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_mam.hrl b/include/mod_mam.hrl index 494c4bd5a..27c892abe 100644 --- a/include/mod_mam.hrl +++ b/include/mod_mam.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_muc.hrl b/include/mod_muc.hrl index e31e01be0..ea096c9fe 100644 --- a/include/mod_muc.hrl +++ b/include/mod_muc.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index f981f9e7c..48d72b111 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -65,6 +65,7 @@ captcha_whitelist = (?SETS):empty() :: gb_sets:set(), mam = false :: boolean(), pubsub = <<"">> :: binary(), + enable_hats = false :: boolean(), lang = ejabberd_option:language() :: binary() }). @@ -87,6 +88,16 @@ nick = <<>> :: binary(), nodes = [] :: [binary()]}). +-record(muc_subscribers, + {subscribers = #{} :: subscribers(), + subscriber_nicks = #{} :: subscriber_nicks(), + subscriber_nodes = #{} :: subscriber_nodes() + }). + +-type subscribers() :: #{ljid() => #subscriber{}}. +-type subscriber_nicks() :: #{binary() => [ljid()]}. +-type subscriber_nodes() :: #{binary() => subscribers()}. + -record(activity, { message_time = 0 :: integer(), @@ -106,15 +117,16 @@ jid = #jid{} :: jid(), config = #config{} :: config(), users = #{} :: users(), - subscribers = #{} :: subscribers(), - subscriber_nicks = #{} :: subscriber_nicks(), + muc_subscribers = #muc_subscribers{} :: #muc_subscribers{}, last_voice_request_time = treap:empty() :: treap:treap(), robots = #{} :: robots(), nicks = #{} :: nicks(), affiliations = #{} :: affiliations(), + roles = #{} :: roles(), history = #lqueue{} :: lqueue(), subject = [] :: [text()], subject_author = <<"">> :: binary(), + hats_users = #{} :: map(), % FIXME on OTP 21+: #{ljid() => #{binary() => binary()}}, just_created = erlang:system_time(microsecond) :: true | integer(), activity = treap:empty() :: treap:treap(), room_shaper = none :: ejabberd_shaper:shaper(), @@ -126,5 +138,4 @@ -type robots() :: #{jid() => {binary(), stanza()}}. -type nicks() :: #{binary() => [ljid()]}. -type affiliations() :: #{ljid() => affiliation() | {affiliation(), binary()}}. --type subscribers() :: #{ljid() => #subscriber{}}. --type subscriber_nicks() :: #{binary() => [ljid()]}. +-type roles() :: #{ljid() => role() | {role(), binary()}}. diff --git a/include/mod_offline.hrl b/include/mod_offline.hrl index 8b78f2e16..ba04f9b87 100644 --- a/include/mod_offline.hrl +++ b/include/mod_offline.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_privacy.hrl b/include/mod_privacy.hrl index 1f481eec8..99cd2310d 100644 --- a/include/mod_privacy.hrl +++ b/include/mod_privacy.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_private.hrl b/include/mod_private.hrl index 1c551a212..14e3bc8e3 100644 --- a/include/mod_private.hrl +++ b/include/mod_private.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_proxy65.hrl b/include/mod_proxy65.hrl index be2797bca..13f4d8c6a 100644 --- a/include/mod_proxy65.hrl +++ b/include/mod_proxy65.hrl @@ -2,7 +2,7 @@ %%% RFC 1928 constants. %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_push.hrl b/include/mod_push.hrl index 9ad1e0405..161b68bdf 100644 --- a/include/mod_push.hrl +++ b/include/mod_push.hrl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_roster.hrl b/include/mod_roster.hrl index 27d2aab28..6ec05b3da 100644 --- a/include/mod_roster.hrl +++ b/include/mod_roster.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_shared_roster.hrl b/include/mod_shared_roster.hrl index 515243862..a18d37414 100644 --- a/include/mod_shared_roster.hrl +++ b/include/mod_shared_roster.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_vcard.hrl b/include/mod_vcard.hrl index 24b219ec5..14f8380b8 100644 --- a/include/mod_vcard.hrl +++ b/include/mod_vcard.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/include/mod_vcard_xupdate.hrl b/include/mod_vcard_xupdate.hrl deleted file mode 100644 index 9b5bfac30..000000000 --- a/include/mod_vcard_xupdate.hrl +++ /dev/null @@ -1,22 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne -%%% -%%% This program is free software; you can redistribute it and/or -%%% modify it under the terms of the GNU General Public License as -%%% published by the Free Software Foundation; either version 2 of the -%%% License, or (at your option) any later version. -%%% -%%% This program is distributed in the hope that it will be useful, -%%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -%%% General Public License for more details. -%%% -%%% You should have received a copy of the GNU General Public License along -%%% with this program; if not, write to the Free Software Foundation, Inc., -%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -%%% -%%%---------------------------------------------------------------------- - --record(vcard_xupdate, {us = {<<>>, <<>>} :: {binary(), binary()}, - hash = <<>> :: binary()}). diff --git a/include/mqtt.hrl b/include/mqtt.hrl index 4d3ff5672..6458a1234 100644 --- a/include/mqtt.hrl +++ b/include/mqtt.hrl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. diff --git a/include/pubsub.hrl b/include/pubsub.hrl index 8496b95ac..1811aa926 100644 --- a/include/pubsub.hrl +++ b/include/pubsub.hrl @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,7 +23,7 @@ -define(ERR_EXTENDED(E, C), mod_pubsub:extended_error(E, C)). %% The actual limit can be configured with mod_pubsub's option max_items_node --define(MAXITEMS, 10). +-define(MAXITEMS, 1000). %% this is currently a hard limit. %% Would be nice to have it configurable. diff --git a/man/ejabberd.yml.5 b/man/ejabberd.yml.5 index f25f57250..dab3bcc50 100644 --- a/man/ejabberd.yml.5 +++ b/man/ejabberd.yml.5 @@ -1,13 +1,13 @@ '\" t .\" Title: ejabberd.yml .\" Author: [see the "AUTHOR" section] -.\" Generator: DocBook XSL Stylesheets v1.79.1 -.\" Date: 07/21/2021 +.\" Generator: DocBook XSL Stylesheets vsnapshot +.\" Date: 10/12/2022 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" -.TH "EJABBERD\&.YML" "5" "07/21/2021" "\ \&" "\ \&" +.TH "EJABBERD\&.YML" "5" "10/12/2022" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -52,7 +52,7 @@ YAML is indentation sensitive, so make sure you respect indentation, or otherwis .sp .5v .RE .sp -Logically, configuration options are splitted into 3 main categories: \fIModules\fR, \fIListeners\fR and everything else called \fITop Level\fR options\&. Thus this document is splitted into 3 main chapters describing each category separately\&. So, the contents of ejabberd\&.yml will typically look like this: +Logically, configuration options are split into 3 main categories: \fIModules\fR, \fIListeners\fR and everything else called \fITop Level\fR options\&. Thus this document is split into 3 main chapters describing each category separately\&. So, the contents of ejabberd\&.yml will typically look like this: .sp .if n \{\ .RS 4 @@ -82,7 +82,7 @@ All options can be changed in runtime by running \fIejabberdctl reload\-config\f .sp Some options can be specified for particular virtual host(s) only using \fIhost_config\fR or \fIappend_host_config\fR options\&. Such options are called \fIlocal\fR\&. Examples are \fImodules\fR, \fIauth_method\fR and \fIdefault_db\fR\&. The options that cannot be defined per virtual host are called \fIglobal\fR\&. Examples are \fIloglevel\fR, \fIcertfiles\fR and \fIlisten\fR\&. It is a configuration mistake to put \fIglobal\fR options under \fIhost_config\fR or \fIappend_host_config\fR section \- ejabberd will refuse to load such configuration\&. .sp -It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/21\&.07/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&. +It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/22\&.10/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&. .sp Note that this document is intended to provide comprehensive description of all configuration options that can be consulted to understand the meaning of a particular option, its format and possible values\&. It will be quite hard to understand how to configure ejabberd by reading this document only \- for this purpose the reader is recommended to read online Configuration Guide available at https://docs\&.ejabberd\&.im/admin/configuration\&. .SH "TOP LEVEL OPTIONS" @@ -91,7 +91,8 @@ This section describes top level options of ejabberd\&. .PP \fBaccess_rules\fR: \fI{AccessName: {allow|deny: ACLRules|ACLName}}\fR .RS 4 -The option specifies access rules\&. Each access rule is assigned a name that can be referenced from other parts of the configuration file (mostly from +This option defines +Access Rules\&. Each access rule is assigned a name that can be referenced from other parts of the configuration file (mostly from \fIaccess\fR options of ejabberd modules)\&. Each rule definition may contain arbitrary number of \fIallow\fR @@ -316,14 +317,46 @@ means that the same username can be taken multiple times in anonymous login mode .PP \fBanonymous_protocol\fR: \fIlogin_anon | sasl_anon | both\fR .RS 4 +Define what anonymous protocol will be used: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} \fIlogin_anon\fR means that the anonymous login method will be used\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} \fIsasl_anon\fR means that the SASL Anonymous method will be used\&. -\fIboth\fR -means that SASL Anonymous and login anonymous are both enabled\&. The default value is -\fIsasl_anon\fR\&. .RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIboth\fR +means that SASL Anonymous and login anonymous are both enabled\&. +.RE +.RE +.sp +The default value is \fIsasl_anon\fR\&. .PP \fBapi_permissions\fR: \fI[Permission, \&.\&.\&.]\fR .RS 4 @@ -375,7 +408,7 @@ A list of authentication methods to use\&. If several methods are defined, authe This is used by the contributed module \fIejabberd_auth_http\fR that can be installed from the -\fIejabberd\-contrib\fR +ejabberd\-contrib Git repository\&. Please refer to that module\(cqs README file for details\&. .RE .sp @@ -383,14 +416,36 @@ Git repository\&. Please refer to that module\(cqs README file for details\&. .PP \fBauth_password_format\fR: \fIplain | scram\fR .RS 4 -The option defines in what format the users passwords are stored\&. +The option defines in what format the users passwords are stored: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} \fIplain\fR: The password is stored as plain text in the database\&. This is risky because the passwords can be read if your database gets compromised\&. This is the default value\&. This format allows clients to authenticate using: the old Jabber Non\-SASL (XEP\-0078), SASL PLAIN, SASL DIGEST\-MD5, and SASL SCRAM\-SHA\-1\&. -\fIscram\fR: The password is not stored, only some information that allows to verify the hash provided by the client\&. It is impossible to obtain the original plain password from the stored information; for this reason, when this value is configured it cannot be changed to plain anymore\&. This format allows clients to authenticate using: SASL PLAIN and SASL SCRAM\-SHA\-1\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIscram\fR: The password is not stored, only some information that allows to verify the hash provided by the client\&. It is impossible to obtain the original plain password from the stored information; for this reason, when this value is configured it cannot be changed to plain anymore\&. This format allows clients to authenticate using: SASL PLAIN and SASL SCRAM\-SHA\-1\&. The default value is +\fIplain\fR\&. +.RE .RE .PP \fBauth_scram_hash\fR: \fIsha | sha256 | sha512\fR .RS 4 -Hash algorith that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. +Hash algorithm that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. The default value is +\fIsha\fR\&. .RE .PP \fBauth_use_cache\fR: \fItrue | false\fR @@ -471,12 +526,12 @@ Whether to enable or disable TLS compression for c2s connections\&. The default Path to a file of CA root certificates\&. The default is to use system defined file if possible\&. .RE .sp -For server conections, this \fIca_file\fR option is overriden by the s2s_cafile option\&. +For server connections, this \fIca_file\fR option is overridden by the s2s_cafile option\&. .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 The time of a cached item to keep in cache\&. Once it\(cqs expired, the corresponding item is erased from cache\&. The default value is -\fIone hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see +\fI1 hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see \fIauth_cache_life_time\fR, \fIoauth_cache_life_time\fR, \fIrouter_cache_life_time\fR, and @@ -507,7 +562,9 @@ A maximum number of items (not memory!) in cache\&. The rule of thumb, for all t .PP \fBcaptcha_cmd\fR: \fIPath\fR .RS 4 -Full path to a script that generates CAPTCHA images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. +Full path to a script that generates +CAPTCHA +images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. .RE .PP \fBcaptcha_host\fR: \fIString\fR @@ -519,13 +576,17 @@ instead\&. .PP \fBcaptcha_limit\fR: \fIpos_integer() | infinity\fR .RS 4 -Maximum number of CAPTCHA generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is +Maximum number of +CAPTCHA +generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is \fIinfinity\fR\&. .RE .PP \fBcaptcha_url\fR: \fIURL\fR .RS 4 -An URL where CAPTCHA requests should be sent\&. NOTE: you need to configure +An URL where +CAPTCHA +requests should be sent\&. NOTE: you need to configure \fIrequest_handlers\fR for \fIejabberd_http\fR @@ -815,7 +876,8 @@ Path to the file that contains the JWK Key\&. The default value is .RS 4 The option defines the default language of server strings that can be seen by XMPP clients\&. If an XMPP client does not possess \fIxml:lang\fR -attribute, the specified language is used\&. +attribute, the specified language is used\&. The default value is +\fI"en"\fR\&. .RE .PP \fBldap_backups\fR: \fI[Host, \&.\&.\&.]\fR @@ -944,11 +1006,31 @@ The option for listeners configuration\&. See the Listen Modules section for details\&. .RE +.sp +\fINote\fR about the next option: added in 22\&.10: +.PP +\fBlog_burst_limit_count\fR: \fINumber\fR +.RS 4 +The number of messages to accept in +log_burst_limit_window_time +period before starting to drop them\&. Default 500 +.RE +.sp +\fINote\fR about the next option: added in 22\&.10: +.PP +\fBlog_burst_limit_window_time\fR: \fINumber\fR +.RS 4 +The time period to rate\-limit log messages by\&. Defaults to 1 second\&. +.RE .PP \fBlog_rotate_count\fR: \fINumber\fR .RS 4 The number of rotated log files to keep\&. The default value is -\fI1\fR\&. +\fI1\fR, which means that only keeps +ejabberd\&.log\&.0, +error\&.log\&.0 +and +crash\&.log\&.0\&. .RE .PP \fBlog_rotate_size\fR: \fIpos_integer() | infinity\fR @@ -989,8 +1071,7 @@ seconds\&. .RS 4 This option can be used to tune tick time parameter of \fInet_kernel\fR\&. It tells Erlang VM how often nodes should check if intra\-node communication was not interrupted\&. This option must have identical value on all nodes, or it will lead to subtle bugs\&. Usually leaving default value of this is option is best, tweak it only if you know what you are doing\&. The default value is -\fI1\fR -minute\&. +\fI1 minute\fR\&. .RE .PP \fBnew_sql_schema\fR: \fItrue | false\fR @@ -998,7 +1079,7 @@ minute\&. Whether to use \fInew\fR SQL schema\&. All schemas are located at -https://github\&.com/processone/ejabberd/tree/21\&.07/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The +https://github\&.com/processone/ejabberd/tree/22\&.10/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The \fInew\fR schema allows to handle several XMPP domains in a single ejabberd database\&. Using this \fInew\fR @@ -1296,9 +1377,10 @@ seconds\&. .PP \fBs2s_access\fR: \fIAccess\fR .RS 4 -The access rule to restrict server\-to\-server connections\&. The default value is -\fIall\fR -which means no restrictions are applied\&. +This +Access Rule +defines to what remote servers can s2s connections be established\&. The default value is +\fIall\fR; no restrictions are applied, it is allowed to connect s2s to/from all remote servers\&. .RE .PP \fBs2s_cafile\fR: \fIPath\fR @@ -1392,8 +1474,7 @@ if the latter is not set\&. \fBs2s_timeout\fR: \fItimeout()\fR .RS 4 A time to wait before closing an idle s2s connection\&. The default value is -\fI10\fR -minutes\&. +\fI10 minutes\fR\&. .RE .PP \fBs2s_tls_compression\fR: \fItrue | false\fR @@ -1713,7 +1794,7 @@ module\&. The default value is obtained at compile time from the underlying vers .RS 4 This option enables validation for \fIOrigin\fR -header to protect against connections from other domains than given in the configuration file\&. In this way, the lower layer load balancer can be chosen for a specific ejabberd implementation while still providing a secure Websocket connection\&. The default value is +header to protect against connections from other domains than given in the configuration file\&. In this way, the lower layer load balancer can be chosen for a specific ejabberd implementation while still providing a secure WebSocket connection\&. The default value is \fIignore\fR\&. An example value of the \fIURL\fR is "https://test\&.example\&.org:8081"\&. @@ -1721,7 +1802,7 @@ is "https://test\&.example\&.org:8081"\&. .PP \fBwebsocket_ping_interval\fR: \fItimeout()\fR .RS 4 -Defines time between pings sent by the server to a client (Websocket level protocol pings are used for this) to keep a connection active\&. If the client doesn\(cqt respond to two consecutive pings, the connection will be assumed as closed\&. The value of +Defines time between pings sent by the server to a client (WebSocket level protocol pings are used for this) to keep a connection active\&. If the client doesn\(cqt respond to two consecutive pings, the connection will be assumed as closed\&. The value of \fI0\fR can be used to disable the feature\&. This option makes the server sending pings only for connections using the RFC compliant protocol\&. For older style connections the server expects that whitespace pings would be used for this purpose\&. The default value is \fI60\fR @@ -1795,24 +1876,7 @@ Details for some commands: \fIsrg\-create\fR: If you want to put a group Name with blankspaces, use the characters "\*(Aq and \*(Aq" to define when the Name starts and ends\&. See an example below\&. .RE .sp -.it 1 an-trap -.nr an-no-space-flag 1 -.nr an-break-flag 1 -.br -.ps +1 -\fBAvailable options:\fR -.RS 4 -.PP -\fBmodule_resource\fR: \fIResource\fR -.RS 4 -Indicate the resource that the XMPP stanzas must use in the FROM or TO JIDs\&. This is only useful in the -\fIget_vcard*\fR -and -\fIset_vcard*\fR -commands\&. The default value is -\fImod_admin_extra\fR\&. -.RE -.RE +The module has no options\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -1835,8 +1899,7 @@ access_rules: vcard_set: \- allow: adminextraresource modules: - mod_admin_extra: - module_resource: "modadminextraf8x,31ad" + mod_admin_extra: {} mod_vcard: access_set: vcard_set .fi @@ -1884,7 +1947,7 @@ ejabberdctl srg\-create g1 example\&.org "\*(AqGroup number 1\*(Aq" this_is_g1 g .RE .SS "mod_admin_update_sql" .sp -This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR ejabberdctl command\&. +This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR API\&. .sp The module has no options\&. .SS "mod_announce" @@ -2021,7 +2084,7 @@ The module depends on \fImod_vcard\fR, \fImod_vcard_xupdate\fR and \fImod_pubsub .PP \fBconvert\fR: \fI{From: To}\fR .RS 4 -Defines image convertion rules: the format in +Defines image conversion rules: the format in \fIFrom\fR will be converted to format in \fITo\fR\&. The value of @@ -2046,7 +2109,7 @@ convert: .PP \fBrate_limit\fR: \fINumber\fR .RS 4 -Limit any given JID by the number of avatars it is able to convert per minute\&. This is to protect the server from image convertion DoS\&. The default value is +Limit any given JID by the number of avatars it is able to convert per minute\&. This is to protect the server from image conversion DoS\&. The default value is \fI10\fR\&. .RE .RE @@ -2119,7 +2182,7 @@ This module depends on \fImod_privacy\fR where all the configuration is performe The module has no options\&. .SS "mod_bosh" .sp -This module implements XMPP over BOSH as defined in XEP\-0124 and XEP\-0206\&. BOSH stands for Bidirectional\-streams Over Synchronous HTTP\&. It makes it possible to simulate long lived connections required by XMPP over the HTTP protocol\&. In practice, this module makes it possible to use XMPP in a browser without Websocket support and more generally to have a way to use XMPP while having to get through an HTTP proxy\&. +This module implements XMPP over BOSH as defined in XEP\-0124 and XEP\-0206\&. BOSH stands for Bidirectional\-streams Over Synchronous HTTP\&. It makes it possible to simulate long lived connections required by XMPP over the HTTP protocol\&. In practice, this module makes it possible to use XMPP in a browser without WebSocket support and more generally to have a way to use XMPP while having to get through an HTTP proxy\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -2189,9 +2252,9 @@ option, but applied to this module only\&. .PP \fBram_db_type\fR: \fImnesia | sql | redis\fR .RS 4 -Same as +Same as top\-level \fIdefault_ram_db\fR -but applied to this module only\&. +option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR @@ -2321,6 +2384,160 @@ While a client is inactive, queue presence stanzas that indicate (un)availabilit The module provides server configuration functionality via XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR to be loaded\&. .sp The module has no options\&. +.SS "mod_conversejs" +.sp +This module serves a simple page for the Converse XMPP web browser client\&. +.sp +This module is available since ejabberd 21\&.12\&. Several options were improved in ejabberd 22\&.05\&. +.sp +To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. +.sp +Make sure either \fImod_bosh\fR or \fIejabberd_http_ws\fR request_handlers are enabled\&. +.sp +When \fIconversejs_css\fR and \fIconversejs_script\fR are \fIauto\fR, by default they point to the public Converse client\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options:\fR +.RS 4 +.PP +\fBbosh_service_url\fR: \fIauto | BoshURL\fR +.RS 4 +BOSH service URL to which Converse can connect to\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured BOSH request handler\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBconversejs_css\fR: \fIauto | URL\fR +.RS 4 +Converse CSS URL\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fIauto\fR\&. +.RE +.sp +\fINote\fR about the next option: added in 22\&.05: +.PP +\fBconversejs_options\fR: \fI{Name: Value}\fR +.RS 4 +Specify additional options to be passed to Converse\&. See +Converse configuration\&. Only boolean, integer and string values are supported; lists are not supported\&. +.RE +.sp +\fINote\fR about the next option: added in 22\&.05: +.PP +\fBconversejs_resources\fR: \fIPath\fR +.RS 4 +Local path to the Converse files\&. If not set, the public Converse client will be used instead\&. +.RE +.PP +\fBconversejs_script\fR: \fIauto | URL\fR +.RS 4 +Converse main script URL\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBdefault_domain\fR: \fIDomain\fR +.RS 4 +Specify a domain to act as the default for user JIDs\&. The keyword +\fI@HOST@\fR +is replaced with the hostname\&. The default value is +\fI@HOST@\fR\&. +.RE +.PP +\fBwebsocket_url\fR: \fIauto | WebSocketURL\fR +.RS 4 +A WebSocket URL to which Converse can connect to\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured WebSocket request handler\&. The default value is +\fIauto\fR\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExamples:\fR +.RS 4 +.sp +Manually setup WebSocket url, and use the public Converse client: +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /bosh: mod_bosh + /websocket: ejabberd_http_ws + /conversejs: mod_conversejs + +modules: + mod_bosh: {} + mod_conversejs: + websocket_url: "ws://@HOST@:5280/websocket" +.fi +.if n \{\ +.RE +.\} +.sp +Host Converse locally and let auto detection of WebSocket and Converse URLs: +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 443 + module: ejabberd_http + tls: true + request_handlers: + /websocket: ejabberd_http_ws + /conversejs: mod_conversejs + +modules: + mod_conversejs: + conversejs_resources: "/home/ejabberd/conversejs\-9\&.0\&.0/package/dist" +.fi +.if n \{\ +.RE +.\} +.sp +Configure some additional options for Converse +.sp +.if n \{\ +.RS 4 +.\} +.nf +modules: + mod_conversejs: + websocket_url: auto + conversejs_options: + auto_away: 30 + clear_cache_on_logout: true + i18n: "pt" + locked_domain: "@HOST@" + message_archiving: always + theme: dracula +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_delegation" .sp This module is an implementation of XEP\-0355: Namespace Delegation\&. Only admin mode has been implemented by now\&. Namespace delegation allows external services to handle IQ using specific namespace\&. This may be applied for external PEP service\&. @@ -2479,11 +2696,11 @@ server_info: \- modules: all name: abuse\-addresses - urls: [mailto:abuse@shakespeare\&.lit] + urls: ["mailto:abuse@shakespeare\&.lit"] \- modules: [mod_muc] name: "Web chatroom logs" - urls: [http://www\&.example\&.org/muc\-logs] + urls: ["http://www\&.example\&.org/muc\-logs"] \- modules: [mod_disco] name: feedback\-addresses @@ -2559,17 +2776,114 @@ The number of C2S authentication failures to trigger the IP ban\&. The default v \fI20\fR\&. .RE .RE +.SS "mod_host_meta" +.sp +This module serves small \fIhost\-meta\fR files as described in XEP\-0156: Discovering Alternative XMPP Connection Methods\&. +.sp +This module is available since ejabberd 22\&.05\&. +.sp +To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. +.sp +Notice it only works if ejabberd_http has tls enabled\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options:\fR +.RS 4 +.PP +\fBbosh_service_url\fR: \fIundefined | auto | BoshURL\fR +.RS 4 +BOSH service URL to announce\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured BOSH request handler\&. The default value is +\fIauto\fR\&. +.RE +.PP +\fBwebsocket_url\fR: \fIundefined | auto | WebSocketURL\fR +.RS 4 +WebSocket URL to announce\&. The keyword +\fI@HOST@\fR +is replaced with the real virtual host name\&. If set to +\fIauto\fR, it will build the URL of the first configured WebSocket request handler\&. The default value is +\fIauto\fR\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExample:\fR +.RS 4 +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 443 + module: ejabberd_http + tls: true + request_handlers: + /bosh: mod_bosh + /ws: ejabberd_http_ws + /\&.well\-known/host\-meta: mod_host_meta + /\&.well\-known/host\-meta\&.json: mod_host_meta + +modules: + mod_bosh: {} + mod_host_meta: + bosh_service_url: "https://@HOST@:5443/bosh" + websocket_url: "wss://@HOST@:5443/ws" +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_http_api" .sp -This module provides a ReST API to call ejabberd commands using JSON data\&. +This module provides a ReST interface to call ejabberd API commands using JSON data\&. .sp -To use this module, in addition to adding it to the \fImodules\fR section, you must also add it to \fIrequest_handlers\fR of some listener\&. +To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp To use a specific API version N, when defining the URL path in the request_handlers, add a \fIvN\fR\&. For example: \fI/api/v2: mod_http_api\fR .sp To run a command, send a POST request to the corresponding URL: \fIhttp://localhost:5280/api/\fR .sp The module has no options\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExample:\fR +.RS 4 +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /api: mod_http_api + +modules: + mod_http_api: {} +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_http_fileserver" .sp This simple module serves files from the local disk over HTTP\&. @@ -2697,7 +3011,7 @@ modules: .sp This module allows for requesting permissions to upload a file via HTTP as described in XEP\-0363: HTTP File Upload\&. If the request is accepted, the client receives a URL for uploading the file and another URL from which that file can later be downloaded\&. .sp -In order to use this module, it must be configured as a \fIrequest_handler\fR for \fIejabberd_http\fR listener\&. +In order to use this module, it must be enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -2746,7 +3060,9 @@ This option defines the permission bits of uploaded files\&. The bits are specif .PP \fBget_url\fR: \fIURL\fR .RS 4 -This option specifies the initial part of the GET URLs used for downloading the files\&. By default, it is set to the same value as +This option specifies the initial part of the GET URLs used for downloading the files\&. The default value is +\fIundefined\fR\&. When this option is +\fIundefined\fR, this option is set to the same value as \fIput_url\fR\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: if GET requests are handled by \fImod_http_upload\fR, the \fIget_url\fR @@ -2795,7 +3111,7 @@ A name of the service in the Service Discovery\&. This will only be displayed by .PP \fBput_url\fR: \fIURL\fR .RS 4 -This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is "https://@HOST@:5443"\&. +This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is "https://@HOST@:5443/upload"\&. .RE .PP \fBrm_on_unregister\fR: \fItrue | false\fR @@ -3530,21 +3846,24 @@ This option specifies who is allowed to administrate the Multi\-User Chat servic .PP \fBaccess_create\fR: \fIAccessName\fR .RS 4 -To configure who is allowed to create new rooms at the Multi\-User Chat service, this option can be used\&. By default any account in the local ejabberd server is allowed to create rooms\&. +To configure who is allowed to create new rooms at the Multi\-User Chat service, this option can be used\&. The default value is +\fIall\fR, which means everyone is allowed to create rooms\&. .RE .PP \fBaccess_mam\fR: \fIAccessName\fR .RS 4 To configure who is allowed to modify the \fImam\fR -room option\&. By default any account in the local ejabberd server is allowed to modify that option\&. +room option\&. The default value is +\fIall\fR, which means everyone is allowed to modify that option\&. .RE .PP \fBaccess_persistent\fR: \fIAccessName\fR .RS 4 To configure who is allowed to modify the \fIpersistent\fR -room option\&. By default any account in the local ejabberd server is allowed to modify that option\&. +room option\&. The default value is +\fIall\fR, which means everyone is allowed to modify that option\&. .RE .PP \fBaccess_register\fR: \fIAccessName\fR @@ -3553,14 +3872,23 @@ This option specifies who is allowed to register nickname within the Multi\-User \fIall\fR for backward compatibility, which means that any user is allowed to register any free nick\&. .RE +.sp +\fINote\fR about the next option: added in 22\&.05: +.PP +\fBcleanup_affiliations_on_start\fR: \fItrue | false\fR +.RS 4 +Remove affiliations for non\-existing local users on startup\&. The default value is +\fIfalse\fR\&. +.RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 -Define the type of persistent storage where the module will store room information\&. The default is the storage defined by the global option -\fIdefault_db\fR, or -\fImnesia\fR -if omitted\&. +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. .RE +.sp +\fINote\fR about the next option: improved in 22\&.05: .PP \fBdefault_room_options\fR: \fIOptions\fR .RS 4 @@ -3618,6 +3946,12 @@ Allow visitors to send status text in presence updates\&. If disallowed, the sta \fItrue\fR\&. .RE .PP +\fBallow_voice_requests\fR: \fItrue | false\fR +.RS 4 +Allow visitors in a moderated room to request voice\&. The default value is +\fItrue\fR\&. +.RE +.PP \fBanonymous\fR: \fItrue | false\fR .RS 4 The room is anonymous: occupants don\(cqt 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 @@ -3632,6 +3966,17 @@ in order to accept their join in the room\&. The default value is \fIfalse\fR\&. .RE .PP +\fBdescription\fR: \fIRoom Description\fR +.RS 4 +Short description of the room\&. The default value is an empty string\&. +.RE +.PP +\fBenable_hats\fR: \fItrue | false\fR +.RS 4 +Allow extended roles as defined in XEP\-0317 Hats\&. The default value is +\fIfalse\fR\&. +.RE +.PP \fBlang\fR: \fILanguage\fR .RS 4 Preferred language for the discussions in the room\&. The language format should conform to RFC 5646\&. There is no value by default\&. @@ -3729,10 +4074,26 @@ The list of participants is public, without requiring to enter the room\&. The d \fItrue\fR\&. .RE .PP +\fBpubsub\fR: \fIPubSub Node\fR +.RS 4 +XMPP URI of associated Publish/Subscribe node\&. The default value is an empty string\&. +.RE +.PP \fBtitle\fR: \fIRoom Title\fR .RS 4 A human\-readable title of the room\&. There is no default value .RE +.PP +\fBvcard\fR: \fIvCard\fR +.RS 4 +A custom vCard for the room\&. See the equivalent mod_muc option\&.The default value is an empty string\&. +.RE +.PP +\fBvoice_request_min_interval\fR: \fINumber\fR +.RS 4 +Minimum interval between voice requests, in seconds\&. The default value is +\fI1800\fR\&. +.RE .RE .PP \fBhibernation_timeout\fR: \fIinfinity | Seconds\fR @@ -3825,7 +4186,8 @@ This option defines the number of service admins or room owners allowed to enter .PP \fBmax_users_presence\fR: \fINumber\fR .RS 4 -This option defines after how many users in the room, it is considered overcrowded\&. When a MUC room is considered overcrowed, presence broadcasts are limited to reduce load, traffic and excessive presence "storm" received by participants\&. +This option defines after how many users in the room, it is considered overcrowded\&. When a MUC room is considered overcrowed, presence broadcasts are limited to reduce load, traffic and excessive presence "storm" received by participants\&. The default value is +\fI1000\fR\&. .RE .PP \fBmin_message_interval\fR: \fINumber\fR @@ -3860,9 +4222,9 @@ option, but applied to this module only\&. .PP \fBram_db_type\fR: \fImnesia | sql\fR .RS 4 -Define the type of volatile (in\-memory) storage where the module will store room information (\fImuc_online_room\fR -and -\fImuc_online_users\fR)\&. +Same as top\-level +\fIdefault_ram_db\fR +option, but applied to this module only\&. .RE .PP \fBregexp_room_id\fR: \fIstring()\fR @@ -3936,7 +4298,24 @@ This module provides commands to administer local MUC services and their MUC roo .sp This module depends on \fImod_muc\fR\&. .sp -The module has no options\&. +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options:\fR +.RS 4 +.sp +\fINote\fR about the next option: added in 22\&.05: +.PP +\fBsubscribe_room_many_max_users\fR: \fINumber\fR +.RS 4 +How many users can be subscribed to a room at once using the +\fIsubscribe_room_many\fR +command\&. The default value is +\fI50\fR\&. +.RE +.RE .SS "mod_muc_log" .sp This module enables optional logging of Multi\-User Chat (MUC) public conversations to HTML\&. Once you enable this module, users can join a room using a 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\&. @@ -4435,11 +4814,13 @@ option, but applied to this module only\&. .PP \fBuse_mam_for_storage\fR: \fItrue | false\fR .RS 4 -This is an experimental option\&. Enabling this option will make +This is an experimental option\&. Enabling this option, \fImod_offline\fR -not use the former spool table for storing MucSub offline messages, but will use the archive table instead\&. This use of the archive table is cleaner and it makes it possible for clients to slowly drop the former offline use case and rely on message archive instead\&. It also further reduces the storage required when you enabled MucSub\&. Enabling this option has a known drawback for the moment: most of flexible message retrieval queries don\(cqt work (those that allow retrieval/deletion of messages by id), but this specification is not widely used\&. The default value is +uses the +\fImod_mam\fR +archive table instead of its own spool table to retrieve the messages received when the user was offline\&. This allows client developers to slowly drop XEP\-0160 and rely on XEP\-0313 instead\&. It also further reduces the storage required when you enable MucSub\&. Enabling this option has a known drawback for the moment: most of flexible message retrieval queries don\(cqt work (those that allow retrieval/deletion of messages by id), but this specification is not widely used\&. The default value is \fIfalse\fR -to keep former behaviour as default and ensure this option is disabled\&. +to keep former behaviour as default\&. .RE .RE .sp @@ -4498,8 +4879,7 @@ This module implements support for XEP\-0199: XMPP Ping and periodic keepalives\ \fBping_ack_timeout\fR: \fItimeout()\fR .RS 4 How long to wait before deeming that a client has not answered a given server ping request\&. The default value is -\fI32\fR -seconds\&. +\fIundefined\fR\&. .RE .PP \fBping_interval\fR: \fItimeout()\fR @@ -4531,7 +4911,7 @@ means destroying the underlying connection, \fInone\fR means to do nothing\&. NOTE: when \fImod_stream_mgmt\fR -module is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is +is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is \fInone\fR\&. .RE .RE @@ -4936,7 +5316,9 @@ A port number to listen for incoming connections\&. The default value is .PP \fBram_db_type\fR: \fImnesia | redis | sql\fR .RS 4 -Define the type of volatile (in\-memory) storage where the module will store room information\&. +Same as top\-level +\fIdefault_ram_db\fR +option, but applied to this module only\&. .RE .PP \fBrecbuf\fR: \fISize\fR @@ -5135,11 +5517,19 @@ To specify whether or not pubsub should cache last items\&. Value is or \fIfalse\fR\&. If not defined, pubsub does not cache last items\&. On systems with not so many nodes, caching last items speeds up pubsub and allows to raise user connection rate\&. The cost is memory usage, as every item is stored in memory\&. .RE +.sp +\fINote\fR about the next option: added in 21\&.12: .PP -\fBmax_items_node\fR: \fIMaxItems\fR +\fBmax_item_expire_node\fR: \fItimeout() | infinity\fR +.RS 4 +Specify the maximum item epiry time\&. Default value is: +\fIinfinity\fR\&. +.RE +.PP +\fBmax_items_node\fR: \fInon_neg_integer() | infinity\fR .RS 4 Define the maximum number of items that can be stored in a node\&. Default value is: -\fI10\fR\&. +\fI1000\fR\&. .RE .PP \fBmax_nodes_discoitems\fR: \fIpos_integer() | infinity\fR @@ -5259,7 +5649,7 @@ plugin handles the default behaviour and follows standard XEP\-0060 implementati .IP \(bu 2.3 .\} \fIpep\fR -plugin adds extention to handle Personal Eventing Protocol (XEP\-0163) to the PubSub engine\&. Adding pep allows to handle PEP automatically\&. +plugin adds extension to handle Personal Eventing Protocol (XEP\-0163) to the PubSub engine\&. Adding pep allows to handle PEP automatically\&. .RE .RE .PP @@ -5437,9 +5827,9 @@ The module depends on \fImod_push\fR\&. .PP \fBresume_timeout\fR: \fItimeout()\fR .RS 4 -This option specifies the period of time until the session of a disconnected push client times out\&. This timeout is only in effect as long as no push notification is issued\&. Once that happened, the resumption timeout configured for the +This option specifies the period of time until the session of a disconnected push client times out\&. This timeout is only in effect as long as no push notification is issued\&. Once that happened, the resumption timeout configured for \fImod_stream_mgmt\fR -module is restored\&. The default value is +is restored\&. The default value is \fI72\fR hours\&. .RE @@ -5499,7 +5889,7 @@ Change the password from an existing account on the server\&. Delete an existing account on the server\&. .RE .sp -This module reads also another option defined globally for the server: \fIregistration_timeout\fR\&. Please check that option documentation in the section with top\-level options\&. +This module reads also the top\-level \fIregistration_timeout\fR option defined globally for the server, so please check that option documentation too\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -5527,12 +5917,21 @@ doesn\(cqt allow to register new accounts from s2s or existing c2s sessions\&. Y .RS 4 Specify rules to restrict access for user unregistration\&. By default any user is able to unregister their account\&. .RE +.sp +\fINote\fR about the next option: added in 21\&.12: +.PP +\fBallow_modules\fR: \fIall | [Module, \&.\&.\&.]\fR +.RS 4 +List of modules that can register accounts, or +\fIall\fR\&. The default value is +\fIall\fR, which is equivalent to something like +\fI[mod_register, mod_register_web]\fR\&. +.RE .PP \fBcaptcha_protected\fR: \fItrue | false\fR .RS 4 -Protect registrations with CAPTCHA (see section -CAPTCHA -of the Configuration Guide)\&. The default is +Protect registrations with +CAPTCHA\&. The default is \fIfalse\fR\&. .RE .PP @@ -5551,7 +5950,8 @@ This option sets the minimum Shannon entropy for passwords\&. The value \fIEntropy\fR -is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is 0, i\&.e\&. no checks are performed\&. +is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is +\fI0\fR, i\&.e\&. no checks are performed\&. .RE .PP \fBredirect_url\fR: \fIURL\fR @@ -5610,13 +6010,40 @@ Change the password from an existing account on the server\&. Unregister an existing account on the server\&. .RE .sp -This module supports CAPTCHA image to register a new account\&. To enable this feature, configure the options \fIcaptcha_cmd\fR and \fIcaptcha_url\fR, which are documented in the section with top\-level options\&. +This module supports CAPTCHA to register a new account\&. To enable this feature, configure the top\-level \fIcaptcha_cmd\fR and top\-level \fIcaptcha_url\fR options\&. .sp -As an example usage, the users of the host \fIexample\&.org\fR can visit the page: \fIhttps://example\&.org:5281/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&. +As an example usage, the users of the host \fIlocalhost\fR can visit the page: \fIhttps://localhost:5280/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&. .sp -The module depends on \fImod_register\fR where all the configuration is performed\&. +This module is enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers, no need to enable in \fImodules\fR\&. The module depends on \fImod_register\fR where all the configuration is performed\&. .sp The module has no options\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExample:\fR +.RS 4 +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /register: mod_register_web + +modules: + mod_register: {} +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_roster" .sp This module implements roster management as defined in RFC6121 Section 2\&. The module also adds support for XEP\-0237: Roster Versioning\&. @@ -5910,12 +6337,9 @@ option, but applied to this module only\&. .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 -Define the type of storage where the module will create the tables and store user information\&. The default is the storage defined by the global option -\fIdefault_db\fR, or -\fImnesia\fR -if omitted\&. If -\fIsql\fR -value is defined, make sure you have defined the database\&. +Same as top\-level +\fIdefault_db\fR +option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR @@ -6521,7 +6945,8 @@ minute\&. .RS 4 Same as top\-level \fIcache_life_time\fR -option, but applied to this module only\&. +option, but applied to this module only\&. The default value is +\fI48 hours\fR\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR @@ -6594,8 +7019,7 @@ This option defines which access rule will be used to control who is allowed to \fBcredentials_lifetime\fR: \fItimeout()\fR .RS 4 The lifetime of temporary credentials offered to clients\&. If ejabberd\(cqs built\-in TURN service is used, TURN relays allocated using temporary credentials will be terminated shortly after the credentials expired\&. The default value is -\fI12\fR -hours\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a +\fI12 hours\fR\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a \fIsecret\fR is specified (see below)\&. .RE @@ -7121,7 +7545,7 @@ The module depends on \fImod_vcard\fR\&. .ps -1 .br .sp -Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR module for providing backward compatibility\&. +Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR for providing backward compatibility\&. .sp .5v .RE .sp @@ -7189,13 +7613,13 @@ TODO ProcessOne\&. .SH "VERSION" .sp -This document describes the configuration file of ejabberd 21\&.04\&.131\&. Configuration options of other ejabberd versions may differ significantly\&. +This document describes the configuration file of ejabberd 22\&.10\&. Configuration options of other ejabberd versions may differ significantly\&. .SH "REPORTING BUGS" .sp Report bugs to https://github\&.com/processone/ejabberd/issues .SH "SEE ALSO" .sp -Default configuration file: https://github\&.com/processone/ejabberd/blob/21\&.07/ejabberd\&.yml\&.example +Default configuration file: https://github\&.com/processone/ejabberd/blob/22\&.10/ejabberd\&.yml\&.example .sp Main site: https://ejabberd\&.im .sp @@ -7206,4 +7630,4 @@ Configuration Guide: https://docs\&.ejabberd\&.im/admin/configuration Source code: https://github\&.com/processone/ejabberd .SH "COPYING" .sp -Copyright (c) 2002\-2021 ProcessOne\&. +Copyright (c) 2002\-2022 ProcessOne\&. diff --git a/mix.exs b/mix.exs index c33710b45..93e9a7d81 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Ejabberd.MixProject do [app: :ejabberd, version: version(), description: description(), - elixir: "~> 1.4", + elixir: elixir_required_version(), elixirc_paths: ["lib"], compile_path: ".", compilers: [:asn1] ++ Mix.compilers, @@ -24,10 +24,11 @@ defmodule Ejabberd.MixProject do case config(:vsn) do :false -> "0.0.0" # ./configure wasn't run: vars.config not created '0.0' -> "0.0.0" # the full git repository wasn't downloaded + 'latest.0' -> "0.0.0" # running 'docker-ejabberd/ecs/build.sh latest' [_, _, ?., _, _] = x -> - head = String.replace(:erlang.list_to_binary(x), ~r/0+([0-9])/, "\\1") + head = String.replace(:erlang.list_to_binary(x), ~r/\.0+([0-9])/, ".\\1") <> - vsn -> String.replace(:erlang.list_to_binary(vsn), ~r/0+([0-9])/, "\\1") + vsn -> String.replace(:erlang.list_to_binary(vsn), ~r/\.0+([0-9])/, ".\\1") end end @@ -42,22 +43,13 @@ defmodule Ejabberd.MixProject do extra_applications: [:mix], applications: [:idna, :inets, :kernel, :sasl, :ssl, :stdlib, :base64url, :fast_tls, :fast_xml, :fast_yaml, :jiffy, :jose, - :p1_utils, :stringprep, :stun, :yconf], - included_applications: [:lager, :mnesia, :os_mon, - :cache_tab, :eimp, :esip, :mqtree, :p1_acme, + :p1_utils, :stringprep, :syntax_tools, :yconf], + included_applications: [:mnesia, :os_mon, + :cache_tab, :eimp, :mqtree, :p1_acme, :p1_oauth2, :pkix, :xmpp] ++ cond_apps()] end - defp if_function_exported(mod, fun, arity, okResult) do - :code.ensure_loaded(mod) - if :erlang.function_exported(mod, fun, arity) do - okResult - else - [] - end - end - defp if_version_above(ver, okResult) do if :erlang.system_info(:otp_release) > ver do okResult @@ -77,17 +69,19 @@ defmodule Ejabberd.MixProject do defp erlc_options do # Use our own includes + includes from all dependencies includes = ["include"] ++ deps_include(["fast_xml", "xmpp", "p1_utils"]) - result = [:debug_info, {:d, :ELIXIR_ENABLED}] ++ + result = [{:d, :ELIXIR_ENABLED}] ++ cond_options() ++ Enum.map(includes, fn (path) -> {:i, path} end) ++ if_version_above('20', [{:d, :DEPRECATED_GET_STACKTRACE}]) ++ + if_version_above('20', [{:d, :HAVE_URI_STRING}]) ++ + if_version_above('20', [{:d, :HAVE_ERL_ERROR}]) ++ if_version_below('21', [{:d, :USE_OLD_HTTP_URI}]) ++ if_version_below('22', [{:d, :LAGER}]) ++ + if_version_below('21', [{:d, :NO_CUSTOMIZE_HOSTNAME_CHECK}]) ++ if_version_below('23', [{:d, :USE_OLD_CRYPTO_HMAC}]) ++ if_version_below('23', [{:d, :USE_OLD_PG2}]) ++ if_version_below('24', [{:d, :COMPILER_REPORTS_ONLY_LINES}]) ++ - if_version_below('24', [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) ++ - if_function_exported(:erl_error, :format_exception, 6, [{:d, :HAVE_ERL_ERROR}]) + if_version_below('24', [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) defines = for {:d, value} <- result, do: {:d, value} result ++ [{:d, :ALL_DEFS, defines}] end @@ -95,36 +89,32 @@ defmodule Ejabberd.MixProject do defp cond_options do for {:true, option} <- [{config(:sip), {:d, :SIP}}, {config(:stun), {:d, :STUN}}, - {config(:roster_gateway_workaround), {:d, :ROSTER_GATWAY_WORKAROUND}}, + {config(:debug), :debug_info}, + {not config(:debug), {:debug_info, false}}, + {config(:roster_gateway_workaround), {:d, :ROSTER_GATEWAY_WORKAROUND}}, {config(:new_sql_schema), {:d, :NEW_SQL_SCHEMA}} ], do: option end defp deps do - [{:base64url, "~> 0.0.1"}, + [{:base64url, "~> 1.0"}, {:cache_tab, "~> 1.0"}, - {:distillery, "~> 2.0"}, {:eimp, "~> 1.0"}, - {:esip, "~> 1.0"}, {:ex_doc, ">= 0.0.0", only: :dev}, {:fast_tls, "~> 1.1"}, {:fast_xml, "~> 1.1"}, {:fast_yaml, "~> 1.0"}, {:idna, "~> 6.0"}, - {:jiffy, "~> 1.0.5"}, - {:jose, "~> 1.8"}, - {:lager, "~> 3.9.1"}, + {:jiffy, "~> 1.1.1"}, + {:jose, "~> 1.11.1"}, {:mqtree, "~> 1.0"}, {:p1_acme, "~> 1.0"}, - {:p1_mysql, "~> 1.0"}, {:p1_oauth2, "~> 0.6"}, - {:p1_pgsql, "~> 1.1"}, {:p1_utils, "~> 1.0"}, {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, - {:stun, "~> 1.0"}, - {:xmpp, "~> 1.5"}, + {:xmpp, ">= 1.6.0"}, {:yconf, "~> 1.0"}] ++ cond_deps() end @@ -144,15 +134,24 @@ defmodule Ejabberd.MixProject do defp cond_deps do for {:true, dep} <- [{config(:pam), {:epam, "~> 1.0"}}, {config(:redis), {:eredis, "~> 1.2.0"}}, + {config(:sip), {:esip, "~> 1.0"}}, {config(:zlib), {:ezlib, "~> 1.0"}}, - {config(:lua), {:luerl, "~> 0.3.1"}}, - {config(:sqlite), {:sqlite3, "~> 1.1"}}], do: + {if_version_below('22', true), {:lager, "~> 3.9.1"}}, + {config(:lua), {:luerl, "~> 1.0"}}, + {config(:mysql), {:p1_mysql, "~> 1.0.20"}}, + {config(:pgsql), {:p1_pgsql, "~> 1.1"}}, + {config(:sqlite), {:sqlite3, "~> 1.1"}}, + {config(:stun), {:stun, "~> 1.0"}}], do: dep end defp cond_apps do - for {:true, app} <- [{config(:redis), :eredis}, + for {:true, app} <- [{config(:pam), :epam}, + {config(:lua), :luerl}, + {config(:redis), :eredis}, + {if_version_below('22', true), :lager}, {config(:mysql), :p1_mysql}, + {config(:sip), :esip}, {config(:odbc), :odbc}, {config(:pgsql), :p1_pgsql}, {config(:sqlite), :sqlite3}], do: @@ -165,7 +164,7 @@ defmodule Ejabberd.MixProject do "COPYING", "README.md", "mix.exs", "rebar.config", "rebar.config.script", "vars.config"], maintainers: ["ProcessOne"], - licenses: ["GPLv2"], + licenses: ["GPL-2.0-or-later"], links: %{"Site" => "https://www.ejabberd.im", "Documentation" => "http://docs.ejabberd.im", "Source" => "https://github.com/processone/ejabberd", @@ -186,6 +185,35 @@ defmodule Ejabberd.MixProject do end end + defp elixir_required_version do + case {Map.get(System.get_env(), "RELIVE", "false"), + MapSet.member?(MapSet.new(System.argv()), "release")} + do + {"true", _} -> + case Version.match?(System.version(), "~> 1.11") do + false -> + IO.puts("ERROR: To use 'make relive', Elixir 1.11.0 or higher is required.") + _ -> :ok + end + "~> 1.11" + {_, true} -> + case Version.match?(System.version(), "~> 1.10") do + false -> + IO.puts("ERROR: To build releases, Elixir 1.10.0 or higher is required.") + _ -> :ok + end + case Version.match?(System.version(), "< 1.11.4") + and :erlang.system_info(:otp_release) > '23' do + true -> + IO.puts("ERROR: To build releases with Elixir lower than 1.11.4, Erlang/OTP lower than 24 is required.") + _ -> :ok + end + "~> 1.10" + _ -> + "~> 1.4" + end + end + defp releases do maybe_tar = case Mix.env() do :prod -> [:tar] @@ -208,11 +236,14 @@ defmodule Ejabberd.MixProject do libdir: config(:libdir), sysconfdir: config(:sysconfdir), localstatedir: config(:localstatedir), - docdir: config(:docdir), + config_dir: config(:config_dir), + logs_dir: config(:logs_dir), + spool_dir: config(:spool_dir), erl: config(:erl), epmd: config(:epmd), bindir: Path.join([config(:release_dir), "releases", version()]), release_dir: config(:release_dir), + erts_dir: config(:erts_dir), erts_vsn: "erts-#{release.erts_version}" ] ro = "rel/overlays" @@ -229,7 +260,7 @@ defmodule Ejabberd.MixProject do end # Mix/Elixir lower than 1.11.0 use config/releases.exs instead of runtime.exs - case Version.match?(System.version, ">= 1.11.0") do + case Version.match?(System.version, "~> 1.11") do true -> :ok false -> @@ -238,37 +269,55 @@ defmodule Ejabberd.MixProject do execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.template > ejabberdctl.example1") Mix.Generator.copy_template("ejabberdctl.example1", "ejabberdctl.example2", assigns) - execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2 > ejabberdctl.example3") - execute.("sed -e 's|ERLANG_NODE=ejabberd@localhost|ERLANG_NODE=ejabberd|g' ejabberdctl.example3 > ejabberdctl.example4") - execute.("sed -e 's|INSTALLUSER=|ERL_OPTIONS=\"-setcookie \\$\\(cat \"\\${SCRIPT_DIR%/*}/releases/COOKIE\")\"\\nINSTALLUSER=|g' ejabberdctl.example4 > ejabberdctl.example5") - Mix.Generator.copy_template("ejabberdctl.example5", "#{ro}/bin/ejabberdctl", assigns) + execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2> ejabberdctl.example2a") + Mix.Generator.copy_template("ejabberdctl.example2a", "ejabberdctl.example2b", assigns) + execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2b > ejabberdctl.example3") + execute.("sed -e 's|^ERLANG_NODE=ejabberd@localhost|ERLANG_NODE=ejabberd|g' ejabberdctl.example3 > ejabberdctl.example4") + execute.("sed -e 's|^ERLANG_OPTS=\"|ERLANG_OPTS=\"-boot ../releases/#{release.version}/start_clean -boot_var RELEASE_LIB ../lib |' ejabberdctl.example4 > ejabberdctl.example5") + execute.("sed -e 's|^INSTALLUSER=|ERL_OPTIONS=\"-setcookie \\$\\(cat \"\\${SCRIPT_DIR%/*}/releases/COOKIE\")\"\\nINSTALLUSER=|g' ejabberdctl.example5 > ejabberdctl.example6") + Mix.Generator.copy_template("ejabberdctl.example6", "#{ro}/bin/ejabberdctl", assigns) File.chmod("#{ro}/bin/ejabberdctl", 0o755) File.rm("ejabberdctl.example1") File.rm("ejabberdctl.example2") + File.rm("ejabberdctl.example2a") + File.rm("ejabberdctl.example2b") File.rm("ejabberdctl.example3") File.rm("ejabberdctl.example4") File.rm("ejabberdctl.example5") + File.rm("ejabberdctl.example6") suffix = case Mix.env() do :dev -> - Mix.Generator.copy_file("test/ejabberd_SUITE_data/ca.pem", "#{ro}/etc/ejabberd/ca.pem") - Mix.Generator.copy_file("test/ejabberd_SUITE_data/cert.pem", "#{ro}/etc/ejabberd/cert.pem") + Mix.Generator.copy_file("test/ejabberd_SUITE_data/ca.pem", "#{ro}/conf/ca.pem") + Mix.Generator.copy_file("test/ejabberd_SUITE_data/cert.pem", "#{ro}/conf/cert.pem") ".example" _ -> "" end - Mix.Generator.copy_file("ejabberd.yml.example", "#{ro}/etc/ejabberd/ejabberd.yml#{suffix}") - Mix.Generator.copy_file("ejabberdctl.cfg.example", "#{ro}/etc/ejabberd/ejabberdctl.cfg#{suffix}") - Mix.Generator.copy_file("inetrc", "#{ro}/etc/ejabberd/inetrc") - Mix.Generator.copy_template("rel/vm.args.mix", "#{ro}/etc/ejabberd/vm.args", assigns) + Mix.Generator.copy_file("ejabberd.yml.example", "#{ro}/conf/ejabberd.yml#{suffix}") + Mix.Generator.copy_file("ejabberdctl.cfg.example", "#{ro}/conf/ejabberdctl.cfg#{suffix}") + Mix.Generator.copy_file("inetrc", "#{ro}/conf/inetrc") Enum.each(File.ls!("sql"), fn x -> Mix.Generator.copy_file("sql/#{x}", "#{ro}/lib/ejabberd-#{release.version}/priv/sql/#{x}") end) - Mix.Generator.create_directory("#{ro}/var/lib/ejabberd") + File.cp_r!("include", "#{ro}/lib/ejabberd-#{release.version}/include") + for {name, details} <- Map.to_list(release.applications) do + {_, is_otp_app} = List.keyfind(details, :otp_app?, 0) + {_, vsn} = List.keyfind(details, :vsn, 0) + {_, path} = List.keyfind(details, :path, 0) + source_dir = case is_otp_app do + :true -> "#{path}/include" + :false -> "deps/#{name}/include" + end + target_dir = "#{ro}/lib/#{name}-#{vsn}/include" + File.exists?(source_dir) + && File.mkdir_p(target_dir) + && File.cp_r!(source_dir, target_dir) + end case Mix.env() do :dev -> execute.("REL_DIR_TEMP=$PWD/rel/overlays/ rel/setup-dev.sh") diff --git a/mix.lock b/mix.lock index a5b158e0f..eb097dc7d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,36 +1,35 @@ %{ - "artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"}, - "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, - "cache_tab": {:hex, :cache_tab, "1.0.29", "6c161988620b788d8df28c8f6af557571609c8e4b671dbadab295a4722cd501b", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a02a638021cce91ed1a8628dcbb4795bf5c01c9d11db8c613065923142824ce9"}, - "distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, - "eimp": {:hex, :eimp, "1.0.21", "2e918a5dc9a1959ef8713a2360499e3baeee64cfd7881bd9d1f361ca9ddf07e8", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "998f58538f58aa0cff103414994d7ce56dc253e6576cd6fb40c1ead64aa73a28"}, + "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"}, + "cache_tab": {:hex, :cache_tab, "1.0.30", "6d35eecfb65fbe5fc85988503a27338d32de01243f3fc8ea3ee7161af08725a4", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "6d8a5e00d8f84c42627706a6dbedb02e34d58495f3ed61935c8475ca0531cda0"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, + "eimp": {:hex, :eimp, "1.0.22", "fa9b376ef0b50e8455db15c7c11dea4522c6902e04412288aab436d26335f6eb", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "b3b9ffb1d9a5f4a2ba88ac418a819164932d9a9d3a2fc3d32ca338ce855c4392"}, "epam": {:hex, :epam, "1.0.12", "2a5625d4133bca4b3943791a3f723ba764455a461ae9b6ba5debb262efcf4b40", [:rebar3], [], "hexpm", "54c166c4459cef72f2990a3d89a8f0be27180fe0ab0f24b28ddcc3b815f49f7f"}, - "esip": {:hex, :esip, "1.0.43", "1cbdc073073f80b9b50e2759f66ca13a353eb4f874bcf92501bd4cd767e34d46", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.44", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "b2c758ae52c4588e0399c0b4ce550bfa56551a5a2f828a28389f2614797e4f4b"}, - "ex_doc": {:hex, :ex_doc, "0.25.0", "4070a254664ee5495c2f7cce87c2f43064a8752f7976f2de4937b65871b05223", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2d90883bd4f3d826af0bde7fea733a4c20adba1c79158e2330f7465821c8949b"}, - "ezlib": {:hex, :ezlib, "1.0.10", "c1c24eb18944cfde55f0574e9922d5b0392fa864282f769f82b2ea15e54f6003", [:rebar3], [], "hexpm", "1d317f1d85373686199eb3b4164d3477e95033ac68e45a95ba18e7b7a8c23241"}, - "fast_tls": {:hex, :fast_tls, "1.1.13", "828cdc75e1e8fce8158846d2b971d8b4fe2b2ddcc75b759e88d751079bf78afd", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "d1f422af40c7777fe534496f508ee86515cb929ad10f7d1d56aa94ce899b44a0"}, - "fast_xml": {:hex, :fast_xml, "1.1.47", "bd1d6c081b69c7bce0d2f22b013c1b864ed2588d48f34e2156d9428f8f772c66", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "dd014c45247498effb9a28cf98cb716db79be635ad1e98c951240763119f24c7"}, - "fast_yaml": {:hex, :fast_yaml, "1.0.32", "43f53a2c8572f2e4d66cd4e787fc6761b1c65b9132e42c511d8b9540b0989d65", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "7258e322739ff0824237ebe44cd158e0bf52cd27a15fe731cf92f4b4c70b913e"}, - "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm", "99cb4128cffcb3227581e5d4d803d5413fa643f4eb96523f77d9e6937d994ceb"}, + "eredis": {:hex, :eredis, "1.2.0", "0b8e9cfc2c00fa1374cd107ea63b49be08d933df2cf175e6a89b73dd9c380de4", [:rebar3], [], "hexpm", "d9b5abef2c2c8aba8f32aa018203e0b3dc8b1157773b254ab1d4c2002317f1e1"}, + "esip": {:hex, :esip, "1.0.48", "3b3b3afc798be9458517d4fd2730674322368e54c2c1211aa630327354946d1b", [:rebar3], [{:fast_tls, "1.1.16", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.2.6", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "02b9fc6e071415cbc62105f5115aeb68d11184bdad3960da7b62ea3e99e7fccf"}, + "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "ezlib": {:hex, :ezlib, "1.0.12", "ffe906ba10d03aaee7977e1e0e81d9ffc3bb8b47fb9cd8e2e453507a2e56221f", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "30e94355fb42260aab6e12582cb0c56bf233515e655c8aeaf48760e7561e4ebb"}, + "fast_tls": {:hex, :fast_tls, "1.1.16", "85fa7f3112ea4ff5ccb4f3abadc130a8c855ad74eb00869487399cb0c322d208", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "aa08cca89b4044e74f1f12e399817d8beaeae3ee006c98a893c0bfb1d81fba51"}, + "fast_xml": {:hex, :fast_xml, "1.1.49", "67d9bfcadd04efd930e0ee1412b5ea09d3e791f1fdbd4d3e9a8c8f29f8bfed8c", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "01da064d2f740818956961036637fee2475c17bf8aab9442217f90dc77883593"}, + "fast_yaml": {:hex, :fast_yaml, "1.0.34", "3be1ed8a37fe87a53f7f2ad1ee9586dcc257103d0b1d1f0ee6306cad9d54c29a", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "926dc1798399418d3983bd53356f3395b01c07a550f6b8d1dd5d6cc07c22c1c9"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, - "jiffy": {:hex, :jiffy, "1.0.5", "a69b58faf7123534c20e1b0b7ae97ac52079ca02ed4b6989b4b380179cd63a54", [:rebar3], [], "hexpm", "b617a53f46ae84f20d0c38951367dc947a2cf8cff922aa5c6ac6b64b8b052289"}, - "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm", "6429c4fee52b2dda7861ee19a4f09c8c1ffa213bee3a1ec187828fde95d447ed"}, - "lager": {:hex, :lager, "3.9.2", "4cab289120eb24964e3886bd22323cb5fefe4510c076992a23ad18cf85413d8c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "7f904d9e87a8cb7e66156ed31768d1c8e26eba1d54f4bc85b1aa4ac1f6340c28"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, + "jiffy": {:hex, :jiffy, "1.1.1", "aca10f47aa91697bf24ab9582c74e00e8e95474c7ef9f76d4f1a338d0f5de21b", [:rebar3], [], "hexpm", "62e1f0581c3c19c33a725c781dfa88410d8bff1bbafc3885a2552286b4785c4c"}, + "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, + "luerl": {:hex, :luerl, "1.0.0", "1b68c30649323590d5339b967b419260500ffe520cd3abc1987482a82d3b5a6c", [:rebar3], [], "hexpm", "c17bc45cb4b0845ec975387f9a5d8c81ab60456698527a29c96f78992af86bd1"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "mqtree": {:hex, :mqtree, "1.0.14", "d201a79b51a9232b80e764b4b77a866f7c30a90c7ac6205d71f391eb3ea7eb31", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8626dac5e862b575eaf4836f0fc1be5a7c8435c378c5a309e34ee012d48b6f6e"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, - "p1_acme": {:hex, :p1_acme, "1.0.13", "fec71df416004ce49e295f4846fe5ba3478b41fbe4f73a06b4a8fbc967d6e659", [:rebar3], [{:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.0.5", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.9.0", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.12", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "a2ce9d4904304df020c8e92e8577e0fc88f32623540656317c7e25440b4ac8d2"}, - "p1_mysql": {:hex, :p1_mysql, "1.0.19", "22f1be58397780a7d580a954e7af66cde32a29dee1a24ab2aa196272fc654a4a", [:rebar3], [], "hexpm", "88f6cdb510e8959c14b6ae84ccda04967e3de239228f859d8341da67949622b1"}, - "p1_oauth2": {:hex, :p1_oauth2, "0.6.10", "09ba1fbd447b1f480b223903e36d0415f21be592a1b00db964eea01285749028", [:rebar3], [], "hexpm", "c79cb61ababee4a8c85409b7f4932035797c093aeef1f9f53985e512b26f2a64"}, - "p1_pgsql": {:hex, :p1_pgsql, "1.1.12", "10ae79eeb35ea98c0424a8b6420542fef9e4469eb12ccf41475d10840c291e68", [:rebar3], [], "hexpm", "32203f779e01cf0353270df24833a1d831ad7cb3e3e8e35a7556dfa1f40948d5"}, - "p1_utils": {:hex, :p1_utils, "1.0.23", "7f94466ada69bd982ea7bb80fbca18e7053e7d0b82c9d9e37621fa508587069b", [:rebar3], [], "hexpm", "47f21618694eeee5006af1c88731ad86b757161e7823c29b6f73921b571c8502"}, - "pkix": {:hex, :pkix, "1.0.8", "98ea05243847fd4504f7c7a0cd82cecd1010ac327a082e1c674c5384006eae75", [:rebar3], [], "hexpm", "399508819501fab9d2e586dfa601b5ee3ef22b5612d3db58204dd2d089ef45d7"}, - "stringprep": {:hex, :stringprep, "1.0.27", "02808c7024bc6285ca6a8a67e7addfc16f35dda55551a582c5181d8ea960e890", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a5967b1144ca8002a58a03d16dd109fbd0bcdb82616cead2f983944314af6a00"}, - "stun": {:hex, :stun, "1.0.44", "30b6b774864b24b05ba901291abe583bff19081e7c4efb3361df50b781ec9d3b", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "e45bba816cbefff01d820e49e66814f450df25a7a468a70d68d1e64218d46520"}, + "mqtree": {:hex, :mqtree, "1.0.15", "bc54d8b88698fdaebc1e27a9ac43688b927e3dbc05bd5cee4057e69a89a8cf17", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "294ac43c9b3d372e24eeea56c259e19c655522dcff64a55c401a639663b9d829"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "p1_acme": {:hex, :p1_acme, "1.0.20", "c976cbca2dd1bdcf71a6e17fb512e30451b5f258694157f7b63963767ee26560", [:rebar3], [{:base64url, "1.0.1", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.1.1", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.11.1", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.14", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "70e0ecf8c8729dfc01f6a15279ef9fa4003c3b5af47b6732d9312296a8ba4f5c"}, + "p1_mysql": {:hex, :p1_mysql, "1.0.20", "08aeade83a24902a5fca2dbf78fa674eef25ca4e66250b4be8bd3580f35880e7", [:rebar3], [], "hexpm", "12152e8feadcf8ce586334314ca27cb088f12e0a5c850c496a8df69859390877"}, + "p1_oauth2": {:hex, :p1_oauth2, "0.6.11", "96b4e85c08355720523c2f892011a81a07994d15c179ce4dd82d704fecad15b2", [:rebar3], [], "hexpm", "9c3c6ae59382b9525473bb02a32949889808f33f95f6db10594fd92acd1f63db"}, + "p1_pgsql": {:hex, :p1_pgsql, "1.1.19", "dc615844fd22a2e45182018d5bcc6b757ac19f576fab3fe6d69e1c0ff25cee2b", [:rebar3], [{:xmpp, "1.6.0", [hex: :xmpp, repo: "hexpm", optional: false]}], "hexpm", "4b0c6d30bbf881feb01171d13f444a6e05e1d19b6926e3f56f4028823d02730b"}, + "p1_utils": {:hex, :p1_utils, "1.0.25", "2d39b5015a567bbd2cc7033eeb93a7c60d8c84efe1ef69a3473faa07fa268187", [:rebar3], [], "hexpm", "9219214428f2c6e5d3187ff8eb9a8783695c2427420be9a259840e07ada32847"}, + "pkix": {:hex, :pkix, "1.0.9", "eb20b2715d71a23b4fe7e754dae9281a964b51113d0bba8adf9da72bf9d65ac2", [:rebar3], [], "hexpm", "daab2c09cdd4eda05c9b45a5c00e994a1a5f27634929e1377e2e59b707103e3a"}, + "sqlite3": {:hex, :sqlite3, "1.1.13", "94a6e0508936514e1493efeb9b939a9bbfa861f4b8dc93ef174ae88a1d9381d3", [:rebar3], [], "hexpm", "b77fad096d1ae9553ad8551ea75bd0d64a2f5b09923a7ca48b14215564dbfc48"}, + "stringprep": {:hex, :stringprep, "1.0.29", "02f23e8c3a219a3dfe40a22e908bece3a2f68af0ff599ea8a7b714ecb21e62ee", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "928eba304c3006eb1512110ebd7b87db163b00859a09375a1e4466152c6c462a"}, + "stun": {:hex, :stun, "1.2.6", "5d1978d340ea20efb28bc1e58779a3a1d64568c66168db4d20692e76ce813d5e", [:rebar3], [{:fast_tls, "1.1.16", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "21aed098457e5099e925129459590592e001c470cf7503e5614a7a6b688ff146"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, - "xmpp": {:hex, :xmpp, "1.5.4", "6cd8144b3fe04745dc2cb3e746d6f2a963bb283db48a61f159b49cbe3fab8623", [:rebar3], [{:ezlib, "1.0.10", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.47", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.27", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "3bc2b5cb24e52964fb11641422ce2b7ba7c261dd50080689a1cbe3d952a9db35"}, - "yconf": {:hex, :yconf, "1.0.12", "78c119d39bb805207fcb7671cb884805d75ee89c9ec98632b678f90a597dee2c", [:rebar3], [{:fast_yaml, "1.0.32", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "12faa51c281e95bcb6abf185fd034a242209621a7bb04b6cc411c867b192e207"}, + "xmpp": {:hex, :xmpp, "1.6.0", "2ca2180eac1a97e929d1cfa1e4faabef4f32a719331c7f56e47d305c0ec8e438", [:rebar3], [{:ezlib, "1.0.12", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.16", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.49", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.29", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "5fd723c95bce79600a8f44ba79cf5d3b1dac80af65493a4a414e39791f7dd7e9"}, + "yconf": {:hex, :yconf, "1.0.14", "b216f385f729b338385b25176f6e4fe8cabfdf7ede9c40a35b2e77fc93e98fc8", [:rebar3], [{:fast_yaml, "1.0.34", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "a8a9262553c11ed4cd13cc8e656e53acb00f9385f0a50cd235af7d02e9204bce"}, } diff --git a/priv/msgs/ar.msg b/priv/msgs/ar.msg new file mode 100644 index 000000000..b33565af4 --- /dev/null +++ b/priv/msgs/ar.msg @@ -0,0 +1,11 @@ +%% Generated automatically +%% DO NOT EDIT: run `make translations` instead +%% To improve translations please read: +%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ + +{" (Add * to the end of field to match substring)"," (أضف * في نهاية الحقل لمطابقة السلسلة الفرعية)"}. +{" has set the subject to: "," حدد الموضوع إلى: "}. +{"# participants","# المشاركين"}. +{"A description of the node","وصف العقدة"}. +{"A Web Page","موقع الكتروني"}. +{"'Displayed groups' not added (they do not exist!): ","لم تتم إضافة \"المجموعات المعروضة\" (فهي غير موجودة!): "}. diff --git a/priv/msgs/ca.msg b/priv/msgs/ca.msg index 3d9c9d175..4e6c8d864 100644 --- a/priv/msgs/ca.msg +++ b/priv/msgs/ca.msg @@ -20,6 +20,7 @@ {"Access model","Model d'Accés"}. {"Account doesn't exist","El compte no existeix"}. {"Action on user","Acció en l'usuari"}. +{"Add a hat to a user","Afegir un barret a un usuari"}. {"Add Jabber ID","Afegir Jabber ID"}. {"Add New","Afegir nou"}. {"Add User","Afegir usuari"}. @@ -78,6 +79,7 @@ {"Changing role/affiliation is not allowed","No està permès canviar el rol/afiliació"}. {"Channel already exists","El canal ja existeix"}. {"Channel does not exist","El canal no existeix"}. +{"Channel JID","JID del Canal"}. {"Channels","Canals"}. {"Characters not allowed:","Caràcters no permesos:"}. {"Chatroom configuration modified","Configuració de la sala de xat modificada"}. @@ -97,6 +99,7 @@ {"Configuration","Configuració"}. {"Connected Resources:","Recursos connectats:"}. {"Contact Addresses (normally, room owner or owners)","Adreces de contacte (normalment, propietaris de la sala)"}. +{"Contrib Modules","Mòduls Contrib"}. {"Country","Pais"}. {"CPU Time:","Temps de CPU:"}. {"Current Discussion Topic","Assumpte de discussió actual"}. @@ -136,6 +139,7 @@ {"Elements","Elements"}. {"Email Address","Adreça de correu"}. {"Email","Correu"}. +{"Enable hats","Activar barrets"}. {"Enable logging","Habilitar el registre de la conversa"}. {"Enable message archiving","Activar l'emmagatzematge de missatges"}. {"Enabling push without 'node' attribute is not supported","No està suportat activar Push sense l'atribut 'node'"}. @@ -170,6 +174,8 @@ {"Full List of Room Admins","Llista completa de administradors de la sala"}. {"Full List of Room Owners","Llista completa de propietaris de la sala"}. {"Full Name","Nom complet"}. +{"Get List of Online Users","Obté la llista d'usuaris en línia"}. +{"Get List of Registered Users","Obté la llista d'usuaris registrats"}. {"Get Number of Online Users","Obtenir Número d'Usuaris Connectats"}. {"Get Number of Registered Users","Obtenir Número d'Usuaris Registrats"}. {"Get Pending","Obtenir Pendents"}. @@ -186,6 +192,9 @@ {"has been kicked because of an affiliation change","ha sigut expulsat a causa d'un canvi d'afiliació"}. {"has been kicked because the room has been changed to members-only","ha sigut expulsat perquè la sala ara és només per a membres"}. {"has been kicked","ha sigut expulsat"}. +{"Hat title","Títol del barret"}. +{"Hat URI","URI del barret"}. +{"Hats limit exceeded","El límit de tràfic ha sigut sobrepassat"}. {"Host unknown","Host desconegut"}. {"Host","Host"}. {"HTTP File Upload","HTTP File Upload"}. @@ -208,6 +217,8 @@ {"Incorrect value of 'action' attribute","Valor incorrecte del atribut 'action'"}. {"Incorrect value of 'action' in data form","Valor incorrecte de 'action' al formulari de dades"}. {"Incorrect value of 'path' in data form","Valor incorrecte de 'path' al formulari de dades"}. +{"Installed Modules:","Mòduls instal·lats:"}. +{"Install","Instal·lar"}. {"Insufficient privilege","Privilegi insuficient"}. {"Internal server error","Error intern del servidor"}. {"Invalid 'from' attribute in forwarded message","Atribut 'from' invàlid al missatge reenviat"}. @@ -224,6 +235,8 @@ {"January","Gener"}. {"JID normalization denied by service policy","S'ha denegat la normalització del JID per política del servei"}. {"JID normalization failed","Ha fallat la normalització del JID"}. +{"Joined MIX channels of ~ts","Canals MIX units de ~ts"}. +{"Joined MIX channels:","Canals MIX units:"}. {"joins the room","entra a la sala"}. {"July","Juliol"}. {"June","Juny"}. @@ -237,6 +250,8 @@ {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Els bits menys significants del hash SHA-256 del text deurien ser iguals a l'etiqueta hexadecimal"}. {"leaves the room","surt de la sala"}. {"List of rooms","Llista de sales"}. +{"List of users with hats","Llista d'usuaris amb barrets"}. +{"List users with hats","Llista d'usuaris amb barrets"}. {"Logging","Registre"}. {"Low level update script","Script d'actualització de baix nivell"}. {"Make participants list public","Crear una llista de participants pública"}. @@ -249,7 +264,7 @@ {"Malformed username","Nom d'usuari mal format"}. {"MAM preference modification denied by service policy","Se t'ha denegat la modificació de la preferència de MAM per política del servei"}. {"March","Març"}. -{"Max # of items to persist","Màxim # d'elements que persistixen"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Màxim # d'elements a persistir, o `max` per a no tindre altre límit més que el màxim imposat pel servidor"}. {"Max payload size in bytes","Màxim tamany del payload en bytes"}. {"Maximum file size","Mida màxima de fitxer"}. {"Maximum Number of History Messages Returned by Room","Numero màxim de missatges de l'historia que retorna la sala"}. @@ -324,6 +339,7 @@ {"Node index not found","Index de node no trobat"}. {"Node not found","Node no trobat"}. {"Node ~p","Node ~p"}. +{"Node","Node"}. {"Nodeprep has failed","Ha fallat Nodeprep"}. {"Nodes","Nodes"}. {"None","Cap"}. @@ -339,8 +355,9 @@ {"Number of Offline Messages","Número de missatges offline"}. {"Number of online users","Número d'usuaris connectats"}. {"Number of registered users","Número d'Usuaris Registrats"}. -{"Number of seconds after which to automatically purge items","Número de segons després dels quals es purgaran automàticament elements"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Número de segons després dels quals es purgaran automàticament elements, o `max` per a no tindre altre límit més que el màxim imposat pel servidor"}. {"Occupants are allowed to invite others","Els ocupants poden invitar a altres"}. +{"Occupants are allowed to query others","Els ocupants poden enviar peticions a altres"}. {"Occupants May Change the Subject","Els ocupants poden canviar el Tema"}. {"October","Octubre"}. {"Offline Messages:","Missatges fora de línia:"}. @@ -367,11 +384,13 @@ {"Only those on a whitelist may subscribe and retrieve items","Només qui estiga a una llista blanca pot subscriure's i recuperar elements"}. {"Organization Name","Nom de la organizació"}. {"Organization Unit","Unitat de la organizació"}. +{"Other Modules Available:","Altres mòduls disponibles:"}. {"Outgoing s2s Connections:","Connexions d'eixida s2s:"}. {"Outgoing s2s Connections","Connexions s2s d'eixida"}. {"Owner privileges required","Es requerixen privilegis de propietari de la sala"}. {"Packet relay is denied by service policy","S'ha denegat el reenviament del paquet per política del servei"}. {"Packet","Paquet"}. +{"Participant ID","ID del Participant"}. {"Participant","Participant"}. {"Password Verification","Verificació de la Contrasenya"}. {"Password Verification:","Verificació de la Contrasenya:"}. @@ -418,6 +437,7 @@ {"Registered Users:","Usuaris registrats:"}. {"Register","Registrar"}. {"Remote copy","Còpia remota"}. +{"Remove a hat from a user","Eliminar un barret d'un usuari"}. {"Remove All Offline Messages","Eliminar tots els missatges offline"}. {"Remove User","Eliminar usuari"}. {"Remove","Borrar"}. @@ -473,6 +493,7 @@ {"Shut Down Service","Apager el Servei"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clients XMPP poden emmagatzemar la teva contrasenya al ordinador, però només hauries de fer això al teu ordinador personal, per raons de seguretat."}. +{"Sources Specs:","Especificacions de Codi Font:"}. {"Specify the access model","Especificar el model d'accés"}. {"Specify the event message type","Especifica el tipus de missatge d'event"}. {"Specify the publisher model","Especificar el model del publicant"}. @@ -518,8 +539,10 @@ {"The JIDs of those to contact with questions","Els JIDs a qui contactar amb preguntes"}. {"The JIDs of those with an affiliation of owner","Els JIDs de qui tenen una afiliació de propietaris"}. {"The JIDs of those with an affiliation of publisher","Els JIDs de qui tenen una afiliació de publicadors"}. +{"The list of all online users","La llista de tots els usuaris en línia"}. +{"The list of all users","La llista de tots els usuaris"}. {"The list of JIDs that may associate leaf nodes with a collection","La llista de JIDs que poden associar nodes fulla amb una col·lecció"}. -{"The maximum number of child nodes that can be associated with a collection","El màxim número de nodes fills que poden associar-se amb una col·lecció"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","El màxim número de nodes fills que poden associar-se amb una col·lecció, o `max` per a no tindre altre límit més que el màxim imposat pel servidor"}. {"The minimum number of milliseconds between sending any two notification digests","El número mínim de mil·lisegons entre l'enviament de dos resums de notificacions"}. {"The name of the node","El nom del node"}. {"The node is a collection node","El node es una col·lecció"}. @@ -544,7 +567,6 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","El tipus de dades al node, usualment especificat pel namespace del payload (si n'hi ha)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","La URL de uns transformació XSL que pot ser aplicada als payloads per a generar un element apropiat de contingut de missatge."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","La URL de una transformació XSL que pot ser aplicada al format de payload per a generar un resultat valid de Data Forms, que el client puga mostrar usant un métode generic de Data Forms"}. -{"The username is not valid","El nom d'usuari no es vàlid"}. {"There was an error changing the password: ","Hi ha hagut un error canviant la contrasenya: "}. {"There was an error creating the account: ","Hi ha hagut un error creant el compte: "}. {"There was an error deleting the account: ","Hi ha hagut un error esborrant el compte: "}. @@ -571,7 +593,7 @@ {"Too many users in this conference","N'hi ha massa usuaris en esta sala de conferència"}. {"To","Per a"}. {"Total rooms","Sales totals"}. -{"Traffic rate limit is exceeded","El llímit de tràfic ha sigut sobrepassat"}. +{"Traffic rate limit is exceeded","El límit de tràfic ha sigut sobrepassat"}. {"Transactions Aborted:","Transaccions Avortades:"}. {"Transactions Committed:","Transaccions Realitzades:"}. {"Transactions Logged:","Transaccions registrades:"}. @@ -583,6 +605,7 @@ {"Unauthorized","No autoritzat"}. {"Unexpected action","Acció inesperada"}. {"Unexpected error condition: ~p","Condició d'error inesperada: ~p"}. +{"Uninstall","Desinstal·lar"}. {"Unregister an XMPP account","Anul·lar el registre d'un compte XMPP"}. {"Unregister","Anul·lar el registre"}. {"Unselect All","Deseleccionar tots"}. @@ -593,7 +616,10 @@ {"Update ~p","Actualitzar ~p"}. {"Update plan","Pla d'actualització"}. {"Update script","Script d'actualització"}. +{"Update specs to get modules source, then install desired ones.","Actualitza les especificacions per obtindre el codi font dels mòduls, després instal·la els que vulgues."}. +{"Update Specs","Actualitzar Especificacions"}. {"Update","Actualitzar"}. +{"Upgrade","Actualitza"}. {"Uptime:","Temps en marxa:"}. {"URL for Archived Discussion Logs","URL dels Arxius de Discussions"}. {"User already exists","El usuari ja existeix"}. @@ -616,6 +642,7 @@ {"Value of '~s' should be integer","El valor de '~s' deuria ser un numero enter"}. {"Value 'set' of 'type' attribute is not allowed","El valor 'set' a l'atribut 'type' no és permès"}. {"vCard User Search","vCard recerca d'usuari"}. +{"View joined MIX channels","Vore els canals MIX units"}. {"View Queue","Vore Cua"}. {"View Roster","Vore Llista de contactes"}. {"Virtual Hosts","Hosts virtuals"}. diff --git a/priv/msgs/cs.msg b/priv/msgs/cs.msg index 7dc4fd16b..c6d3b2207 100644 --- a/priv/msgs/cs.msg +++ b/priv/msgs/cs.msg @@ -181,7 +181,6 @@ {"Make room public searchable","Nastavit místnost jako veřejnou"}. {"Malformed username","Chybně formátováné jméno uživatele"}. {"March",". března"}. -{"Max # of items to persist","Maximální počet položek, které je možné natrvalo uložit"}. {"Max payload size in bytes","Maximální náklad v bajtech"}. {"Maximum Number of Occupants","Počet účastníků"}. {"May",". května"}. diff --git a/priv/msgs/de.msg b/priv/msgs/de.msg index d35d1a92d..a0c71f903 100644 --- a/priv/msgs/de.msg +++ b/priv/msgs/de.msg @@ -20,6 +20,7 @@ {"Access model","Zugriffsmodell"}. {"Account doesn't exist","Konto existiert nicht"}. {"Action on user","Aktion auf Benutzer"}. +{"Add a hat to a user","Funktion zu einem Benutzer hinzufügen"}. {"Add Jabber ID","Jabber-ID hinzufügen"}. {"Add New","Neue(n) hinzufügen"}. {"Add User","Benutzer hinzufügen"}. @@ -136,6 +137,7 @@ {"Elements","Elemente"}. {"Email Address","E-Mail-Adresse"}. {"Email","E-Mail"}. +{"Enable hats","Funktion einschalten"}. {"Enable logging","Protokollierung aktivieren"}. {"Enable message archiving","Nachrichtenarchivierung aktivieren"}. {"Enabling push without 'node' attribute is not supported","push ohne 'node'-Attribut zu aktivieren wird nicht unterstützt"}. @@ -170,6 +172,8 @@ {"Full List of Room Admins","Vollständige Liste der Raumadmins"}. {"Full List of Room Owners","Vollständige Liste der Raumbesitzer"}. {"Full Name","Vollständiger Name"}. +{"Get List of Online Users","Liste der angemeldeten Benutzer abrufen"}. +{"Get List of Registered Users","Liste der registrierten Benutzer abrufen"}. {"Get Number of Online Users","Anzahl der angemeldeten Benutzer abrufen"}. {"Get Number of Registered Users","Anzahl der registrierten Benutzer abrufen"}. {"Get Pending","Ausstehende abrufen"}. @@ -186,6 +190,9 @@ {"has been kicked because of an affiliation change","wurde wegen einer Änderung der Zugehörigkeit hinausgeworfen"}. {"has been kicked because the room has been changed to members-only","wurde hinausgeworfen weil der Raum zu Nur-Mitglieder geändert wurde"}. {"has been kicked","wurde hinausgeworfen"}. +{"Hat title","Funktionstitel"}. +{"Hat URI","Funktions-URI"}. +{"Hats limit exceeded","Funktionslimit wurde überschritten"}. {"Host unknown","Host unbekannt"}. {"Host","Host"}. {"HTTP File Upload","HTTP-Dateiupload"}. @@ -237,6 +244,8 @@ {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Niederwertigstes Bit des SHA-256-Hashes des Textes sollte hexadezimalem Label gleichen"}. {"leaves the room","verlässt den Raum"}. {"List of rooms","Liste von Räumen"}. +{"List of users with hats","Liste der Benutzer mit Funktionen"}. +{"List users with hats","Benutzer mit Funktionen auflisten"}. {"Logging","Protokollierung"}. {"Low level update script","Low-Level-Aktualisierungsscript"}. {"Make participants list public","Teilnehmerliste öffentlich machen"}. @@ -249,7 +258,7 @@ {"Malformed username","Ungültiger Benutzername"}. {"MAM preference modification denied by service policy","Modifikation der MAM-Präferenzen aufgrund der Dienstrichtlinien verweigert"}. {"March","März"}. -{"Max # of items to persist","Maximale Anzahl persistenter Items"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Maximale Anzahl der aufzubewahrenden Elemente oder `max`, wenn es keine spezifische Begrenzung gibt, außer einer vom Server festgelegten Höchstzahl"}. {"Max payload size in bytes","Maximale Nutzdatengröße in Bytes"}. {"Maximum file size","Maximale Dateigröße"}. {"Maximum Number of History Messages Returned by Room","Maximale Anzahl der vom Raum zurückgegebenen History-Nachrichten"}. @@ -340,7 +349,7 @@ {"Number of Offline Messages","Anzahl der Offline-Nachrichten"}. {"Number of online users","Anzahl der angemeldeten Benutzer"}. {"Number of registered users","Anzahl der registrierten Benutzer"}. -{"Number of seconds after which to automatically purge items","Anzahl der Sekunden, nach der Items automatisch gelöscht werden"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Anzahl der Sekunden, nach denen Elemente automatisch gelöscht werden sollen, oder `max`, wenn es keine spezifische Grenze gibt, außer einer vom Server festgelegten Höchstgrenze"}. {"Occupants are allowed to invite others","Teilnehmer dürfen andere einladen"}. {"Occupants May Change the Subject","Teilnehmer dürfen das Thema ändern"}. {"October","Oktober"}. @@ -419,6 +428,7 @@ {"Registered Users","Registrierte Benutzer"}. {"Registered Users:","Registrierte Benutzer:"}. {"Remote copy","Fernkopie"}. +{"Remove a hat from a user","Eine Funktion bei einem Benutzer entfernen"}. {"Remove All Offline Messages","Alle Offline-Nachrichten löschen"}. {"Remove User","Benutzer löschen"}. {"Remove","Entfernen"}. @@ -437,7 +447,7 @@ {"Roles and Affiliations that May Retrieve Member List","Rollen und Zugehörigkeiten die Mitgliederliste abrufen dürfen"}. {"Roles for which Presence is Broadcasted","Rollen für welche die Präsenz übertragen wird"}. {"Roles that May Send Private Messages","Rollen die Privatnachrichten senden dürfen"}. -{"Room Configuration","Raum-Konfiguration"}. +{"Room Configuration","Raumkonfiguration"}. {"Room creation is denied by service policy","Anlegen des Raumes aufgrund der Dienstrichtlinien verweigert"}. {"Room description","Raumbeschreibung"}. {"Room Occupants","Raumteilnehmer"}. @@ -519,8 +529,10 @@ {"The JIDs of those to contact with questions","Die JIDs jener, die bei Fragen zu kontaktieren sind"}. {"The JIDs of those with an affiliation of owner","Die JIDs jener mit einer Zugehörigkeit von Besitzer"}. {"The JIDs of those with an affiliation of publisher","Die JIDs jener mit einer Zugehörigkeit von Veröffentlicher"}. +{"The list of all online users","Die Liste aller angemeldeter Benutzer"}. +{"The list of all users","Die Liste aller Benutzer"}. {"The list of JIDs that may associate leaf nodes with a collection","Die Liste der JIDs die Blattknoten mit einer Sammlung verknüpfen dürfen"}. -{"The maximum number of child nodes that can be associated with a collection","Die maximale Anzahl der Kindknoten die mit einer Sammlung verknüpft werden können"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","Die Höchstzahl der untergeordneten Knoten, die einer Sammlung zugeordnet werden können, oder `max`, wenn es keine spezifische Begrenzung gibt, sondern nur eine vom Server festgelegte Höchstzahl"}. {"The minimum number of milliseconds between sending any two notification digests","Die minimale Anzahl an Millisekunden zwischen dem Senden von zwei Benachrichtigungs-Übersichten"}. {"The name of the node","Der Name des Knotens"}. {"The node is a collection node","Der Knoten ist ein Sammlungsknoten"}. @@ -545,7 +557,6 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","Die Art der Knotendaten, üblicherweise vom Namensraum der Nutzdaten angegeben (gegebenenfalls)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","Die URL einer XSL-Transformation welche auf Nutzdaten angewendet werden kann, um ein geeignetes Nachrichtenkörper-Element zu generieren."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","Die URL einer XSL-Transformation welche auf das Nutzdaten-Format angewendet werden kann, um ein gültiges Data Forms-Ergebnis zu generieren das der Client mit Hilfe einer generischen Data Forms-Rendering-Engine anzeigen könnte"}. -{"The username is not valid","Der Benutzername ist nicht gültig"}. {"There was an error changing the password: ","Es trat ein Fehler beim Ändern des Passwortes auf: "}. {"There was an error creating the account: ","Es trat ein Fehler beim Erstellen des Kontos auf: "}. {"There was an error deleting the account: ","Es trat ein Fehler beim Löschen des Kontos auf: "}. diff --git a/priv/msgs/el.msg b/priv/msgs/el.msg index c8fc69e63..ec07d58c1 100644 --- a/priv/msgs/el.msg +++ b/priv/msgs/el.msg @@ -249,7 +249,6 @@ {"Malformed username","Λανθασμένη μορφή ονόματος χρήστη"}. {"MAM preference modification denied by service policy","Άρνηση αλλαγής προτιμήσεων MAM, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"March","Μάρτιος"}. -{"Max # of items to persist","Μέγιστος αριθμός μόνιμων στοιχείων"}. {"Max payload size in bytes","Μέγιστο μέγεθος φορτίου σε bytes"}. {"Maximum file size","Μέγιστο μέγεθος αρχείου"}. {"Maximum Number of History Messages Returned by Room","Μέγιστος αριθμός μηνυμάτων Ιστορικού που επιστρέφονται από την Αίθουσα"}. @@ -339,7 +338,6 @@ {"Number of Offline Messages","Πλήθος μηνυμάτων Χωρίς Σύνδεση"}. {"Number of online users","Αριθμός συνδεδεμένων χρηστών"}. {"Number of registered users","Αριθμός εγγεγραμμένων χρηστών"}. -{"Number of seconds after which to automatically purge items","Πλήθος δευτερολέπτων μετά τα οποία αυτομάτως εκκαθαρίζονται αντικείμενα"}. {"Occupants are allowed to invite others","Οι συμμετέχοντες μπορούν να προσκαλέσουν και άλλους"}. {"Occupants May Change the Subject","Επιτρέψτε στους χρήστες να αλλάζουν το Θέμα"}. {"October","Οκτώβριος"}. @@ -519,7 +517,6 @@ {"The JIDs of those with an affiliation of owner","Το JID αυτών που σχετίζονται με τον ιδιοκτήτη"}. {"The JIDs of those with an affiliation of publisher","Το JID αυτών που σχετίζονται με τον εκδότη"}. {"The list of JIDs that may associate leaf nodes with a collection","Λίστα των JIDs που μπορούν να σχετίζουν leaf κόμβους με μια Συλλογή"}. -{"The maximum number of child nodes that can be associated with a collection","Το μέγιστο πλήθος θυγατρικών κόμβων που μπορούν να συσχετιστούν με μία Συλλογή"}. {"The minimum number of milliseconds between sending any two notification digests","Το ελάχιστο πλήθος χιλιοστών του δευτερολέπτου μεταξύ της αποστολής δύο συγχωνεύσεων ειδοποιήσεων"}. {"The name of the node","Το όνομα του κόμβου"}. {"The node is a collection node","Ο κόμβος είναι κόμβος Συλλογής"}. @@ -544,7 +541,6 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","Ο τύπος των δεδομένων του κόμβου συνήθως προσδιορίζεται από το namespace του φόρτου εργασιών (αν υπάρχουν)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","Το URL ενός μετασχηματισμού XSL το οποίο μπορεί να εφαρμοστεί σε φόρτους εργασίας για να παραχθεί το κατάλληλο στοιχείο του σώματος του μηνύματος."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","Το URL ενός μετασχηματισμού XSL, το οποίο μπορεί να εφαρμοστεί στους τύπους φόρτου εργασίας για να παραχθεί έγκυρο αποτέλεσμα Data Forms, τέτοιο που ο πελάτης μπορεί να εμφανίσει, χρησιμοποιώντας μια ευρείας χρήσης μηχανή επεξεργασίας Data Forms"}. -{"The username is not valid","Το όνομα Χρήστη δεν είναι έγκυρο"}. {"There was an error changing the password: ","Παρουσιάστηκε σφάλμα κατά την αλλαγή του κωδικού πρόσβασης: "}. {"There was an error creating the account: ","Υπήρξε ένα σφάλμα κατά τη δημιουργία του λογαριασμού: "}. {"There was an error deleting the account: ","Υπήρξε ένα σφάλμα κατά τη διαγραφή του λογαριασμού: "}. diff --git a/priv/msgs/eo.msg b/priv/msgs/eo.msg index 91932f66d..a7d9ad214 100644 --- a/priv/msgs/eo.msg +++ b/priv/msgs/eo.msg @@ -186,7 +186,6 @@ {"Make room persistent","Farigu babilejon daŭra"}. {"Make room public searchable","Farigu babilejon publike trovebla"}. {"March","Marĉo"}. -{"Max # of items to persist","Maksimuma kiomo de eroj en konservado"}. {"Max payload size in bytes","Maksimuma aĵo-grando je bajtoj"}. {"Maximum file size","Maksimuma grando de dosiero"}. {"Maximum Number of Occupants","Limigo de nombro de partoprenantoj"}. diff --git a/priv/msgs/es.msg b/priv/msgs/es.msg index 1f658343a..f3cebdb44 100644 --- a/priv/msgs/es.msg +++ b/priv/msgs/es.msg @@ -20,6 +20,7 @@ {"Access model","Modelo de Acceso"}. {"Account doesn't exist","La cuenta no existe"}. {"Action on user","Acción en el usuario"}. +{"Add a hat to a user","Añade un sombrero a un usuario"}. {"Add Jabber ID","Añadir Jabber ID"}. {"Add New","Añadir nuevo"}. {"Add User","Añadir usuario"}. @@ -78,6 +79,7 @@ {"Changing role/affiliation is not allowed","No está permitido cambiar el rol/afiliación"}. {"Channel already exists","El canal ya existe"}. {"Channel does not exist","El canal no existe"}. +{"Channel JID","JID del Canal"}. {"Channels","Canales"}. {"Characters not allowed:","Caracteres no permitidos:"}. {"Chatroom configuration modified","Configuración de la sala modificada"}. @@ -97,6 +99,7 @@ {"Configuration","Configuración"}. {"Connected Resources:","Recursos conectados:"}. {"Contact Addresses (normally, room owner or owners)","Direcciones de contacto (normalmente la del dueño o dueños de la sala)"}. +{"Contrib Modules","Módulos Contrib"}. {"Country","País"}. {"CPU Time:","Tiempo consumido de CPU:"}. {"Current Discussion Topic","Tema de discusión actual"}. @@ -136,6 +139,7 @@ {"Elements","Elementos"}. {"Email Address","Dirección de correo electrónico"}. {"Email","Correo electrónico"}. +{"Enable hats","Activar sombreros"}. {"Enable logging","Guardar históricos"}. {"Enable message archiving","Activar el almacenamiento de mensajes"}. {"Enabling push without 'node' attribute is not supported","No está soportado activar Push sin el atributo 'node'"}. @@ -170,6 +174,8 @@ {"Full List of Room Admins","Lista completa de administradores de la sala"}. {"Full List of Room Owners","Lista completa de dueños de la sala"}. {"Full Name","Nombre completo"}. +{"Get List of Online Users","Ver lista de usuarios conectados"}. +{"Get List of Registered Users","Ver lista de usuarios registrados"}. {"Get Number of Online Users","Ver número de usuarios conectados"}. {"Get Number of Registered Users","Ver número de usuarios registrados"}. {"Get Pending","Obtener pendientes"}. @@ -186,6 +192,9 @@ {"has been kicked because of an affiliation change","ha sido expulsado por un cambio de su afiliación"}. {"has been kicked because the room has been changed to members-only","ha sido expulsado porque la sala es ahora solo para miembros"}. {"has been kicked","ha sido expulsado"}. +{"Hat title","Título del sombrero"}. +{"Hat URI","Dirección del sombrero"}. +{"Hats limit exceeded","Se ha excedido el límite de sombreros"}. {"Host unknown","Dominio desconocido"}. {"Host","Dominio"}. {"HTTP File Upload","Subir fichero por HTTP"}. @@ -208,6 +217,8 @@ {"Incorrect value of 'action' attribute","Valor incorrecto del atributo 'action'"}. {"Incorrect value of 'action' in data form","Valor incorrecto de 'action' en el formulario de datos"}. {"Incorrect value of 'path' in data form","Valor incorrecto de 'path' en el formulario de datos"}. +{"Installed Modules:","Módulos Instalados:"}. +{"Install","Instalar"}. {"Insufficient privilege","Privilegio insuficiente"}. {"Internal server error","Error interno en el servidor"}. {"Invalid 'from' attribute in forwarded message","Atributo 'from' no válido en el mensaje reenviado"}. @@ -224,6 +235,8 @@ {"January","Enero"}. {"JID normalization denied by service policy","Se ha denegado la normalización del JID por política del servicio"}. {"JID normalization failed","Ha fallado la normalización del JID"}. +{"Joined MIX channels of ~ts","Canales MIX unidos de ~ts"}. +{"Joined MIX channels:","Canales MIX unidos:"}. {"joins the room","entra en la sala"}. {"July","Julio"}. {"June","Junio"}. @@ -237,6 +250,8 @@ {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Los bits menos significativos del hash SHA-256 del texto deberían ser iguales a la etiqueta hexadecimal"}. {"leaves the room","sale de la sala"}. {"List of rooms","Lista de salas"}. +{"List of users with hats","Lista de usuarios con sombreros"}. +{"List users with hats","Listar usuarios con sombreros"}. {"Logging","Histórico de mensajes"}. {"Low level update script","Script de actualización a bajo nivel"}. {"Make participants list public","La lista de participantes es pública"}. @@ -249,7 +264,7 @@ {"Malformed username","Nombre de usuario mal formado"}. {"MAM preference modification denied by service policy","Se ha denegado modificar la preferencia MAM por política del servicio"}. {"March","Marzo"}. -{"Max # of items to persist","Máximo # de elementos que persisten"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Máximo # de elementos a persistir, o `max` para no especificar un límite, más que el máximo impuesto por el servidor"}. {"Max payload size in bytes","Máximo tamaño del contenido en bytes"}. {"Maximum file size","Tamaño máximo de fichero"}. {"Maximum Number of History Messages Returned by Room","Máximo número de mensajes del historial devueltos por la sala"}. @@ -324,6 +339,7 @@ {"Node index not found","No se ha encontrado índice de nodo"}. {"Node not found","Nodo no encontrado"}. {"Node ~p","Nodo ~p"}. +{"Node","Nodo"}. {"Nodeprep has failed","Ha fallado el procesado del nombre de nodo (nodeprep)"}. {"Nodes","Nodos"}. {"None","Ninguno"}. @@ -339,8 +355,9 @@ {"Number of Offline Messages","Número de mensajes diferidos"}. {"Number of online users","Número de usuarios conectados"}. {"Number of registered users","Número de usuarios registrados"}. -{"Number of seconds after which to automatically purge items","Número de segundos después de los cuales se purgarán elementos automáticamente"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Número de segundos después de los cuales se purgarán elementos automáticamente, o `max` para no especificar un límite, más que el máximo impuesto por el servidor"}. {"Occupants are allowed to invite others","Los ocupantes pueden invitar a otras personas"}. +{"Occupants are allowed to query others","Los ocupantes pueden enviar peticiones a otros"}. {"Occupants May Change the Subject","Los ocupantes pueden cambiar el Asunto"}. {"October","Octubre"}. {"Offline Messages","Mensajes diferidos"}. @@ -367,11 +384,13 @@ {"Only those on a whitelist may subscribe and retrieve items","Solo quienes están en una lista blanca pueden suscribirse y recibir elementos"}. {"Organization Name","Nombre de la organización"}. {"Organization Unit","Unidad de la organización"}. +{"Other Modules Available:","Otros módulos disponibles:"}. {"Outgoing s2s Connections","Conexiones S2S salientes"}. {"Outgoing s2s Connections:","Conexiones S2S salientes:"}. {"Owner privileges required","Se requieren privilegios de propietario de la sala"}. {"Packet relay is denied by service policy","Se ha denegado el reenvío del paquete por política del servicio"}. {"Packet","Paquete"}. +{"Participant ID","ID del Participante"}. {"Participant","Participante"}. {"Password Verification","Verificación de la contraseña"}. {"Password Verification:","Verificación de la contraseña:"}. @@ -418,6 +437,7 @@ {"Registered Users:","Usuarios registrados:"}. {"Register","Registrar"}. {"Remote copy","Copia remota"}. +{"Remove a hat from a user","Quitarle un sombrero a un usuario"}. {"Remove All Offline Messages","Borrar todos los mensajes diferidos"}. {"Remove User","Eliminar usuario"}. {"Remove","Borrar"}. @@ -473,6 +493,7 @@ {"Shut Down Service","Detener el servicio"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Algunos clientes XMPP pueden guardar tu contraseña en la máquina, pero solo deberías hacer esto en tu propia máquina personal, por razones de seguridad."}. +{"Sources Specs:","Especificaciones de Códigos Fuente:"}. {"Specify the access model","Especifica el modelo de acceso"}. {"Specify the event message type","Especifica el tipo del mensaje de evento"}. {"Specify the publisher model","Especificar el modelo del publicante"}. @@ -518,8 +539,10 @@ {"The JIDs of those to contact with questions","Los JIDs a quienes contactar con preguntas"}. {"The JIDs of those with an affiliation of owner","Los JIDs de quienes tienen una afiliación de dueños"}. {"The JIDs of those with an affiliation of publisher","Los JIDs de quienes tienen una afiliación de publicadores"}. +{"The list of all online users","La lista de todos los usuarios conectados"}. +{"The list of all users","La lista de todos los usuarios"}. {"The list of JIDs that may associate leaf nodes with a collection","La lista de JIDs que pueden asociar nodos hijo con una colección"}. -{"The maximum number of child nodes that can be associated with a collection","El número máximo de nodos hijo que pueden asociarse a una colección"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","El número máximo de nodos hijo que pueden asociarse a una colección, o `max` para no especificar un límite, más que el máximo impuesto por el servidor"}. {"The minimum number of milliseconds between sending any two notification digests","El número mínimo de milisegundos entre dos envíos de resumen de notificaciones"}. {"The name of the node","El nombre del nodo"}. {"The node is a collection node","El nodo es una colección"}. @@ -544,7 +567,6 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","El tipo de datos del nodo, usualmente especificado por el namespace del payload (en caso de haberlo)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","La URL de una transformación XSL que puede aplicarse a payloads para generar un elemento de contenido del mensaje apropiado."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","La URL de una transformación XSL que puede aplicarse al formato de payload para generar un resultado de Formulario de Datos válido, que el cliente pueda mostrar usando un mecanismo de dibujado genérico de Formulario de Datos"}. -{"The username is not valid","El nombre de usuario no es válido"}. {"There was an error changing the password: ","Hubo uno error al cambiar la contaseña: "}. {"There was an error creating the account: ","Hubo uno error al crear la cuenta: "}. {"There was an error deleting the account: ","Hubo un error borrando la cuenta: "}. @@ -571,7 +593,7 @@ {"Too many users in this conference","Demasiados usuarios en esta sala"}. {"To","Para"}. {"Total rooms","Salas totales"}. -{"Traffic rate limit is exceeded","Se ha exedido el límite de tráfico"}. +{"Traffic rate limit is exceeded","Se ha excedido el límite de tráfico"}. {"Transactions Aborted:","Transacciones abortadas:"}. {"Transactions Committed:","Transacciones finalizadas:"}. {"Transactions Logged:","Transacciones registradas:"}. @@ -583,6 +605,7 @@ {"Unauthorized","No autorizado"}. {"Unexpected action","Acción inesperada"}. {"Unexpected error condition: ~p","Condición de error inesperada: ~p"}. +{"Uninstall","Desinstalar"}. {"Unregister an XMPP account","Borrar una cuenta XMPP"}. {"Unregister","Borrar"}. {"Unselect All","Deseleccionar todo"}. @@ -593,7 +616,10 @@ {"Update ~p","Actualizar ~p"}. {"Update plan","Plan de actualización"}. {"Update script","Script de actualización"}. +{"Update specs to get modules source, then install desired ones.","Actualizar Especificaciones para conseguir el código fuente de los módulos, luego instala los que quieras."}. +{"Update Specs","Actualizar Especificaciones"}. {"Update","Actualizar"}. +{"Upgrade","Actualizar"}. {"Uptime:","Tiempo desde el inicio:"}. {"URL for Archived Discussion Logs","URL del registro de discusiones archivadas"}. {"User already exists","El usuario ya existe"}. @@ -616,6 +642,7 @@ {"Value of '~s' should be integer","El valor de '~s' debería ser un entero"}. {"Value 'set' of 'type' attribute is not allowed","El valor 'set' del atributo 'type' no está permitido"}. {"vCard User Search","Búsqueda de vCard de usuarios"}. +{"View joined MIX channels","Ver los canales MIX unidos"}. {"View Queue","Ver Cola"}. {"View Roster","Ver Lista de contactos"}. {"Virtual Hosts","Dominios Virtuales"}. diff --git a/priv/msgs/fr.msg b/priv/msgs/fr.msg index f33d6115d..aa8499c1b 100644 --- a/priv/msgs/fr.msg +++ b/priv/msgs/fr.msg @@ -5,11 +5,20 @@ {" (Add * to the end of field to match substring)"," (Ajouter * à la fin du champ pour correspondre à la sous-chaîne)"}. {" has set the subject to: "," a défini le sujet sur : "}. +{"# participants","# participants"}. {"A description of the node","Une description du nœud"}. {"A friendly name for the node","Un nom convivial pour le nœud"}. {"A password is required to enter this room","Un mot de passe est nécessaire pour accéder à ce salon"}. +{"A Web Page","Une page Web"}. {"Accept","Accepter"}. {"Access denied by service policy","L'accès au service est refusé"}. +{"Access model of authorize","Modèle d’accès de « autoriser »"}. +{"Access model of open","Modèle d’accès de « ouvrir »"}. +{"Access model of presence","Modèle d’accès de « présence »"}. +{"Access model of roster","Modèle d’accès de « liste »"}. +{"Access model of whitelist","Modèle d’accès de « liste blanche »"}. +{"Access model","Modèle d’accès"}. +{"Account doesn't exist","Le compte n’existe pas"}. {"Action on user","Action sur l'utilisateur"}. {"Add Jabber ID","Ajouter un Jabber ID"}. {"Add New","Ajouter"}. @@ -19,7 +28,9 @@ {"Administrator privileges required","Les droits d'administrateur sont nécessaires"}. {"All activity","Toute activité"}. {"All Users","Tous les utilisateurs"}. +{"Allow subscription","Autoriser l’abonnement"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Autoriser ce Jabber ID à s'abonner à ce nœud PubSub ?"}. +{"Allow this person to register with the room?","Autoriser cette personne à enregistrer ce salon ?"}. {"Allow users to change the subject","Autoriser les utilisateurs à changer le sujet"}. {"Allow users to query other users","Autoriser les utilisateurs à envoyer des requêtes aux autres utilisateurs"}. {"Allow users to send invites","Autoriser les utilisateurs à envoyer des invitations"}. @@ -28,8 +39,25 @@ {"Allow visitors to send private messages to","Autoriser les visiteurs à envoyer des messages privés"}. {"Allow visitors to send status text in presence updates","Autoriser les visiteurs à envoyer un message d'état avec leur présence"}. {"Allow visitors to send voice requests","Permettre aux visiteurs d'envoyer des demandes de 'voice'"}. +{"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Un groupe LDAP associé qui définit l’adhésion à un salon ; cela devrait être un nom distingué LDAP selon la définition spécifique à l’implémentation ou au déploiement d’un groupe."}. {"Announcements","Annonces"}. +{"Answer associated with a picture","Réponse associée à une image"}. +{"Answer associated with a video","Réponse associée à une vidéo"}. +{"Answer associated with speech","Réponse associée à un discours"}. +{"Answer to a question","Réponse à une question"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","N’importe qui dans le groupe de la liste spécifiée peut s’abonner et récupérer les items"}. +{"Anyone may associate leaf nodes with the collection","N’importe qui peut associer les feuilles avec la collection"}. +{"Anyone may publish","N’importe qui peut publier"}. +{"Anyone may subscribe and retrieve items","N’importe qui peut s’abonner et récupérer les items"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","N’importe qui avec un abonnement de présence peut s’abonner et récupérer les items"}. +{"Anyone with Voice","N’importe qui avec Voice"}. +{"Anyone","N’importe qui"}. {"April","Avril"}. +{"Attribute 'channel' is required for this request","L’attribut « channel » est requis pour la requête"}. +{"Attribute 'id' is mandatory for MIX messages","L’attribut « id » est obligatoire pour les messages MIX"}. +{"Attribute 'jid' is not allowed here","L’attribut « jid » n’est pas autorisé ici"}. +{"Attribute 'node' is not allowed here","L’attribut « node » n’est pas autorisé ici"}. +{"Attribute 'to' of stanza that triggered challenge","L’attribut « to » de la strophe qui a déclenché le défi"}. {"August","Août"}. {"Automatic node creation is not enabled","La creation implicite de nœud n'est pas disponible"}. {"Backup Management","Gestion des sauvegardes"}. @@ -43,10 +71,14 @@ {"Cannot remove active list","La liste active ne peut être supprimée"}. {"Cannot remove default list","La liste par défaut ne peut être supprimée"}. {"CAPTCHA web page","Page web de CAPTCHA"}. +{"Challenge ID","Identifiant du défi"}. {"Change Password","Modifier le mot de passe"}. {"Change User Password","Changer le mot de passe de l'utilisateur"}. {"Changing password is not allowed","La modification du mot de passe n'est pas autorisée"}. {"Changing role/affiliation is not allowed","La modification role/affiliation n'est pas autorisée"}. +{"Channel already exists","Ce canal existe déjà"}. +{"Channel does not exist","Le canal n’existe pas"}. +{"Channels","Canaux"}. {"Characters not allowed:","Caractères non autorisés :"}. {"Chatroom configuration modified","Configuration du salon modifiée"}. {"Chatroom is created","Le salon de discussion est créé"}. @@ -58,30 +90,39 @@ {"Choose storage type of tables","Choisissez un type de stockage pour les tables"}. {"Choose whether to approve this entity's subscription.","Choisissez d'approuver ou non l'abonnement de cette entité."}. {"City","Ville"}. +{"Client acknowledged more stanzas than sent by server","Le client accuse réception de plus de strophes que ce qui est envoyé par le serveur"}. {"Commands","Commandes"}. {"Conference room does not exist","Le salon de discussion n'existe pas"}. {"Configuration of room ~s","Configuration pour le salon ~s"}. {"Configuration","Configuration"}. {"Connected Resources:","Ressources connectées :"}. +{"Contact Addresses (normally, room owner or owners)","Adresses de contact (normalement les administrateurs du salon)"}. {"Country","Pays"}. {"CPU Time:","Temps CPU :"}. +{"Current Discussion Topic","Sujet de discussion courant"}. {"Database failure","Échec sur la base de données"}. {"Database Tables at ~p","Tables de base de données sur ~p"}. {"Database Tables Configuration at ","Configuration des tables de base de données sur "}. {"Database","Base de données"}. {"December","Décembre"}. {"Default users as participants","Les utilisateurs sont participant par défaut"}. +{"Delete content","Supprimer le contenu"}. {"Delete message of the day on all hosts","Supprimer le message du jour sur tous les domaines"}. {"Delete message of the day","Supprimer le message du jour"}. {"Delete Selected","Suppression des éléments sélectionnés"}. +{"Delete table","Supprimer la table"}. {"Delete User","Supprimer l'utilisateur"}. {"Deliver event notifications","Envoyer les notifications d'événement"}. {"Deliver payloads with event notifications","Inclure le contenu du message avec la notification"}. {"Description:","Description :"}. {"Disc only copy","Copie sur disque uniquement"}. +{"'Displayed groups' not added (they do not exist!): ","« Groupes affichés » non ajoutés (ils n’existent pas !) : "}. {"Displayed:","Affichés :"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Ne révélez votre mot de passe à personne, pas même aux administrateurs du serveur XMPP."}. {"Dump Backup to Text File at ","Enregistrer la sauvegarde dans un fichier texte sur "}. {"Dump to Text File","Sauvegarder dans un fichier texte"}. +{"Duplicated groups are not allowed by RFC6121","Les groupes dupliqués ne sont pas autorisés par la RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Spécifie dynamiquement un « réponse à » de l’item de l’éditeur"}. {"Edit Properties","Modifier les propriétés"}. {"Either approve or decline the voice request.","Accepter ou refuser la demande de voix."}. {"ejabberd MUC module","Module MUC ejabberd"}. @@ -90,6 +131,7 @@ {"ejabberd SOCKS5 Bytestreams module","Module SOCKS5 Bytestreams per ejabberd"}. {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Console Web d'administration de ejabberd"}. +{"ejabberd","ejabberd"}. {"Elements","Éléments"}. {"Email Address","Adresse courriel"}. {"Email","Courriel"}. @@ -103,6 +145,7 @@ {"Enter path to jabberd14 spool file","Entrez le chemin vers le fichier spool de Jabberd 1.4"}. {"Enter path to text file","Entrez le chemin vers le fichier texte"}. {"Enter the text you see","Tapez le texte que vous voyez"}. +{"Erlang XMPP Server","Serveur XMPP Erlang"}. {"Error","Erreur"}. {"Exclude Jabber IDs from CAPTCHA challenge","Exempter des Jabberd IDs du test CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exporter toutes les tables vers un fichier SQL :"}. @@ -116,12 +159,18 @@ {"Failed to parse HTTP response","Échec de lecture de la réponse HTTP"}. {"Failed to process option '~s'","Échec de traitement de l'option '~s'"}. {"Family Name","Nom de famille"}. +{"FAQ Entry","Entrée FAQ"}. {"February","Février"}. {"File larger than ~w bytes","Taille de fichier suppérieur à ~w octets"}. {"Fill in the form to search for any matching XMPP User","Complétez le formulaire pour rechercher un utilisateur XMPP correspondant"}. {"Friday","Vendredi"}. +{"From ~ts","De ~ts"}. {"From","De"}. +{"Full List of Room Admins","Liste complète des administrateurs des salons"}. +{"Full List of Room Owners","Liste complète des propriétaires des salons"}. {"Full Name","Nom complet"}. +{"Get List of Online Users","Récupérer les utilisateurs en ligne"}. +{"Get List of Registered Users","Récupérer les utilisateurs enregistrés"}. {"Get Number of Online Users","Récupérer le nombre d'utilisateurs en ligne"}. {"Get Number of Registered Users","Récupérer le nombre d'utilisateurs enregistrés"}. {"Get User Last Login Time","Récupérer la dernière date de connexion de l'utilisateur"}. @@ -137,9 +186,11 @@ {"has been kicked because of an affiliation change","a été éjecté à cause d'un changement d'autorisation"}. {"has been kicked because the room has been changed to members-only","a été éjecté car la salle est désormais réservée aux membres"}. {"has been kicked","a été expulsé"}. +{"Hats limit exceeded","La limite a été dépassée"}. {"Host unknown","Serveur inconnu"}. {"Host","Serveur"}. {"HTTP File Upload","Téléversement de fichier HTTP"}. +{"Idle connection","Connexion inactive"}. {"If you don't see the CAPTCHA image here, visit the web page.","SI vous ne voyez pas l'image CAPTCHA ici, visitez la page web."}. {"Import Directory","Importer un répertoire"}. {"Import File","Importer un fichier"}. @@ -176,8 +227,10 @@ {"July","Juillet"}. {"June","Juin"}. {"Just created","Vient d'être créé"}. +{"Label:","Étiquette :"}. {"Last Activity","Dernière activité"}. {"Last login","Dernière connexion"}. +{"Last message","Dernier message"}. {"Last month","Dernier mois"}. {"Last year","Dernière année"}. {"leaves the room","quitte le salon"}. @@ -192,7 +245,6 @@ {"Make room public searchable","Rendre le salon public"}. {"Malformed username","Nom d'utilisateur invalide"}. {"March","Mars"}. -{"Max # of items to persist","Nombre maximum d'éléments à stocker"}. {"Max payload size in bytes","Taille maximum pour le contenu du message en octet"}. {"Maximum file size","Taille maximale du fichier"}. {"Maximum Number of History Messages Returned by Room","Nombre maximal de messages d'historique renvoyés par salle"}. @@ -201,6 +253,7 @@ {"May","Mai"}. {"Membership is required to enter this room","Vous devez être membre pour accèder à ce salon"}. {"Members:","Membres :"}. +{"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.","Mémorisez votre mot de passe, ou écrivez-le sur un papier conservé dans un endroit secret. Dans XMPP il n'y a pas de mécanisme pour retrouver votre mot de passe si vous l'avez oublié."}. {"Memory","Mémoire"}. {"Message body","Corps du message"}. {"Message not found in forwarded payload","Message non trouvé dans l'enveloppe transférée"}. @@ -210,10 +263,13 @@ {"Middle Name","Autre nom"}. {"Minimum interval between voice requests (in seconds)","Intervalle minimum entre les demandes de 'voice' (en secondes)"}. {"Moderator privileges required","Les droits de modérateur sont nécessaires"}. +{"Moderator","Modérateur"}. +{"Moderators Only","Modérateurs uniquement"}. {"Modified modules","Modules mis à jour"}. {"Module failed to handle the query","Échec de traitement de la demande"}. {"Monday","Lundi"}. {"Multicast","Multidiffusion"}. +{"Multiple elements are not allowed by RFC6121","Les multiples éléments ne sont pas autorisés avec RFC6121"}. {"Multi-User Chat","Discussion de groupe"}. {"Name","Nom"}. {"Name:","Nom :"}. @@ -223,15 +279,20 @@ {"Neither 'role' nor 'affiliation' attribute found","Attribut 'role' ou 'affiliation' absent"}. {"Never","Jamais"}. {"New Password:","Nouveau mot de passe :"}. +{"Nickname can't be empty","Le pseudonyme ne peut être laissé vide"}. {"Nickname Registration at ","Enregistrement d'un pseudo sur "}. {"Nickname ~s does not exist in the room","Le pseudo ~s n'existe pas dans ce salon"}. {"Nickname","Pseudo"}. +{"No address elements found","Aucun élément d'adresse trouvé"}. +{"No addresses element found","Aucun élément d'adresses trouvé"}. {"No 'affiliation' attribute found","Attribut 'affiliation' absent"}. {"No available resource found","Aucune ressource disponible"}. {"No body provided for announce message","Pas de corps de message pour l'annonce"}. +{"No child elements found","Aucun élément enfant trouvé"}. {"No data form found","Formulaire non trouvé"}. {"No Data","Aucune information disponible"}. {"No features available","Aucune fonctionalité disponible"}. +{"No element found","Aucun élément trouvé"}. {"No hook has processed this command","Aucun gestionnaire n'a pris en charge cette commande"}. {"No info about last activity found","Aucune activité précédente trouvée"}. {"No 'item' element found","Aucun élément 'item' trouvé"}. @@ -249,23 +310,30 @@ {"No services available","Aucun service disponible"}. {"No statistics found for this item","Pas de statistiques"}. {"No 'to' attribute found in the invitation","L'élément 'to' est absent de l'invitation"}. +{"Nobody","Personne"}. {"Node already exists","Ce nœud existe déjà"}. {"Node ID","Identifiant du nœud"}. {"Node index not found","Index de nœud non trouvé"}. {"Node not found","Nœud non trouvé"}. {"Node ~p","Nœud ~p"}. +{"Node","Nœud"}. {"Nodeprep has failed","Échec de formattage"}. {"Nodes","Nœuds"}. {"None","Aucun"}. +{"Not allowed","Non autorisé"}. {"Not Found","Nœud non trouvé"}. {"Not subscribed","Pas abonné"}. {"Notify subscribers when items are removed from the node","Avertir les abonnés lorsque des éléments sont supprimés sur le nœud"}. {"Notify subscribers when the node configuration changes","Avertir les abonnés lorsque la configuration du nœud change"}. {"Notify subscribers when the node is deleted","Avertir les abonnés lorsque le nœud est supprimé"}. {"November","Novembre"}. +{"Number of answers required","Nombre de réponses requises"}. {"Number of occupants","Nombre d'occupants"}. +{"Number of Offline Messages","Nombre de messages hors ligne"}. {"Number of online users","Nombre d'utilisateurs en ligne"}. {"Number of registered users","Nombre d'utilisateurs enregistrés"}. +{"Occupants are allowed to invite others","Les occupants sont autorisés à inviter d’autres personnes"}. +{"Occupants May Change the Subject","Les occupants peuvent changer le sujet"}. {"October","Octobre"}. {"Offline Messages","Messages en attente"}. {"Offline Messages:","Messages hors ligne :"}. @@ -274,6 +342,7 @@ {"Online Users:","Utilisateurs connectés :"}. {"Online Users","Utilisateurs en ligne"}. {"Online","En ligne"}. +{"Only admins can see this","Seuls les administrateurs peuvent voir cela"}. {"Only deliver notifications to available users","Envoyer les notifications uniquement aux utilisateurs disponibles"}. {"Only or tags are allowed","Seul le tag ou est autorisé"}. {"Only element is allowed in this query","Seul l'élément est autorisé dans cette requête"}. @@ -283,6 +352,7 @@ {"Only moderators can approve voice requests","Seuls les modérateurs peuvent accépter les requêtes voix"}. {"Only occupants are allowed to send messages to the conference","Seuls les occupants peuvent envoyer des messages à la conférence"}. {"Only occupants are allowed to send queries to the conference","Seuls les occupants sont autorisés à envoyer des requêtes à la conférence"}. +{"Only publishers may publish","Seuls les éditeurs peuvent publier"}. {"Only service administrators are allowed to send service messages","Seuls les administrateurs du service sont autoriser à envoyer des messages de service"}. {"Organization Name","Nom de l'organisation"}. {"Organization Unit","Unité de l'organisation"}. @@ -290,6 +360,7 @@ {"Outgoing s2s Connections:","Connexions s2s sortantes :"}. {"Owner privileges required","Les droits de propriétaire sont nécessaires"}. {"Packet","Paquet"}. +{"Participant","Participant"}. {"Password Verification","Vérification du mot de passe"}. {"Password Verification:","Vérification du mot de passe :"}. {"Password","Mot de passe"}. @@ -299,13 +370,21 @@ {"Pending","En suspens"}. {"Period: ","Période : "}. {"Persist items to storage","Stockage persistant des éléments"}. +{"Persistent","Persistant"}. {"Ping query is incorrect","Requête ping incorrecte"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ces options sauvegardent uniquement la base de données interne Mnesia. Si vous utilisez le module ODBC vous devez sauvegarde votre base SQL séparément."}. {"Please, wait for a while before sending new voice request","Attendez un moment avant de re-lancer une requête de voix"}. {"Pong","Pong"}. +{"Possessing 'ask' attribute is not allowed by RFC6121","L'appartenance de l'attribut 'ask' n'est pas autorisé avec RFC6121"}. {"Present real Jabber IDs to","Rendre le Jabber ID réel visible pour"}. -{"private, ","privé"}. +{"Previous session not found","Session précédente introuvable"}. +{"Previous session PID has been killed","Le précédent PID de session a été tuée"}. +{"Previous session PID has exited","Le précédent PID de session a quitté"}. +{"Previous session PID is dead","Le précédent PID de session est mort"}. +{"Previous session timed out","La session précédente a expiré"}. +{"private, ","privé, "}. +{"Public","Public"}. {"Publish-Subscribe","Publication-Abonnement"}. {"PubSub subscriber request","Demande d'abonnement PubSub"}. {"Purge all items when the relevant publisher goes offline","Purger tous les items lorsque publieur est hors-ligne"}. @@ -314,7 +393,12 @@ {"RAM and disc copy","Copie en mémoire vive (RAM) et sur disque"}. {"RAM copy","Copie en mémoire vive (RAM)"}. {"Really delete message of the day?","Confirmer la suppression du message du jour ?"}. +{"Receive notification from all descendent nodes","Recevoir les notifications de tous les nœuds descendants"}. +{"Receive notification from direct child nodes only","Recevoir les notifications des nœuds enfants seulement"}. +{"Receive notification of new items only","Recevoir les notifications des nouveaux éléments uniquement"}. +{"Receive notification of new nodes only","Recevoir les notifications de tous les nouveaux nœuds descendants"}. {"Recipient is not in the conference room","Le destinataire n'est pas dans la conférence"}. +{"Register an XMPP account","Inscrire un compte XMPP"}. {"Registered Users","Utilisateurs enregistrés"}. {"Registered Users:","Utilisateurs enregistrés :"}. {"Register","Enregistrer"}. @@ -323,6 +407,9 @@ {"Remove User","Supprimer l'utilisateur"}. {"Remove","Supprimer"}. {"Replaced by new connection","Remplacé par une nouvelle connexion"}. +{"Request has timed out","La demande a expiré"}. +{"Request is ignored","La demande est ignorée"}. +{"Requested role","Rôle demandé"}. {"Resources","Ressources"}. {"Restart Service","Redémarrer le service"}. {"Restart","Redémarrer"}. @@ -337,28 +424,38 @@ {"Room Occupants","Occupants du salon"}. {"Room title","Titre du salon"}. {"Roster groups allowed to subscribe","Groupes de liste de contact autorisés à s'abonner"}. +{"Roster of ~ts","Liste de contacts de ~ts"}. {"Roster size","Taille de la liste de contacts"}. +{"Roster:","Liste de contacts :"}. {"RPC Call Error","Erreur d'appel RPC"}. {"Running Nodes","Nœuds actifs"}. +{"~s invites you to the room ~s","~s vous invite dans la salle de discussion ~s"}. {"Saturday","Samedi"}. {"Script check","Validation du script"}. {"Search Results for ","Résultats de recherche pour "}. +{"Search the text","Recherche le texte"}. +{"Search until the date","Rechercher jusqu’à la date"}. {"Search users in ","Rechercher des utilisateurs "}. +{"Select All","Tout sélectionner"}. {"Send announcement to all online users on all hosts","Envoyer l'annonce à tous les utilisateurs en ligne sur tous les serveurs"}. {"Send announcement to all online users","Envoyer l'annonce à tous les utilisateurs en ligne"}. {"Send announcement to all users on all hosts","Envoyer une annonce à tous les utilisateurs de tous les domaines"}. {"Send announcement to all users","Envoyer l'annonce à tous les utilisateurs"}. {"September","Septembre"}. {"Server:","Serveur :"}. +{"Service list retrieval timed out","La récupération de la liste des services a expiré"}. {"Set message of the day and send to online users","Définir le message du jour et l'envoyer aux utilisateurs en ligne"}. {"Set message of the day on all hosts and send to online users","Définir le message du jour pour tous domaines et l'envoyer aux utilisateurs en ligne"}. {"Shared Roster Groups","Groupes de liste de contacts partagée"}. {"Show Integral Table","Montrer la table intégralement"}. {"Show Ordinary Table","Montrer la table ordinaire"}. {"Shut Down Service","Arrêter le service"}. +{"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. +{"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Certains clients XMPP peuvent stocker votre mot de passe sur votre ordinateur. N'utilisez cette fonctionnalité que si vous avez confiance en la sécurité de votre ordinateur."}. {"Specify the access model","Définir le modèle d'accès"}. {"Specify the event message type","Définir le type de message d'événement"}. {"Specify the publisher model","Définir le modèle de publication"}. +{"Stanza ID","Identifiant Stanza"}. {"Statistics of ~p","Statistiques de ~p"}. {"Statistics","Statistiques"}. {"Stop","Arrêter"}. @@ -366,33 +463,64 @@ {"Storage Type","Type de stockage"}. {"Store binary backup:","Sauvegarde binaire :"}. {"Store plain text backup:","Sauvegarde texte :"}. +{"Stream management is already enabled","La gestion des flux est déjà activée"}. {"Subject","Sujet"}. {"Submit","Soumettre"}. {"Submitted","Soumis"}. {"Subscriber Address","Adresse de l'abonné"}. +{"Subscribers may publish","Les souscripteurs peuvent publier"}. {"Subscription","Abonnement"}. {"Subscriptions are not allowed","Les abonnement ne sont pas autorisés"}. {"Sunday","Dimanche"}. +{"Text associated with a picture","Texte associé à une image"}. +{"Text associated with a sound","Texte associé à un son"}. +{"Text associated with a video","Texte associé à une vidéo"}. +{"Text associated with speech","Texte associé au discours"}. {"That nickname is already in use by another occupant","Le pseudo est déjà utilisé par un autre occupant"}. {"That nickname is registered by another person","Le pseudo est enregistré par une autre personne"}. -{"The CAPTCHA is valid.","Le CAPTCHA est valide"}. +{"The account already exists","Le compte existe déjà"}. +{"The account was not unregistered","Le compte n’a pas été désinscrit"}. +{"The body text of the last received message","Le corps du texte du dernier message reçu"}. +{"The CAPTCHA is valid.","Le CAPTCHA est valide."}. {"The CAPTCHA verification has failed","La vérification du CAPTCHA a échoué"}. +{"The captcha you entered is wrong","Le captcha que vous avez saisi est erroné"}. {"The collections with which a node is affiliated","Les collections avec lesquelle un nœud est affilié"}. +{"The datetime when the node was created","La date à laquelle le nœud a été créé"}. {"The default language of the node","La langue par défaut du nœud"}. {"The feature requested is not supported by the conference","La demande de fonctionalité n'est pas supportée par la conférence"}. +{"The JID of the node creator","Le JID du créateur du nœud"}. +{"The list of all online users","Les utilisateurs en ligne"}. +{"The list of all users","La liste de tous les utilisateurs"}. {"The name of the node","Le nom du nœud"}. +{"The node is a collection node","Le nœud est un nœud de collecte"}. +{"The node is a leaf node (default)","Ce nœud est un nœud feuille (défaut)"}. +{"The number of subscribers to the node","Le nombre d’enregistrés au nœud"}. +{"The number of unread or undelivered messages","Le nombre de messages non lus ou non remis"}. {"The password contains unacceptable characters","Le mot de passe contient des caractères non-acceptables"}. {"The password is too weak","Le mot de passe est trop faible"}. {"the password is","le mot de passe est"}. +{"The password of your XMPP account was successfully changed.","Le mot de passe de votre compte XMPP a été modifié avec succès."}. +{"The password was not changed","Le mot de passe n’a pas été modifié"}. +{"The passwords are different","Les mots de passe sont différents"}. {"The query is only allowed from local users","La requête n'est autorisé qu'aux utilisateurs locaux"}. {"The query must not contain elements","La requête ne doit pas contenir d'élément "}. +{"The room subject can be modified by participants","Le sujet du salon peut être modifié par les participants"}. +{"The sender of the last received message","L’expéditeur du dernier message reçu"}. +{"The subscription identifier associated with the subscription request","L’identificateur d’abonnement associé à la demande d’abonnement"}. +{"There was an error changing the password: ","Une erreur s’est produite lors de la modification du mot de passe : "}. {"There was an error creating the account: ","Il y a eu une erreur en créant le compte : "}. {"There was an error deleting the account: ","Il y a eu une erreur en effaçant le compte : "}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","C'est insensible à la casse : macbeth est identique à MacBeth et Macbeth."}. +{"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Cette page permet de créer un compte XMPP sur ce serveur XMPP. Votre JID (Jabber IDentifier, identifiant Jabber) sera de la forme : nom@serveur. Prière de lire avec attention les instructions pour remplir correctement ces champs."}. +{"This page allows to unregister an XMPP account in this XMPP server.","Cette page permet de désenregistrer un compte XMPP sur ce serveur XMPP."}. {"This room is not anonymous","Ce salon n'est pas anonyme"}. +{"This service can not process the address: ~s","Ce service ne peut pas traiter l’adresse : ~s"}. {"Thursday","Jeudi"}. {"Time delay","Délais"}. +{"Timed out waiting for stream resumption","Expiration du délai d’attente pour la reprise du flux"}. {"Time","Heure"}. {"To register, visit ~s","Pour vous enregistrer, visitez ~s"}. +{"To ~ts","À ~ts"}. {"To","A"}. {"Token TTL","Jeton TTL"}. {"Too many active bytestreams","Trop de flux SOCKS5 actifs"}. @@ -400,6 +528,7 @@ {"Too many elements","Trop d'éléments "}. {"Too many elements","Trop d'éléments "}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Trop (~p) d'authentification ont échoué pour cette adresse IP (~s). L'adresse sera débloquée à ~s UTC"}. +{"Too many receiver fields were specified","Trop de champs de récepteurs ont été spécifiés"}. {"Too many unacked stanzas","Trop de stanzas sans accusé de réception (ack)"}. {"Too many users in this conference","Trop d'utilisateurs dans cette conférence"}. {"Total rooms","Nombre de salons"}. @@ -413,8 +542,12 @@ {"Unable to register route on existing local domain","Impossible d'enregistrer la route sur un domaine locale existant"}. {"Unauthorized","Non autorisé"}. {"Unexpected action","Action inattendu"}. +{"Unexpected error condition: ~p","Condition d’erreur inattendue : ~p"}. +{"Unregister an XMPP account","Annuler l’enregistrement d’un compte XMPP"}. {"Unregister","Désinscrire"}. +{"Unselect All","Tout désélectionner"}. {"Unsupported element","Elément non supporté"}. +{"Unsupported version","Version non prise en charge"}. {"Update message of the day (don't send)","Mise à jour du message du jour (pas d'envoi)"}. {"Update message of the day on all hosts (don't send)","Mettre à jour le message du jour sur tous les domaines (ne pas envoyer)"}. {"Update plan","Plan de mise à jour"}. @@ -422,12 +555,15 @@ {"Update script","Script de mise à jour"}. {"Update","Mettre à jour"}. {"Uptime:","Temps depuis le démarrage :"}. +{"URL for Archived Discussion Logs","URL des journaux de discussion archivés"}. {"User already exists","L'utilisateur existe déjà"}. -{"User JID","JID de l'utilisateur "}. +{"User JID","JID de l'utilisateur"}. {"User (jid)","Utilisateur (jid)"}. {"User Management","Gestion des utilisateurs"}. +{"User removed","Utilisateur supprimé"}. {"User session not found","Session utilisateur non trouvée"}. {"User session terminated","Session utilisateur terminée"}. +{"User ~ts","Utilisateur ~ts"}. {"Username:","Nom d'utilisateur :"}. {"Users are not allowed to register accounts so quickly","Les utilisateurs ne sont pas autorisés à enregistrer des comptes si rapidement"}. {"Users Last Activity","Dernière activité des utilisateurs"}. @@ -440,15 +576,27 @@ {"Value of '~s' should be integer","La valeur de '~s' doit être un entier"}. {"Value 'set' of 'type' attribute is not allowed","La valeur de l'attribut 'type' ne peut être 'set'"}. {"vCard User Search","Recherche dans l'annnuaire"}. +{"View Queue","Afficher la file d’attente"}. {"Virtual Hosts","Serveurs virtuels"}. {"Visitors are not allowed to change their nicknames in this room","Les visiteurs ne sont pas autorisés à changer de pseudo dans ce salon"}. {"Visitors are not allowed to send messages to all occupants","Les visiteurs ne sont pas autorisés à envoyer des messages à tout les occupants"}. +{"Visitor","Visiteur"}. {"Voice request","Demande de voix"}. {"Voice requests are disabled in this conference","Les demandes de voix sont désactivées dans cette conférence"}. {"Wednesday","Mercredi"}. {"When a new subscription is processed","Quand un nouvel abonnement est traité"}. {"When to send the last published item","A quel moment envoyer le dernier élément publié"}. -{"Whether to allow subscriptions","Autoriser l'abonnement ?"}. +{"Whether an entity wants to receive or disable notifications","Quand une entité veut recevoir ou désactiver les notifications"}. +{"Whether owners or publisher should receive replies to items","Quand les propriétaires ou annonceurs doivent revoir des réponses à leurs éléments"}. +{"Whether to allow subscriptions","Autoriser ou non les abonnements"}. +{"Whether to notify owners about new subscribers and unsubscribes","Quand notifier le propriétaire à propos des nouvelles souscriptions et désinscriptions"}. +{"Wrong parameters in the web formulary","Paramètres erronés dans le formulaire Web"}. +{"Wrong xmlns","Xmlns incorrect"}. +{"XMPP Account Registration","Enregistrement de compte XMPP"}. +{"XMPP Domains","Domaines XMPP"}. +{"You are being removed from the room because of a system shutdown","Vous avez été éjecté du salon de discussion en raison de l'arrêt du système"}. +{"You are not joined to the channel","Vous n'avez pas rejoint ce canal"}. +{"You can later change your password using an XMPP client.","Vous pouvez modifier ultérieurement votre mot de passe à l’aide d’un client XMPP."}. {"You have been banned from this room","Vous avez été exclus de ce salon"}. {"You have joined too many conferences","Vous avec rejoint trop de conférences"}. {"You must fill in field \"Nickname\" in the form","Vous devez préciser le champ \"pseudo\" dans le formulaire"}. @@ -458,4 +606,6 @@ {"Your active privacy list has denied the routing of this stanza.","Votre règle de flitrage active a empêché le routage de ce stanza."}. {"Your contact offline message queue is full. The message has been discarded.","La file d'attente de message de votre contact est pleine. Votre message a été détruit."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Vos messages pour ~s sont bloqués. Pour les débloquer, veuillez visiter ~s"}. +{"Your XMPP account was successfully registered.","Votre compte XMPP a été enregistré avec succès."}. +{"Your XMPP account was successfully unregistered.","Votre compte XMPP a été désinscrit avec succès."}. {"You're not allowed to create nodes","Vous n'êtes pas autorisé à créer des nœuds"}. diff --git a/priv/msgs/gl.msg b/priv/msgs/gl.msg index 817605f1a..07b35e994 100644 --- a/priv/msgs/gl.msg +++ b/priv/msgs/gl.msg @@ -181,7 +181,6 @@ {"Make room public searchable","Sala publicamente visible"}. {"Malformed username","Nome de usuario mal formado"}. {"March","Marzo"}. -{"Max # of items to persist","Máximo # de elementos que persisten"}. {"Max payload size in bytes","Máximo tamaño do payload en bytes"}. {"Maximum Number of Occupants","Número máximo de ocupantes"}. {"May","Maio"}. diff --git a/priv/msgs/he.msg b/priv/msgs/he.msg index d73e43acb..5a4926a88 100644 --- a/priv/msgs/he.msg +++ b/priv/msgs/he.msg @@ -170,7 +170,6 @@ {"Make room public searchable","הפוך חדר לחדר שנתון לחיפוש פומבי"}. {"Malformed username","שם משתמש פגום"}. {"March","מרץ"}. -{"Max # of items to persist","מספר מרבי של פריטים לקיבוע"}. {"Max payload size in bytes","גודל מרבי של מטען ייעוד (payload) ביחידות מידה של byte"}. {"Maximum Number of Occupants","מספר מרבי של נוכחים"}. {"May","מאי"}. diff --git a/priv/msgs/hu.msg b/priv/msgs/hu.msg index abb7d3f2f..bf7782e49 100644 --- a/priv/msgs/hu.msg +++ b/priv/msgs/hu.msg @@ -388,7 +388,6 @@ {"The query is only allowed from local users","A lekérdezés csak helyi felhasználóktól engedélyezett"}. {"The query must not contain elements","A lekérdezés nem tartalmazhat elemeket"}. {"The stanza MUST contain only one element, one element, or one element","A stanzának csak egyetlen elemet, egyetlen elemet vagy egyetlen elemet KELL tartalmaznia"}. -{"The username is not valid","A felhasználónév nem érvényes"}. {"There was an error creating the account: ","Hiba történt a fiók létrehozásakor: "}. {"There was an error deleting the account: ","Hiba történt a fiók törlésekor: "}. {"This room is not anonymous","Ez a szoba nem névtelen"}. diff --git a/priv/msgs/id.msg b/priv/msgs/id.msg index 722a8988e..08a3df2b4 100644 --- a/priv/msgs/id.msg +++ b/priv/msgs/id.msg @@ -227,7 +227,6 @@ {"Make room persistent","Buat ruangan menjadi permanent"}. {"Make room public searchable","Buat ruangan dapat dicari"}. {"March","Maret"}. -{"Max # of items to persist","Max item untuk bertahan"}. {"Max payload size in bytes","Max kapasitas ukuran dalam bytes"}. {"Maximum Number of Occupants","Maksimum Jumlah Penghuni"}. {"May","Mei"}. @@ -423,7 +422,6 @@ {"the password is","kata sandinya"}. {"The password was not changed","Kata sandi belum berubah"}. {"The passwords are different","Kata sandi berbeda"}. -{"The username is not valid","Nama pengguna tidak valid"}. {"There was an error changing the password: ","Ada kesalahan saat merubah kata kunci: "}. {"There was an error creating the account: ","Ada kesalahan saat membuat akun: "}. {"There was an error deleting the account: ","Ada kesalahan saat menghapus akun: "}. diff --git a/priv/msgs/it.msg b/priv/msgs/it.msg index 627707e89..8f117c9f2 100644 --- a/priv/msgs/it.msg +++ b/priv/msgs/it.msg @@ -145,7 +145,6 @@ {"Make room persistent","Rendere la stanza persistente"}. {"Make room public searchable","Rendere la sala visibile al pubblico"}. {"March","Marzo"}. -{"Max # of items to persist","Numero massimo di elementi da conservare persistentemente"}. {"Max payload size in bytes","Dimensione massima del contenuto del messaggio in byte"}. {"Maximum Number of Occupants","Numero massimo di occupanti"}. {"May","Maggio"}. diff --git a/priv/msgs/ja.msg b/priv/msgs/ja.msg index 89531449e..dbdb1ed8a 100644 --- a/priv/msgs/ja.msg +++ b/priv/msgs/ja.msg @@ -191,7 +191,6 @@ {"Make room public searchable","チャットルームを検索可"}. {"Malformed username","不正な形式のユーザー名"}. {"March","3月"}. -{"Max # of items to persist","アイテムの最大保存数"}. {"Max payload size in bytes","最大ぺイロードサイズ (byte)"}. {"Maximum file size","最大ファイルサイズ"}. {"Maximum Number of Occupants","最大在室者数"}. @@ -367,7 +366,6 @@ {"The password of your XMPP account was successfully changed.","XMPP アカウントのパスワード変更に成功しました。"}. {"The password was not changed","このパスワードは変更されませんでした"}. {"The passwords are different","このパスワードが違います"}. -{"The username is not valid","ユーザー名が正しくありません"}. {"There was an error creating the account: ","アカウントの作成中にエラーが発生しました: "}. {"There was an error deleting the account: ","アカウントの削除中にエラーが発生しました: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","大文字と小文字は区別しません: macbeth は MacBeth や Macbeth と同じです。"}. diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg index 65f359987..5a5010b89 100644 --- a/priv/msgs/nl.msg +++ b/priv/msgs/nl.msg @@ -150,7 +150,6 @@ {"Make room persistent","Chatruimte blijvend maken"}. {"Make room public searchable","Chatruimte doorzoekbaar maken"}. {"March","Maart"}. -{"Max # of items to persist","Maximum aantal in het geheugen te bewaren items"}. {"Max payload size in bytes","Maximumgrootte van bericht in bytes"}. {"Maximum Number of Occupants","Maximum aantal aanwezigen"}. {"May","Mei"}. diff --git a/priv/msgs/no.msg b/priv/msgs/no.msg index 40b6a6727..e883518a4 100644 --- a/priv/msgs/no.msg +++ b/priv/msgs/no.msg @@ -165,7 +165,6 @@ {"Make room persistent","Gjør rommet vedvarende"}. {"Make room public searchable","Gjør rommet offentlig søkbart"}. {"March","mars"}. -{"Max # of items to persist","Høyeste # elementer som skal lagres"}. {"Maximum file size","Maksimal filstørrelse"}. {"Maximum Number of History Messages Returned by Room","Maksimalt antall historikkmeldinger tilbudt av rommet"}. {"May","mai"}. @@ -333,7 +332,6 @@ {"The query must not contain elements","Spørringen kan ikke inneholde -elementer"}. {"The room subject can be modified by participants","Romemnet kan endres av dets deltagere"}. {"The sender of the last received message","Avsender for sist mottatte melding"}. -{"The username is not valid","Brukernavnet er ikke gyldig"}. {"There was an error creating the account: ","En feil inntraff under oppretting av kontoen: "}. {"There was an error deleting the account: ","En feil inntraff under sletting av kontoen: "}. {"This room is not anonymous","Dette rommet er ikke anonymt"}. diff --git a/priv/msgs/pl.msg b/priv/msgs/pl.msg index 3f9e80a8f..52cc374a1 100644 --- a/priv/msgs/pl.msg +++ b/priv/msgs/pl.msg @@ -181,7 +181,6 @@ {"Make room public searchable","Pozwól wyszukiwać pokój"}. {"Malformed username","Nieprawidłowa nazwa użytkownika"}. {"March","Marzec"}. -{"Max # of items to persist","Maksymalna liczba przechowywanych przedmiotów"}. {"Max payload size in bytes","Maksymalna wielkość powiadomienia w bajtach"}. {"Maximum Number of Occupants","Maksymalna liczba uczestników"}. {"May","Maj"}. diff --git a/priv/msgs/pt-br.msg b/priv/msgs/pt-br.msg index dab5d2d99..bd295de2e 100644 --- a/priv/msgs/pt-br.msg +++ b/priv/msgs/pt-br.msg @@ -20,6 +20,7 @@ {"Access model","Modelo de acesso"}. {"Account doesn't exist","A conta não existe"}. {"Action on user","Ação no usuário"}. +{"Add a hat to a user","Adiciona um chapéu num usuário"}. {"Add Jabber ID","Adicionar ID jabber"}. {"Add New","Adicionar novo"}. {"Add User","Adicionar usuário"}. @@ -78,6 +79,7 @@ {"Changing role/affiliation is not allowed","Não é permitida a alteração da função/afiliação"}. {"Channel already exists","O canal já existe"}. {"Channel does not exist","O canal não existe"}. +{"Channel JID","Canal JID"}. {"Channels","Canais"}. {"Characters not allowed:","Caracteres não aceitos:"}. {"Chatroom configuration modified","Configuração da sala de bate-papo modificada"}. @@ -97,6 +99,7 @@ {"Configuration","Configuração"}. {"Connected Resources:","Recursos conectados:"}. {"Contact Addresses (normally, room owner or owners)","Endereços de contato (normalmente, o proprietário ou os proprietários da sala)"}. +{"Contrib Modules","Módulos contrib"}. {"Country","País"}. {"CPU Time:","Tempo da CPU:"}. {"Current Discussion Topic","Assunto em discussão"}. @@ -136,6 +139,7 @@ {"Elements","Elementos"}. {"Email Address","Endereço de e-mail"}. {"Email","Email"}. +{"Enable hats","Ativa chapéus"}. {"Enable logging","Permitir criação de logs"}. {"Enable message archiving","Habilitar arquivamento de mensagens"}. {"Enabling push without 'node' attribute is not supported","Abilitar push sem o atributo 'node' não é suportado"}. @@ -170,6 +174,8 @@ {"Full List of Room Admins","Lista completa dos administradores das salas"}. {"Full List of Room Owners","Lista completa dos proprietários das salas"}. {"Full Name","Nome completo"}. +{"Get List of Online Users","Obter a lista de usuários online"}. +{"Get List of Registered Users","Obter a lista de usuários registrados"}. {"Get Number of Online Users","Obter Número de Usuários Online"}. {"Get Number of Registered Users","Obter Número de Usuários Registrados"}. {"Get Pending","Obter os pendentes"}. @@ -186,6 +192,9 @@ {"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. {"has been kicked because the room has been changed to members-only","foi desconectado porque a política da sala mudou, só membros são permitidos"}. {"has been kicked","foi removido"}. +{"Hat title","Título do chapéu"}. +{"Hat URI","URI do chapéu"}. +{"Hats limit exceeded","O limite dos chapéus foi excedido"}. {"Host unknown","Máquina desconhecida"}. {"Host","Máquina"}. {"HTTP File Upload","Upload de arquivo HTTP"}. @@ -208,6 +217,8 @@ {"Incorrect value of 'action' attribute","Valor incorreto do atributo 'action'"}. {"Incorrect value of 'action' in data form","Valor incorreto de 'action' no formulário de dados"}. {"Incorrect value of 'path' in data form","Valor incorreto de 'path' no formulário de dados"}. +{"Installed Modules:","Módulos instalados:"}. +{"Install","Instalar"}. {"Insufficient privilege","Privilégio insuficiente"}. {"Internal server error","Erro interno do servidor"}. {"Invalid 'from' attribute in forwarded message","Atributo 'from' inválido na mensagem reenviada"}. @@ -224,6 +235,8 @@ {"January","Janeiro"}. {"JID normalization denied by service policy","Normalização JID negada por causa da política de serviços"}. {"JID normalization failed","A normalização JID falhou"}. +{"Joined MIX channels of ~ts","Entrou no canais MIX do ~ts"}. +{"Joined MIX channels:","Uniu-se aos canais MIX:"}. {"joins the room","Entrar na sala"}. {"July","Julho"}. {"June","Junho"}. @@ -237,6 +250,8 @@ {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Bits menos significativos do hash sha-256 do texto devem ser iguais ao rótulo hexadecimal"}. {"leaves the room","Sair da sala"}. {"List of rooms","Lista de salas"}. +{"List of users with hats","Lista os usuários com chapéus"}. +{"List users with hats","Lista os usuários com chapéus"}. {"Logging","Registrando no log"}. {"Low level update script","Script de atualização low level"}. {"Make participants list public","Tornar pública a lista de participantes"}. @@ -249,7 +264,7 @@ {"Malformed username","Nome de usuário inválido"}. {"MAM preference modification denied by service policy","Modificação de preferência MAM negada por causa da política de serviços"}. {"March","Março"}. -{"Max # of items to persist","Máximo # de elementos que persistem"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Máximo # de itens para persistir ou `max` para nenhum limite específico que não seja um servidor imposto como máximo"}. {"Max payload size in bytes","Máximo tamanho do payload em bytes"}. {"Maximum file size","Tamanho máximo do arquivo"}. {"Maximum Number of History Messages Returned by Room","Quantidade máxima das mensagens do histórico que foram devolvidas por sala"}. @@ -340,8 +355,9 @@ {"Number of Offline Messages","Quantidade das mensagens offline"}. {"Number of online users","Número de usuários online"}. {"Number of registered users","Número de usuários registrados"}. -{"Number of seconds after which to automatically purge items","Quantidade de segundos para excluir os itens automaticamente"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Quantidade de segundos após limpar automaticamente os itens ou `max` para nenhum limite específico que não seja um servidor imposto máximo"}. {"Occupants are allowed to invite others","As pessoas estão autorizadas a convidar outras pessoas"}. +{"Occupants are allowed to query others","Os ocupantes estão autorizados a consultar os outros"}. {"Occupants May Change the Subject","As pessoas talvez possam alterar o assunto"}. {"October","Outubro"}. {"Offline Messages","Mensagens offline"}. @@ -368,11 +384,13 @@ {"Only those on a whitelist may subscribe and retrieve items","Apenas aqueles presentes em uma lista branca podem se inscrever e recuperar os itens"}. {"Organization Name","Nome da organização"}. {"Organization Unit","Departamento/Unidade"}. +{"Other Modules Available:","Outros módulos disponíveis:"}. {"Outgoing s2s Connections","Conexões s2s de Saída"}. {"Outgoing s2s Connections:","Saída das conexões s2s:"}. {"Owner privileges required","Se requer privilégios de proprietário da sala"}. {"Packet relay is denied by service policy","A retransmissão de pacote é negada por causa da política de serviço"}. {"Packet","Pacote"}. +{"Participant ID","ID do participante"}. {"Participant","Participante"}. {"Password Verification:","Verificação da Senha:"}. {"Password Verification","Verificação de Senha"}. @@ -419,6 +437,7 @@ {"Registered Users","Usuários Registrados"}. {"Register","Registrar"}. {"Remote copy","Cópia remota"}. +{"Remove a hat from a user","Remove um chapéu de um usuário"}. {"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. {"Remove User","Remover usuário"}. {"Remove","Remover"}. @@ -474,6 +493,7 @@ {"Shut Down Service","Parar Serviço"}. {"SOCKS5 Bytestreams","Bytestreams SOCKS5"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clientes XMPP podem armazenar a sua senha no seu computador, só faça isso no seu computador particular por questões de segurança."}. +{"Sources Specs:","Especificações das fontes:"}. {"Specify the access model","Especificar os modelos de acesso"}. {"Specify the event message type","Especificar o tipo de mensagem para o evento"}. {"Specify the publisher model","Especificar o modelo do publicante"}. @@ -519,8 +539,10 @@ {"The JIDs of those to contact with questions","Os JIDs daqueles para entrar em contato com perguntas"}. {"The JIDs of those with an affiliation of owner","Os JIDs daqueles com uma afiliação de proprietário"}. {"The JIDs of those with an affiliation of publisher","Os JIDs daqueles com uma afiliação de editor"}. +{"The list of all online users","A lista de todos os usuários online"}. +{"The list of all users","A lista de todos os usuários"}. {"The list of JIDs that may associate leaf nodes with a collection","A lista dos JIDs que podem associar as páginas dos nós em uma coleção"}. -{"The maximum number of child nodes that can be associated with a collection","A quantidade máxima dos nós relacionados que podem ser associados com uma coleção"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","A quantidade máxima de nós relacionados que podem ser associados a uma coleção ou `máximo` para nenhum limite específico que não seja um servidor imposto no máximo"}. {"The minimum number of milliseconds between sending any two notification digests","O número mínimo de milissegundos entre o envio do resumo das duas notificações"}. {"The name of the node","O nome do nó"}. {"The node is a collection node","O nó é um nó da coleção"}. @@ -545,7 +567,6 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","O tipo dos dados do nó, normalmente definido pelo espaço dos nomes da carga útil (caso haja)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","O URL da transformação XSL que pode ser aplicada nas cargas úteis para gerar um elemento apropriado no corpo da mensagem."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","A URL de uma transformação XSL que pode ser aplicada ao formato de carga útil para gerar um Formulário de Dados válido onde o cliente possa exibir usando um mecanismo genérico de renderização do Formulários de Dados"}. -{"The username is not valid","O nome do usuário não é válido"}. {"There was an error changing the password: ","Houve um erro ao alterar a senha: "}. {"There was an error creating the account: ","Houve um erro ao criar esta conta: "}. {"There was an error deleting the account: ","Houve um erro ao deletar esta conta: "}. @@ -584,6 +605,7 @@ {"Unauthorized","Não Autorizado"}. {"Unexpected action","Ação inesperada"}. {"Unexpected error condition: ~p","Condição de erro inesperada: ~p"}. +{"Uninstall","Desinstalar"}. {"Unregister an XMPP account","Excluir uma conta XMPP"}. {"Unregister","Deletar registro"}. {"Unselect All","Desmarcar todos"}. @@ -594,7 +616,10 @@ {"Update ~p","Atualizar ~p"}. {"Update plan","Plano de Atualização"}. {"Update script","Script de atualização"}. +{"Update specs to get modules source, then install desired ones.","Atualize as especificações para obter a fonte dos módulos e instale os que desejar."}. +{"Update Specs","Atualizar as especificações"}. {"Update","Atualizar"}. +{"Upgrade","Atualização"}. {"Uptime:","Tempo de atividade:"}. {"URL for Archived Discussion Logs","A URL para o arquivamento dos registros da discussão"}. {"User already exists","Usuário já existe"}. @@ -617,6 +642,7 @@ {"Value of '~s' should be integer","Valor de '~s' deveria ser um inteiro"}. {"Value 'set' of 'type' attribute is not allowed","Valor 'set' não permitido para atributo 'type'"}. {"vCard User Search","Busca de Usuário vCard"}. +{"View joined MIX channels","Exibir os canais MIX aderidos"}. {"View Queue","Exibir a fila"}. {"View Roster","Ver a lista"}. {"Virtual Hosts","Hosts virtuais"}. diff --git a/priv/msgs/pt.msg b/priv/msgs/pt.msg index 65e98cc74..99a8de3a3 100644 --- a/priv/msgs/pt.msg +++ b/priv/msgs/pt.msg @@ -20,6 +20,7 @@ {"Access model","Modelo de acesso"}. {"Account doesn't exist","A conta não existe"}. {"Action on user","Acção no utilizador"}. +{"Add a hat to a user","Adiciona um chapéu num utilizador"}. {"Add Jabber ID","Adicionar ID jabber"}. {"Add New","Adicionar novo"}. {"Add User","Adicionar utilizador"}. @@ -136,6 +137,7 @@ {"Elements","Elementos"}. {"Email Address","Endereço de e-mail"}. {"Email","Email"}. +{"Enable hats","Ativa chapéus"}. {"Enable logging","Permitir criação de logs"}. {"Enable message archiving","Ativar arquivamento de mensagens"}. {"Enabling push without 'node' attribute is not supported","Abilitar push sem o atributo 'node' não é suportado"}. @@ -186,6 +188,9 @@ {"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. {"has been kicked because the room has been changed to members-only","foi desconectado porque a política da sala mudou, só membros são permitidos"}. {"has been kicked","foi removido"}. +{"Hat title","Título do chapéu"}. +{"Hat URI","URI do chapéu"}. +{"Hats limit exceeded","O limite dos chapéus foi excedido"}. {"Host unknown","Máquina desconhecida"}. {"Host","Máquina"}. {"HTTP File Upload","Upload de ficheiros por HTTP"}. @@ -237,6 +242,8 @@ {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Bits menos significativos do hash sha-256 do texto devem ser iguais ao rótulo hexadecimal"}. {"leaves the room","Sair da sala"}. {"List of rooms","Lista de salas"}. +{"List of users with hats","Lista os utilizadores com chapéus"}. +{"List users with hats","Lista os utilizadores com chapéus"}. {"Logging","Registando no log"}. {"Low level update script","Script de atualização low level"}. {"Make participants list public","Tornar pública a lista de participantes"}. @@ -249,7 +256,7 @@ {"Malformed username","Nome de utilizador inválido"}. {"MAM preference modification denied by service policy","Modificação de preferência MAM negada por causa da política de serviços"}. {"March","Março"}. -{"Max # of items to persist","Máximo # de elementos que persistem"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","Máximo # de itens para persistir ou `max` para nenhum limite específico que não seja um servidor imposto como máximo"}. {"Max payload size in bytes","Máximo tamanho do payload em bytes"}. {"Maximum file size","Tamanho máximo do ficheiro"}. {"Maximum Number of History Messages Returned by Room","Quantidade máxima das mensagens do histórico que foram devolvidas por sala"}. @@ -340,7 +347,7 @@ {"Number of Offline Messages","Quantidade das mensagens offline"}. {"Number of online users","Quantidade de utilizadores online"}. {"Number of registered users","Quantidade de utilizadores registados"}. -{"Number of seconds after which to automatically purge items","Quantidade de segundos para excluir os itens automaticamente"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","Quantidade de segundos após limpar automaticamente os itens ou `max` para nenhum limite específico que não seja um servidor imposto máximo"}. {"Occupants are allowed to invite others","As pessoas estão autorizadas a convidar outras pessoas"}. {"Occupants May Change the Subject","As pessoas talvez possam alterar o assunto"}. {"October","Outubro"}. @@ -419,6 +426,7 @@ {"Registered Users:","Utilizadores registados:"}. {"Register","Registar"}. {"Remote copy","Cópia remota"}. +{"Remove a hat from a user","Remove um chapéu de um utilizador"}. {"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. {"Remove User","Eliminar utilizador"}. {"Remove","Remover"}. @@ -520,7 +528,7 @@ {"The JIDs of those with an affiliation of owner","Os JIDs daqueles com uma afiliação de proprietário"}. {"The JIDs of those with an affiliation of publisher","Os JIDs daqueles com uma afiliação de editor"}. {"The list of JIDs that may associate leaf nodes with a collection","A lista dos JIDs que podem associar as páginas dos nós numa coleção"}. -{"The maximum number of child nodes that can be associated with a collection","A quantidade máxima dos nós relacionados que podem ser associados com uma coleção"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","A quantidade máxima de nós relacionados que podem ser associados a uma coleção ou `máximo` para nenhum limite específico que não seja um servidor imposto no máximo"}. {"The minimum number of milliseconds between sending any two notification digests","A quantidade mínima de milissegundos entre o envio do resumo das duas notificações"}. {"The name of the node","O nome do nó"}. {"The node is a collection node","O nó é um nó da coleção"}. @@ -545,7 +553,6 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","O tipo dos dados do nó, normalmente definido pelo espaço dos nomes da carga útil (caso haja)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","O URL da transformação XSL que pode ser aplicada nas cargas úteis para gerar um elemento apropriado no corpo da mensagem."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","A URL de uma transformação XSL que pode ser aplicada ao formato de carga útil para gerar um Formulário de Dados válido onde o cliente possa exibir usando um mecanismo genérico de renderização do Formulários de Dados"}. -{"The username is not valid","O nome do utilizador não é válido"}. {"There was an error changing the password: ","Houve um erro ao alterar a palavra-passe: "}. {"There was an error creating the account: ","Houve um erro ao criar esta conta: "}. {"There was an error deleting the account: ","Houve um erro ao deletar esta conta: "}. diff --git a/priv/msgs/ru.msg b/priv/msgs/ru.msg index c52cd7e41..962c83ae8 100644 --- a/priv/msgs/ru.msg +++ b/priv/msgs/ru.msg @@ -7,6 +7,7 @@ {" has set the subject to: "," установил(а) тему: "}. {"A friendly name for the node","Легко запоминаемое имя для узла"}. {"A password is required to enter this room","Чтобы войти в эту конференцию, нужен пароль"}. +{"A Web Page","Веб-страница"}. {"Accept","Принять"}. {"Access denied by service policy","Доступ запрещён политикой службы"}. {"Account doesn't exist","Учётная запись не существует"}. @@ -20,6 +21,7 @@ {"All activity","Вся статистика"}. {"All Users","Все пользователи"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Разрешить этому Jabber ID подписаться на данный узел?"}. +{"Allow this person to register with the room?","Разрешить пользователю зарегистрироваться в комнате?"}. {"Allow users to change the subject","Разрешить пользователям изменять тему"}. {"Allow users to query other users","Разрешить iq-запросы к пользователям"}. {"Allow users to send invites","Разрешить пользователям посылать приглашения"}. @@ -201,7 +203,6 @@ {"Malformed username","Недопустимое имя пользователя"}. {"MAM preference modification denied by service policy","Изменение настроек архива сообщений запрещено политикой службы"}. {"March","марта"}. -{"Max # of items to persist","Максимальное число сохраняемых публикаций"}. {"Max payload size in bytes","Максимальный размер полезной нагрузки в байтах"}. {"Maximum Number of Occupants","Максимальное количество участников"}. {"May","мая"}. @@ -411,7 +412,6 @@ {"The query is only allowed from local users","Запрос доступен только для локальных пользователей"}. {"The query must not contain elements","Запрос не должен содержать элементов "}. {"The stanza MUST contain only one element, one element, or one element","Строфа может содержать только один элемент , один элемент или один элемент "}. -{"The username is not valid","Недопустимое имя пользователя"}. {"There was an error creating the account: ","Ошибка при создании аккаунта:"}. {"There was an error deleting the account: ","Ошибка при удалении аккаунта:"}. {"This room is not anonymous","Эта комната не анонимная"}. diff --git a/priv/msgs/sk.msg b/priv/msgs/sk.msg index f78303ec4..aaee00ef1 100644 --- a/priv/msgs/sk.msg +++ b/priv/msgs/sk.msg @@ -144,7 +144,6 @@ {"Make room persistent","Nastaviť miestnosť ako trvalú"}. {"Make room public searchable","Nastaviť miestnosť ako verejne prehľadávateľnú"}. {"March","Marec"}. -{"Max # of items to persist","Maximálny počet položiek, ktoré je možné natrvalo uložiť"}. {"Max payload size in bytes","Maximálny náklad v bajtoch"}. {"Maximum Number of Occupants","Počet účastníkov"}. {"May","Máj"}. diff --git a/priv/msgs/sq.msg b/priv/msgs/sq.msg index fa3d7cb11..e6305d935 100644 --- a/priv/msgs/sq.msg +++ b/priv/msgs/sq.msg @@ -331,7 +331,6 @@ {"The password was not changed","Fjalëkalimi s’u ndryshua"}. {"The passwords are different","Fjalëkalimet janë të ndryshëm"}. {"The sender of the last received message","Dërguesi i mesazhit të fundit të marrë"}. -{"The username is not valid","Emri i përdoruesit s’është i vlefshëm"}. {"There was an error changing the password: ","Pati një gabim në ndryshimin e fjalëkalimit: "}. {"There was an error creating the account: ","Pati një gabim në krijimin e llogarisë: "}. {"This room is not anonymous","Kjo dhomë s’është anonime"}. diff --git a/priv/msgs/sv.msg b/priv/msgs/sv.msg index 42d75a679..afd38e2f6 100644 --- a/priv/msgs/sv.msg +++ b/priv/msgs/sv.msg @@ -129,7 +129,6 @@ {"Make room persistent","Gör rummet permanent"}. {"Make room public searchable","Gör rummet publikt sökbart"}. {"March","Mars"}. -{"Max # of items to persist","Högsta antal dataposter som sparas"}. {"Max payload size in bytes","Högsta innehållsstorlek i bytes"}. {"Maximum Number of Occupants","Maximalt antal av användare"}. {"May","Maj"}. diff --git a/priv/msgs/th.msg b/priv/msgs/th.msg index bafd48567..9ec823668 100644 --- a/priv/msgs/th.msg +++ b/priv/msgs/th.msg @@ -110,7 +110,6 @@ {"Make room persistent","สร้างเป็นห้องถาวร"}. {"Make room public searchable","สร้างเป็นห้องที่บุคคลทั่วไปสามารถค้นหาได้"}. {"March","มีนาคม"}. -{"Max # of items to persist","จำนวนสูงสุดของรายการที่ยืนยัน"}. {"Max payload size in bytes","ขนาดสูงสุดของส่วนของข้อมูล (payload) มีหน่วยเป็นไบต์"}. {"Maximum Number of Occupants","จำนวนผู้ครอบครองห้องสูงสุด"}. {"May","พฤษภาคม"}. diff --git a/priv/msgs/tr.msg b/priv/msgs/tr.msg index 0db9497b1..8acdf3491 100644 --- a/priv/msgs/tr.msg +++ b/priv/msgs/tr.msg @@ -143,7 +143,6 @@ {"Make room persistent","Odayı kalıcı hale getir"}. {"Make room public searchable","Odayı herkes tarafından aranabilir hale getir"}. {"March","Mart"}. -{"Max # of items to persist","Kalıcı hale getirilecek en fazla öğe sayısı"}. {"Max payload size in bytes","En fazla yük (payload) boyutu (bayt olarak)"}. {"Maximum Number of Occupants","Odada En Fazla Bulunabilecek Kişi Sayısı"}. {"May","Mayıs"}. diff --git a/priv/msgs/uk.msg b/priv/msgs/uk.msg index 2ec5d0ebe..e2b99949f 100644 --- a/priv/msgs/uk.msg +++ b/priv/msgs/uk.msg @@ -3,12 +3,18 @@ %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ +{" (Add * to the end of field to match substring)"," Заповніть поля для пошуку користувача Jabber (Додайте * в кінець поля для пошуку підрядка)"}. {" has set the subject to: "," встановив(ла) тему: "}. +{"# participants","# учасників"}. +{"A description of the node","Опис вузла"}. {"A friendly name for the node","Псевдонім для вузла"}. {"A password is required to enter this room","Щоб зайти в цю конференцію, необхідно ввести пароль"}. +{"A Web Page","Веб-сторінка"}. {"Accept","Прийняти"}. {"Access denied by service policy","Доступ заборонений політикою служби"}. +{"Account doesn't exist","Обліковий запис не існує"}. {"Action on user","Дія над користувачем"}. +{"Add a hat to a user","Додати капелюх користувачу"}. {"Add Jabber ID","Додати Jabber ID"}. {"Add New","Додати"}. {"Add User","Додати користувача"}. @@ -17,7 +23,9 @@ {"Administrator privileges required","Необхідні права адміністратора"}. {"All activity","Вся статистика"}. {"All Users","Всі користувачі"}. -{"Allow this Jabber ID to subscribe to this pubsub node?","Чи дозволити цьому Jabber ID підписатись новини наданого вузла"}. +{"Allow subscription","Дозволити підписку"}. +{"Allow this Jabber ID to subscribe to this pubsub node?","Дозволити цьому Jabber ID підписатись на даний pubsub-вузол?"}. +{"Allow this person to register with the room?","Дозволити цій людині зареєструватися в кімнаті?"}. {"Allow users to change the subject","Дозволити користувачам змінювати тему"}. {"Allow users to query other users","Дозволити iq-запити до користувачів"}. {"Allow users to send invites","Дозволити користувачам надсилати запрошення"}. @@ -27,17 +35,41 @@ {"Allow visitors to send status text in presence updates","Дозволити відвідувачам відсилати текст статусу в оновленнях присутності"}. {"Allow visitors to send voice requests","Дозволити відвідувачам надсилати голосові запрошення"}. {"Announcements","Сповіщення"}. +{"Answer associated with a picture","Відповідь, пов’язана зі зображенням"}. +{"Answer associated with a video","Відповідь, пов'язана з відео"}. +{"Answer associated with speech","Відповідь, пов'язана з мовленням"}. +{"Answer to a question","Відповідь на запитання"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","Будь-хто в зазначеному списку груп(и) може підписатися та отримати елементи"}. +{"Anyone may associate leaf nodes with the collection","Будь-хто може зв'язати вузли листів з колекцією"}. +{"Anyone may publish","Будь-хто може опублікувати"}. +{"Anyone may subscribe and retrieve items","Будь-хто може підписатися та отримати елементи"}. +{"Anyone with Voice","Усі, хто має голос"}. {"April","квітня"}. +{"Attribute 'channel' is required for this request","Для цього запиту потрібен атрибут \"канал\""}. +{"Attribute 'id' is mandatory for MIX messages","Для MIX повідомлень потрібен атрибут \"id\""}. +{"Attribute 'jid' is not allowed here","Атрибут 'jid' тут заборонений"}. +{"Attribute 'node' is not allowed here","Атрибут \"вузол\" тут заборонений"}. {"August","серпня"}. +{"Automatic node creation is not enabled","Автоматичне створення вузлів не ввімкнено"}. {"Backup Management","Керування резервним копіюванням"}. {"Backup of ~p","Резервне копіювання ~p"}. {"Backup to File at ","Резервне копіювання в файл на "}. {"Backup","Резервне копіювання"}. {"Bad format","Неправильний формат"}. {"Birthday","День народження"}. +{"Both the username and the resource are required","Потрібне ім'я користувача та ресурс"}. +{"Bytestream already activated","Потік байтів вже активовано"}. +{"Cannot remove active list","Неможливо видалити активний список"}. +{"Cannot remove default list","Неможливо видалити список за промовчанням"}. {"CAPTCHA web page","Адреса капчі"}. +{"Challenge ID","ID виклику"}. {"Change Password","Змінити пароль"}. {"Change User Password","Змінити Пароль Користувача"}. +{"Changing password is not allowed","Зміна пароля заборонена"}. +{"Changing role/affiliation is not allowed","Зміна ролі/рангу заборонена"}. +{"Channel already exists","Канал уже існує"}. +{"Channel does not exist","Канал не існує"}. +{"Channels","Канали"}. {"Characters not allowed:","Заборонені символи:"}. {"Chatroom configuration modified","Конфігурація кімнати змінилась"}. {"Chatroom is created","Створено кімнату"}. @@ -47,41 +79,55 @@ {"Chatrooms","Кімнати"}. {"Choose a username and password to register with this server","Виберіть назву користувача та пароль для реєстрації на цьому сервері"}. {"Choose storage type of tables","Оберіть тип збереження таблиць"}. -{"Choose whether to approve this entity's subscription.","Вирішіть, чи задовольнити запит цього об'єкту на підписку"}. +{"Choose whether to approve this entity's subscription.","Вирішіть, чи задовольнити запит цього об'єкту на підписку."}. {"City","Місто"}. +{"Client acknowledged more stanzas than sent by server","Клієнт підтвердив більше повідомлень, ніж було відправлено сервером"}. {"Commands","Команди"}. {"Conference room does not exist","Конференція не існує"}. {"Configuration of room ~s","Конфігурація кімнати ~s"}. {"Configuration","Конфігурація"}. {"Connected Resources:","Підключені ресурси:"}. +{"Contact Addresses (normally, room owner or owners)","Контактні адреси (зазвичай, власника або власників кімнати)"}. {"Country","Країна"}. {"CPU Time:","Процесорний час:"}. +{"Current Discussion Topic","Поточна тема обговорення"}. +{"Database failure","Помилка база даних"}. {"Database Tables at ~p","Таблиці бази даних на ~p"}. {"Database Tables Configuration at ","Конфігурація таблиць бази даних на "}. {"Database","База даних"}. {"December","грудня"}. {"Default users as participants","Зробити користувачів учасниками за замовчуванням"}. +{"Delete content","Видалити вміст"}. {"Delete message of the day on all hosts","Видалити повідомлення дня на усіх хостах"}. {"Delete message of the day","Видалити повідомлення дня"}. {"Delete Selected","Видалити виділені"}. +{"Delete table","Видалити таблицю"}. {"Delete User","Видалити Користувача"}. {"Deliver event notifications","Доставляти сповіщення про події"}. {"Deliver payloads with event notifications","Доставляти разом з повідомленнями про публікації самі публікації"}. {"Description:","Опис:"}. {"Disc only copy","Тільки диск"}. +{"'Displayed groups' not added (they do not exist!): ","\"Відображені групи\" не додано (вони не існують!): "}. +{"Displayed:","Відображено:"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Нікому не кажіть свій пароль, навіть адміністраторам XMPP-сервера."}. {"Dump Backup to Text File at ","Копіювання в текстовий файл на "}. {"Dump to Text File","Копіювання в текстовий файл"}. +{"Duplicated groups are not allowed by RFC6121","RFC6121 забороняє дублювати групи"}. {"Edit Properties","Змінити параметри"}. -{"Either approve or decline the voice request.","Підтвердить або відхилите голосовий запит"}. +{"Either approve or decline the voice request.","Підтвердіть або відхиліть голосовий запит."}. +{"ejabberd HTTP Upload service","Служба відвантаження по HTTP для ejabberd"}. {"ejabberd MUC module","ejabberd MUC модуль"}. {"ejabberd Multicast service","Мультікаст ejabberd сервіс"}. {"ejabberd Publish-Subscribe module","Модуль ejabberd Публікації-Підписки"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams модуль"}. {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Веб-інтерфейс Адміністрування ejabberd"}. +{"ejabberd","ejabberd"}. {"Elements","Елементи"}. +{"Email Address","Адреса ел. пошти"}. {"Email","Електронна пошта"}. -{"Enable logging","Включити журнал роботи"}. +{"Enable hats","Увімкнути капелюхи"}. +{"Enable logging","Увімкнути журнал роботи"}. {"Enable message archiving","Ввімкнути архівацію повідомлень"}. {"End User Session","Закінчити Сеанс Користувача"}. {"Enter nickname you want to register","Введіть псевдонім, який ви хочете зареєструвати"}. @@ -90,16 +136,29 @@ {"Enter path to jabberd14 spool file","Введіть шлях до файла зі спула jabberd14"}. {"Enter path to text file","Введіть шлях до текстового файла"}. {"Enter the text you see","Введіть текст, що ви бачите"}. +{"Erlang XMPP Server","Ерланґ XMPP Сервер"}. {"Error","Помилка"}. {"Exclude Jabber IDs from CAPTCHA challenge","Пропускати ці Jabber ID без CAPTCHA-запиту"}. -{"Export all tables as SQL queries to a file:","Експорт усіх таблиць, як SQL запити, у файл"}. +{"Export all tables as SQL queries to a file:","Експортувати всі таблиці у файл як SQL запити:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Експорт даних всіх користувачів сервера до файлу PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Експорт даних користувачів домена до файлу PIEFXIS (XEP-0227):"}. +{"External component failure","Помилка зовнішнього компонента"}. +{"External component timeout","Тайм-аут зовнішнього компонента"}. +{"Failed to activate bytestream","Не вдалося активувати потік байтів"}. {"Failed to extract JID from your voice request approval","Помилка витягнення JID з вашого схвалення голосового запиту"}. +{"Failed to map delegated namespace to external component","Не вдалося зіставити делегований простір імен із зовнішнім компонентом"}. +{"Failed to parse HTTP response","Не вдалося розібрати HTTP-відповідь"}. +{"Failed to process option '~s'","Не вдалося обробити параметр \"~s\""}. {"Family Name","Прізвище"}. +{"FAQ Entry","Запис в ЧаПи"}. {"February","лютого"}. +{"File larger than ~w bytes","Файл більший, ніж ~w байт"}. +{"Fill in the form to search for any matching XMPP User","Заповніть форму для пошуку будь-якого відповідного користувача XMPP"}. {"Friday","П'ятниця"}. +{"From ~ts","Від ~ts"}. {"From","Від кого"}. +{"Full List of Room Admins","Повний перелік адміністраторів кімнати"}. +{"Full List of Room Owners","Повний перелік власників кімнати"}. {"Full Name","Повне ім'я"}. {"Get Number of Online Users","Отримати Кількість Підключених Користувачів"}. {"Get Number of Registered Users","Отримати Кількість Зареєстрованих Користувачів"}. @@ -107,6 +166,7 @@ {"Get User Password","Отримати Пароль Користувача"}. {"Get User Statistics","Отримати Статистику по Користувачу"}. {"Grant voice to this person?","Надати голос персоні?"}. +{"Groups that will be displayed to the members","Групи, які показуватимуться учасникам"}. {"Groups","Групи"}. {"Group","Група"}. {"has been banned","заборонили вхід в кімнату"}. @@ -114,8 +174,11 @@ {"has been kicked because of an affiliation change","вигнано з кімнати внаслідок зміни рангу"}. {"has been kicked because the room has been changed to members-only","вигнано з кімнати тому, що вона стала тільки для учасників"}. {"has been kicked","вигнали з кімнати"}. +{"Host unknown","Невідоме ім'я сервера"}. {"Host","Хост"}. -{"If you don't see the CAPTCHA image here, visit the web page.","Якщо ви не бачите зображення капчі, перейдіть за за цією адресою."}. +{"HTTP File Upload","Відвантаження файлів по HTTP"}. +{"Idle connection","Неактивне підключення"}. +{"If you don't see the CAPTCHA image here, visit the web page.","Якщо ви не бачите зображення CAPTCHA, перейдіть за адресою."}. {"Import Directory","Імпорт з директорії"}. {"Import File","Імпорт з файла"}. {"Import user data from jabberd14 spool file:","Імпорт користувачів з файла спула jabberd14:"}. @@ -123,27 +186,46 @@ {"Import users data from a PIEFXIS file (XEP-0227):","Імпорт даних користовучів з файлу PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Імпорт користувачів з діректорії спула jabberd14:"}. {"Import Users from Dir at ","Імпортування користувача з директорії на "}. -{"Import Users From jabberd14 Spool Files","Імпорт користувачів з jabberd14 файлів \"Spool\""}. +{"Import Users From jabberd14 Spool Files","Імпорт користувачів з jabberd14 файлів \"Spool\""}. +{"Improper domain part of 'from' attribute","Неправильна доменна частина атрибута \"from\""}. {"Improper message type","Неправильний тип повідомлення"}. {"Incoming s2s Connections:","Вхідні s2s-з'єднання:"}. +{"Incorrect CAPTCHA submit","Неправильний ввід CAPTCHA"}. +{"Incorrect data form","Неправильна форма даних"}. {"Incorrect password","Неправильний пароль"}. +{"Incorrect value of 'action' attribute","Неправильне значення атрибута \"action\""}. +{"Incorrect value of 'action' in data form","Неправильне значення \"action\" у формі даних"}. +{"Incorrect value of 'path' in data form","Неправильне значення \"path\" у формі даних"}. +{"Insufficient privilege","Недостатньо привілеїв"}. +{"Internal server error","Внутрішня помилка сервера"}. +{"Invalid 'from' attribute in forwarded message","Неприйнятний атрибут \"from\" у пересланому повідомленні"}. +{"Invalid node name","Неприйнятне ім'я вузла"}. +{"Invalid 'previd' value","Неприйнятне значення \"previd\""}. +{"Invitations are not allowed in this conference","Запрошення на цю конференцію не допускаються"}. {"IP addresses","IP адреси"}. {"is now known as","змінив(ла) псевдонім на"}. {"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","Не дозволяється відправляти помилкові повідомлення в кімнату. Учасник (~s) відправив помилкове повідомлення (~s), та був виганий з кімнати"}. {"It is not allowed to send private messages of type \"groupchat\"","Не дозволяється надсилати приватні повідомлення типу \"groupchat\""}. -{"It is not allowed to send private messages to the conference","Не дозволяється надсилати приватні повідомлення в конференцію"}. +{"It is not allowed to send private messages to the conference","Не дозволяється надсилати приватні повідомлення в конференцію"}. {"It is not allowed to send private messages","Приватні повідомлення не дозволені"}. {"Jabber ID","Jabber ID"}. {"January","січня"}. +{"JID normalization failed","Помилка нормалізації JID"}. {"joins the room","увійшов(ла) в кімнату"}. {"July","липня"}. {"June","червня"}. +{"Just created","Щойно створено"}. +{"Label:","Мітка:"}. {"Last Activity","Останнє підключення"}. {"Last login","Останнє підключення"}. +{"Last message","Останнє повідомлення"}. {"Last month","За останній місяць"}. {"Last year","За останній рік"}. {"leaves the room","вийшов(ла) з кімнати"}. {"List of rooms","Перелік кімнат"}. +{"List of users with hats","Список користувачів із капелюхами"}. +{"List users with hats","Список користувачів із капелюхами"}. +{"Logging","Журналювання"}. {"Low level update script","Низькорівневий сценарій поновлення"}. {"Make participants list public","Зробити список учасників видимим всім"}. {"Make room CAPTCHA protected","Зробити кімнату захищеною капчею"}. @@ -152,44 +234,89 @@ {"Make room password protected","Зробити кімнату захищеною паролем"}. {"Make room persistent","Зробити кімнату постійною"}. {"Make room public searchable","Зробити кімнату видимою всім"}. +{"Malformed username","Неправильне ім’я користувача"}. {"March","березня"}. -{"Max # of items to persist","Максимальне число збережених публікацій"}. {"Max payload size in bytes","Максимальний розмір корисного навантаження в байтах"}. +{"Maximum file size","Макс. розмір файлу"}. +{"Maximum Number of History Messages Returned by Room","Максимальна кількість повідомлень історії на кімнату"}. +{"Maximum number of items to persist","Максимальна кількість елементів для збереження"}. {"Maximum Number of Occupants","Максимальна кількість учасників"}. {"May","травня"}. +{"Members not added (inexistent vhost!): ","Учасників не додано (вірт. сервер не існує!): "}. {"Membership is required to enter this room","В цю конференцію можуть входити тільки її члени"}. {"Members:","Члени:"}. {"Memory","Пам'ять"}. {"Message body","Тіло повідомлення"}. +{"Message not found in forwarded payload","Повідомлення не знайдено в пересланому вмісті"}. +{"Messages from strangers are rejected","Повідомлення від незнайомців відхиляються"}. +{"Messages of type headline","Повідомлення типу \"заголовок\""}. +{"Messages of type normal","Повідомлення типу \"звичайні\""}. {"Middle Name","По-батькові"}. {"Minimum interval between voice requests (in seconds)","Мінімальний інтервал між голосовими запитами (в секундах)"}. {"Moderator privileges required","Необхідні права модератора"}. {"Moderator","Модератор"}. {"Modified modules","Змінені модулі"}. +{"Module failed to handle the query","Модулю не вдалося обробити запит"}. {"Monday","Понеділок"}. {"Multicast","Мультікаст"}. +{"Multiple elements are not allowed by RFC6121","Кілька елементів не дозволені RFC6121"}. {"Multi-User Chat","Багато-користувальницький чат"}. {"Name","Назва"}. {"Name:","Назва:"}. +{"Neither 'jid' nor 'nick' attribute found","Не знайдено ні атрибута \"jid\", ні \"nick\""}. +{"Neither 'role' nor 'affiliation' attribute found","Не знайдено ні атрибута \"role\", ні \"affiliation\""}. {"Never","Ніколи"}. {"New Password:","Новий Пароль:"}. +{"Nickname can't be empty","Псевдонім не може бути порожнім"}. {"Nickname Registration at ","Реєстрація псевдоніма на "}. {"Nickname ~s does not exist in the room","Псевдонім ~s в кімнаті відсутній"}. {"Nickname","Псевдонім"}. +{"No address elements found","Не знайдено елементів адреси"}. +{"No addresses element found","Не знайдено елемента адрес"}. +{"No 'affiliation' attribute found","Не знайдено атрибут \"affiliation\""}. +{"No available resource found","Не знайдено доступного ресурсу"}. {"No body provided for announce message","Тіло оголошення має бути непустим"}. +{"No child elements found","Не знайдено дочірніх елементів"}. +{"No data form found","Не знайдено форми даних"}. {"No Data","Немає даних"}. +{"No features available","Немає доступних функцій"}. +{"No element found","Вузол не знайдено"}. +{"No hook has processed this command","Жоден хук не обробив цю команду"}. +{"No info about last activity found","Не знайдено інформації про останню діяльність"}. +{"No 'item' element found","Елемент \"item\" не знайдено"}. +{"No items found in this query","У цьому запиті не знайдено жодного елемента"}. {"No limit","Без обмежень"}. +{"No module is handling this query","Жоден модуль не може обробити цей запит"}. +{"No node specified","Вузол не вказано"}. +{"No 'password' found in data form","Не знайдено \"пароль\" у формі даних"}. +{"No 'password' found in this query","Не знайдено \"пароль\" у цьому запиті"}. +{"No 'path' found in data form","Не знайдено \"path\" у формі даних"}. +{"No pending subscriptions found","Не знайдено очікуваних рішення підписок"}. +{"No privacy list with this name found","Немає списку конфіденційності з такою назвою"}. +{"No private data found in this query","Приватних даних у цьому запиті не знайдено"}. +{"No running node found","Не знайдено запущеного вузла"}. +{"No services available","Немає доступних сервісів"}. +{"No statistics found for this item","Для цього елемента статистичні дані не знайдено"}. +{"No 'to' attribute found in the invitation","У запрошенні не знайдено атрибут \"до\""}. +{"Node already exists","Вузол уже існує"}. {"Node ID","ID вузла"}. +{"Node index not found","Індекс вузла не знайдено"}. {"Node not found","Вузол не знайдено"}. {"Node ~p","Вузол ~p"}. +{"Nodeprep has failed","Не вдалося виконати Nodeprep"}. {"Nodes","Вузли"}. +{"Node","Вузол"}. {"None","Немає"}. +{"Not allowed","Не дозволяється"}. {"Not Found","не знайдено"}. +{"Not subscribed","Не підписаний"}. {"Notify subscribers when items are removed from the node","Повідомляти абонентів про видалення публікацій із збірника"}. {"Notify subscribers when the node configuration changes","Повідомляти абонентів про зміни в конфігурації збірника"}. {"Notify subscribers when the node is deleted","Повідомляти абонентів про видалення збірника"}. {"November","листопада"}. +{"Number of answers required","Кількість необхідних відповідей"}. {"Number of occupants","Кількість присутніх"}. +{"Number of Offline Messages","Кількість автономних повідомлень"}. {"Number of online users","Кількість підключених користувачів"}. {"Number of registered users","Кількість зареєстрованих користувачів"}. {"October","грудня"}. @@ -200,19 +327,26 @@ {"Online Users","Підключені користувачі"}. {"Online Users:","Підключені користувачі:"}. {"Online","Підключений"}. +{"Only admins can see this","Тільки адміністратори можуть це бачити"}. +{"Only collection node owners may associate leaf nodes with the collection","Лише власники вузлів колекції можуть асоціювати листові вузли з колекцією"}. {"Only deliver notifications to available users","Доставляти повідомленнями тільки доступним користувачам"}. +{"Only or tags are allowed","Дозволені лише теги або "}. +{"Only element is allowed in this query","У цьому запиті дозволено лише елемент "}. {"Only members may query archives of this room","Тільки модератори можуть запитувати архіви цієї кімнати"}. {"Only moderators and participants are allowed to change the subject in this room","Тільки модератори та учасники можуть змінювати тему в цій кімнаті"}. {"Only moderators are allowed to change the subject in this room","Тільки модератори можуть змінювати тему в цій кімнаті"}. {"Only moderators can approve voice requests","Тільки модератори можуть схвалювати голосові запити"}. {"Only occupants are allowed to send messages to the conference","Тільки присутнім дозволяється надсилати повідомленняя в конференцію"}. {"Only occupants are allowed to send queries to the conference","Тільки присутнім дозволяється відправляти запити в конференцію"}. +{"Only publishers may publish","Тільки видавці можуть публікувати"}. {"Only service administrators are allowed to send service messages","Тільки адміністратор сервісу може надсилати службові повідомлення"}. +{"Only those on a whitelist may associate leaf nodes with the collection","Лише ті, хто входить до білого списку, можуть асоціювати листові вузли з колекцією"}. {"Organization Name","Назва організації"}. {"Organization Unit","Відділ організації"}. {"Outgoing s2s Connections","Вихідні s2s-з'єднання"}. {"Outgoing s2s Connections:","Вихідні s2s-з'єднання:"}. {"Owner privileges required","Необхідні права власника"}. +{"Packet relay is denied by service policy","Пересилання пакетів заборонене політикою сервісу"}. {"Packet","Пакет"}. {"Participant","Учасник"}. {"Password Verification","Перевірка Пароля"}. @@ -222,22 +356,25 @@ {"Path to Dir","Шлях до директорії"}. {"Path to File","Шлях до файла"}. {"Pending","Очікування"}. -{"Period: ","Період"}. +{"Period: ","Період: "}. {"Persist items to storage","Зберегати публікації до сховища"}. {"Ping","Пінг"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Зауважте, що ця опція відповідає за резервне копіювання тільки вбудованної бази даних Mnesia. Якщо Ви також використовуєте інше сховище для даних (наприклад за допомогою модуля ODBC), то його резервне копіювання потрібно робити окремо."}. {"Please, wait for a while before sending new voice request","Будь ласка, почекайте деякий час перед тим, як знову відправляти голосовий запит"}. {"Pong","Понг"}. {"Present real Jabber IDs to","Зробити реальні Jabber ID учасників видимими"}. +{"Previous session not found","Попередній сеанс не знайдено"}. {"private, ","приватна, "}. {"Publish-Subscribe","Публікація-Підписка"}. {"PubSub subscriber request","Запит на підписку PubSub"}. {"Purge all items when the relevant publisher goes offline","Видалити всі елементи, коли особа, що їх опублікувала, вимикається від мережі"}. +{"Push record not found","Push-запис не знайдено"}. {"Queries to the conference members are not allowed in this room","Запити до користувачів в цій конференції заборонені"}. {"RAM and disc copy","ОЗП та диск"}. {"RAM copy","ОЗП"}. {"Really delete message of the day?","Насправді, видалити повідомлення дня?"}. {"Recipient is not in the conference room","Адресата немає в конференції"}. +{"Register an XMPP account","Зареєструвати XMPP-запис"}. {"Registered Users","Зареєстровані користувачі"}. {"Registered Users:","Зареєстровані користувачі:"}. {"Register","Реєстрація"}. @@ -255,15 +392,20 @@ {"Restore plain text backup immediately:","Відновити з текстової резервної копії негайно:"}. {"Restore","Відновлення з резервної копії"}. {"Roles for which Presence is Broadcasted","Ролі для яких поширюється наявність"}. +{"Roles that May Send Private Messages","Ролі, що можуть надсилати приватні повідомлення"}. {"Room Configuration","Конфігурація кімнати"}. {"Room creation is denied by service policy","Створювати конференцію заборонено політикою служби"}. {"Room description","Опис кімнати"}. {"Room Occupants","Учасники кімнати"}. +{"Room terminates","Кімната припиняється"}. {"Room title","Назва кімнати"}. {"Roster groups allowed to subscribe","Дозволені для підписки групи ростера"}. +{"Roster of ~ts","Список контактів ~ts"}. {"Roster size","Кількість контактів"}. +{"Roster:","Список контактів:"}. {"RPC Call Error","Помилка виклику RPC"}. {"Running Nodes","Працюючі вузли"}. +{"~s invites you to the room ~s","~s запрошує вас до кімнати ~s"}. {"Saturday","Субота"}. {"Script check","Перевірка сценарію"}. {"Search Results for ","Результати пошуку в "}. @@ -292,26 +434,46 @@ {"Store plain text backup:","Зберегти текстову резервну копію:"}. {"Subject","Тема"}. {"Submitted","Відправлено"}. -{"Submit","Відправити"}. +{"Submit","Надіслати"}. {"Subscriber Address","Адреса абонента"}. {"Subscription","Підписка"}. {"Sunday","Неділя"}. {"That nickname is already in use by another occupant","Псевдонім зайнято кимось з присутніх"}. {"That nickname is registered by another person","Псевдонім зареєстровано кимось іншим"}. -{"The CAPTCHA is valid.","Перевірку капчею закінчено успішно"}. +{"The account was not unregistered","Обліковий запис не було видалено"}. +{"The CAPTCHA is valid.","Перевірку CAPTCHA успішно завершено."}. {"The CAPTCHA verification has failed","Перевірку капчею не пройдено"}. {"The collections with which a node is affiliated","Колекція, до якої входить вузол"}. {"The password is too weak","Пароль надто простий"}. -{"the password is","пароль:"}. -{"There was an error creating the account: ","Помилка при створенні акаунту:"}. +{"the password is","паролем є"}. +{"The presence states for which an entity wants to receive notifications","Стан присутності, для якого сутність хоче отримувати сповіщення"}. +{"The query is only allowed from local users","Запит дозволено лише від локальних користувачів"}. +{"The query must not contain elements","Запит не повинен містити елементів "}. +{"The room subject can be modified by participants","Тема кімнати може бути змінена учасниками"}. +{"The sender of the last received message","Відправник останнього отриманого повідомлення"}. +{"The stanza MUST contain only one element, one element, or one element","Строфа ПОВИННА містити лише один елемент , один елемент або один елемент "}. +{"The subscription identifier associated with the subscription request","Ідентифікатор підписки, пов’язаний із запитом на підписку"}. +{"There was an error changing the password: ","Помилка при зміні пароля: "}. +{"There was an error creating the account: ","Помилка при створенні облікового запису: "}. {"There was an error deleting the account: ","Помилка при видаленні акаунту: "}. +{"This page allows to unregister an XMPP account in this XMPP server.","Ця сторінка дозволяє видалити свій обліковий запис з XMPP-сервера."}. {"This room is not anonymous","Ця кімната не анонімна"}. {"Thursday","Четвер"}. {"Time delay","Час затримки"}. +{"Timed out waiting for stream resumption","Час очікування на відновлення потоку закінчився"}. {"Time","Час"}. +{"To register, visit ~s","Щоб зареєструватися, відвідайте ~s"}. +{"To ~ts","До ~ts"}. +{"Token TTL","Токен TTL"}. +{"Too many active bytestreams","Надто багато активних потоків байтів"}. {"Too many CAPTCHA requests","Надто багато CAPTCHA-запитів"}. -{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Забагато (~p) помилок авторизації з цієї IP адреси (~s). Адресу буде розблоковано о ~s UTC"}. +{"Too many child elements","Надто багато дочірніх елементів"}. +{"Too many elements","Надто багато елементів "}. +{"Too many elements","Надто багато елементів "}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Забагато (~p) помилок авторизації з цієї IP адреси (~s). Адресу буде розблоковано о ~s UTC"}. +{"Too many receiver fields were specified","Вказано забагато одержувачів"}. {"Too many unacked stanzas","Занадто багато пакетів без відповідей"}. +{"Too many users in this conference","Надто багато користувачів у цій конференції"}. {"Total rooms","Всього кімнат"}. {"To","Кому"}. {"Traffic rate limit is exceeded","Швидкість передачі інформації було перевищено"}. @@ -319,10 +481,18 @@ {"Transactions Committed:","Транзакції завершені:"}. {"Transactions Logged:","Транзакції запротокольовані:"}. {"Transactions Restarted:","Транзакції перезапущені:"}. +{"~ts's Offline Messages Queue","Черга автономних повідомлень ~ts"}. {"Tuesday","Вівторок"}. {"Unable to generate a CAPTCHA","Нема можливості згенерувати капчу"}. +{"Unable to register route on existing local domain","Неможливо зареєструвати маршрут на наявному локальному домені"}. {"Unauthorized","Не авторизовано"}. +{"Unexpected action","Несподівана дія"}. +{"Unexpected error condition: ~p","Умова несподіваної помилки: ~p"}. +{"Unregister an XMPP account","Видалити обліковий запис XMPP"}. {"Unregister","Видалити"}. +{"Unselect All","Скасувати виділення з усіх"}. +{"Unsupported element","Непідтримуваний елемент "}. +{"Unsupported version","Непідтримувана версія"}. {"Update message of the day (don't send)","Оновити повідомлення дня (не надсилати)"}. {"Update message of the day on all hosts (don't send)","Оновити повідомлення дня на всіх хостах (не надсилати)"}. {"Update plan","План оновлення"}. @@ -330,15 +500,26 @@ {"Update script","Сценарій поновлення"}. {"Update","Обновити"}. {"Uptime:","Час роботи:"}. +{"URL for Archived Discussion Logs","URL-адреса для журналів архівних обговорень"}. +{"User already exists","Користувач уже існує"}. {"User JID","JID Користувача"}. +{"User (jid)","Користувач (jid)"}. {"User Management","Управління Користувачами"}. +{"User removed","Користувача видалено"}. +{"User session not found","Сеанс користувача не знайдено"}. +{"User session terminated","Сеанс користувача припинено"}. +{"User ~ts","Користувач ~ts"}. {"Username:","Ім'я користувача:"}. {"Users are not allowed to register accounts so quickly","Користувачам не дозволено так часто реєструвати облікові записи"}. {"Users Last Activity","Статистика останнього підключення користувачів"}. {"Users","Користувачі"}. {"User","Користувач"}. {"Validate","Затвердити"}. +{"Value of '~s' should be boolean","Значення \"~s\" має бути логічним"}. +{"Value of '~s' should be datetime string","Значення \"~s\" має бути рядком дати і часу"}. +{"Value of '~s' should be integer","Значення \"~s\" має бути цілим числом"}. {"vCard User Search","Пошук користувачів по vCard"}. +{"View Queue","Переглянути чергу"}. {"Virtual Hosts","віртуальні хости"}. {"Visitors are not allowed to change their nicknames in this room","Відвідувачам не дозволяється змінювати псевдонім в цій кімнаті"}. {"Visitors are not allowed to send messages to all occupants","Відвідувачам не дозволяється надсилати повідомлення всім присутнім"}. @@ -346,9 +527,24 @@ {"Voice requests are disabled in this conference","Голосові запити відключені в цій конференції"}. {"Voice request","Голосовий запит"}. {"Wednesday","Середа"}. +{"When a new subscription is processed","Під час обробки нової підписки"}. {"When to send the last published item","Коли надсилати останній опублікований елемент"}. +{"Whether owners or publisher should receive replies to items","Чи повинні власники або видавець отримувати відповіді на елементи"}. +{"Whether the node is a leaf (default) or a collection","Чи є вузол листом (типово) чи колекцією"}. {"Whether to allow subscriptions","Дозволяти підписку"}. +{"Whether to make all subscriptions temporary, based on subscriber presence","Чи робити всі підписки тимчасовими, залежно від присутності читача"}. +{"Whether to notify owners about new subscribers and unsubscribes","Чи повідомляти власників про нових читачів та їх втрату"}. +{"Who may associate leaf nodes with a collection","Хто може пов’язувати листові вузли з колекцією"}. +{"Wrong parameters in the web formulary","Неправильні параметри у веб-формі"}. +{"Wrong xmlns","Неправильний xmlns"}. +{"XMPP Account Registration","Реєстрація облікового запису XMPP"}. +{"XMPP Domains","Домени XMPP"}. +{"XMPP URI of Associated Publish-Subscribe Node","XMPP URI-адреса асоційованого вузла публікацій-підписок"}. +{"You are being removed from the room because of a system shutdown","Ви будете видалені з кімнати через завершення роботи системи"}. +{"You are not joined to the channel","Ви не приєднані до каналу"}. +{"You can later change your password using an XMPP client.","Пізніше ви можете змінити пароль за допомогою XMPP-клієнта."}. {"You have been banned from this room","Вам заборонено входити в цю конференцію"}. +{"You have joined too many conferences","Ви приєднані до надто великої кількості конференцій"}. {"You must fill in field \"Nickname\" in the form","Вам необхідно заповнити поле \"Псевдонім\" у формі"}. {"You need a client that supports x:data and CAPTCHA to register","Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:data"}. {"You need a client that supports x:data to register the nickname","Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:data"}. @@ -356,3 +552,6 @@ {"Your active privacy list has denied the routing of this stanza.","Маршрутизація цієї строфи була відмінена активним списком приватності."}. {"Your contact offline message queue is full. The message has been discarded.","Черга повідомлень, що не були доставлені, переповнена. Повідомлення не було збережено."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ваші повідомлення до ~s блокуються. Для розблокування відвідайте ~s"}. +{"Your XMPP account was successfully registered.","Ваш обліковий запис XMPP успішно зареєстровано."}. +{"Your XMPP account was successfully unregistered.","Ваш обліковий запис XMPP успішно видалено."}. +{"You're not allowed to create nodes","Вам заборонено створювати вузли"}. diff --git a/priv/msgs/vi.msg b/priv/msgs/vi.msg index d403256f8..600ef6422 100644 --- a/priv/msgs/vi.msg +++ b/priv/msgs/vi.msg @@ -110,7 +110,6 @@ {"Make room persistent","Tạo phòng bền vững"}. {"Make room public searchable","Tạo phòng có thể tìm kiếm công khai"}. {"March","Tháng Ba"}. -{"Max # of items to persist","Số mục tối đa để lưu trữ"}. {"Max payload size in bytes","Kích thước dung lượng byte tối đa"}. {"Maximum Number of Occupants","Số Lượng Người Tham Dự Tối Đa"}. {"May","Tháng Năm"}. diff --git a/priv/msgs/wa.msg b/priv/msgs/wa.msg index f6e1fd4a8..f6b618465 100644 --- a/priv/msgs/wa.msg +++ b/priv/msgs/wa.msg @@ -153,7 +153,6 @@ {"Make room persistent","Rinde li såle permaninte"}. {"Make room public searchable","Rinde li såle di berdelaedje cweråve publicmint"}. {"March","måss"}. -{"Max # of items to persist","Nombe macsimoms di cayets permanints"}. {"Max payload size in bytes","Contnou macsimom en octets"}. {"Maximum Number of Occupants","Nombe macsimom di prezints"}. {"May","may"}. diff --git a/priv/msgs/zh.msg b/priv/msgs/zh.msg index 8f099b608..8eb4f244b 100644 --- a/priv/msgs/zh.msg +++ b/priv/msgs/zh.msg @@ -4,7 +4,7 @@ %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (在字段末添加*来匹配子串)"}. -{" has set the subject to: ","已将标题设置为: "}. +{" has set the subject to: "," 已将标题设置为: "}. {"# participants","# 个参与人"}. {"A description of the node","该节点的描述"}. {"A friendly name for the node","该节点的友好名称"}. @@ -20,10 +20,11 @@ {"Access model","访问模型"}. {"Account doesn't exist","账号不存在"}. {"Action on user","对用户的动作"}. +{"Add a hat to a user","给用户添加头衔"}. {"Add Jabber ID","添加Jabber ID"}. {"Add New","添加新用户"}. {"Add User","添加用户"}. -{"Administration of ","管理"}. +{"Administration of ","管理 "}. {"Administration","管理"}. {"Administrator privileges required","需要管理员权限"}. {"All activity","所有活动"}. @@ -62,7 +63,7 @@ {"Automatic node creation is not enabled","未启用自动节点创建"}. {"Backup Management","备份管理"}. {"Backup of ~p","~p的备份"}. -{"Backup to File at ","备份文件位于"}. +{"Backup to File at ","备份文件位于 "}. {"Backup","备份"}. {"Bad format","格式错误"}. {"Birthday","出生日期"}. @@ -78,6 +79,7 @@ {"Changing role/affiliation is not allowed","不允许修改角色/单位"}. {"Channel already exists","频道已存在"}. {"Channel does not exist","频道不存在"}. +{"Channel JID","频道 JID"}. {"Channels","频道"}. {"Characters not allowed:","不允许字符:"}. {"Chatroom configuration modified","聊天室配置已修改"}. @@ -97,12 +99,13 @@ {"Configuration","配置"}. {"Connected Resources:","已连接资源:"}. {"Contact Addresses (normally, room owner or owners)","联系人地址 (通常为房间持有人)"}. +{"Contrib Modules","Contrib 模块"}. {"Country","国家"}. {"CPU Time:","CPU时间:"}. {"Current Discussion Topic","当前讨论话题"}. {"Database failure","数据库失败"}. {"Database Tables at ~p","位于~p的数据库表"}. -{"Database Tables Configuration at ","数据库表格配置位于"}. +{"Database Tables Configuration at ","数据库表格配置位于 "}. {"Database","数据库"}. {"December","十二月"}. {"Default users as participants","用户默认被视为参与人"}. @@ -119,12 +122,12 @@ {"'Displayed groups' not added (they do not exist!): ","'显示的群组' 未被添加 (它们不存在!): "}. {"Displayed:","已显示:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","不要将密码告诉任何人, 就算是XMPP服务器的管理员也不可以."}. -{"Dump Backup to Text File at ","转储备份到文本文件于"}. +{"Dump Backup to Text File at ","将备份转储到位于以下位置的文本文件 "}. {"Dump to Text File","转储到文本文件"}. {"Duplicated groups are not allowed by RFC6121","按照RFC6121的规则,不允许有重复的群组"}. {"Dynamically specify a replyto of the item publisher","为项目发布者动态指定一个 replyto"}. {"Edit Properties","编辑属性"}. -{"Either approve or decline the voice request.","接受或拒绝声音请求"}. +{"Either approve or decline the voice request.","接受或拒绝声音请求."}. {"ejabberd HTTP Upload service","ejabberd HTTP 上传服务"}. {"ejabberd MUC module","ejabberd MUC 模块"}. {"ejabberd Multicast service","ejabberd多重映射服务"}. @@ -136,6 +139,7 @@ {"Elements","元素"}. {"Email Address","电邮地址"}. {"Email","电子邮件"}. +{"Enable hats","启用头衔"}. {"Enable logging","启用服务器端聊天记录"}. {"Enable message archiving","启用消息归档"}. {"Enabling push without 'node' attribute is not supported","不支持未使用'node'属性就开启推送"}. @@ -170,10 +174,12 @@ {"Full List of Room Admins","房间管理员完整列表"}. {"Full List of Room Owners","房间持有人完整列表"}. {"Full Name","全名"}. +{"Get List of Online Users","获取在线用户列表"}. +{"Get List of Registered Users","获取注册用户列表"}. {"Get Number of Online Users","获取在线用户数"}. {"Get Number of Registered Users","获取注册用户数"}. {"Get Pending","获取挂起"}. -{"Get User Last Login Time","获取用户上次登陆时间"}. +{"Get User Last Login Time","获取用户上次登录时间"}. {"Get User Password","获取用户密码"}. {"Get User Statistics","获取用户统计"}. {"Given Name","中间名"}. @@ -186,6 +192,9 @@ {"has been kicked because of an affiliation change","因联属关系改变而被踢出"}. {"has been kicked because the room has been changed to members-only","因该房间改为只对会员开放而被踢出"}. {"has been kicked","已被踢出"}. +{"Hat title","头衔标题"}. +{"Hat URI","头衔 URI"}. +{"Hats limit exceeded","已超过头衔限制"}. {"Host unknown","主人未知"}. {"Host","主机"}. {"HTTP File Upload","HTTP文件上传"}. @@ -194,10 +203,10 @@ {"Import Directory","导入目录"}. {"Import File","导入文件"}. {"Import user data from jabberd14 spool file:","从 jabberd14 Spool 文件导入用户数据:"}. -{"Import User from File at ","导入用户的文件位于"}. +{"Import User from File at ","从以下位置的文件导入用户 "}. {"Import users data from a PIEFXIS file (XEP-0227):","从 PIEFXIS 文件 (XEP-0227) 导入用户数据:"}. {"Import users data from jabberd14 spool directory:","从jabberd14 Spool目录导入用户数据:"}. -{"Import Users from Dir at ","导入用户的目录位于"}. +{"Import Users from Dir at ","从以下位置目录导入用户 "}. {"Import Users From jabberd14 Spool Files","从 jabberd14 Spool 文件导入用户"}. {"Improper domain part of 'from' attribute","不恰当的'from'属性域名部分"}. {"Improper message type","不恰当的消息类型"}. @@ -208,6 +217,8 @@ {"Incorrect value of 'action' attribute","'action' 属性的值不正确"}. {"Incorrect value of 'action' in data form","数据表单中 'action' 的值不正确"}. {"Incorrect value of 'path' in data form","数据表单中 'path' 的值不正确"}. +{"Installed Modules:","已安装的模块:"}. +{"Install","安装"}. {"Insufficient privilege","权限不足"}. {"Internal server error","内部服务器错误"}. {"Invalid 'from' attribute in forwarded message","转发的信息中 'from' 属性的值无效"}. @@ -224,19 +235,23 @@ {"January","一月"}. {"JID normalization denied by service policy","JID规范化被服务策略拒绝"}. {"JID normalization failed","JID规范化失败"}. +{"Joined MIX channels of ~ts","加入了 ~ts 的 MIX 频道"}. +{"Joined MIX channels:","加入了 MIX 频道:"}. {"joins the room","加入房间"}. {"July","七月"}. {"June","六月"}. {"Just created","刚刚创建"}. {"Label:","标签:"}. {"Last Activity","上次活动"}. -{"Last login","上次登陆"}. +{"Last login","上次登录"}. {"Last message","最近消息"}. {"Last month","上个月"}. {"Last year","上一年"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","文本的SHA-256哈希的最低有效位应等于十六进制标签"}. {"leaves the room","离开房间"}. {"List of rooms","房间列表"}. +{"List of users with hats","有头衔用户的列表"}. +{"List users with hats","有头衔用户列表"}. {"Logging","正在记录"}. {"Low level update script","低级别更新脚本"}. {"Make participants list public","公开参与人列表"}. @@ -249,7 +264,7 @@ {"Malformed username","用户名无效"}. {"MAM preference modification denied by service policy","MAM偏好被服务策略拒绝"}. {"March","三月"}. -{"Max # of items to persist","允许持久化的最大内容条目数"}. +{"Max # of items to persist, or `max` for no specific limit other than a server imposed maximum","要保留的最大项目数 #,`max`表示除了服务器强加的最大值之外没有特定限制"}. {"Max payload size in bytes","最大有效负载字节数"}. {"Maximum file size","最大文件大小"}. {"Maximum Number of History Messages Returned by Room","房间返回的历史消息最大值"}. @@ -288,7 +303,7 @@ {"Never","从未"}. {"New Password:","新密码:"}. {"Nickname can't be empty","昵称不能为空"}. -{"Nickname Registration at ","昵称注册于"}. +{"Nickname Registration at ","昵称注册于 "}. {"Nickname ~s does not exist in the room","昵称~s不在该房间"}. {"Nickname","昵称"}. {"No address elements found","没有找到地址的各元素"}. @@ -326,6 +341,7 @@ {"Node ~p","节点~p"}. {"Nodeprep has failed","Nodeprep 已失效"}. {"Nodes","节点"}. +{"Node","节点"}. {"None","无"}. {"Not allowed","不允许"}. {"Not Found","没有找到"}. @@ -339,8 +355,9 @@ {"Number of Offline Messages","离线消息数量"}. {"Number of online users","在线用户数"}. {"Number of registered users","注册用户数"}. -{"Number of seconds after which to automatically purge items","自动清除项目要等待的秒数"}. +{"Number of seconds after which to automatically purge items, or `max` for no specific limit other than a server imposed maximum","等待多少秒后自动清除项目,“max”表示除服务器施加的最大值外没有特定限制"}. {"Occupants are allowed to invite others","允许成员邀请其他人"}. +{"Occupants are allowed to query others","成员可查询其他人"}. {"Occupants May Change the Subject","成员可以修改主题"}. {"October","十月"}. {"Offline Messages","离线消息"}. @@ -367,11 +384,13 @@ {"Only those on a whitelist may subscribe and retrieve items","仅白名单用户可以订阅和检索内容项"}. {"Organization Name","组织名称"}. {"Organization Unit","组织单位"}. +{"Other Modules Available:","其他可用模块:"}. {"Outgoing s2s Connections","出站 s2s 连接"}. {"Outgoing s2s Connections:","出站 s2s 连接:"}. {"Owner privileges required","需要持有人权限"}. {"Packet relay is denied by service policy","包中继被服务策略拒绝"}. {"Packet","数据包"}. +{"Participant ID","参与者 ID"}. {"Participant","参与人"}. {"Password Verification:","密码确认:"}. {"Password Verification","确认密码"}. @@ -418,6 +437,7 @@ {"Registered Users:","注册用户:"}. {"Register","注册"}. {"Remote copy","远程复制"}. +{"Remove a hat from a user","移除用户头衔"}. {"Remove All Offline Messages","移除所有离线消息"}. {"Remove User","删除用户"}. {"Remove","移除"}. @@ -428,7 +448,7 @@ {"Resources","资源"}. {"Restart Service","重启服务"}. {"Restart","重启"}. -{"Restore Backup from File at ","要恢复的备份文件位于"}. +{"Restore Backup from File at ","从以下位置的文件恢复备份 "}. {"Restore binary backup after next ejabberd restart (requires less memory):","在下次 ejabberd 重启后恢复二进制备份(需要的内存更少):"}. {"Restore binary backup immediately:","立即恢复二进制备份:"}. {"Restore plain text backup immediately:","立即恢复普通文本备份:"}. @@ -455,7 +475,7 @@ {"Search Results for ","搜索结果属于关键词 "}. {"Search the text","搜索文本"}. {"Search until the date","搜索截至日期"}. -{"Search users in ","搜索用户于"}. +{"Search users in ","在以下位置搜索用户 "}. {"Select All","全选"}. {"Send announcement to all online users on all hosts","发送通知给所有主机的在线用户"}. {"Send announcement to all online users","发送通知给所有在线用户"}. @@ -473,6 +493,7 @@ {"Shut Down Service","关闭服务"}. {"SOCKS5 Bytestreams","SOCKS5 字节流"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","某些 XMPP 客户端可以在计算机里存储你的密码. 处于安全考虑, 请仅在你的个人计算机里使用该功能."}. +{"Sources Specs:","源参数:"}. {"Specify the access model","指定访问范例"}. {"Specify the event message type","指定事件消息类型"}. {"Specify the publisher model","指定发布人范例"}. @@ -518,8 +539,10 @@ {"The JIDs of those to contact with questions","问题联系人的JID"}. {"The JIDs of those with an affiliation of owner","隶属所有人的JID"}. {"The JIDs of those with an affiliation of publisher","隶属发布人的JID"}. +{"The list of all online users","所有在线用户列表"}. +{"The list of all users","所有用户列表"}. {"The list of JIDs that may associate leaf nodes with a collection","可以将叶节点与集合关联的JID列表"}. -{"The maximum number of child nodes that can be associated with a collection","可以与集合关联的最大子节点数"}. +{"The maximum number of child nodes that can be associated with a collection, or `max` for no specific limit other than a server imposed maximum","可以与集合相关联的最大子节点数,“max”表示除服务器施加的最大值外没有特定限制"}. {"The minimum number of milliseconds between sending any two notification digests","发送任何两个通知摘要之间的最小毫秒数"}. {"The name of the node","该节点的名称"}. {"The node is a collection node","该节点是集合节点"}. @@ -544,13 +567,12 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","节点数据的类型, 如果有, 通常由有效负载的名称空间指定"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","XSL转换的URL,可以将其应用于有效负载以生成适当的消息正文元素。"}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","XSL转换的URL, 可以将其应用于有效负载格式, 以生成有效的数据表单结果, 客户端可以使用通用数据表单呈现引擎来显示该结果"}. -{"The username is not valid","用户名无效"}. {"There was an error changing the password: ","修改密码出错: "}. {"There was an error creating the account: ","帐户创建出错: "}. {"There was an error deleting the account: ","帐户删除失败: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","此处不区分大小写: macbeth 与 MacBeth 和 Macbeth 是一样的."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","本页面允许在此服务器上注册XMPP帐户. 你的JID (Jabber ID) 的形式如下: 用户名@服务器. 请仔细阅读说明并正确填写相应字段."}. -{"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此XMPP服务器上注销XMPP帐户"}. +{"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此 XMPP 服务器上注销 XMPP 帐户。"}. {"This room is not anonymous","此房间不是匿名房间"}. {"This service can not process the address: ~s","此服务无法处理地址: ~s"}. {"Thursday","星期四"}. @@ -565,7 +587,7 @@ {"Too many child elements","太多子元素"}. {"Too many elements","太多 元素"}. {"Too many elements","太多 元素"}. -{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多. 该地址将在UTC时间~s被禁用."}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多。将在UTC时间 ~s 解除对该地址的封锁"}. {"Too many receiver fields were specified","指定的接收者字段太多"}. {"Too many unacked stanzas","未被确认的节太多"}. {"Too many users in this conference","该会议的用户太多"}. @@ -583,6 +605,7 @@ {"Unauthorized","未认证的"}. {"Unexpected action","意外行为"}. {"Unexpected error condition: ~p","意外错误条件: ~p"}. +{"Uninstall","卸载"}. {"Unregister an XMPP account","注销XMPP帐户"}. {"Unregister","取消注册"}. {"Unselect All","取消全选"}. @@ -593,7 +616,10 @@ {"Update plan","更新计划"}. {"Update ~p","更新~p"}. {"Update script","更新脚本"}. +{"Update specs to get modules source, then install desired ones.","更新参数获取模块源,然后安装所需的模块。"}. +{"Update Specs","更新参数"}. {"Update","更新"}. +{"Upgrade","升级"}. {"Uptime:","正常运行时间:"}. {"URL for Archived Discussion Logs","已归档对话日志的URL"}. {"User already exists","用户已存在"}. @@ -616,6 +642,7 @@ {"Value of '~s' should be integer","'~s' 的值应为整数"}. {"Value 'set' of 'type' attribute is not allowed","不允许 'type' 属性的 'set' 值"}. {"vCard User Search","vCard用户搜索"}. +{"View joined MIX channels","查看已加入的 MIX 频道"}. {"View Queue","查看队列"}. {"View Roster","查看花名册"}. {"Virtual Hosts","虚拟主机"}. @@ -656,7 +683,7 @@ {"You need a client that supports x:data to register the nickname","您需要一个支持 x:data 的客户端来注册昵称"}. {"You need an x:data capable client to search","您需要一个兼容 x:data 的客户端来搜索"}. {"Your active privacy list has denied the routing of this stanza.","你的活跃私聊列表拒绝了在此房间进行路由分发."}. -{"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满. 消息已被丢弃"}. +{"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满。消息已被丢弃。"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","您发送给~s的消息已被阻止. 要解除阻止, 请访问 ~s"}. {"Your XMPP account was successfully registered.","你的XMPP帐户注册成功."}. {"Your XMPP account was successfully unregistered.","你的XMPP帐户注销成功."}. diff --git a/rebar b/rebar index 24cf4a68c..b6f011846 100755 Binary files a/rebar and b/rebar differ diff --git a/rebar.config b/rebar.config index 798e6350b..14c8c81e5 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -19,8 +19,8 @@ %%%---------------------------------------------------------------------- {deps, [{base64url, ".*", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}, - {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.29"}}}, - {eimp, ".*", {git, "https://github.com/processone/eimp", {tag, "1.0.21"}}}, + {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.30"}}}, + {eimp, ".*", {git, "https://github.com/processone/eimp", {tag, "1.0.22"}}}, {if_var_true, tools, {ejabberd_po, ".*", {git, "https://github.com/processone/ejabberd-po", {branch, "main"}}}}, {if_var_true, elixir, @@ -30,40 +30,51 @@ {if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis", {tag, "v1.2.0"}}}}, {if_var_true, sip, - {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.43"}}}}, + {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.48"}}}}, {if_var_true, zlib, - {ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.10"}}}}, - {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.13"}}}, - {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.47"}}}, - {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.32"}}}, + {ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.12"}}}}, + {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.16"}}}, + {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.49"}}}, + {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.34"}}}, {idna, ".*", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}}, - {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.0.5"}}}, - {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.9.0"}}}, - {lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}}, + {if_version_above, "19", + {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.1.1"}}}, + {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.1.0"}}} % for R19 and below + }, + {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}, + {if_version_below, "22", + {lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}} + }, {if_var_true, lua, - {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.3"}}}}, - {mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.14"}}}, - {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.13"}}}, + {if_not_rebar3, + {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}} + }}, + {if_var_true, lua, + {if_rebar3, + {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0.0"}}} + }}, + {mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.15"}}}, + {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.20"}}}, {if_var_true, mysql, - {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.19"}}}}, - {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.10"}}}, + {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.20"}}}}, + {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.11"}}}, {if_var_true, pgsql, - {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.12"}}}}, - {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.23"}}}, - {pkix, ".*", {git, "https://github.com/processone/pkix", {tag, "1.0.8"}}}, + {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.19"}}}}, + {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.25"}}}, + {pkix, ".*", {git, "https://github.com/processone/pkix", {tag, "1.0.9"}}}, {if_not_rebar3, %% Needed because modules are not fully migrated to new structure and mix {if_var_true, elixir, {rebar_elixir_plugin, ".*", {git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}}, {if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.13"}}}}, - {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.27"}}}, + {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.29"}}}, {if_var_true, stun, - {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.44"}}}}, - {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.5.4"}}}, - {yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.12"}}} + {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.2.6"}}}}, + {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.6.0"}}}, + {yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.14"}}} ]}. -{gitonly_deps, [elixir, luerl]}. +{gitonly_deps, [elixir]}. {if_var_true, latest_deps, {floating_deps, [cache_tab, @@ -94,6 +105,8 @@ {erl_opts, [nowarn_deprecated_function, {i, "include"}, {if_version_above, "20", {d, 'DEPRECATED_GET_STACKTRACE'}}, + {if_version_above, "20", {d, 'HAVE_ERL_ERROR'}}, + {if_version_above, "20", {d, 'HAVE_URI_STRING'}}, {if_version_below, "21", {d, 'USE_OLD_HTTP_URI'}}, {if_version_below, "22", {d, 'LAGER'}}, {if_version_below, "21", {d, 'NO_CUSTOMIZE_HOSTNAME_CHECK'}}, @@ -101,15 +114,14 @@ {if_version_below, "23", {d, 'USE_OLD_PG2'}}, {if_version_below, "24", {d, 'COMPILER_REPORTS_ONLY_LINES'}}, {if_version_below, "24", {d, 'SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL'}}, - {if_var_match, db_type, mssql, {d, 'mssql'}}, + {if_version_below, "25", {d, 'OTP_BELOW_25'}}, {if_var_false, debug, no_debug_info}, {if_var_true, debug, debug_info}, {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, {if_var_true, new_sql_schema, {d, 'NEW_SQL_SCHEMA'}}, - {if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}}, + {if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATEWAY_WORKAROUND'}}, {if_var_true, sip, {d, 'SIP'}}, {if_var_true, stun, {d, 'STUN'}}, - {if_have_fun, {erl_error, format_exception, 6}, {d, 'HAVE_ERL_ERROR'}}, {src_dirs, [src, {if_rebar3, sql}, {if_var_true, tools, tools}, @@ -177,14 +189,12 @@ {sys_config, "./rel/sys.config"}, {vm_args, "./rel/vm.args"}, {overlay_vars, "vars.config"}, - {extended_start_script, true}, - {overlay, [{mkdir, "var/log/ejabberd"}, - {mkdir, "var/lock"}, - {mkdir, "var/lib/ejabberd"}, - {mkdir, "etc/ejabberd"}, - {copy, "rel/files/erl", "\{\{erts_vsn\}\}/bin/erl"}, % in rebar2 this prepends erts- + {overlay, [{mkdir, "logs"}, + {mkdir, "database"}, + {mkdir, "conf"}, + {copy, "rel/files/erl", "erts-\{\{erts_vsn\}\}/bin/erl"}, {template, "ejabberdctl.template", "bin/ejabberdctl"}, - {copy, "inetrc", "etc/ejabberd/inetrc"}, + {copy, "inetrc", "conf/inetrc"}, {copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"}, {copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]} ]}. @@ -193,20 +203,29 @@ {dev_mode, false}, {include_erts, true}, {include_src, true}, + {generate_start_script, false}, {overlay, [{copy, "sql/*", "lib/ejabberd-\{\{release_version\}\}/priv/sql/"}, - {copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"}, - {copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}]}]}]}, + {copy, "ejabberdctl.cfg.example", "conf/ejabberdctl.cfg"}, + {copy, "ejabberd.yml.example", "conf/ejabberd.yml"}]}]}]}, {dev, [{post_hooks, [{release, "rel/setup-dev.sh"}]}, {relx, [{debug_info, keep}, {dev_mode, true}, {include_erts, true}, {include_src, false}, - {overlay, [{copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg.example"}, - {copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml.example"}, - {copy, "test/ejabberd_SUITE_data/ca.pem", "etc/ejabberd/"}, - {copy, "test/ejabberd_SUITE_data/cert.pem", "etc/ejabberd/"}]}]}]}, + {generate_start_script, true}, + {extended_start_script, true}, + {overlay, [{copy, "ejabberdctl.cfg.example", "conf/ejabberdctl.cfg.example"}, + {copy, "ejabberd.yml.example", "conf/ejabberd.yml.example"}, + {copy, "test/ejabberd_SUITE_data/ca.pem", "conf/"}, + {copy, "test/ejabberd_SUITE_data/cert.pem", "conf/"}]}]}]}, {test, [{erl_opts, [nowarn_export_all]}]}]}. +{alias, [{relive, [{shell, "--apps ejabberd \ + --config rel/relive.config \ + --script rel/relive.escript \ + --name ejabberd@localhost"}]} +]}. + %% Local Variables: %% mode: erlang %% End: diff --git a/rebar.config.script b/rebar.config.script index efd51d6ba..33aeb8130 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -387,8 +387,8 @@ Rules = [ ]}]), []}, {[plugins], IsRebar3 and (os:getenv("GITHUB_ACTIONS") == "true"), AppendList([{coveralls, {git, - "https://github.com/RoadRunnr/coveralls-erl.git", - {branch, "feature/git-info"}}} ]), []}, + "https://github.com/processone/coveralls-erl.git", + {branch, "addjsonfile"}}} ]), []}, {[overrides], [post_hook_configure], SystemDeps == false, AppendList2(GenDepsConfigure), [], []}, {[ct_extra_params], [eunit_compile_opts], true, diff --git a/rebar3 b/rebar3 index c052f9ac6..bf1935ce6 100755 Binary files a/rebar3 and b/rebar3 differ diff --git a/rel/relive.config b/rel/relive.config new file mode 100644 index 000000000..7e3901fd4 --- /dev/null +++ b/rel/relive.config @@ -0,0 +1,3 @@ +[{mnesia, [{dir, "_build/relive/database"}]}, + {ejabberd, [{config, "_build/relive/conf/ejabberd.yml"}, + {log_path, "_build/relive/logs/ejabberd.log"}]}]. diff --git a/rel/relive.escript b/rel/relive.escript new file mode 100644 index 000000000..3ee2de0f3 --- /dev/null +++ b/rel/relive.escript @@ -0,0 +1,26 @@ +#!/usr/bin/env escript + +main(_) -> + Base = "_build/relive", + prepare(Base, "", none), + prepare(Base, "conf", {os, cmd, "rel/setup-relive.sh"}), + prepare(Base, "database", none), + prepare(Base, "logs", none), + c:erlangrc([os:cmd("echo -n $HOME")]), + ok. + +prepare(BaseDir, SuffixDir, MFA) -> + Dir = filename:join(BaseDir, SuffixDir), + case file:make_dir(Dir) of + ok -> + io:format("Preparing relive dir ~s...~n", [Dir]), + case MFA of + none -> ok; + {M, F, A} -> M:F(A) + end; + {error, eexist} -> + ok; + {error, LogsError} -> + io:format("Error creating dir ~s: ~p~n", [Dir, LogsError]), + halt(1) + end. diff --git a/rel/reltool.config.script b/rel/reltool.config.script index 459077964..ac3f5c7cb 100644 --- a/rel/reltool.config.script +++ b/rel/reltool.config.script @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov -%%% @copyright (C) 2013-2021, Evgeniy Khramtsov +%%% @copyright (C) 2013-2022, Evgeniy Khramtsov %%% @doc %%% %%% @end @@ -88,16 +88,15 @@ Sys = [{lib_dirs, []}, end, OTPApps). Overlay = [ - {mkdir, "var/log/ejabberd"}, - {mkdir, "var/lock"}, - {mkdir, "var/lib/ejabberd"}, - {mkdir, "etc/ejabberd"}, + {mkdir, "logs"}, + {mkdir, "database"}, + {mkdir, "conf"}, {mkdir, "doc"}, {template, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, {template, "../ejabberdctl.template", "bin/ejabberdctl"}, - {copy, "../ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"}, - {copy, "../ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}, - {copy, "../inetrc", "etc/ejabberd/inetrc"}, + {copy, "../ejabberdctl.cfg.example", "conf/ejabberdctl.cfg"}, + {copy, "../ejabberd.yml.example", "conf/ejabberd.yml"}, + {copy, "../inetrc", "conf/inetrc"}, {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"} ], diff --git a/rel/setup-dev.sh b/rel/setup-dev.sh index 8040f3dd0..79171ffe0 100755 --- a/rel/setup-dev.sh +++ b/rel/setup-dev.sh @@ -2,10 +2,10 @@ echo -n "===> Preparing dev configuration files: " PWD_DIR=`pwd` REL_DIR=$PWD_DIR/_build/dev/rel/ejabberd/ -CON_DIR=$REL_DIR/etc/ejabberd/ +CON_DIR=$REL_DIR/conf/ [ -z "$REL_DIR_TEMP" ] && REL_DIR_TEMP=$REL_DIR -CON_DIR_TEMP=$REL_DIR_TEMP/etc/ejabberd/ +CON_DIR_TEMP=$REL_DIR_TEMP/conf/ BIN_DIR_TEMP=$REL_DIR_TEMP/bin/ cd $CON_DIR_TEMP diff --git a/rel/setup-relive.sh b/rel/setup-relive.sh new file mode 100755 index 000000000..4e726be88 --- /dev/null +++ b/rel/setup-relive.sh @@ -0,0 +1,30 @@ +PWD_DIR=`pwd` +REL_DIR=$PWD_DIR/_build/relive/ +CON_DIR=$REL_DIR/conf/ + +[ -z "$REL_DIR_TEMP" ] && REL_DIR_TEMP=$REL_DIR +CON_DIR_TEMP=$REL_DIR_TEMP/conf/ + +make ejabberdctl.relive +chmod +x ejabberdctl.relive +mv ejabberdctl.relive $REL_DIR/ejabberdctl + +cp inetrc $CON_DIR/ +cp ejabberdctl.cfg.example $CON_DIR/ejabberdctl.cfg.example +cp ejabberd.yml.example $CON_DIR/ejabberd.yml.example +cp test/ejabberd_SUITE_data/ca.pem $CON_DIR +cp test/ejabberd_SUITE_data/cert.pem $CON_DIR + +cd $CON_DIR_TEMP + +sed -i "s|# certfiles:|certfiles:\n - $CON_DIR/cert.pem|g" ejabberd.yml.example +sed -i "s|certfiles:|ca_file: $CON_DIR/ca.pem\ncertfiles:|g" ejabberd.yml.example +sed -i 's|^acl:$|acl:\n admin: [user: admin]|g' ejabberd.yml.example +[ ! -f "$CON_DIR/ejabberd.yml" ] \ + && echo -n "ejabberd.yml " \ + && mv ejabberd.yml.example ejabberd.yml + +sed -i "s|#' POLL|EJABBERD_BYPASS_WARNINGS=true\n\n#' POLL|g" ejabberdctl.cfg.example +[ ! -f "$CON_DIR/ejabberdctl.cfg" ] \ + && echo -n "ejabberdctl.cfg " \ + && mv ejabberdctl.cfg.example ejabberdctl.cfg diff --git a/rel/sys.config b/rel/sys.config index ff6f0f368..26b0d0c61 100644 --- a/rel/sys.config +++ b/rel/sys.config @@ -1,2 +1,2 @@ -[{ejabberd, [{config, "etc/ejabberd/ejabberd.yml"}, - {log_path, "var/log/ejabberd/ejabberd.log"}]}]. +[{ejabberd, [{config, "conf/ejabberd.yml"}, + {log_path, "logs/ejabberd.log"}]}]. diff --git a/rel/vm.args b/rel/vm.args index 29858b0dc..6301f464d 100644 --- a/rel/vm.args +++ b/rel/vm.args @@ -4,7 +4,7 @@ ## Cookie for distributed erlang #-setcookie ejabberd --mnesia dir \"var/lib/ejabberd\" +-mnesia dir \"database\" ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive ## (Disabled by default..use with caution!) diff --git a/rel/vm.args.mix b/rel/vm.args.mix deleted file mode 100644 index 711bd3189..000000000 --- a/rel/vm.args.mix +++ /dev/null @@ -1,14 +0,0 @@ -## Customize flags given to the VM: https://erlang.org/doc/man/erl.html -## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here - --boot ../releases/<%= @version %>/start_clean --boot_var RELEASE_LIB ../lib - -## Number of dirty schedulers doing IO work (file, sockets, and others) -##+SDio 5 - -## Increase number of concurrent ports/sockets -##+Q 65536 - -## Tweak GC to run more often -##-env ERL_FULLSWEEP_AFTER 10 diff --git a/sql/lite.new.sql b/sql/lite.new.sql index 96c880358..9eb34c974 100644 --- a/sql/lite.new.sql +++ b/sql/lite.new.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -285,6 +285,7 @@ CREATE TABLE muc_room ( ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room (host, created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -330,6 +331,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers(jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid); CREATE TABLE motd ( diff --git a/sql/lite.sql b/sql/lite.sql index 087035d7f..0580fcbaa 100644 --- a/sql/lite.sql +++ b/sql/lite.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -260,6 +260,7 @@ CREATE TABLE muc_room ( ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room (host, created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -302,6 +303,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers(jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid); CREATE TABLE motd ( diff --git a/sql/mssql.sql b/sql/mssql.sql index bb7861527..e05a7b51d 100644 --- a/sql/mssql.sql +++ b/sql/mssql.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -32,7 +32,7 @@ CREATE TABLE [dbo].[archive] ( [kind] [varchar] (10) NULL, [nick] [varchar] (250) NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [archive_PK] PRIMARY KEY CLUSTERED + CONSTRAINT [archive_PK] PRIMARY KEY CLUSTERED ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -56,7 +56,7 @@ CREATE TABLE [dbo].[archive_prefs] ( [always] [text] NOT NULL, [never] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [archive_prefs_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [archive_prefs_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -76,7 +76,7 @@ CREATE TABLE [dbo].[last] ( [username] [varchar] (250) NOT NULL, [seconds] [text] NOT NULL, [state] [text] NOT NULL, - CONSTRAINT [last_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [last_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -86,7 +86,7 @@ CREATE TABLE [dbo].[motd] ( [username] [varchar] (250) NOT NULL, [xml] [text] NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [motd_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [motd_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -114,6 +114,8 @@ CREATE TABLE [dbo].[muc_room] ( CREATE UNIQUE CLUSTERED INDEX [muc_room_name_host] ON [muc_room] (name, host) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); +CREATE INDEX [muc_room_host_created_at] ON [muc_registered] (host, nick) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[muc_online_room] ( [name] [varchar] (250) NOT NULL, @@ -150,11 +152,12 @@ CREATE TABLE [dbo].[muc_room_subscribers] ( CREATE UNIQUE CLUSTERED INDEX [muc_room_subscribers_host_room_jid] ON [muc_room_subscribers] (host, room, jid); CREATE INDEX [muc_room_subscribers_host_jid] ON [muc_room_subscribers] (host, jid); +CREATE INDEX [muc_room_subscribers_jid] ON [muc_room_subscribers] (jid); CREATE TABLE [dbo].[privacy_default_list] ( [username] [varchar] (250) NOT NULL, [name] [varchar] (250) NOT NULL, - CONSTRAINT [privacy_default_list_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [privacy_default_list_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -165,7 +168,7 @@ CREATE TABLE [dbo].[privacy_list] ( [name] [varchar] (250) NOT NULL, [id] [bigint] IDENTITY(1,1) NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [privacy_list_PK] PRIMARY KEY CLUSTERED + CONSTRAINT [privacy_list_PK] PRIMARY KEY CLUSTERED ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -244,7 +247,7 @@ CREATE TABLE [dbo].[pubsub_state] ( [affiliation] [char] (1) NOT NULL, [subscriptions] [text] NOT NULL DEFAULT '', [stateid] [bigint] IDENTITY(1,1) NOT NULL, - CONSTRAINT [pubsub_state_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [pubsub_state_PRIMARY] PRIMARY KEY CLUSTERED ( [stateid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -271,7 +274,7 @@ CREATE TABLE [dbo].[pubsub_node] ( [parent] [varchar] (255) NOT NULL DEFAULT '', [plugin] [text] NOT NULL, [nodeid] [bigint] IDENTITY(1,1) NOT NULL, - CONSTRAINT [pubsub_node_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [pubsub_node_PRIMARY] PRIMARY KEY CLUSTERED ( [nodeid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -286,7 +289,7 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW CREATE TABLE [dbo].[roster_version] ( [username] [varchar] (250) NOT NULL, [version] [text] NOT NULL, - CONSTRAINT [roster_version_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [roster_version_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -347,7 +350,7 @@ CREATE TABLE [dbo].[spool] ( [xml] [text] NOT NULL, [seq] [bigint] IDENTITY(1,1) NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [spool_PK] PRIMARY KEY CLUSTERED + CONSTRAINT [spool_PK] PRIMARY KEY CLUSTERED ( [seq] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -364,7 +367,7 @@ CREATE TABLE [dbo].[sr_group] ( [name] [varchar] (250) NOT NULL, [opts] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [sr_group_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [sr_group_PRIMARY] PRIMARY KEY CLUSTERED ( [name] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -392,7 +395,7 @@ CREATE TABLE [dbo].[users] ( [salt] [text] NOT NULL DEFAULT '', [iterationcount] [smallint] NOT NULL DEFAULT 0, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -402,7 +405,7 @@ CREATE TABLE [dbo].[vcard] ( [username] [varchar] (250) NOT NULL, [vcard] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), - CONSTRAINT [vcard_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [vcard_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -433,7 +436,7 @@ CREATE TABLE [dbo].[vcard_search] ( [lorgname] [varchar] (250) NOT NULL, [orgunit] [text] NOT NULL, [lorgunit] [varchar] (250) NOT NULL, - CONSTRAINT [vcard_search_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [vcard_search_PRIMARY] PRIMARY KEY CLUSTERED ( [lusername] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -501,7 +504,7 @@ CREATE TABLE [dbo].[oauth_token] ( [jid] [text] NOT NULL, [scope] [text] NOT NULL, [expire] [bigint] NOT NULL, - CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED ( [token] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -525,7 +528,7 @@ CREATE TABLE [dbo].[bosh] ( [sid] [varchar] (255) NOT NULL, [node] [varchar] (255) NOT NULL, [pid] [varchar](100) NOT NULL - CONSTRAINT [bosh_PRIMARY] PRIMARY KEY CLUSTERED + CONSTRAINT [bosh_PRIMARY] PRIMARY KEY CLUSTERED ( [sid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) @@ -545,7 +548,7 @@ WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW CREATE UNIQUE INDEX [i_push_ut] ON [push_session] (username, timestamp) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); -CREATE TABLE [dbo].[mqtt_pub]( +CREATE TABLE [dbo].[mqtt_pub] ( [username] [varchar](191) NOT NULL, [server_host] [varchar](191) NOT NULL, [resource] [varchar](191) NOT NULL, diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql index c6d74bb91..dc514becf 100644 --- a/sql/mysql.new.sql +++ b/sql/mysql.new.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -86,7 +86,7 @@ CREATE TABLE sr_user ( PRIMARY KEY (server_host(191), jid, grp) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE UNIQUE INDEX i_sr_user_sh_jid_group ON sr_group(server_host(191), jid, grp); +CREATE UNIQUE INDEX i_sr_user_sh_jid_group ON sr_user(server_host(191), jid, grp); CREATE INDEX i_sr_user_sh_jid ON sr_user(server_host(191), jid); CREATE INDEX i_sr_user_sh_grp ON sr_user(server_host(191), grp); @@ -301,6 +301,7 @@ CREATE TABLE muc_room ( ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); +CREATE INDEX i_muc_room_host_created_at ON muc_room(host(75), created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -347,6 +348,7 @@ CREATE TABLE muc_room_subscribers ( ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid USING BTREE ON muc_room_subscribers(jid); CREATE TABLE motd ( username varchar(191) NOT NULL, diff --git a/sql/mysql.old-to-new.sql b/sql/mysql.old-to-new.sql index 59c9befe2..9614d55a8 100644 --- a/sql/mysql.old-to-new.sql +++ b/sql/mysql.old-to-new.sql @@ -77,6 +77,7 @@ BEGIN ALTER TABLE `last` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `sr_group` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `name`; ALTER TABLE `sr_group` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `sr_group` ADD UNIQUE INDEX `i_sr_group_sh_name` (`server_host`, `name`); ALTER TABLE `sr_group` ADD PRIMARY KEY (`server_host`, `name`); ALTER TABLE `muc_registered` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; ALTER TABLE `muc_registered` ALTER COLUMN `server_host` DROP DEFAULT; @@ -99,6 +100,7 @@ BEGIN ALTER TABLE `sr_user` DROP INDEX `i_sr_user_jid_group`; ALTER TABLE `sr_user` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `jid`; ALTER TABLE `sr_user` ALTER COLUMN `server_host` DROP DEFAULT; + ALTER TABLE `sr_user` ADD UNIQUE INDEX `i_sr_user_sh_jid_group` (`server_host`, `jid`, `grp`); ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_jid` (`server_host`, `jid`); ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_grp` (`server_host`, `grp`); ALTER TABLE `sr_user` ADD PRIMARY KEY (`server_host`, `jid`, `grp`); diff --git a/sql/mysql.sql b/sql/mysql.sql index 7feaf5d0a..ae4a73312 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -276,6 +276,7 @@ CREATE TABLE muc_room ( ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); +CREATE INDEX i_muc_room_host_created_at ON muc_room(host(75), created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -319,6 +320,7 @@ CREATE TABLE muc_room_subscribers ( ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid USING BTREE ON muc_room_subscribers(jid); CREATE TABLE motd ( username varchar(191) PRIMARY KEY, diff --git a/sql/pg.new.sql b/sql/pg.new.sql index 8a35ec6c9..6700a4771 100644 --- a/sql/pg.new.sql +++ b/sql/pg.new.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -156,6 +156,30 @@ -- CREATE INDEX i_sm_sh_username ON sm USING btree (server_host, username); -- ALTER TABLE sm ALTER COLUMN server_host DROP DEFAULT; +-- ALTER TABLE push_session ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_push_usn; +-- DROP INDEX i_push_ut; +-- ALTER TABLE push_session ADD PRIMARY KEY (server_host, username, timestamp); +-- CREATE UNIQUE INDEX i_push_session_susn ON push_session USING btree (server_host, username, service, node); +-- ALTER TABLE push_session ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE mix_pam ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_mix_pam; +-- DROP INDEX i_mix_pam_us; +-- CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); +-- CREATE INDEX i_mix_pam_us ON mix_pam (username, server_host); +-- ALTER TABLE mix_pam ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE route ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_route; +-- CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); +-- ALTER TABLE i_route ALTER COLUMN server_host DROP DEFAULT; + +-- ALTER TABLE mqtt_pub ADD COLUMN server_host text NOT NULL DEFAULT ''; +-- DROP INDEX i_mqtt_topic; +-- CREATE UNIQUE INDEX i_mqtt_topic_server ON mqtt_pub (topic, server_host); +-- ALTER TABLE mqtt_pub ALTER COLUMN server_host DROP DEFAULT; + CREATE TABLE users ( username text NOT NULL, @@ -305,7 +329,7 @@ CREATE TABLE vcard_search ( lorgname text NOT NULL, orgunit text NOT NULL, lorgunit text NOT NULL, - PRIMARY KEY (server_host, username) + PRIMARY KEY (server_host, lusername) ); CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); @@ -444,6 +468,7 @@ CREATE TABLE muc_room ( ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room USING btree (host, created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -489,6 +514,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers USING btree (jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); CREATE TABLE motd ( diff --git a/sql/pg.sql b/sql/pg.sql index 0e3d4c8b8..4cf4f0cbd 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -1,5 +1,5 @@ -- --- ejabberd, Copyright (C) 2002-2021 ProcessOne +-- ejabberd, Copyright (C) 2002-2022 ProcessOne -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as @@ -278,6 +278,7 @@ CREATE TABLE muc_room ( ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); +CREATE INDEX i_muc_room_host_created_at ON muc_room USING btree (host, created_at); CREATE TABLE muc_registered ( jid text NOT NULL, @@ -320,6 +321,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers USING btree (jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); CREATE TABLE motd ( diff --git a/src/acl.erl b/src/acl.erl index 5db79eba9..7e03298ba 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/econf.erl b/src/econf.erl index cb8cb8506..1a8711b48 100644 --- a/src/econf.erl +++ b/src/econf.erl @@ -3,7 +3,7 @@ %%% Purpose : Validator for ejabberd configuration options %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd.app.src.script b/src/ejabberd.app.src.script index 67a7a470d..4c8745146 100644 --- a/src/ejabberd.app.src.script +++ b/src/ejabberd.app.src.script @@ -14,15 +14,14 @@ Vars = case file:consult(filename:join([filename:dirname(SCRIPT), "..", "vars.co Vars ++ [{modules, []}, {registered, []}, - {applications, [kernel, sasl, ssl, stdlib]}, + {applications, [kernel, sasl, ssl, stdlib, syntax_tools]}, {included_applications, - [inets, mnesia, os_mon, + [compiler, inets, mnesia, os_mon, cache_tab, eimp, fast_tls, fast_xml, fast_yaml, - lager, p1_acme, p1_utils, pkix, diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 112654741..048eb7d98 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -52,7 +52,7 @@ halt() -> ejabberd_logger:flush(), erlang:halt(1, [{flush, true}]). -%% @spec () -> false | string() +-spec get_pid_file() -> false | string(). get_pid_file() -> case os:getenv("EJABBERD_PID_PATH") of false -> diff --git a/src/ejabberd_access_permissions.erl b/src/ejabberd_access_permissions.erl index 916475df9..d4c9dd018 100644 --- a/src/ejabberd_access_permissions.erl +++ b/src/ejabberd_access_permissions.erl @@ -5,7 +5,7 @@ %%% Created : 7 Sep 2016 by Paweł Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -344,10 +344,10 @@ validator(from) -> fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; - (A) -> (econf:enum([ejabberd_xmlrpc, mod_http_api, ejabberd_ctl]))(A) + (A) -> (econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl]))(A) end, lists:flatten(L)); (A) -> - [(econf:enum([ejabberd_xmlrpc, mod_http_api, ejabberd_ctl]))(A)] + [(econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl]))(A)] end; validator(what) -> econf:and_then( diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 42f17baa1..c67b7a0d7 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,7 +27,8 @@ %% Hooks -export([ejabberd_started/0, register_certfiles/0, cert_expired/2]). %% ejabberd commands --export([request_certificate/1, revoke_certificate/1, list_certificates/0]). +-export([get_commands_spec/0, request_certificate/1, + revoke_certificate/1, list_certificates/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, format_status/2]). diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 2eeb97b99..97e217cbc 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -5,7 +5,7 @@ %%% Created : 7 May 2006 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,7 +30,8 @@ -export([start_link/0, %% Server - status/0, reopen_log/0, rotate_log/0, + status/0, stop/0, restart/0, + reopen_log/0, rotate_log/0, set_loglevel/1, stop_kindly/2, send_service_message_all_mucs/2, registered_vhosts/0, @@ -40,7 +41,7 @@ %% Cluster join_cluster/1, leave_cluster/1, list_cluster/0, %% Erlang - update_list/0, update/1, + update_list/0, update/1, update/0, %% Accounts register/3, unregister/2, registered_users/1, @@ -59,8 +60,8 @@ restore/1, % Still used by some modules clear_cache/0, gc/0, - get_commands_spec/0 - ]). + get_commands_spec/0, + delete_old_messages_batch/4, delete_old_messages_status/1, delete_old_messages_abort/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -113,18 +114,21 @@ get_commands_spec() -> args = [], result = {res, restuple}}, #ejabberd_commands{name = stop, tags = [server], desc = "Stop ejabberd gracefully", - module = init, function = stop, + module = ?MODULE, function = stop, args = [], result = {res, rescode}}, #ejabberd_commands{name = restart, tags = [server], desc = "Restart ejabberd gracefully", - module = init, function = restart, + module = ?MODULE, function = restart, args = [], result = {res, rescode}}, - #ejabberd_commands{name = reopen_log, tags = [logs, server], - desc = "Reopen the log files", + #ejabberd_commands{name = reopen_log, tags = [logs], + desc = "Reopen the log files after being renamed", + longdesc = "This can be useful when an external tool is " + "used for log rotation. See " + "https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files", policy = admin, module = ?MODULE, function = reopen_log, args = [], result = {res, rescode}}, - #ejabberd_commands{name = rotate_log, tags = [logs, server], + #ejabberd_commands{name = rotate_log, tags = [logs], desc = "Rotate the log files", module = ?MODULE, function = rotate_log, args = [], result = {res, rescode}}, @@ -139,14 +143,14 @@ get_commands_spec() -> args_example = [60, <<"Server will stop now.">>], args = [{delay, integer}, {announcement, string}], result = {res, rescode}}, - #ejabberd_commands{name = get_loglevel, tags = [logs, server], + #ejabberd_commands{name = get_loglevel, tags = [logs], desc = "Get the current loglevel", module = ejabberd_logger, function = get, result_desc = "Tuple with the log level number, its keyword and description", result_example = warning, args = [], result = {levelatom, atom}}, - #ejabberd_commands{name = set_loglevel, tags = [logs, server], + #ejabberd_commands{name = set_loglevel, tags = [logs], desc = "Set the loglevel", module = ?MODULE, function = set_loglevel, args_desc = ["Desired logging level: none | emergency | alert | critical " @@ -178,6 +182,8 @@ get_commands_spec() -> result = {res, restuple}}, #ejabberd_commands{name = unregister, tags = [accounts], desc = "Unregister a user", + longdesc = "This deletes the authentication and all the " + "data associated to the account (roster, vcard...).", policy = admin, module = ?MODULE, function = unregister, args_desc = ["Username", "Local vhost served by ejabberd"], @@ -200,7 +206,7 @@ get_commands_spec() -> result_example = [<<"example.com">>, <<"anon.example.com">>], args = [], result = {vhosts, {list, {vhost, string}}}}, - #ejabberd_commands{name = reload_config, tags = [server, config], + #ejabberd_commands{name = reload_config, tags = [config], desc = "Reload config file in memory", module = ?MODULE, function = reload_config, args = [], @@ -254,21 +260,21 @@ get_commands_spec() -> module = ejabberd_piefxis, function = import_file, args_desc = ["Full path to the PIEFXIS file"], args_example = ["/var/lib/ejabberd/example.com.xml"], - args = [{file, string}], result = {res, rescode}}, + args = [{file, binary}], result = {res, rescode}}, #ejabberd_commands{name = export_piefxis, tags = [mnesia], desc = "Export data of all users in the server to PIEFXIS files (XEP-0227)", module = ejabberd_piefxis, function = export_server, args_desc = ["Full path to a directory"], args_example = ["/var/lib/ejabberd/"], - args = [{dir, string}], result = {res, rescode}}, + args = [{dir, binary}], result = {res, rescode}}, #ejabberd_commands{name = export_piefxis_host, tags = [mnesia], desc = "Export data of users in a host to PIEFXIS files (XEP-0227)", module = ejabberd_piefxis, function = export_host, args_desc = ["Full path to a directory", "Vhost to export"], args_example = ["/var/lib/ejabberd/", "example.com"], - args = [{dir, string}, {host, string}], result = {res, rescode}}, + args = [{dir, binary}, {host, binary}], result = {res, rescode}}, - #ejabberd_commands{name = delete_mnesia, tags = [mnesia, sql], + #ejabberd_commands{name = delete_mnesia, tags = [mnesia], desc = "Delete elements in Mnesia database for a given vhost", module = ejd2sql, function = delete, args_desc = ["Vhost which content will be deleted in Mnesia database"], @@ -314,6 +320,39 @@ get_commands_spec() -> args_desc = ["Number of days"], args_example = [31], args = [{days, integer}], result = {res, rescode}}, + #ejabberd_commands{name = delete_old_messages_batch, tags = [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 = [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 = [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", @@ -326,7 +365,7 @@ get_commands_spec() -> args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"], args = [{host, string}, {file, string}], result = {res, rescode}}, - #ejabberd_commands{name = set_master, tags = [mnesia], + #ejabberd_commands{name = set_master, tags = [cluster], desc = "Set master node of the clustered Mnesia tables", longdesc = "If you provide as nodename \"self\", this " "node will be set as its own master.", @@ -345,31 +384,41 @@ get_commands_spec() -> {oldbackup, string}, {newbackup, string}], result = {res, restuple}}, #ejabberd_commands{name = backup, tags = [mnesia], - desc = "Store the database to backup file", + desc = "Backup the Mnesia database to a binary file", module = ?MODULE, function = backup_mnesia, args_desc = ["Full path for the destination backup file"], args_example = ["/var/lib/ejabberd/database.backup"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = restore, tags = [mnesia], - desc = "Restore the database from backup file", + desc = "Restore the Mnesia database from a binary backup file", + longdesc = "This restores immediately from a " + "binary backup file the internal Mnesia " + "database. This will consume a lot of memory if " + "you have a large database, you may prefer " + "'install_fallback'.", module = ?MODULE, function = restore_mnesia, args_desc = ["Full path to the backup file"], args_example = ["/var/lib/ejabberd/database.backup"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = dump, tags = [mnesia], - desc = "Dump the database to a text file", + desc = "Dump the Mnesia database to a text file", module = ?MODULE, function = dump_mnesia, args_desc = ["Full path for the text file"], args_example = ["/var/lib/ejabberd/database.txt"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = dump_table, tags = [mnesia], - desc = "Dump a table to a text file", + desc = "Dump a Mnesia table to a text file", module = ?MODULE, function = dump_table, args_desc = ["Full path for the text file", "Table name"], args_example = ["/var/lib/ejabberd/table-muc-registered.txt", "muc_registered"], args = [{file, string}, {table, string}], result = {res, restuple}}, #ejabberd_commands{name = load, tags = [mnesia], - desc = "Restore the database from a text file", + desc = "Restore Mnesia database from a text dump file", + longdesc = "Restore immediately. This is not " + "recommended for big databases, as it will " + "consume much time, memory and processor. In " + "that case it's preferable to use 'backup' and " + "'install_fallback'.", module = ?MODULE, function = load_mnesia, args_desc = ["Full path to the text file"], args_example = ["/var/lib/ejabberd/database.txt"], @@ -385,7 +434,14 @@ get_commands_spec() -> args_example = ["roster"], args = [{table, string}], result = {res, string}}, #ejabberd_commands{name = install_fallback, tags = [mnesia], - desc = "Install the database from a fallback file", + desc = "Install Mnesia database from a binary backup file", + longdesc = "The binary backup file is " + "installed as fallback: it will be used to " + "restore the database at the next ejabberd " + "start. This means that, after running this " + "command, you have to restart ejabberd. This " + "command requires less memory than + 'restore'.", module = ?MODULE, function = install_fallback_mnesia, args_desc = ["Full path to the fallback file"], args_example = ["/var/lib/ejabberd/database.fallback"], @@ -424,6 +480,16 @@ status() -> end, {Is_running, String1 ++ 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, []). @@ -518,6 +584,15 @@ update_module(ModuleNameString) -> {error, Reason} -> {error, Reason} end. +update() -> + io:format("Compiling ejabberd...~n", []), + os:cmd("make"), + Mods = ejabberd_admin:update_list(), + io:format("Updating modules: ~p~n", [Mods]), + ejabberd_admin:update("all"), + io:format("Updated modules: ", []), + Mods -- ejabberd_admin:update_list(). + %%% %%% Account management %%% @@ -640,6 +715,64 @@ delete_old_messages(Days) -> {atomic, _} = mod_offline:remove_old_messages(Days, Host) 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"} + 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, + 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" + end. + %%% %%% Mnesia management %%% diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 161f1dab1..45e92d064 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 35e4921b7..c64c508d9 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -5,7 +5,7 @@ %%% Created : 23 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index dd7ff28eb..bfe4e0cac 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -5,7 +5,7 @@ %%% Created : 17 Feb 2006 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index a513c24ba..a5320b7bc 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_auth_jwt.erl b/src/ejabberd_auth_jwt.erl index 6df742305..d1fe4d15a 100644 --- a/src/ejabberd_auth_jwt.erl +++ b/src/ejabberd_auth_jwt.erl @@ -5,7 +5,7 @@ %%% Created : 16 Mar 2019 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -85,7 +85,14 @@ check_password(User, AuthzId, Server, Token) -> end end. -user_exists(_User, _Host) -> {nocache, false}. +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}. use_cache(_) -> false. @@ -139,6 +146,9 @@ check_jwt_token(User, Server, Token) -> {false, _, _} -> false catch - error:{badarg, _} -> + A:B -> + ?DEBUG("jose_jwt:verify failed ~n for account ~p@~p~n " + " JWK and token: ~p~n with error: ~p", + [User, Server, {JWK, Token}, {A, B}]), false end. diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 7f5bd87c8..9195d2498 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -151,7 +151,6 @@ get_users(Server, []) -> count_users(Server, Opts) -> length(get_users(Server, Opts)). -%% @spec (User, Server) -> true | false | {error, Error} user_exists(User, Server) -> case catch user_exists_ldap(User, Server) of {'EXIT', _Error} -> {nocache, {error, db_failure}}; diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index 168ee76f5..bb1aa92bf 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index e3b0eba6e..aa48a1931 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -5,7 +5,7 @@ %%% Created : 5 Jul 2007 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index 1f7106c59..ca7fd0889 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -299,6 +299,20 @@ export(_Server) -> ["username=%(LUser)s", "server_host=%(LServer)s", "password=%(Password)s"])]; + (Host, {passwd, {LUser, LServer}, + {scram, StoredKey1, ServerKey, Salt, IterationCount}}) + when LServer == Host -> + Hash = sha, + StoredKey = scram_hash_encode(Hash, StoredKey1), + [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"])]; (Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram}) when LServer == Host -> StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey), diff --git a/src/ejabberd_backend_sup.erl b/src/ejabberd_backend_sup.erl index c80dd8685..575e66e40 100644 --- a/src/ejabberd_backend_sup.erl +++ b/src/ejabberd_backend_sup.erl @@ -2,7 +2,7 @@ %%% Created : 24 Feb 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_batch.erl b/src/ejabberd_batch.erl new file mode 100644 index 000000000..406a79e21 --- /dev/null +++ b/src/ejabberd_batch.erl @@ -0,0 +1,205 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_batch.erl +%%% Author : Paweł Chmielowski +%%% Purpose : Batch tasks manager +%%% Created : 8 mar 2022 by Paweł Chmielowski +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(ejabberd_batch). +-author("pawel@process-one.net"). + +-behaviour(gen_server). + +-include("logger.hrl"). + +%% API +-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([register_task/5, task_status/1, abort_task/1]). + +-define(SERVER, ?MODULE). + +-record(state, {tasks = #{}}). +-record(task, {state = not_started, pid, steps, done_steps}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +%% @doc Spawns the server and registers the local name (unique) +-spec(start_link() -> + {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). +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{}}). +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} + 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} + 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} + 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{}}). +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} + 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} + 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} + 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{}}). +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()). +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()}). +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}) + end. diff --git a/src/ejabberd_bosh.erl b/src/ejabberd_bosh.erl index e9d2f0c0a..3bcf4f6f6 100644 --- a/src/ejabberd_bosh.erl +++ b/src/ejabberd_bosh.erl @@ -5,7 +5,7 @@ %%% Created : 20 Jul 2011 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 3218cce51..ec5c610e8 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -2,7 +2,7 @@ %%% Created : 8 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -285,12 +285,14 @@ process_closed(State, Reason) -> stop_async(self()), State#{stop_reason => Reason}. -process_terminated(#{sid := SID, socket := Socket, - jid := JID, user := U, server := S, resource := R} = State, +process_terminated(#{sid := SID, jid := JID, user := U, server := S, resource := R} = State, Reason) -> Status = format_reason(State, Reason), ?INFO_MSG("(~ts) Closing c2s session for ~ts: ~ts", - [xmpp_socket:pp(Socket), jid:encode(JID), Status]), + [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)}, @@ -305,10 +307,12 @@ process_terminated(#{sid := SID, socket := Socket, end, bounce_message_queue(SID, JID), State1; -process_terminated(#{socket := Socket, - stop_reason := {tls, _}} = State, Reason) -> +process_terminated(#{stop_reason := {tls, _}} = State, Reason) -> ?WARNING_MSG("(~ts) Failed to secure c2s connection: ~ts", - [xmpp_socket:pp(Socket), 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. @@ -773,9 +777,9 @@ broadcast_presence_unavailable(#{jid := JID, pres_a := PresA} = State, Pres, Roster = ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]), lists:foldl( - fun(#roster{jid = LJID, subscription = Sub}, Acc) + fun(#roster_item{jid = ItemJID, subscription = Sub}, Acc) when Sub == both; Sub == from -> - maps:put(LJID, 1, Acc); + maps:put(jid:tolower(ItemJID), 1, Acc); (_, Acc) -> Acc end, #{BareJID => 1}, Roster); @@ -809,8 +813,7 @@ broadcast_presence_available(#{jid := JID} = State, [], [{LUser, LServer}]), {FJIDs, TJIDs} = lists:foldl( - fun(#roster{jid = LJID, subscription = Sub}, {F, T}) -> - To = jid:make(LJID), + 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 @@ -839,10 +842,9 @@ broadcast_presence_available(#{jid := JID} = State, Items = ejabberd_hooks:run_fold( roster_get, LServer, [], [{LUser, LServer}]), JIDs = lists:foldl( - fun(#roster{jid = LJID, subscription = Sub}, Tos) + fun(#roster_item{jid = To, subscription = Sub}, Tos) when Sub == both orelse Sub == from -> - To = jid:make(LJID), - P = xmpp:set_to(Pres, jid:make(LJID)), + P = xmpp:set_to(Pres, To), case privacy_check_packet(State, P, out) of allow -> [To|Tos]; deny -> Tos diff --git a/src/ejabberd_c2s_config.erl b/src/ejabberd_c2s_config.erl index b6c57ef98..4100ae23c 100644 --- a/src/ejabberd_c2s_config.erl +++ b/src/ejabberd_c2s_config.erl @@ -6,7 +6,7 @@ %%% Created : 2 Nov 2007 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index 343a8e54e..69b14915f 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -5,7 +5,7 @@ %%% Created : 26 Apr 2008 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl index f9f233da8..0a5364beb 100644 --- a/src/ejabberd_cluster.erl +++ b/src/ejabberd_cluster.erl @@ -3,7 +3,7 @@ %%% Created : 5 Jul 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_cluster_mnesia.erl b/src/ejabberd_cluster_mnesia.erl index 313cf9257..81bf010d7 100644 --- a/src/ejabberd_cluster_mnesia.erl +++ b/src/ejabberd_cluster_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 7 Oct 2015 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 0f86f23d3..5371f4da4 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -5,7 +5,7 @@ %%% Created : 20 May 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -41,6 +41,7 @@ get_tags_commands/0, get_tags_commands/1, register_commands/1, + register_commands/2, unregister_commands/1, get_commands_spec/0, get_commands_definition/0, @@ -90,6 +91,17 @@ get_commands_spec() -> "that will have example invocation include in markdown document"], result_desc = "0 if command failed, 1 when succeeded", args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + result_example = ok}, + #ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation], + desc = "Generates markdown documentation for ejabberd_commands", + 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() -> @@ -129,10 +141,13 @@ code_change(_OldVsn, State, _Extra) -> -spec register_commands([ejabberd_commands()]) -> ok. register_commands(Commands) -> + register_commands(unknown, Commands). + +register_commands(Definer, Commands) -> lists:foreach( fun(Command) -> %% XXX check if command exists - mnesia:dirty_write(Command) + mnesia:dirty_write(Command#ejabberd_commands{definer = Definer}) %% ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands), diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl index 4981577b6..0fe5f6402 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -5,7 +5,7 @@ %%% Created : 20 May 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,6 +28,7 @@ -export([generate_html_output/3]). -export([generate_md_output/3]). +-export([generate_tags_md/1]). -include("ejabberd_commands.hrl"). @@ -360,8 +361,16 @@ gen_param(Name, Type, Desc, HTMLOutput) -> [?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]), ?TAG(dd, ?RAW(Desc))]. -gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc, - args=Args, args_desc=ArgsDesc, note=Note, +make_tags(HTMLOutput) -> + TagsList = ejabberd_commands:get_tags_commands(1000000), + lists:map(fun(T) -> gen_tags(T, HTMLOutput) end, TagsList). + +-dialyzer({no_match, gen_tags/2}). +gen_tags({TagName, Commands}, HTMLOutput) -> + [?TAG(h1, TagName) | [?TAG(p, ?RAW("* *`"++C++"`*")) || C <- Commands]]. + +gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, + args=Args, args_desc=ArgsDesc, note=Note, definer=Definer, result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) -> try ArgsText = case ArgsDesc of @@ -389,6 +398,17 @@ gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc, [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] end end, + TagsText = [?RAW("*`"++atom_to_list(Tag)++"`* ") || Tag <- Tags], + IsDefinerMod = case Definer of + unknown -> true; + _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes))) + end, + ModuleText = case IsDefinerMod of + true -> + [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("*`"++atom_to_list(Definer)++"`*"))]; + false -> + [] + end, NoteEl = case Note of "" -> []; _ -> ?TAG('div', "note-down", ?RAW(Note)) @@ -403,6 +423,8 @@ gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc, end, ?TAG(h2, <<"Arguments:">>), ArgsText, ?TAG(h2, <<"Result:">>), ResultText, + ?TAG(h2, <<"Tags:">>), ?TAG(p, TagsText)] + ++ ModuleText ++ [ ?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)] catch _:Ex -> @@ -421,12 +443,13 @@ find_commands_definitions() -> lists:flatmap(fun(P) -> Mod = list_to_atom(filename:rootname(P)), code:ensure_loaded(Mod), - case erlang:function_exported(Mod, get_commands_spec, 0) of + Cs = case erlang:function_exported(Mod, get_commands_spec, 0) of true -> apply(Mod, get_commands_spec, []); _ -> [] - end + end, + [C#ejabberd_commands{definer = Mod} || C <- Cs] end, filelib:wildcard("*.beam", Path)) end. @@ -466,15 +489,25 @@ generate_md_output(File, RegExp, Languages) -> end, Cmds2), Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], Langs = binary:split(Languages, <<",">>, [global]), - Header = <<"---\ntitle: Administration API reference\ntoc: true\nmenu: Administration API\norder: 40\n" + Header = <<"---\ntitle: Administration API reference\ntoc: true\nmenu: API Reference\norder: 1\n" "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_commands'\n---\n\n" - "This section describes API of ejabberd.">>, + "This section describes API of ejabberd.\n">>, Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds4), {ok, Fh} = file:open(File, [write]), io:format(Fh, "~ts~ts", [Header, Out]), file:close(Fh), ok. +generate_tags_md(File) -> + Header = <<"---\ntitle: API Tags\ntoc: true\nmenu: API Tags\norder: 2\n" + "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_tags'\n---\n\n" + "This section enumerates the tags and their associated API.\n">>, + Tags = make_tags(false), + {ok, Fh} = file:open(File, [write]), + io:format(Fh, "~ts~ts", [Header, Tags]), + file:close(Fh), + ok. + html_pre() -> " diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index ec3d2cf8c..c5d8c4494 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -5,7 +5,7 @@ %%% Created : 14 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_config_transformer.erl b/src/ejabberd_config_transformer.erl index 053a59c21..7a1a03506 100644 --- a/src/ejabberd_config_transformer.erl +++ b/src/ejabberd_config_transformer.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -103,7 +103,7 @@ transform(_Host, acme, ACME, Acc) -> ACME1 = lists:map( fun({ca_url, URL} = Opt) -> case misc:uri_parse(URL) of - {ok, _, "acme-v01.api.letsencrypt.org", _, _} -> + {ok, _, _, "acme-v01.api.letsencrypt.org", _, _, _} -> NewURL = ejabberd_acme:default_directory_url(), ?WARNING_MSG("ACME directory URL ~ts defined in " "option acme->ca_url is deprecated " diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index ce642727f..e715aac00 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jan 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -141,15 +141,6 @@ process(["status"], _Version) -> ?STATUS_SUCCESS end; -process(["stop"], _Version) -> - %%ejabberd_cover:stop(), - init:stop(), - ?STATUS_SUCCESS; - -process(["restart"], _Version) -> - init:restart(), - ?STATUS_SUCCESS; - %% TODO: Mnesia operations should not be hardcoded in ejabberd_ctl module. %% For now, I leave them there to avoid breaking those commands for people that %% may be using it (as format of response is going to change). @@ -227,11 +218,11 @@ process(Args, Version) -> end, Code. -%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} +-spec process2(Args::[string()], AccessCommands::any()) -> + {String::string(), Code::integer()}. process2(Args, AccessCommands) -> process2(Args, AccessCommands, ?DEFAULT_VERSION). -%% @spec (Args::[string()], AccessCommands, Version) -> {String::string(), Code::integer()} process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) -> process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, Version); @@ -280,7 +271,6 @@ determine_string_type(String, Version) -> %% Command calling %%----------------------------- -%% @spec (Args::[string()], Auth, AccessCommands, Version) -> string() | integer() | {string(), integer()} try_run_ctp(Args, Auth, AccessCommands, Version) -> try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of false when Args /= [] -> @@ -301,7 +291,6 @@ try_run_ctp(Args, Auth, AccessCommands, Version) -> {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} end. -%% @spec (Args::[string()], Auth, AccessCommands, Version) -> string() | integer() | {string(), integer()} try_call_command(Args, Auth, AccessCommands, Version) -> try call_command(Args, Auth, AccessCommands, Version) of {Reason, wrong_command_arguments} -> @@ -325,7 +314,11 @@ try_call_command(Args, Auth, AccessCommands, Version) -> ?STATUS_ERROR} end. -%% @spec (Args::[string()], Auth, AccessCommands, Version) -> string() | integer() | {string(), integer()} | {error, ErrorType} +-spec call_command(Args::[string()], + Auth::noauth | {binary(), binary(), binary(), true}, + AccessCommands::[any()], + Version::integer()) -> + string() | integer() | {string(), integer()} | {error, ErrorType::any()}. call_command([CmdString | Args], Auth, _AccessCommands, Version) -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), @@ -378,7 +371,11 @@ format_arg("", string) -> format_arg(Arg, string) -> NumChars = integer_to_list(length(Arg)), Parse = "~" ++ NumChars ++ "c", - format_arg2(Arg, Parse). + format_arg2(Arg, Parse); +format_arg(Arg, Format) -> + S = unicode:characters_to_binary(Arg, utf8), + JSON = jiffy:decode(S), + mod_http_api:format_arg(JSON, Format). format_arg2(Arg, Parse)-> {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), @@ -525,6 +522,7 @@ print_usage(Version) -> print_usage(HelpMode, MaxC, ShCode, Version) -> AllCommands = [ + {"help", ["[arguments]"], "Get help"}, {"status", [], "Get ejabberd status"}, {"stop", [], "Stop ejabberd"}, {"restart", [], "Restart ejabberd"}, @@ -772,7 +770,8 @@ print_usage_help(MaxC, ShCode) -> %% Print usage command %%----------------------------- -%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean(), Version) -> ok +-spec print_usage_commands2(CmdSubString::string(), MaxC::integer(), + ShCode::boolean(), Version::integer()) -> ok. print_usage_commands2(CmdSubString, MaxC, ShCode, Version) -> %% Get which command names match this substring AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], @@ -818,7 +817,8 @@ filter_commands_regexp(All, Glob) -> end, All). -%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean(), Version) -> ok +-spec print_usage_command(Cmd::string(), MaxC::integer(), + ShCode::boolean(), Version::integer()) -> ok. print_usage_command(Cmd, MaxC, ShCode, Version) -> Name = list_to_atom(Cmd), C = ejabberd_commands:get_command_definition(Name, Version), @@ -827,6 +827,7 @@ print_usage_command(Cmd, MaxC, ShCode, Version) -> print_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, + definer = Definer, desc = Desc, args = ArgsDef, longdesc = LongDesc, @@ -851,6 +852,15 @@ print_usage_command2(Cmd, C, MaxC, ShCode) -> TagsFmt = [" ",?B("Tags"),":", prepare_long_line(8, MaxC, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])], + IsDefinerMod = case Definer of + unknown -> true; + _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes))) + end, + ModuleFmt = case IsDefinerMod of + true -> [" ",?B("Module"),": ", atom_to_list(Definer), "\n\n"]; + false -> [] + end, + DescFmt = [" ",?B("Description"),":", prepare_description(15, MaxC, Desc)], LongDescFmt = case LongDesc of @@ -866,7 +876,7 @@ print_usage_command2(Cmd, C, MaxC, ShCode) -> case Cmd of "help" -> ok; _ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, - "\n\n", XmlrpcFmt, TagsFmt, "\n\n", DescFmt, "\n\n"], []) + "\n\n", XmlrpcFmt, TagsFmt, "\n\n", ModuleFmt, DescFmt, "\n\n"], []) end, print([LongDescFmt, NoteEjabberdctl], []). diff --git a/src/ejabberd_db_sup.erl b/src/ejabberd_db_sup.erl index 248325cf1..1701779ec 100644 --- a/src/ejabberd_db_sup.erl +++ b/src/ejabberd_db_sup.erl @@ -2,7 +2,7 @@ %%% Created : 13 June 2019 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_doc.erl b/src/ejabberd_doc.erl index 77f9fab48..ba28c8116 100644 --- a/src/ejabberd_doc.erl +++ b/src/ejabberd_doc.erl @@ -2,7 +2,7 @@ %%% File : ejabberd_doc.erl %%% Purpose : Options documentation generator %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -48,7 +48,10 @@ man(Lang) -> {[{M, Descr, DocOpts, #{example => Example}}|Mods], SubMods}; #{opts := DocOpts} -> {ParentMod, Backend} = strip_backend_suffix(M), - {Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)} + {Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)}; + #{} -> + warn("module ~s is not properly documented", [M]), + Acc catch _:undef -> case erlang:function_exported( M, mod_options, 1) of @@ -231,9 +234,9 @@ man_header(Lang) -> "indentation, or otherwise you will get pretty cryptic " "configuration errors.")), io_lib:nl(), - tr(Lang, ?T("Logically, configuration options are splitted into 3 main categories: " + tr(Lang, ?T("Logically, configuration options are split into 3 main categories: " "'Modules', 'Listeners' and everything else called 'Top Level' options. " - "Thus this document is splitted into 3 main chapters describing each " + "Thus this document is split into 3 main chapters describing each " "category separately. So, the contents of ejabberd.yml will typically " "look like this:")), io_lib:nl(), diff --git a/src/ejabberd_hooks.erl b/src/ejabberd_hooks.erl index 605b7e226..3a440e455 100644 --- a/src/ejabberd_hooks.erl +++ b/src/ejabberd_hooks.erl @@ -5,7 +5,7 @@ %%% Created : 8 Aug 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -46,12 +46,23 @@ handle_info/2, terminate/2]). + +-export( + [ + get_tracing_options/3, + trace_off/3, + trace_on/5,human_readable_time_string/1 + ] +). + -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -record(state, {}). -type hook() :: {Seq :: integer(), Module :: atom(), Function :: atom() | fun()}. +-define(TRACE_HOOK_KEY, '$trace_hook'). +-define(TIMING_KEY, '$trace_hook_timer'). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- @@ -104,7 +115,18 @@ run(Hook, Args) -> run(Hook, Host, Args) -> try ets:lookup(hooks, {Hook, Host}) of [{_, Ls}] -> - run1(Ls, Hook, Args); + case erlang:get(?TRACE_HOOK_KEY) of + undefined -> + run1(Ls, Hook, Args); + TracingHooksOpts -> + case do_get_tracing_options(Hook, Host, TracingHooksOpts) of + undefined -> + run1(Ls, Hook, Args); + TracingOpts -> + foreach_start_hook_tracing(TracingOpts, Hook, Host, Args), + run2(Ls, Hook, Args, Host, TracingOpts) + end + end; [] -> ok catch _:badarg -> @@ -124,13 +146,75 @@ run_fold(Hook, Val, Args) -> run_fold(Hook, Host, Val, Args) -> try ets:lookup(hooks, {Hook, Host}) of [{_, Ls}] -> - run_fold1(Ls, Hook, Val, Args); + case erlang:get(?TRACE_HOOK_KEY) of + undefined -> + run_fold1(Ls, Hook, Val, Args); + TracingHooksOpts -> + case do_get_tracing_options(Hook, Host, TracingHooksOpts) of + undefined -> + run_fold1(Ls, Hook, Val, Args); + TracingOpts -> + fold_start_hook_tracing(TracingOpts, Hook, Host, [Val | Args]), + run_fold2(Ls, Hook, Val, Args, Host, TracingOpts) + end + end; [] -> Val catch _:badarg -> Val end. +get_tracing_options(Hook, Host, Pid) when Pid == erlang:self() -> + do_get_tracing_options(Hook, Host, erlang:get(?TRACE_HOOK_KEY)); +get_tracing_options(Hook, Host, Pid) when erlang:is_pid(Pid) -> + case erlang:process_info(Pid, dictionary) of + {_, DictPropList} -> + case lists:keyfind(?TRACE_HOOK_KEY, 1, DictPropList) of + {_, TracingHooksOpts} -> + do_get_tracing_options(Hook, Host, TracingHooksOpts); + _ -> + undefined + end; + _ -> + undefined + end. + +trace_on(Hook, Host, Pid, #{}=Opts, Timeout) when Pid == erlang:self() -> + do_trace_on(Hook, Host, Opts, Timeout); +trace_on(Hook, Host, Proc, #{}=Opts, Timeout) -> + try sys:replace_state( + Proc, + fun(State) -> + do_trace_on(Hook, Host, Opts, Timeout), + State + end, + 15000 + ) of + _ -> % process state + ok + catch + _:Reason -> + {error, Reason} + end. + +trace_off(Hook, Host, Pid) when Pid == erlang:self() -> + do_trace_off(Hook, Host); +trace_off(Hook, Host, Proc) -> + try sys:replace_state( + Proc, + fun(State) -> + do_trace_off(Hook, Host), + State + end, + 15000 + ) of + _ -> % process state + ok + catch + _:Reason -> + {error, Reason} + end. + %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- @@ -247,3 +331,425 @@ safe_apply(Hook, Module, Function, Args) -> misc:format_exception(2, E, R, Stack)|Args]), 'EXIT' end. + +%%%---------------------------------------------------------------------- +%%% Internal tracing functions +%%%---------------------------------------------------------------------- + +do_trace_on(Hook, Host, Opts, Timeout) when erlang:is_list(Host) -> + do_trace_on(Hook, erlang:list_to_binary(Host), Opts, Timeout); +do_trace_on(Hook, Host, Opts, undefined) -> + case erlang:get(?TRACE_HOOK_KEY) of + _ when Hook == all andalso Host == <<"*">> -> + % Trace everything: + erlang:put(?TRACE_HOOK_KEY, #{all => #{<<"*">> => Opts}}); + #{all := #{<<"*">> := _}} -> % Already tracing everything + % Update Opts: + erlang:put(?TRACE_HOOK_KEY, #{all => #{<<"*">> => Opts}}); + #{all := HostOpts} when Hook == all -> % Already Tracing everything for some hosts + % Add/Update Host and Opts: + erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts#{Host => Opts}}); + #{all := _} -> % Already tracing everything and Hook is not all + ok; + #{} when Hook == all -> + % Remove other hooks by just adding all: + erlang:put(?TRACE_HOOK_KEY, #{all => #{Host => Opts}}); + #{}=TraceHooksOpts when Host == <<"*">> -> % Want to trace a hook for all hosts + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => #{Host => Opts}}); + #{}=TraceHooksOpts -> + case maps:get(Hook, TraceHooksOpts, #{}) of + #{<<"*">> := _} -> % Already tracing this hook for all hosts + ok; + HostOpts -> + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => HostOpts#{Host => Opts}}) + end; + undefined -> + erlang:put(?TRACE_HOOK_KEY, #{Hook => #{Host => Opts}}) + end, + ok; +do_trace_on(Hook, Host, Opts, TimeoutSeconds) -> % Trace myself `Timeout` time + Timeout = timer:seconds(TimeoutSeconds), + ParentPid = erlang:self(), + try erlang:spawn( + fun() -> + MonitorRef = erlang:monitor(process, ParentPid), + receive + {_, MonitorRef, _, _, _} -> + ok + after Timeout -> + trace_off(Hook, Host, ParentPid) + end, + erlang:exit(normal) + end + ) of + _ -> + do_trace_on(Hook, Host, Opts, undefined) % ok + catch + _:Reason -> % system_limit + {error, Reason} + end. + +do_trace_off(Hook, Host) when erlang:is_list(Host) -> + do_trace_off(Hook, erlang:list_to_binary(Host)); +do_trace_off(Hook, Host) -> + case erlang:get(?TRACE_HOOK_KEY) of + _ when Hook == all andalso Host == <<"*">> -> + % Remove all tracing: + erlang:erase(?TRACE_HOOK_KEY); + #{all := HostOpts} when Hook == all -> % Already tracing all hooks + % Remove Host: + HostOpts2 = maps:remove(Host, HostOpts), + if + HostOpts2 == #{} -> + % Remove all tracing: + erlang:erase(?TRACE_HOOK_KEY); + true -> + erlang:put(?TRACE_HOOK_KEY, #{all => HostOpts2}) + end; + #{}=TraceHooksOpts when Host == <<"*">> -> + % Remove tracing of this hook for all hosts: + TraceHooksOpts2 = maps:remove(Hook, TraceHooksOpts), + if + TraceHooksOpts2 == #{} -> + % Remove all tracing: + erlang:erase(?TRACE_HOOK_KEY); + true -> + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts2) + end; + #{}=TraceHooksOpts -> + case maps:get(Hook, TraceHooksOpts, undefined) of + #{}=HostOpts -> + NewHostOpts = maps:remove(Host, HostOpts), + if + NewHostOpts == #{} -> + % Remove hook: + erlang:put(?TRACE_HOOK_KEY, maps:remove(Hook, TraceHooksOpts)); + true -> + erlang:put(?TRACE_HOOK_KEY, TraceHooksOpts#{Hook => NewHostOpts}) + end; + _ -> + ok + end; + undefined -> + ok + end, + ok. + +do_get_tracing_options(Hook, Host, MaybeMap) -> + case MaybeMap of + undefined -> + undefined; + #{all := #{<<"*">> := Opts}} -> % Tracing everything + Opts; + #{all := HostOpts} -> % Tracing all hooks for some hosts + maps:get(Host, HostOpts, undefined); + #{}=TraceHooksOpts -> + HostOpts = maps:get(Hook, TraceHooksOpts, #{}), + case maps:get(Host, HostOpts, undefined) of + undefined -> + maps:get(<<"*">>, HostOpts, undefined); + Opts -> + Opts + end + end. + +run2([], Hook, Args, Host, Opts) -> + foreach_stop_hook_tracing(Opts, Hook, Host, Args, undefined), + ok; +run2([{Seq, Module, Function} | Ls], Hook, Args, Host, TracingOpts) -> + foreach_start_callback_tracing(TracingOpts, Hook, Host, Module, Function, Args, Seq), + Res = safe_apply(Hook, Module, Function, Args), + foreach_stop_callback_tracing(TracingOpts, Hook, Host, Module, Function, Args, Seq, Res), + case Res of + 'EXIT' -> + run2(Ls, Hook, Args, Host, TracingOpts); + stop -> + foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, {Module, Function, Seq, Ls}), + ok; + _ -> + run2(Ls, Hook, Args, Host, TracingOpts) + end. + +run_fold2([], Hook, Val, Args, Host, Opts) -> + fold_stop_hook_tracing(Opts, Hook, Host, [Val | Args], undefined), + Val; +run_fold2([{Seq, Module, Function} | Ls], Hook, Val, Args, Host, TracingOpts) -> + fold_start_callback_tracing(TracingOpts, Hook, Host, Module, Function, [Val | Args], Seq), + Res = safe_apply(Hook, Module, Function, [Val | Args]), + fold_stop_callback_tracing(TracingOpts, Hook, Host, Module, Function, [Val | Args], Seq, Res), + case Res of + 'EXIT' -> + run_fold2(Ls, Hook, Val, Args, Host, TracingOpts); + stop -> + fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {old, Val}, Ls}), + Val; + {stop, NewVal} -> + fold_stop_hook_tracing(TracingOpts, Hook, Host, [Val | Args], {Module, Function, Seq, {new, NewVal}, Ls}), + NewVal; + NewVal -> + run_fold2(Ls, Hook, NewVal, Args, Host, TracingOpts) + end. + +foreach_start_hook_tracing(TracingOpts, Hook, Host, Args) -> + run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], foreach). + +foreach_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) -> + run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], foreach). + +foreach_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) -> + run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], foreach). + +foreach_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) -> + run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], foreach). + +fold_start_hook_tracing(TracingOpts, Hook, Host, Args) -> + run_event_handlers(TracingOpts, Hook, Host, start_hook, [Args], fold). + +fold_stop_hook_tracing(TracingOpts, Hook, Host, Args, BreakCallback) -> + run_event_handlers(TracingOpts, Hook, Host, stop_hook, [Args, BreakCallback], fold). + +fold_start_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq) -> + run_event_handlers(TracingOpts, Hook, Host, start_callback, [Mod, Func, Args, Seq], fold). + +fold_stop_callback_tracing(TracingOpts, Hook, Host, Mod, Func, Args, Seq, Res) -> + run_event_handlers(TracingOpts, Hook, Host, stop_callback, [Mod, Func, Args, Seq, Res], fold). + +run_event_handlers(TracingOpts, Hook, Host, Event, EventArgs, RunType) -> + EventHandlerList = maps:get(event_handler_list, TracingOpts, default_tracing_event_handler_list()), + EventHandlerOpts = maps:get(event_handler_options, TracingOpts, #{}), + if + erlang:is_list(EventHandlerList) -> + lists:foreach( + fun(EventHandler) -> + try + if + erlang:is_function(EventHandler) -> + erlang:apply( + EventHandler, + [Event, EventArgs, RunType, Hook, Host, EventHandlerOpts, TracingOpts] + ); + true -> + EventHandler:handle_hook_tracing_event( + Event, + EventArgs, + RunType, + Hook, + Host, + EventHandlerOpts, + TracingOpts + ) + end + of + _ -> + ok + catch + ?EX_RULE(E, R, St) -> + Stack = ?EX_STACK(St), + ?ERROR_MSG( + "(~0p|~ts|~0p) Tracing event '~0p' handler exception(~0p): ~0p: ~0p", + [Hook, Host, erlang:self(), EventHandler, E, R, Stack] + ), + ok + end + end, + EventHandlerList + ); % ok + true -> + ?ERROR_MSG("(~0p|~ts|~0p) Bad event handler list: ~0p", [Hook, Host, erlang:self(), EventHandlerList]), + ok + end. + +default_tracing_event_handler_list() -> + [fun tracing_timing_event_handler/7]. + +tracing_timing_event_handler(start_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + HookStart = erlang:system_time(nanosecond), + % Generate new event: + run_event_handlers(TracingOpts, Hook, Host, start_hook_timing, EventArgs ++ [HookStart], RunType); +tracing_timing_event_handler(stop_hook, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + HookStop = erlang:system_time(nanosecond), + TimingMap = #{} = erlang:get(?TIMING_KEY), + {HookStart, CallbackList} = maps:get({Hook, Host}, TimingMap), + {CallbackListTiming, CallbackListTotal} = lists:foldl( + fun({_, _, _, CallbackStart, CallbackStop}=CallbackTimingInfo, {CallbackListTimingX, Total}) -> + {CallbackListTimingX ++ [CallbackTimingInfo], Total + (CallbackStop - CallbackStart)} + end, + {[], 0}, + CallbackList + ), + % Generate new event: + run_event_handlers( + TracingOpts, + Hook, + Host, + stop_hook_timing, + EventArgs ++ [HookStart, HookStop, CallbackListTiming, CallbackListTotal], + RunType + ); +tracing_timing_event_handler(start_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + CallbackStart = erlang:system_time(nanosecond), + % Generate new event: + run_event_handlers(TracingOpts, Hook, Host, start_callback_timing, EventArgs ++ [CallbackStart], RunType); +tracing_timing_event_handler(stop_callback, EventArgs, RunType, Hook, Host, _, TracingOpts) -> + CallbackStop = erlang:system_time(nanosecond), + TimingMap = #{} = erlang:get(?TIMING_KEY), + {_, [{_, _, _, CallbackStart} | _]} = maps:get({Hook, Host}, TimingMap), + run_event_handlers( + TracingOpts, + Hook, + Host, + stop_callback_timing, + EventArgs ++ [CallbackStart, CallbackStop], + RunType + ), + ok; +tracing_timing_event_handler(start_hook_timing, [_, HookStart], RunType, Hook, Host, EventHandlerOpts, _) -> + tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing started\n", [Hook, Host, erlang:self(), RunType]), + case erlang:get(?TIMING_KEY) of + #{}=TimingMap -> + erlang:put(?TIMING_KEY, TimingMap#{{Hook, Host} => {HookStart, []}}); + _ -> + erlang:put(?TIMING_KEY, #{{Hook, Host} => {HookStart, []}}) + end, + ok; +tracing_timing_event_handler( + stop_hook_timing, + [_, _, HookStart, HookStop, CallbackListTiming, CallbackListTotal], + RunType, + Hook, + Host, + EventHandlerOpts, + _ +) -> + if + erlang:length(CallbackListTiming) < 2 -> % We don't need sorted timing result + ok; + true -> + CallbackListTimingText = + lists:foldl( + fun({Mod, Func, Arity, Diff}, CallbackListTimingText) -> + CallbackListTimingText + ++ "\n\t" + ++ mfa_string({Mod, Func, Arity}) + ++ " -> " + ++ human_readable_time_string(Diff) + end, + "", + lists:keysort( + 4, + [ + {Mod, Func, Arity, CallbackStop - CallbackStart} || + {Mod, Func, Arity, CallbackStart, CallbackStop} <- CallbackListTiming + ] + ) + ), + tracing_output( + EventHandlerOpts, + "(~0p|~ts|~0p|~0p) All callbacks took ~ts to run. Sorted running time:" + ++ CallbackListTimingText + ++ "\n", + [Hook, Host, erlang:self(), RunType, human_readable_time_string(CallbackListTotal)] + ), + tracing_output( + EventHandlerOpts, + "(~0p|~ts|~0p|~0p) Time calculations for all callbacks took ~ts\n", + [ + Hook, + Host, + erlang:self(), + RunType, + human_readable_time_string((HookStop - HookStart) - CallbackListTotal) + ] + ) + end, + tracing_output(EventHandlerOpts, "(~0p|~ts|~0p|~0p) Timing stopped\n", [Hook, Host, erlang:self(), RunType]), + TimingMap = #{} = erlang:get(?TIMING_KEY), + NewTimingMap = maps:remove({Hook, Host}, TimingMap), + if + NewTimingMap == #{} -> + erlang:erase(?TIMING_KEY); + true -> + erlang:put(?TIMING_KEY, NewTimingMap) + end, + ok; +tracing_timing_event_handler(start_callback_timing, [Mod, Func, Args, _, CallbackStart], _, Hook, Host, _, _) -> + TimingMap = #{} = erlang:get(?TIMING_KEY), + {HookStart, Callbacks} = maps:get({Hook, Host}, TimingMap), + erlang:put( + ?TIMING_KEY, + TimingMap#{ + {Hook, Host} => {HookStart, [{Mod, Func, erlang:length(Args), CallbackStart} | Callbacks]} + } + ), + ok; +tracing_timing_event_handler( + stop_callback_timing, + [Mod, Func, _, _, _, CallbackStart, CallbackStop], + RunType, + Hook, + Host, + EventHandlerOpts, + _ +) -> + TimingMap = #{} = erlang:get(?TIMING_KEY), + {HookStart, [{Mod, Func, Arity, CallbackStart} | Callbacks]} = maps:get({Hook, Host}, TimingMap), + maps:get(output_for_each_callback, maps:get(timing, EventHandlerOpts, #{}), false) andalso tracing_output( + EventHandlerOpts, + "(~0p|~ts|~0p|~0p) " + ++ mfa_string({Mod, Func, Arity}) + ++ " took " + ++ human_readable_time_string(CallbackStop - CallbackStart) + ++ "\n", + [Hook, Host, erlang:self(), RunType] + ), + erlang:put( + ?TIMING_KEY, + TimingMap#{ + {Hook, Host} => {HookStart, [{Mod, Func, Arity, CallbackStart, CallbackStop} | Callbacks]} + } + ), + ok; +tracing_timing_event_handler(_, _, _, _, _, _, _) -> + ok. + +tracing_output(#{output_function := OutputF}, Text, Args) -> + try + OutputF(Text, Args) + of + _ -> + ok + catch + ?EX_RULE(E, R, St) -> + Stack = ?EX_STACK(St), + ?ERROR_MSG("Tracing output function exception(~0p): ~0p: ~0p", [E, R, Stack]), + ok + end; +tracing_output(#{output_log_level := Output}, Text, Args) -> + if + Output == debug -> + ?DEBUG(Text, Args); + true -> % info + ?INFO_MSG(Text, Args) + end, + ok; +tracing_output(Opts, Text, Args) -> + tracing_output(Opts#{output_log_level => info}, Text, Args). + +mfa_string({_, Fun, _}) when erlang:is_function(Fun) -> + io_lib:format("~0p", [Fun]); +mfa_string({Mod, Func, Arity}) -> + erlang:atom_to_list(Mod) ++ ":" ++ erlang:atom_to_list(Func) ++ "/" ++ erlang:integer_to_list(Arity). + +human_readable_time_string(TimeNS) -> + {Time, Unit, Decimals} = + if + TimeNS >= 1000000000 -> + {TimeNS / 1000000000, "", 10}; + TimeNS >= 1000000 -> + {TimeNS / 1000000, "m", 7}; + TimeNS >= 1000 -> + {TimeNS / 1000, "μ", 4}; + true -> + {TimeNS / 1, "n", 0} + end, + erlang:float_to_list(Time, [{decimals, Decimals}, compact]) ++ Unit ++ "s". diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index d577a10d5..fe13868f4 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -5,7 +5,7 @@ %%% Created : 27 Feb 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -535,12 +535,14 @@ analyze_ip_xff({IPLast, Port}, XFF) -> TrustedProxies) of true -> - {ok, IPFirst} = inet_parse:address( - binary_to_list(ClientIP)), - ?DEBUG("The IP ~w was replaced with ~w due to " - "header X-Forwarded-For: ~ts", - [IPLast, IPFirst, XFF]), - IPFirst; + case inet_parse:address(binary_to_list(ClientIP)) of + {ok, IPFirst} -> + ?DEBUG("The IP ~w was replaced with ~w due to " + "header X-Forwarded-For: ~ts", + [IPLast, IPFirst, XFF]), + IPFirst; + E -> throw(E) + end; false -> IPLast end, {IPClient, Port}. diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index eb2100a42..fec53c210 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -5,7 +5,7 @@ %%% Created : 09-10-2010 by Eric Cestari %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_iq.erl b/src/ejabberd_iq.erl index 249c53294..be25dcb9d 100644 --- a/src/ejabberd_iq.erl +++ b/src/ejabberd_iq.erl @@ -5,7 +5,7 @@ %%% Created : 10 Nov 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 93fd2e0fd..41eec8c1e 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -318,6 +318,9 @@ start_connection(Module, Arity, Socket, State, Sup) -> supervisor:start_child(Sup, [{gen_tcp, Socket}, State]) end, case Res of + {ok, Pid, preowned_socket} -> + Module:accept(Pid), + {ok, Pid}; {ok, Pid} -> case gen_tcp:controlling_process(Socket, Pid) of ok -> @@ -457,11 +460,19 @@ config_reloaded() -> ok; {_, OldModule, OldOpts} -> _ = stop_listener(EndPoint, OldModule, OldOpts), - ets:insert(?MODULE, {EndPoint, Module, Opts}), - start_listener(EndPoint, Module, Opts); + case start_listener(EndPoint, Module, Opts) of + {ok, _} -> + ets:insert(?MODULE, {EndPoint, Module, Opts}); + _ -> + ok + end; false -> - ets:insert(?MODULE, {EndPoint, Module, Opts}), - start_listener(EndPoint, Module, Opts) + case start_listener(EndPoint, Module, Opts) of + {ok, _} -> + ets:insert(?MODULE, {EndPoint, Module, Opts}); + _ -> + ok + end end end, New). diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 6dfef9d01..4a12b18f9 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -5,7 +5,7 @@ %%% Created : 30 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index 5ace8115f..3d6c08650 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -5,7 +5,7 @@ %%% Created : 12 May 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2021 ProcessOne +%%% ejabberd, Copyright (C) 2013-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -113,7 +113,6 @@ get_string_env(Name, Default) -> Default end. --spec start() -> ok. start() -> start(info). @@ -181,17 +180,14 @@ do_start(Level) -> lager:set_loghwm(Handler, LogRateLimit) end, gen_event:which_handlers(lager_event)). --spec restart() -> ok. restart() -> Level = ejabberd_option:loglevel(), application:stop(lager), start(Level). --spec reopen_log() -> ok. reopen_log() -> ok. --spec rotate_log() -> ok. rotate_log() -> catch lager_crash_log ! rotate, lists:foreach( @@ -201,7 +197,6 @@ rotate_log() -> ok end, gen_event:which_handlers(lager_event)). --spec get() -> loglevel(). get() -> Handlers = get_lager_handlers(), lists:foldl(fun(lager_console_backend, _Acc) -> @@ -213,7 +208,6 @@ get() -> end, none, Handlers). --spec set(0..5 | loglevel()) -> ok. set(N) when is_integer(N), N>=0, N=<5 -> set(convert_loglevel(N)); set(Level) when ?is_loglevel(Level) -> @@ -255,7 +249,6 @@ get_lager_version() -> false -> "0.0.0" end. --spec flush() -> ok. flush() -> application:stop(lager), application:stop(sasl). @@ -273,13 +266,17 @@ start(Level) -> ErrorLog = filename:join([Dir, "error.log"]), LogRotateSize = get_integer_env(log_rotate_size, 10*1024*1024), LogRotateCount = get_integer_env(log_rotate_count, 1), + LogBurstLimitWindowTime = get_integer_env(log_burst_limit_window_time, 1000), + LogBurstLimitCount = get_integer_env(log_burst_limit_count, 500), Config = #{max_no_bytes => LogRotateSize, max_no_files => LogRotateCount, filesync_repeat_interval => no_repeat, file_check => 1000, sync_mode_qlen => 1000, drop_mode_qlen => 1000, - flush_qlen => 5000}, + flush_qlen => 5000, + burst_limit_window_time => LogBurstLimitWindowTime, + burst_limit_max_count => LogBurstLimitCount}, FmtConfig = #{legacy_header => false, time_designator => $ , max_size => 100*1024, @@ -288,10 +285,11 @@ start(Level) -> ConsoleFmtConfig = FmtConfig#{template => console_template()}, try ok = logger:set_primary_config(level, Level), - ok = logger:update_formatter_config(default, ConsoleFmtConfig), + DefaultHandlerId = get_default_handlerid(), + ok = logger:update_formatter_config(DefaultHandlerId, ConsoleFmtConfig), case quiet_mode() of true -> - ok = logger:set_handler_config(default, level, critical); + ok = logger:set_handler_config(DefaultHandlerId, level, critical); _ -> ok end, @@ -319,6 +317,13 @@ start(Level) -> Err end. +get_default_handlerid() -> + Ids = logger:get_handler_ids(), + case lists:member(default, Ids) of + true -> default; + false -> hd(Ids) + end. + -spec restart() -> ok. restart() -> ok. diff --git a/src/ejabberd_mnesia.erl b/src/ejabberd_mnesia.erl index 3818a1515..88762746c 100644 --- a/src/ejabberd_mnesia.erl +++ b/src/ejabberd_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 17 Nov 2016 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -172,7 +172,10 @@ change_table_copy_type(Name, TabDef) -> if NewType /= CurrType -> ?INFO_MSG("Changing Mnesia table '~ts' from ~ts to ~ts", [Name, CurrType, NewType]), - mnesia_op(change_table_copy_type, [Name, node(), NewType]); + if CurrType == unknown -> mnesia_op(add_table_copy, [Name, node(), NewType]); + true -> + mnesia_op(change_table_copy_type, [Name, node(), NewType]) + end; true -> {atomic, ok} end. diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 9b2c517de..51676ac5d 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -5,7 +5,7 @@ %%% Created : 20 Mar 2015 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -65,6 +65,7 @@ -callback init() -> any(). -callback store(#oauth_token{}) -> ok | {error, any()}. -callback lookup(binary()) -> {ok, #oauth_token{}} | error. +-callback revoke(binary()) -> ok | {error, binary()}. -callback clean(non_neg_integer()) -> any(). -record(oauth_ctx, { @@ -99,12 +100,13 @@ get_commands_spec() -> result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}} }, #ejabberd_commands{name = oauth_revoke_token, tags = [oauth], - desc = "Revoke authorization for a token (only Mnesia)", + desc = "Revoke authorization for a token", + note = "changed in 22.05", module = ?MODULE, function = oauth_revoke_token, - args = [{token, string}], + args = [{token, binary}], policy = restricted, - result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}, - result_desc = "List of remaining tokens" + result = {res, restuple}, + result_desc = "Result code" }, #ejabberd_commands{name = oauth_add_client_password, tags = [oauth], desc = "Add OAUTH client_id with password grant type", @@ -160,8 +162,15 @@ oauth_list_tokens() -> oauth_revoke_token(Token) -> - ok = mnesia:dirty_delete(oauth_token, list_to_binary(Token)), - oauth_list_tokens(). + DBMod = get_db_backend(), + case DBMod:revoke(Token) of + ok -> + ets_cache:delete(oauth_cache, Token, + ejabberd_cluster:get_nodes()), + {ok, ""}; + Other -> + Other + end. oauth_add_client_password(ClientID, ClientName, Secret) -> DBMod = get_db_backend(), diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl index de22f94a6..edb6dd52c 100644 --- a/src/ejabberd_oauth_mnesia.erl +++ b/src/ejabberd_oauth_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 20 Jul 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,13 +28,13 @@ -behaviour(ejabberd_oauth). -export([init/0, - store/1, - lookup/1, - clean/1, - lookup_client/1, - store_client/1, - remove_client/1, - use_cache/0]). + store/1, + lookup/1, + clean/1, + lookup_client/1, + store_client/1, + remove_client/1, + use_cache/0, revoke/1]). -include("ejabberd_oauth.hrl"). @@ -68,6 +68,11 @@ lookup(Token) -> error end. + +-spec revoke(binary()) -> ok | {error, binary()}. +revoke(Token) -> + mnesia:dirty_delete(oauth_token, Token). + clean(TS) -> F = fun() -> Ts = mnesia:select( diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl index b968c9263..a170826fb 100644 --- a/src/ejabberd_oauth_rest.erl +++ b/src/ejabberd_oauth_rest.erl @@ -5,7 +5,7 @@ %%% Created : 26 Jul 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -32,7 +32,7 @@ lookup/1, clean/1, lookup_client/1, - store_client/1]). + store_client/1, revoke/1]). -include("ejabberd_oauth.hrl"). -include("logger.hrl"). @@ -87,6 +87,10 @@ lookup(Token) -> end end. +-spec revoke(binary()) -> ok | {error, binary()}. +revoke(_Token) -> + {error, <<"not available">>}. + clean(_TS) -> ok. diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl index 25be737f0..b73f56b78 100644 --- a/src/ejabberd_oauth_sql.erl +++ b/src/ejabberd_oauth_sql.erl @@ -5,7 +5,7 @@ %%% Created : 27 Jul 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,12 +28,12 @@ -behaviour(ejabberd_oauth). -export([init/0, - store/1, - lookup/1, - clean/1, - lookup_client/1, - store_client/1, - remove_client/1]). + store/1, + lookup/1, + clean/1, + lookup_client/1, + store_client/1, + remove_client/1, revoke/1]). -include("ejabberd_oauth.hrl"). -include("ejabberd_sql_pt.hrl"). @@ -78,6 +78,16 @@ lookup(Token) -> error end. +revoke(Token) -> + case ejabberd_sql:sql_query( + ejabberd_config:get_myname(), + ?SQL("delete from oauth_token where token=%(Token)s")) of + {error, _} -> + {error, <<"db error">>}; + _ -> + ok + end. + clean(TS) -> ejabberd_sql:sql_query( ejabberd_config:get_myname(), diff --git a/src/ejabberd_old_config.erl b/src/ejabberd_old_config.erl index 991677133..47812d894 100644 --- a/src/ejabberd_old_config.erl +++ b/src/ejabberd_old_config.erl @@ -1,7 +1,7 @@ %%%---------------------------------------------------------------------- %%% Purpose: Transform old-style Erlang config to YAML config %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl index 33e9f5738..063502f80 100644 --- a/src/ejabberd_option.erl +++ b/src/ejabberd_option.erl @@ -72,6 +72,8 @@ -export([ldap_tls_verify/0, ldap_tls_verify/1]). -export([ldap_uids/0, ldap_uids/1]). -export([listen/0]). +-export([log_burst_limit_count/0]). +-export([log_burst_limit_window_time/0]). -export([log_rotate_count/0]). -export([log_rotate_size/0]). -export([loglevel/0]). @@ -370,7 +372,7 @@ disable_sasl_mechanisms() -> disable_sasl_mechanisms(Host) -> ejabberd_config:get_option({disable_sasl_mechanisms, Host}). --spec domain_balancing() -> #{binary()=>#{'component_number':=1..1114111, 'type'=>'bare_destination' | 'bare_source' | 'destination' | 'random' | 'source'}}. +-spec domain_balancing() -> #{binary()=>#{'component_number'=>1..1114111, 'type'=>'bare_destination' | 'bare_source' | 'destination' | 'random' | 'source'}}. domain_balancing() -> ejabberd_config:get_option({domain_balancing, global}). @@ -583,6 +585,14 @@ ldap_uids(Host) -> listen() -> ejabberd_config:get_option({listen, global}). +-spec log_burst_limit_count() -> pos_integer(). +log_burst_limit_count() -> + ejabberd_config:get_option({log_burst_limit_count, global}). + +-spec log_burst_limit_window_time() -> pos_integer(). +log_burst_limit_window_time() -> + ejabberd_config:get_option({log_burst_limit_window_time, global}). + -spec log_rotate_count() -> non_neg_integer(). log_rotate_count() -> ejabberd_config:get_option({log_rotate_count, global}). diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl index 22e5774bb..49fc66124 100644 --- a/src/ejabberd_options.erl +++ b/src/ejabberd_options.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -141,7 +141,7 @@ opt_type(domain_balancing) -> #{component_number => econf:int(2, 1000), type => econf:enum([random, source, destination, bare_source, bare_destination])}, - [{required, [component_number]}, {return, map}, unique]), + [{return, map}, unique]), [{return, map}]); opt_type(ext_api_path_oauth) -> econf:binary(); @@ -222,6 +222,10 @@ opt_type(log_rotate_count) -> econf:non_neg_int(); opt_type(log_rotate_size) -> econf:pos_int(infinity); +opt_type(log_burst_limit_window_time) -> + econf:timeout(second); +opt_type(log_burst_limit_count) -> + econf:pos_int(); opt_type(loglevel) -> fun(N) when is_integer(N) -> (econf:and_then( @@ -576,6 +580,8 @@ options() -> {listen, []}, {log_rotate_count, 1}, {log_rotate_size, 10*1024*1024}, + {log_burst_limit_window_time, timer:seconds(1)}, + {log_burst_limit_count, 500}, {max_fsm_queue, undefined}, {modules, []}, {negotiation_timeout, timer:seconds(30)}, @@ -722,6 +728,8 @@ globals() -> loglevel, log_rotate_count, log_rotate_size, + log_burst_limit_count, + log_burst_limit_window_time, negotiation_timeout, net_ticktime, new_sql_schema, diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl index 364d1cdac..167715964 100644 --- a/src/ejabberd_options_doc.erl +++ b/src/ejabberd_options_doc.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -61,11 +61,11 @@ doc() -> desc => ?T("The time of a cached item to keep in cache. " "Once it's expired, the corresponding item is " - "erased from cache. The default value is 'one hour'. " + "erased from cache. The default value is '1 hour'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " - "'auth_cache_life_time', 'oauth_cache_life_time', " - "'router_cache_life_time', and 'sm_cache_life_time'.")}}, + "_`auth_cache_life_time`_, _`oauth_cache_life_time`_, " + "_`router_cache_life_time`_, and _`sm_cache_life_time`_.")}}, {cache_missed, #{value => "true | false", desc => @@ -73,12 +73,12 @@ doc() -> "an attempt to lookup for a value in a database and " "this value is not found and the option is set to 'true', " "this attempt will be cached and no attempts will be " - "performed until the cache expires (see 'cache_life_time'). " + "performed until the cache expires (see _`cache_life_time`_). " "Usually you don't want to change it. Default is 'true'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " - "'auth_cache_missed', 'oauth_cache_missed', " - "'router_cache_missed', and 'sm_cache_missed'.")}}, + "_`auth_cache_missed`_, _`oauth_cache_missed`_, " + "_`router_cache_missed`_, and _`sm_cache_missed`_.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => @@ -93,16 +93,16 @@ doc() -> "performance. The default value is '1000'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " - "'auth_cache_size', 'oauth_cache_size', " - "'router_cache_size', and 'sm_cache_size'.")}}, + "_`auth_cache_size`_, _`oauth_cache_size`_, " + "_`router_cache_size`_, and _`sm_cache_size`_.")}}, {use_cache, #{value => "true | false", desc => ?T("Enable or disable cache. The default is 'true'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " - "'auth_use_cache', 'oauth_use_cache', 'router_use_cache', " - "and 'sm_use_cache'.")}}, + "_`auth_use_cache`_, _`oauth_use_cache`_, _`router_use_cache`_, " + "and _`sm_use_cache`_.")}}, {default_db, #{value => "mnesia | sql", desc => @@ -122,14 +122,14 @@ doc() -> "Modules may have its own value of the option. " "The value of 'ram' means that queues will be kept in memory. " "If value 'file' is set, you may also specify directory " - "in 'queue_dir' option where file queues will be placed. " + "in _`queue_dir`_ option where file queues will be placed. " "The default value is 'ram'.")}}, {version, #{value => "string()", desc => ?T("The option can be used to set custom ejabberd version, " "that will be used by different parts of ejabberd, for " - "example by 'mod_version' module. The default value is " + "example by _`mod_version`_ module. The default value is " "obtained at compile time from the underlying version " "control system.")}}, {acl, @@ -141,7 +141,7 @@ doc() -> "has name 'ACLName': it can be any string except 'all' or 'none' " "(those are predefined names for the rules that match all or nothing " "respectively). The name 'ACLName' can be referenced from other " - "parts of the configuration file, for example in 'access_rules' " + "parts of the configuration file, for example in _`access_rules`_ " "option. The rules of 'ACLName' are represented by mapping " "'pass:[{ACLType: ACLValue}]'. These can be one of the following:")}, [{user, @@ -220,12 +220,14 @@ doc() -> {access_rules, #{value => "{AccessName: {allow|deny: ACLRules|ACLName}}", desc => - ?T("The option specifies access rules. Each access rule is " + ?T("This option defines " + "http://../basic/#access-rules[Access Rules]. " + "Each access rule is " "assigned a name that can be referenced from other parts " "of the configuration file (mostly from 'access' options of " "ejabberd modules). Each rule definition may contain " "arbitrary number of 'allow' or 'deny' sections, and each " - "section may contain any number of ACL rules (see 'acl' option). " + "section may contain any number of ACL rules (see _`acl`_ option). " "There are no access rules defined by default."), example => ["access_rules:", @@ -313,10 +315,12 @@ doc() -> {anonymous_protocol, #{value => "login_anon | sasl_anon | both", desc => - ?T("'login_anon' means that the anonymous login method will be used. " - "'sasl_anon' means that the SASL Anonymous method will be used. " - "'both' means that SASL Anonymous and login anonymous are both " - "enabled. The default value is 'sasl_anon'.")}}, + [?T("Define what anonymous protocol will be used: "), "", + ?T("* 'login_anon' means that the anonymous login method will be used. "), "", + ?T("* 'sasl_anon' means that the SASL Anonymous method will be used. "), "", + ?T("* 'both' means that SASL Anonymous and login anonymous are both " + "enabled."), "", + ?T("The default value is 'sasl_anon'."), ""]}}, {api_permissions, #{value => "[Permission, ...]", desc => @@ -334,18 +338,18 @@ doc() -> {auth_cache_life_time, #{value => "timeout()", desc => - ?T("Same as 'cache_life_time', but applied to authentication cache " - "only. If not set, the value from 'cache_life_time' will be used.")}}, + ?T("Same as _`cache_life_time`_, but applied to authentication cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {auth_cache_missed, #{value => "true | false", desc => - ?T("Same as 'cache_missed', but applied to authentication cache " - "only. If not set, the value from 'cache_missed' will be used.")}}, + ?T("Same as _`cache_missed`_, but applied to authentication cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, {auth_cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as 'cache_size', but applied to authentication cache " - "only. If not set, the value from 'cache_size' will be used.")}}, + ?T("Same as _`cache_size`_, but applied to authentication cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, {auth_method, #{value => "[mnesia | sql | anonymous | external | jwt | ldap | pam, ...]", desc => @@ -359,38 +363,41 @@ doc() -> desc => ?T("This is used by the contributed module " "'ejabberd_auth_http' that can be installed from the " - "'ejabberd-contrib' Git repository. Please refer to that " + "https://github.com/processone/ejabberd-contrib[ejabberd-contrib] " + "Git repository. Please refer to that " "module's README file for details.")}}, {auth_password_format, #{value => "plain | scram", note => "improved in 20.01", desc => - ?T("The option defines in what format the users passwords " - "are stored. 'plain': The password is stored as plain text " + [?T("The option defines in what format the users passwords " + "are stored:"), "", + ?T("* 'plain': The password is stored as plain text " "in the database. This is risky because the passwords " "can be read if your database gets compromised. " "This is the default value. This format allows clients to " "authenticate using: the old Jabber Non-SASL (XEP-0078), " - "SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1. " - "'scram': The password is not stored, only some information " + "SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1. "), "", + ?T("* 'scram': The password is not stored, only some information " "that allows to verify the hash provided by the client. " "It is impossible to obtain the original plain password " "from the stored information; for this reason, when this " "value is configured it cannot be changed to plain anymore. " "This format allows clients to authenticate using: " - "SASL PLAIN and SASL SCRAM-SHA-1.")}}, + "SASL PLAIN and SASL SCRAM-SHA-1."), + ?T("The default value is 'plain'.")]}}, {auth_scram_hash, #{value => "sha | sha256 | sha512", desc => - ?T("Hash algorith that should be used to store password in SCRAM format. " + ?T("Hash algorithm that should be used to store password in SCRAM format. " "You shouldn't change this if you already have passwords generated with " "a different algorithm - users that have such passwords will not be able " - "to authenticate.")}}, + "to authenticate. The default value is 'sha'.")}}, {auth_use_cache, #{value => "true | false", desc => - ?T("Same as 'use_cache', but applied to authentication cache " - "only. If not set, the value from 'use_cache' will be used.")}}, + ?T("Same as _`use_cache`_, but applied to authentication cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, {c2s_cafile, #{value => ?T("Path"), desc => @@ -444,27 +451,27 @@ doc() -> desc => [?T("Path to a file of CA root certificates. " "The default is to use system defined file if possible."), "", - ?T("For server conections, this 'ca_file' option is overriden by the http://../toplevel/#s2s-cafile[s2s_cafile] option."), "" + ?T("For server connections, this 'ca_file' option is overridden by the http://../toplevel/#s2s-cafile[s2s_cafile] option."), "" ]}}, {captcha_cmd, #{value => ?T("Path"), desc => - ?T("Full path to a script that generates CAPTCHA images. " + ?T("Full path to a script that generates http://../basic/#captcha[CAPTCHA] images. " "There is no default value: when this option is not " "set, CAPTCHA functionality is completely disabled.")}}, {captcha_limit, #{value => "pos_integer() | infinity", desc => - ?T("Maximum number of CAPTCHA generated images per minute for " + ?T("Maximum number of http://../basic/#captcha[CAPTCHA] generated images per minute for " "any given JID. The option is intended to protect the server " "from CAPTCHA DoS. The default value is 'infinity'.")}}, {captcha_host, #{value => "String", - desc => ?T("Deprecated. Use 'captcha_url' instead.")}}, + desc => ?T("Deprecated. Use _`captcha_url`_ instead.")}}, {captcha_url, #{value => ?T("URL"), desc => - ?T("An URL where CAPTCHA requests should be sent. NOTE: you need " + ?T("An URL where http://../basic/#captcha[CAPTCHA] requests should be sent. NOTE: you need " "to configure 'request_handlers' for 'ejabberd_http' listener " "as well. There is no default value.")}}, {certfiles, @@ -674,7 +681,8 @@ doc() -> desc => ?T("The option defines the default language of server strings " "that can be seen by XMPP clients. If an XMPP client does not " - "possess 'xml:lang' attribute, the specified language is used.")}}, + "possess 'xml:lang' attribute, the specified language is used. " + "The default value is '\"en\"'.")}}, {ldap_servers, #{value => "[Host, ...]", desc => @@ -684,7 +692,7 @@ doc() -> #{value => "[Host, ...]", desc => ?T("A list of IP addresses or DNS names of LDAP backup servers. " - "When no servers listed in 'ldap_servers' option are reachable, " + "When no servers listed in _`ldap_servers`_ option are reachable, " "ejabberd will try to connect to these backup servers. " "The default is an empty list, i.e. no backup servers specified. " "WARNING: ejabberd doesn't try to reconnect back to the main " @@ -791,7 +799,7 @@ doc() -> "the result set. There is no default value, which means the " "result is not filtered. WARNING: Since this filter makes " "additional LDAP lookups, use it only as the last resort: " - "try to define all filter rules in 'ldap_filter' option if possible."), + "try to define all filter rules in _`ldap_filter`_ option if possible."), example => ["ldap_dn_filter:", " \"(&(name=%s)(owner=%D)(user=%u@%d))\": [sn]"]}}, @@ -799,13 +807,27 @@ doc() -> #{value => ?T("Number"), desc => ?T("The number of rotated log files to keep. " - "The default value is '1'.")}}, + "The default value is '1', which means that only keeps " + "`ejabberd.log.0`, `error.log.0` and `crash.log.0`.")}}, {log_rotate_size, #{value => "pos_integer() | infinity", desc => ?T("The size (in bytes) of a log file to trigger rotation. " "If set to 'infinity', log rotation is disabled. " "The default value is '10485760' (that is, 10 Mb).")}}, + {log_burst_limit_count, + #{value => ?T("Number"), + note => "added in 22.10", + desc => + ?T("The number of messages to accept in " + "`log_burst_limit_window_time` period before starting to " + "drop them. Default 500")}}, + {log_burst_limit_window_time, + #{value => ?T("Number"), + note => "added in 22.10", + desc => + ?T("The time period to rate-limit log messages " + "by. Defaults to 1 second.")}}, {max_fsm_queue, #{value => ?T("Size"), desc => @@ -835,7 +857,7 @@ doc() -> "must have identical value on all nodes, or it will lead to subtle " "bugs. Usually leaving default value of this is option is best, " "tweak it only if you know what you are doing. " - "The default value is '1' minute.")}}, + "The default value is '1 minute'.")}}, {new_sql_schema, #{value => "true | false", desc => @@ -861,13 +883,13 @@ doc() -> {oauth_cache_life_time, #{value => "timeout()", desc => - ?T("Same as 'cache_life_time', but applied to OAuth cache " - "only. If not set, the value from 'cache_life_time' will be used.")}}, + ?T("Same as _`cache_life_time`_, but applied to OAuth cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {oauth_cache_missed, #{value => "true | false", desc => - ?T("Same as 'cache_missed', but applied to OAuth cache " - "only. If not set, the value from 'cache_missed' will be used.")}}, + ?T("Same as _`cache_missed`_, but applied to OAuth cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, {oauth_cache_rest_failure_life_time, #{value => "timeout()", note => "added in 21.01", @@ -877,8 +899,8 @@ doc() -> {oauth_cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as 'cache_size', but applied to OAuth cache " - "only. If not set, the value from 'cache_size' will be used.")}}, + ?T("Same as _`cache_size`_, but applied to OAuth cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, {oauth_client_id_check, #{value => "allow | db | deny", desc => @@ -888,13 +910,13 @@ doc() -> {oauth_use_cache, #{value => "true | false", desc => - ?T("Same as 'use_cache', but applied to OAuth cache " - "only. If not set, the value from 'use_cache' will be used.")}}, + ?T("Same as _`use_cache`_, but applied to OAuth cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, {oauth_db_type, #{value => "mnesia | sql", desc => ?T("Database backend to use for OAuth authentication. " - "The default value is picked from 'default_db' option, or " + "The default value is picked from _`default_db`_ option, or " "if it's not set, 'mnesia' will be used.")}}, {oauth_expire, #{value => "timeout()", @@ -908,7 +930,7 @@ doc() -> desc => ?T("Enable or disable OOM (out-of-memory) killer. " "When system memory raises above the limit defined in " - "'oom_watermark' option, ejabberd triggers OOM killer " + "_`oom_watermark`_ option, ejabberd triggers OOM killer " "to terminate most memory consuming Erlang processes. " "Note that in order to maintain functionality, ejabberd only " "attempts to kill transient processes, such as those managing " @@ -919,14 +941,14 @@ doc() -> desc => ?T("Trigger OOM killer when some of the running Erlang processes " "have messages queue above this 'Size'. Note that " - "such processes won't be killed if 'oom_killer' option is set " + "such processes won't be killed if _`oom_killer`_ option is set " "to 'false' or if 'oom_watermark' is not reached yet.")}}, {oom_watermark, #{value => ?T("Percent"), desc => ?T("A percent of total system memory consumed at which " "OOM killer should be activated with some of the processes " - "possibly be killed (see 'oom_killer' option). Later, when " + "possibly be killed (see _`oom_killer`_ option). Later, when " "memory drops below this 'Percent', OOM killer is deactivated. " "The default value is '80' percents.")}}, {outgoing_s2s_families, @@ -979,7 +1001,7 @@ doc() -> {queue_dir, #{value => ?T("Directory"), desc => - ?T("If 'queue_type' option is set to 'file', use this 'Directory' " + ?T("If _`queue_type`_ option is set to 'file', use this 'Directory' " "to store file queues. The default is to keep queues inside " "Mnesia directory.")}}, {redis_connect_timeout, @@ -1009,8 +1031,8 @@ doc() -> #{value => "ram | file", desc => ?T("The type of request queue for the Redis server. " - "See description of 'queue_type' option for the explanation. " - "The default value is the value defined in 'queue_type' " + "See description of _`queue_type`_ option for the explanation. " + "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {redis_server, #{value => ?T("Hostname"), @@ -1020,7 +1042,7 @@ doc() -> {registration_timeout, #{value => "timeout()", desc => - ?T("This is a global option for module 'mod_register'. " + ?T("This is a global option for module _`mod_register`_. " "It limits the frequency of registrations from a given " "IP or username. So, a user that tries to register a " "new account from the same IP address or JID during " @@ -1043,29 +1065,29 @@ doc() -> {router_cache_life_time, #{value => "timeout()", desc => - ?T("Same as 'cache_life_time', but applied to routing table cache " - "only. If not set, the value from 'cache_life_time' will be used.")}}, + ?T("Same as _`cache_life_time`_, but applied to routing table cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {router_cache_missed, #{value => "true | false", desc => - ?T("Same as 'cache_missed', but applied to routing table cache " - "only. If not set, the value from 'cache_missed' will be used.")}}, + ?T("Same as _`cache_missed`_, but applied to routing table cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, {router_cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as 'cache_size', but applied to routing table cache " - "only. If not set, the value from 'cache_size' will be used.")}}, + ?T("Same as _`cache_size`_, but applied to routing table cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, {router_db_type, #{value => "mnesia | redis | sql", desc => ?T("Database backend to use for routing information. " - "The default value is picked from 'default_ram_db' option, or " + "The default value is picked from _`default_ram_db`_ option, or " "if it's not set, 'mnesia' will be used.")}}, {router_use_cache, #{value => "true | false", desc => - ?T("Same as 'use_cache', but applied to routing table cache " - "only. If not set, the value from 'use_cache' will be used.")}}, + ?T("Same as _`use_cache`_, but applied to routing table cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, {rpc_timeout, #{value => "timeout()", desc => @@ -1076,9 +1098,10 @@ doc() -> {s2s_access, #{value => ?T("Access"), desc => - ?T("The access rule to restrict server-to-server connections. " - "The default value is 'all' which means no restrictions " - "are applied.")}}, + ?T("This http://../basic/#access-rules[Access Rule] defines to " + "what remote servers can s2s connections be established. " + "The default value is 'all'; no restrictions are applied, it is" + " allowed to connect s2s to/from all remote servers.")}}, {s2s_cafile, #{value => ?T("Path"), desc => @@ -1142,8 +1165,8 @@ doc() -> #{value => "ram | file", desc => ?T("The type of a queue for s2s packets. " - "See description of 'queue_type' option for the explanation. " - "The default value is the value defined in 'queue_type' " + "See description of _`queue_type`_ option for the explanation. " + "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {s2s_timeout, #{value => "timeout()", @@ -1171,7 +1194,7 @@ doc() -> desc => ?T("The option defines a set of shapers. Every shaper is assigned " "a name 'ShaperName' that can be used in other parts of the " - "configuration file, such as 'shaper_rules' option. The shaper " + "configuration file, such as _`shaper_rules`_ option. The shaper " "itself is defined by its 'Rate', where 'Rate' stands for the " "maximum allowed incoming rate in **bytes** per second. " "When a connection exceeds this limit, ejabberd stops reading " @@ -1187,9 +1210,9 @@ doc() -> #{value => "{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}", desc => ?T("An entry allowing to declaring shaper to use for matching user/hosts. " - "Semantics is similar to 'access_rules' option, the only difference is " + "Semantics is similar to _`access_rules`_ option, the only difference is " "that instead using 'allow' or 'deny', a name of a shaper (defined in " - "'shaper' option) or a positive number should be used."), + "_`shaper`_ option) or a positive number should be used."), example => ["shaper_rules:", " connections_limit:", @@ -1205,29 +1228,29 @@ doc() -> {sm_cache_life_time, #{value => "timeout()", desc => - ?T("Same as 'cache_life_time', but applied to client sessions table cache " - "only. If not set, the value from 'cache_life_time' will be used.")}}, + ?T("Same as _`cache_life_time`_, but applied to client sessions table cache " + "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {sm_cache_missed, #{value => "true | false", desc => - ?T("Same as 'cache_missed', but applied to client sessions table cache " - "only. If not set, the value from 'cache_missed' will be used.")}}, + ?T("Same as _`cache_missed`_, but applied to client sessions table cache " + "only. If not set, the value from _`cache_missed`_ will be used.")}}, {sm_cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as 'cache_size', but applied to client sessions table cache " - "only. If not set, the value from 'cache_size' will be used.")}}, + ?T("Same as _`cache_size`_, but applied to client sessions table cache " + "only. If not set, the value from _`cache_size`_ will be used.")}}, {sm_db_type, #{value => "mnesia | redis | sql", desc => ?T("Database backend to use for client sessions information. " - "The default value is picked from 'default_ram_db' option, or " + "The default value is picked from _`default_ram_db`_ option, or " "if it's not set, 'mnesia' will be used.")}}, {sm_use_cache, #{value => "true | false", desc => - ?T("Same as 'use_cache', but applied to client sessions table cache " - "only. If not set, the value from 'use_cache' will be used.")}}, + ?T("Same as _`use_cache`_, but applied to client sessions table cache " + "only. If not set, the value from _`use_cache`_ will be used.")}}, {sql_type, #{value => "mssql | mysql | odbc | pgsql | sqlite", desc => @@ -1253,7 +1276,7 @@ doc() -> note => "added in 20.12", desc => ?T("Path to the ODBC driver to use to connect to a Microsoft SQL " - "Server database. This option is only valid if the 'sql_type' " + "Server database. This option is only valid if the _`sql_type`_ " "option is set to 'mssql'. " "The default value is: 'libtdsodbc.so'")}}, {sql_password, @@ -1288,8 +1311,8 @@ doc() -> #{value => "ram | file", desc => ?T("The type of a request queue for the SQL server. " - "See description of 'queue_type' option for the explanation. " - "The default value is the value defined in 'queue_type' " + "See description of _`queue_type`_ option for the explanation. " + "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {sql_server, #{value => ?T("Host"), @@ -1307,15 +1330,15 @@ doc() -> #{value => ?T("Path"), desc => ?T("A path to a file with CA root certificates that will " - "be used to verify SQL connections. Implies 'sql_ssl' " - "and 'sql_ssl_verify' options are set to 'true'. " + "be used to verify SQL connections. Implies _`sql_ssl`_ " + "and _`sql_ssl_verify`_ options are set to 'true'. " "There is no default which means " "certificate verification is disabled.")}}, {sql_ssl_certfile, #{value => ?T("Path"), desc => ?T("A path to a certificate file that will be used " - "for SSL connections to the SQL server. Implies 'sql_ssl' " + "for SSL connections to the SQL server. Implies _`sql_ssl`_ " "option is set to 'true'. There is no default which means " "ejabberd won't provide a client certificate to the SQL " "server.")}}, @@ -1323,8 +1346,8 @@ doc() -> #{value => "true | false", desc => ?T("Whether to verify SSL connection to the SQL server against " - "CA root certificates defined in 'sql_ssl_cafile' option. " - "Implies 'sql_ssl' option is set to 'true'. " + "CA root certificates defined in _`sql_ssl_cafile`_ option. " + "Implies _`sql_ssl`_ option is set to 'true'. " "The default value is 'false'.")}}, {sql_start_interval, #{value => "timeout()", @@ -1366,14 +1389,14 @@ doc() -> "protect against connections from other domains than given " "in the configuration file. In this way, the lower layer load " "balancer can be chosen for a specific ejabberd implementation " - "while still providing a secure Websocket connection. " + "while still providing a secure WebSocket connection. " "The default value is 'ignore'. An example value of the 'URL' is " "\"https://test.example.org:8081\".")}}, {websocket_ping_interval, #{value => "timeout()", desc => ?T("Defines time between pings sent by the server to a client " - "(Websocket level protocol pings are used for this) to keep " + "(WebSocket level protocol pings are used for this) to keep " "a connection active. If the client doesn't respond to two " "consecutive pings, the connection will be assumed as closed. " "The value of '0' can be used to disable the feature. This option " diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 8dff06837..8f45efa99 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -5,7 +5,7 @@ %%% Created : 17 Jul 2008 by Pablo Polvorin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,17 +24,15 @@ %%%---------------------------------------------------------------------- %%% Not implemented: +%%% - PEP nodes export/import +%%% - message archives export/import %%% - write mod_piefxis with ejabberdctl commands -%%% - Export from mod_offline_sql.erl -%%% - Export from mod_private_sql.erl -%%% - XEP-227: 6. Security Considerations %%% - Other schemas of XInclude are not tested, and may not be imported correctly. %%% - If a host has many users, split that host in XML files with 50 users each. -%%%% Headers -module(ejabberd_piefxis). --protocol({xep, 227, '1.0'}). +-protocol({xep, 227, '1.1'}). -export([import_file/1, export_server/1, export_host/2]). @@ -166,33 +164,67 @@ export_users([], _Server, _Fd) -> export_user(User, Server, Fd) -> Password = ejabberd_auth:get_password_s(User, Server), LServer = jid:nameprep(Server), - Pass = case ejabberd_auth:password_format(LServer) of - scram -> format_scram_password(Password); - _ -> Password + {PassPlain, PassScram} = case ejabberd_auth:password_format(LServer) of + scram -> {[], [format_scram_password(Password)]}; + _ when Password == <<"">> -> {[], []}; + _ -> {[{<<"password">>, Password}], []} end, - Els = get_offline(User, Server) ++ + Els = + PassScram ++ + get_offline(User, Server) ++ get_vcard(User, Server) ++ get_privacy(User, Server) ++ get_roster(User, Server) ++ get_private(User, Server), print(Fd, fxml:element_to_binary( #xmlel{name = <<"user">>, - attrs = [{<<"name">>, User}, - {<<"password">>, Pass}], + attrs = [{<<"name">>, User} | PassPlain], children = Els})). format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = ServerKey, salt = Salt, iterationcount = IterationCount}) -> - StoredKeyB64 = base64:encode(StoredKey), - ServerKeyB64 = base64:encode(ServerKey), - SaltB64 = base64:encode(Salt), - IterationCountBin = (integer_to_binary(IterationCount)), - Hash2 = case Hash of - sha -> <<>>; - sha256 -> <<"sha256,">>; - sha512 -> <<"sha512,">> - end, - <<"scram:", Hash2/binary, StoredKeyB64/binary, ",", ServerKeyB64/binary, ",", SaltB64/binary, ",", IterationCountBin/binary>>. + StoredKeyB64 = base64:encode(StoredKey), + ServerKeyB64 = base64:encode(ServerKey), + SaltB64 = base64:encode(Salt), + IterationCountBin = (integer_to_binary(IterationCount)), + MechanismB = case Hash of + sha -> <<"SCRAM-SHA-1">>; + sha256 -> <<"SCRAM-SHA-256">>; + sha512 -> <<"SCRAM-SHA-512">> + end, + Children = + [ + #xmlel{name = <<"iter-count">>, + children = [{xmlcdata, IterationCountBin}]}, + #xmlel{name = <<"salt">>, + children = [{xmlcdata, SaltB64}]}, + #xmlel{name = <<"server-key">>, + children = [{xmlcdata, ServerKeyB64}]}, + #xmlel{name = <<"stored-key">>, + children = [{xmlcdata, StoredKeyB64}]} + ], + #xmlel{name = <<"scram-credentials">>, + attrs = [{<<"xmlns">>, <>}, + {<<"mechanism">>, MechanismB}], + children = Children}. + +parse_scram_password(#xmlel{attrs = Attrs} = El) -> + Hash = case fxml:get_attr_s(<<"mechanism">>, Attrs) of + <<"SCRAM-SHA-1">> -> sha; + <<"SCRAM-SHA-256">> -> sha256; + <<"SCRAM-SHA-512">> -> sha512 + end, + StoredKeyB64 = fxml:get_path_s(El, [{elem, <<"stored-key">>}, cdata]), + ServerKeyB64 = fxml:get_path_s(El, [{elem, <<"server-key">>}, cdata]), + IterationCountBin = fxml:get_path_s(El, [{elem, <<"iter-count">>}, cdata]), + SaltB64 = fxml:get_path_s(El, [{elem, <<"salt">>}, cdata]), + #scram{ + storedkey = base64:decode(StoredKeyB64), + serverkey = base64:decode(ServerKeyB64), + salt = base64:decode(SaltB64), + hash = Hash, + iterationcount = (binary_to_integer(IterationCountBin)) + }; parse_scram_password(PassData) -> Split = binary:split(PassData, <<",">>, [global]), @@ -214,26 +246,30 @@ parse_scram_password(PassData) -> get_vcard(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - case mod_vcard:get_vcard(LUser, LServer) of + try mod_vcard:get_vcard(LUser, LServer) of error -> []; Els -> Els + catch + error:{module_not_loaded, _, _} -> [] end. -spec get_offline(binary(), binary()) -> [xmlel()]. get_offline(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - case mod_offline:get_offline_els(LUser, LServer) of + try mod_offline:get_offline_els(LUser, LServer) of [] -> []; Els -> NewEls = lists:map(fun xmpp:encode/1, Els), [#xmlel{name = <<"offline-messages">>, children = NewEls}] + catch + error:{module_not_loaded, _, _} -> [] end. -spec get_privacy(binary(), binary()) -> [xmlel()]. get_privacy(User, Server) -> - case mod_privacy:get_user_lists(User, Server) of + try mod_privacy:get_user_lists(User, Server) of {ok, #privacy{default = Default, lists = [_|_] = Lists}} -> XLists = lists:map( @@ -246,12 +282,14 @@ get_privacy(User, Server) -> [xmpp:encode(#privacy_query{default = Default, lists = XLists})]; _ -> [] + catch + error:{module_not_loaded, _, _} -> [] end. -spec get_roster(binary(), binary()) -> [xmlel()]. get_roster(User, Server) -> JID = jid:make(User, Server), - case mod_roster:get_roster(User, Server) of + try mod_roster:get_roster(User, Server) of [_|_] = Items -> Subs = lists:flatmap( @@ -278,15 +316,19 @@ get_roster(User, Server) -> [xmpp:encode(#roster_query{items = Rs}) | Subs]; _ -> [] + catch + error:{module_not_loaded, _, _} -> [] end. -spec get_private(binary(), binary()) -> [xmlel()]. get_private(User, Server) -> - case mod_private:get_data(User, Server) of + try mod_private:get_data(User, Server) of [_|_] = Els -> [xmpp:encode(#private{sub_els = Els})]; _ -> [] + catch + error:{module_not_loaded, _, _} -> [] end. process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) -> @@ -398,21 +440,10 @@ process_users([_|Els], State) -> process_users([], State) -> {ok, State}. -process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els}, +process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El, #state{server = LServer} = State) -> Name = fxml:get_attr_s(<<"name">>, Attrs), - Password = fxml:get_attr_s(<<"password">>, Attrs), - PasswordFormat = ejabberd_auth:password_format(LServer), - Pass = case PasswordFormat of - scram -> - case Password of - <<"scram:", PassData/binary>> -> - parse_scram_password(PassData); - P -> P - end; - _ -> Password - end, - + Pass = process_password(El, LServer), case jid:nodeprep(Name) of error -> stop("Invalid 'user': ~ts", [Name]); @@ -420,13 +451,29 @@ process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els}, case ejabberd_auth:try_register(LUser, LServer, Pass) of ok -> process_user_els(Els, State#state{user = LUser}); - {error, invalid_password} when (Password == <<>>) -> + {error, invalid_password} when (Pass == <<>>) -> process_user_els(Els, State#state{user = LUser}); {error, Err} -> stop("Failed to create user '~ts': ~p", [Name, Err]) end end. +process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) -> + {PassPlain, PassOldScram} = case fxml:get_attr_s(<<"password">>, Attrs) of + <<"scram:", PassData/binary>> -> {<<"">>, PassData}; + P -> {P, false} + end, + ScramCred = fxml:get_subtag(El, <<"scram-credentials">>), + PasswordFormat = ejabberd_auth:password_format(LServer), + case {PassPlain, PassOldScram, ScramCred, PasswordFormat} of + {PassPlain, false, false, plain} -> PassPlain; + {<<"">>, false, ScramCred, plain} -> parse_scram_password(ScramCred); + {<<"">>, PassOldScram, false, plain} -> parse_scram_password(PassOldScram); + {PassPlain, false, false, scram} -> PassPlain; + {<<"">>, false, ScramCred, scram} -> parse_scram_password(ScramCred); + {<<"">>, PassOldScram, false, scram} -> parse_scram_password(PassOldScram) + end. + process_user_els([#xmlel{} = El|Els], State) -> case process_user_el(El, State) of {ok, NewState} -> @@ -595,33 +642,26 @@ make_host_filename(FnT, Host) -> make_host_basefilename(Dir, FnT) -> filename:join([Dir, FnT]). -%% @spec () -> string() make_piefxis_xml_head() -> "". -%% @spec () -> string() make_piefxis_xml_tail() -> "". -%% @spec () -> string() make_piefxis_server_head() -> io_lib:format("", [?NS_PIE, ?NS_XI]). -%% @spec () -> string() make_piefxis_server_tail() -> "". -%% @spec (Host::string()) -> string() make_piefxis_host_head(Host) -> io_lib:format("", [?NS_PIE, ?NS_XI, Host]). -%% @spec () -> string() make_piefxis_host_tail() -> "". -%% @spec (Fn::string()) -> string() make_xinclude(Fn) -> Base = filename:basename(Fn), io_lib:format("", [Base]). diff --git a/src/ejabberd_pkix.erl b/src/ejabberd_pkix.erl index 7ccb92cce..c5a945588 100644 --- a/src/ejabberd_pkix.erl +++ b/src/ejabberd_pkix.erl @@ -3,7 +3,7 @@ %%% Created : 4 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_redis.erl b/src/ejabberd_redis.erl index 640a75be1..597c57cfb 100644 --- a/src/ejabberd_redis.erl +++ b/src/ejabberd_redis.erl @@ -4,7 +4,7 @@ %%% Created : 8 May 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_redis_sup.erl b/src/ejabberd_redis_sup.erl index cfc400a7a..6906ef937 100644 --- a/src/ejabberd_redis_sup.erl +++ b/src/ejabberd_redis_sup.erl @@ -3,7 +3,7 @@ %%% Created : 6 Apr 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_regexp.erl b/src/ejabberd_regexp.erl index ef967893e..5841dd4da 100644 --- a/src/ejabberd_regexp.erl +++ b/src/ejabberd_regexp.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2011 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 492beb6d3..7be9475ec 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -5,7 +5,7 @@ %%% Created : 27 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_router_mnesia.erl b/src/ejabberd_router_mnesia.erl index b4144a97d..53adca533 100644 --- a/src/ejabberd_router_mnesia.erl +++ b/src/ejabberd_router_mnesia.erl @@ -2,7 +2,7 @@ %%% Created : 11 Jan 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_router_multicast.erl b/src/ejabberd_router_multicast.erl index e97ccb837..f3aba8407 100644 --- a/src/ejabberd_router_multicast.erl +++ b/src/ejabberd_router_multicast.erl @@ -5,7 +5,7 @@ %%% Created : 11 Aug 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_router_redis.erl b/src/ejabberd_router_redis.erl index 8d75081b3..450aa4b6b 100644 --- a/src/ejabberd_router_redis.erl +++ b/src/ejabberd_router_redis.erl @@ -3,7 +3,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_router_sql.erl b/src/ejabberd_router_sql.erl index 54f7bdfed..07a3c27f8 100644 --- a/src/ejabberd_router_sql.erl +++ b/src/ejabberd_router_sql.erl @@ -3,7 +3,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 8057c9a35..b2b078098 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -5,7 +5,7 @@ %%% Created : 7 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -33,8 +33,8 @@ %% API -export([start_link/0, stop/0, route/1, have_connection/1, - get_connections_pids/1, try_register/1, - remove_connection/2, start_connection/2, start_connection/3, + get_connections_pids/1, + start_connection/2, start_connection/3, dirty_get_connections/0, allow_host/2, incoming_s2s_number/0, outgoing_s2s_number/0, stop_s2s_connections/0, @@ -64,7 +64,7 @@ %% once a server is temporary blocked, it stay blocked for 60 seconds --record(s2s, {fromto :: {binary(), binary()}, +-record(s2s, {fromto :: {binary(), binary()} | '_', pid :: pid()}). -record(state, {}). @@ -112,24 +112,6 @@ is_temporarly_blocked(Host) -> end end. --spec remove_connection({binary(), binary()}, pid()) -> ok. -remove_connection({From, To} = FromTo, Pid) -> - case mnesia:dirty_match_object(s2s, #s2s{fromto = FromTo, pid = Pid}) of - [#s2s{pid = Pid}] -> - F = fun() -> - mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid}) - end, - case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> - ?ERROR_MSG("Failed to unregister s2s connection ~ts -> ~ts: " - "Mnesia failure: ~p", - [From, To, Reason]) - end; - _ -> - ok - end. - -spec have_connection({binary(), binary()}) -> boolean(). have_connection(FromTo) -> case catch mnesia:dirty_read(s2s, FromTo) of @@ -148,31 +130,6 @@ get_connections_pids(FromTo) -> [] end. --spec try_register({binary(), binary()}) -> boolean(). -try_register({From, To} = FromTo) -> - MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo), - MaxS2SConnectionsNumberPerNode = - max_s2s_connections_number_per_node(FromTo), - F = fun () -> - L = mnesia:read({s2s, FromTo}), - NeededConnections = needed_connections_number(L, - MaxS2SConnectionsNumber, - MaxS2SConnectionsNumberPerNode), - if NeededConnections > 0 -> - mnesia:write(#s2s{fromto = FromTo, pid = self()}), - true; - true -> false - end - end, - case mnesia:transaction(F) of - {atomic, Res} -> Res; - {aborted, Reason} -> - ?ERROR_MSG("Failed to register s2s connection ~ts -> ~ts: " - "Mnesia failure: ~p", - [From, To, Reason]), - false - end. - -spec dirty_get_connections() -> [{binary(), binary()}]. dirty_get_connections() -> mnesia:dirty_all_keys(s2s). @@ -269,6 +226,8 @@ init([]) -> {stop, Reason} end. +handle_call({new_connection, Args}, _From, State) -> + {reply, erlang:apply(fun new_connection_int/7, Args), State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. @@ -289,6 +248,21 @@ handle_info({route, Packet}, State) -> misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; +handle_info({'DOWN', _Ref, process, Pid, _Reason}, State) -> + case mnesia:dirty_match_object(s2s, #s2s{fromto = '_', pid = Pid}) of + [#s2s{pid = Pid, fromto = {From, To}} = Obj] -> + F = fun() -> mnesia:delete_object(Obj) end, + case mnesia:transaction(F) of + {atomic, _} -> ok; + {aborted, Reason} -> + ?ERROR_MSG("Failed to unregister s2s connection for pid ~p (~ts -> ~ts):" + "Mnesia failure: ~p", + [Pid, From, To, Reason]) + end, + {noreply, State}; + _ -> + {noreply, State} + end; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. @@ -413,7 +387,7 @@ start_connection(From, To, Opts) -> MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts); true -> - %% We choose a connexion from the pool of opened ones. + %% We choose a connection from the pool of opened ones. {ok, choose_connection(From, L)} end end. @@ -458,6 +432,18 @@ open_several_connections(N, MyServer, Server, From, integer(), integer(), [proplists:property()]) -> [pid()]. new_connection(MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) -> + case whereis(ejabberd_s2s) == self() of + true -> + new_connection_int(MyServer, Server, From, FromTo, + MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts); + false -> + gen_server:call(ejabberd_s2s, {new_connection, [MyServer, Server, From, FromTo, + MaxS2SConnectionsNumber, + MaxS2SConnectionsNumberPerNode, Opts]}) + end. + +new_connection_int(MyServer, Server, From, FromTo, + MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) -> {ok, Pid} = ejabberd_s2s_out:start(MyServer, Server, Opts), F = fun() -> L = mnesia:read({s2s, FromTo}), @@ -474,6 +460,7 @@ new_connection(MyServer, Server, From, FromTo, case TRes of {atomic, Pid1} -> if Pid1 == Pid -> + erlang:monitor(process, Pid), ejabberd_s2s_out:connect(Pid); true -> ejabberd_s2s_out:stop_async(Pid) @@ -602,10 +589,8 @@ allow_host1(MyHost, S2SHost) -> end end. -%% Get information about S2S connections of the specified type. -%% @spec (Type) -> [Info] -%% where Type = in | out -%% Info = [{InfoName::atom(), InfoValue::any()}] +%% @doc Get information about S2S connections of the specified type. +-spec get_info_s2s_connections(Type::in | out) -> [[{InfoName::atom(), InfoValue::any()}]]. get_info_s2s_connections(Type) -> ChildType = case Type of in -> ejabberd_s2s_in_sup; diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 8177a7333..b3ff0c2b4 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -2,7 +2,7 @@ %%% Created : 12 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index d58396533..f7998240c 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -2,7 +2,7 @@ %%% Created : 16 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -318,7 +318,6 @@ handle_info(Info, #{server_host := ServerHost} = State) -> terminate(Reason, #{server := LServer, remote_server := RServer} = State) -> - ejabberd_s2s:remove_connection({LServer, RServer}, self()), State1 = case Reason of normal -> State; _ -> State#{stop_reason => internal_failure} @@ -351,21 +350,12 @@ bounce_queue(State) -> end, State). -spec bounce_message_queue({binary(), binary()}, state()) -> state(). -bounce_message_queue({LServer, RServer} = FromTo, State) -> - Pids = ejabberd_s2s:get_connections_pids(FromTo), - case lists:member(self(), Pids) of - true -> - ?WARNING_MSG("Outgoing s2s connection ~ts -> ~ts is supposed " - "to be unregistered, but pid ~p still presents " - "in 's2s' table", [LServer, RServer, self()]), - State; - false -> - receive {route, Pkt} -> - State1 = bounce_packet(Pkt, State), - bounce_message_queue(FromTo, State1) - after 0 -> - State - end +bounce_message_queue(FromTo, State) -> + receive {route, Pkt} -> + State1 = bounce_packet(Pkt, State), + bounce_message_queue(FromTo, State1) + after 0 -> + State end. -spec bounce_packet(xmpp_element(), state()) -> state(). diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index b854bcb2b..5e386ed7d 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -2,7 +2,7 @@ %%% Created : 11 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_shaper.erl b/src/ejabberd_shaper.erl index d4a883baf..4bd5229fa 100644 --- a/src/ejabberd_shaper.erl +++ b/src/ejabberd_shaper.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sip.erl b/src/ejabberd_sip.erl index 62b9c0333..a85b139e0 100644 --- a/src/ejabberd_sip.erl +++ b/src/ejabberd_sip.erl @@ -5,7 +5,7 @@ %%% Created : 30 Apr 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2021 ProcessOne +%%% ejabberd, Copyright (C) 2013-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index afb95c10b..231e4351e 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -5,7 +5,7 @@ %%% Created : 24 Nov 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sm_mnesia.erl b/src/ejabberd_sm_mnesia.erl index 396e1c6d6..ec321271b 100644 --- a/src/ejabberd_sm_mnesia.erl +++ b/src/ejabberd_sm_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 9 Mar 2015 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl index a436aa923..5f612b926 100644 --- a/src/ejabberd_sm_redis.erl +++ b/src/ejabberd_sm_redis.erl @@ -4,7 +4,7 @@ %%% Created : 11 Mar 2015 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sm_sql.erl b/src/ejabberd_sm_sql.erl index 9c90b75c4..b58b66f0d 100644 --- a/src/ejabberd_sm_sql.erl +++ b/src/ejabberd_sm_sql.erl @@ -4,7 +4,7 @@ %%% Created : 9 Mar 2015 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl index afca5b183..3cd2dc345 100644 --- a/src/ejabberd_sql.erl +++ b/src/ejabberd_sql.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -74,6 +74,7 @@ {db_ref :: undefined | pid(), db_type = odbc :: pgsql | mysql | sqlite | odbc | mssql, db_version :: undefined | non_neg_integer(), + reconnect_count = 0 :: non_neg_integer(), host :: binary(), pending_requests :: p1_queue:queue(), overload_reported :: undefined | integer()}). @@ -375,7 +376,7 @@ connecting(connect, #state{host = Host} = State) -> State1 = State#state{db_ref = Ref, pending_requests = PendingRequests}, State2 = get_db_version(State1), - {next_state, session_established, State2} + {next_state, session_established, State2#state{reconnect_count = 0}} catch _:Reason -> handle_reconnect(Reason, State) end; @@ -442,6 +443,8 @@ handle_sync_event(_Event, _From, StateName, State) -> code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. +handle_info({'EXIT', _Pid, _Reason}, connecting, State) -> + {next_state, connecting, State}; handle_info({'EXIT', _Pid, Reason}, _StateName, State) -> handle_reconnect(Reason, State); handle_info(Info, StateName, State) -> @@ -467,15 +470,25 @@ print_state(State) -> State. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- -handle_reconnect(Reason, #state{host = Host} = State) -> - StartInterval = ejabberd_option:sql_start_interval(Host), +handle_reconnect(Reason, #state{host = Host, reconnect_count = RC} = State) -> + StartInterval0 = ejabberd_option:sql_start_interval(Host), + StartInterval = case RC of + 0 -> erlang:min(5000, StartInterval0); + _ -> StartInterval0 + end, ?WARNING_MSG("~p connection failed:~n" "** Reason: ~p~n" "** Retry after: ~B seconds", [State#state.db_type, Reason, StartInterval div 1000]), + case State#state.db_type of + mysql -> catch p1_mysql_conn:stop(State#state.db_ref); + sqlite -> catch sqlite3:close(sqlite_db(State#state.host)); + pgsql -> catch pgsql:terminate(State#state.db_ref); + _ -> ok + end, p1_fsm:send_event_after(StartInterval, connect), - {next_state, connecting, State}. + {next_state, connecting, State#state{reconnect_count = RC + 1}}. run_sql_cmd(Command, From, State, Timestamp) -> case current_time() >= Timestamp of @@ -483,13 +496,21 @@ run_sql_cmd(Command, From, State, Timestamp) -> State1 = report_overload(State), {next_state, session_established, State1}; false -> - put(?NESTING_KEY, ?TOP_LEVEL_TXN), - put(?STATE_KEY, State), - abort_on_driver_error(outer_op(Command), From, Timestamp) + receive + {'EXIT', _Pid, Reason} -> + PR = p1_queue:in({sql_cmd, Command, From, Timestamp}, + State#state.pending_requests), + handle_reconnect(Reason, State#state{pending_requests = PR}) + after 0 -> + put(?NESTING_KEY, ?TOP_LEVEL_TXN), + put(?STATE_KEY, State), + abort_on_driver_error(outer_op(Command), From, Timestamp) + end end. -%% Only called by handle_call, only handles top level operations. -%% @spec outer_op(Op) -> {error, Reason} | {aborted, Reason} | {atomic, Result} +%% @doc Only called by handle_call, only handles top level operations. +-spec outer_op(Op::{atom(), binary()}) -> + {error, Reason::binary()} | {aborted, Reason::binary()} | {atomic, Result::any()}. outer_op({sql_query, Query}) -> sql_query_internal(Query); outer_op({sql_transaction, F}) -> @@ -541,30 +562,63 @@ outer_transaction(F, NRestarts, _Reason) -> [T]), erlang:exit(implementation_faulty) end, - sql_begin(), - put(?NESTING_KEY, PreviousNestingLevel + 1), - try F() of - Res -> - sql_commit(), - {atomic, Res} - catch - ?EX_RULE(throw, {aborted, Reason}, _) when NRestarts > 0 -> - sql_rollback(), - put(?NESTING_KEY, ?TOP_LEVEL_TXN), + case sql_begin() of + {error, Reason} -> + maybe_restart_transaction(F, NRestarts, Reason, false); + _ -> + put(?NESTING_KEY, PreviousNestingLevel + 1), + try F() of + Res -> + case sql_commit() of + {error, Reason} -> + restart(Reason); + _ -> + {atomic, Res} + end + catch + ?EX_RULE(throw, {aborted, Reason}, _) when NRestarts > 0 -> + maybe_restart_transaction(F, NRestarts, Reason, true); + ?EX_RULE(throw, {aborted, Reason}, Stack) when NRestarts =:= 0 -> + StackTrace = ?EX_STACK(Stack), + ?ERROR_MSG("SQL transaction restarts exceeded~n** " + "Restarts: ~p~n** Last abort reason: " + "~p~n** Stacktrace: ~p~n** When State " + "== ~p", + [?MAX_TRANSACTION_RESTARTS, Reason, + StackTrace, get(?STATE_KEY)]), + maybe_restart_transaction(F, NRestarts, Reason, true); + ?EX_RULE(exit, Reason, _) -> + maybe_restart_transaction(F, 0, Reason, true) + end + end. + +maybe_restart_transaction(F, NRestarts, Reason, DoRollback) -> + Res = case driver_restart_required(Reason) of + true -> + {aborted, Reason}; + _ when DoRollback -> + case sql_rollback() of + {error, Reason2} -> + case driver_restart_required(Reason2) of + true -> + {aborted, Reason2}; + _ -> + continue + end; + _ -> + continue + end; + _ -> + continue + end, + case Res of + continue when NRestarts > 0 -> + put(?NESTING_KEY, ?TOP_LEVEL_TXN), outer_transaction(F, NRestarts - 1, Reason); - ?EX_RULE(throw, {aborted, Reason}, Stack) when NRestarts =:= 0 -> - StackTrace = ?EX_STACK(Stack), - ?ERROR_MSG("SQL transaction restarts exceeded~n** " - "Restarts: ~p~n** Last abort reason: " - "~p~n** Stacktrace: ~p~n** When State " - "== ~p", - [?MAX_TRANSACTION_RESTARTS, Reason, - StackTrace, get(?STATE_KEY)]), - sql_rollback(), + continue -> {aborted, Reason}; - ?EX_RULE(exit, Reason, _) -> - sql_rollback(), - {aborted, Reason} + Other -> + Other end. execute_bloc(F) -> @@ -669,11 +723,10 @@ sql_query_internal(Query) -> pgsql_to_odbc(pgsql:squery(State#state.db_ref, Query, QueryTimeout - 1000)); mysql -> - R = mysql_to_odbc(p1_mysql_conn:squery(State#state.db_ref, + mysql_to_odbc(p1_mysql_conn:squery(State#state.db_ref, [Query], self(), [{timeout, QueryTimeout - 1000}, - {result_type, binary}])), - R; + {result_type, binary}])); sqlite -> Host = State#state.host, sqlite_to_odbc(Host, sqlite3:sql_exec(sqlite_db(Host), Query)) @@ -853,21 +906,22 @@ sql_rollback() -> [{mssql, [<<"rollback transaction;">>]}, {any, [<<"rollback;">>]}]). +driver_restart_required(<<"query timed out">>) -> true; +driver_restart_required(<<"connection closed">>) -> true; +driver_restart_required(<<"Failed sending data on socket", _/binary>>) -> true; +driver_restart_required(<<"SQL connection failed">>) -> true; +driver_restart_required(<<"Communication link failure">>) -> true; +driver_restart_required(_) -> false. %% Generate the OTP callback return tuple depending on the driver result. -abort_on_driver_error({error, <<"query timed out">>} = Reply, From, Timestamp) -> +abort_on_driver_error({Tag, Msg} = Reply, From, Timestamp) when Tag == error; Tag == aborted -> reply(From, Reply, Timestamp), - {stop, timeout, get(?STATE_KEY)}; -abort_on_driver_error({error, <<"Failed sending data on socket", _/binary>>} = Reply, - From, Timestamp) -> - reply(From, Reply, Timestamp), - {stop, closed, get(?STATE_KEY)}; -abort_on_driver_error({error, <<"SQL connection failed">>} = Reply, From, Timestamp) -> - reply(From, Reply, Timestamp), - {stop, timeout, get(?STATE_KEY)}; -abort_on_driver_error({error, <<"Communication link failure">>} = Reply, From, Timestamp) -> - reply(From, Reply, Timestamp), - {stop, closed, get(?STATE_KEY)}; + case driver_restart_required(Msg) of + true -> + handle_reconnect(Msg, get(?STATE_KEY)); + _ -> + {next_state, session_established, get(?STATE_KEY)} + end; abort_on_driver_error(Reply, From, Timestamp) -> reply(From, Reply, Timestamp), {next_state, session_established, get(?STATE_KEY)}. diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl index 130228fe1..0f8465942 100644 --- a/src/ejabberd_sql_pt.erl +++ b/src/ejabberd_sql_pt.erl @@ -5,7 +5,7 @@ %%% Created : 20 Jan 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -41,6 +41,7 @@ server_host_used = false, used_vars = [], use_new_schema, + need_timestamp_pass = false, need_array_pass = false}). -define(QUERY_RECORD, "sql_query"). @@ -50,12 +51,6 @@ -define(MOD, sql__module_). --ifdef(NEW_SQL_SCHEMA). --define(USE_NEW_SCHEMA, true). --else. --define(USE_NEW_SCHEMA, false). --endif. - %%==================================================================== %% API %%==================================================================== @@ -175,17 +170,24 @@ transform_sql(Arg) -> Pos, no_server_host), [] end, - case ParseRes#state.need_array_pass of - true -> + case {ParseRes#state.need_array_pass, ParseRes#state.need_timestamp_pass} of + {true, _} -> {PR1, PR2} = perform_array_pass(ParseRes), {PRO1, PRO2} = perform_array_pass(ParseResOld), set_pos(make_schema_check( - erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(PR2)]), + erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(PR2, pgsql)]), erl_syntax:tuple([erl_syntax:atom(any), make_sql_query(PR1)])]), - erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(PRO2)]), + erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(PRO2, pgsql)]), erl_syntax:tuple([erl_syntax:atom(any), make_sql_query(PRO1)])])), Pos); - false -> + {_, true} -> + set_pos(make_schema_check( + erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(ParseRes, pgsql)]), + erl_syntax:tuple([erl_syntax:atom(any), make_sql_query(ParseRes)])]), + erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(ParseResOld, pgsql)]), + erl_syntax:tuple([erl_syntax:atom(any), make_sql_query(ParseResOld)])])), + Pos); + _ -> set_pos( make_schema_check( make_sql_query(ParseRes), @@ -271,6 +273,8 @@ parse1([$@, $( | S], Acc, State) -> [EVar]); string -> EVar; + timestamp -> + EVar; boolean -> erl_syntax:application( erl_syntax:atom(ejabberd_sql), @@ -301,7 +305,7 @@ parse1([$%, $( | S], Acc, State) -> erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(string)), [erl_syntax:variable(Name)]), - State3#state{'query' = [{var, Var}, + State3#state{'query' = [{var, Var, Type}, {str, "server_host="} | State3#state.'query'], args = [Convert | State3#state.args], @@ -333,21 +337,26 @@ parse1([$%, $( | S], Acc, State) -> erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(IT2)), erl_syntax:variable(Name)]), - State2#state{'query' = [[{var, Var}] | State2#state.'query'], + State2#state{'query' = [[{var, Var, Type}] | State2#state.'query'], need_array_pass = true, args = [[Convert, ConvertArr] | State2#state.args], params = [Var | State2#state.params], param_pos = State2#state.param_pos + 1, used_vars = [Name | State2#state.used_vars]}; _ -> + {TS, Type2} = case Type of + timestamp -> {true, string}; + Other -> {State2#state.need_timestamp_pass, Other} + end, Convert = erl_syntax:application( erl_syntax:record_access( erl_syntax:variable(?ESCAPE_VAR), erl_syntax:atom(?ESCAPE_RECORD), - erl_syntax:atom(Type)), + erl_syntax:atom(Type2)), [erl_syntax:variable(Name)]), - State2#state{'query' = [{var, Var} | State2#state.'query'], + State2#state{'query' = [{var, Var, Type} | State2#state.'query'], + need_timestamp_pass = TS, args = [Convert | State2#state.args], params = [Var | State2#state.params], param_pos = State2#state.param_pos + 1, @@ -365,7 +374,7 @@ parse1("%ESCAPE" ++ S, Acc, State) -> []), Var = State1#state.param_pos, State2 = - State1#state{'query' = [{var, Var} | State1#state.'query'], + State1#state{'query' = [{var, Var, string} | State1#state.'query'], args = [Convert | State1#state.args], params = [Var | State1#state.params], param_pos = State1#state.param_pos + 1}, @@ -403,6 +412,7 @@ parse_name([$), T | S], Acc, 0, IsArg, State) -> $d -> integer; $s -> string; $b -> boolean; + $t -> timestamp; $H when IsArg -> host; _ -> throw({error, State#state.loc, @@ -426,10 +436,10 @@ make_var(V) -> perform_array_pass(State) -> {NQ, PQ, Rest} = lists:foldl( - fun([{var, _} = Var], {N, P, {str, Str} = Prev}) -> + fun([{var, _, _} = Var], {N, P, {str, Str} = Prev}) -> Str2 = re:replace(Str, "(^|\s+)in\s*$", " = any(", [{return, list}]), {[Var, Prev | N], [{str, ")"}, Var, {str, Str2} | P], none}; - ([{var, _}], _) -> + ([{var, _, _}], _) -> throw({error, State#state.loc, ["List variable not following 'in' operator"]}); (Other, {N, P, none}) -> {N, P, Other}; @@ -451,16 +461,27 @@ perform_array_pass(State) -> State#state{query = lists:reverse(PQ2), args = lists:reverse(PA), need_array_pass = false}}. make_sql_query(State) -> + make_sql_query(State, unknown). + +make_sql_query(State, Type) -> Hash = erlang:phash2(State#state{loc = undefined, use_new_schema = true}), SHash = <<"Q", (integer_to_binary(Hash))/binary>>, Query = pack_query(State#state.'query'), EQuery = - lists:map( + lists:flatmap( fun({str, S}) -> - erl_syntax:binary( + [erl_syntax:binary( [erl_syntax:binary_field( - erl_syntax:string(S))]); - ({var, V}) -> make_var(V) + erl_syntax:string(S))])]; + ({var, V, timestamp}) when Type == pgsql -> + [erl_syntax:binary( + [erl_syntax:binary_field( + erl_syntax:string("to_timestamp("))]), + make_var(V), + erl_syntax:binary( + [erl_syntax:binary_field( + erl_syntax:string(", 'YYYY-MM-DD HH24:MI:SS')"))])]; + ({var, V, _}) -> [make_var(V)] end, Query), erl_syntax:record_expr( erl_syntax:atom(?QUERY_RECORD), @@ -549,35 +570,33 @@ parse_upsert_field1([C | S], Acc, ParamPos, Loc) -> make_sql_upsert(Table, ParseRes, Pos) -> check_upsert(ParseRes, Pos), - HasInsertOnlyFields = lists:any( - fun({_, {false}, _}) -> true; - (_) -> false - end, ParseRes), - MySqlReplace = case HasInsertOnlyFields of - false -> - [erl_syntax:clause( - [erl_syntax:atom(mysql), erl_syntax:underscore()], - [], - [make_sql_upsert_mysql(Table, ParseRes), - erl_syntax:atom(ok)])]; - _ -> - [] - end, erl_syntax:fun_expr( [erl_syntax:clause( [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], [erl_syntax:infix_expr( erl_syntax:variable("__Version"), erl_syntax:operator('>='), - erl_syntax:integer(90100))], - [make_sql_upsert_pgsql901(Table, ParseRes), - erl_syntax:atom(ok)])] ++ - MySqlReplace ++ - [erl_syntax:clause( - [erl_syntax:underscore(), erl_syntax:underscore()], - none, - [make_sql_upsert_generic(Table, ParseRes)]) - ]). + erl_syntax:integer(90500))], + [make_sql_upsert_pgsql905(Table, ParseRes), + erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], + [erl_syntax:infix_expr( + erl_syntax:variable("__Version"), + erl_syntax:operator('>='), + erl_syntax:integer(90100))], + [make_sql_upsert_pgsql901(Table, ParseRes), + erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:atom(mysql), erl_syntax:underscore()], + [], + [make_sql_upsert_mysql(Table, ParseRes), + erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:underscore(), erl_syntax:underscore()], + none, + [make_sql_upsert_generic(Table, ParseRes)]) + ]). make_sql_upsert_generic(Table, ParseRes) -> Update = make_sql_query(make_sql_upsert_update(Table, ParseRes)), @@ -643,9 +662,6 @@ make_sql_upsert_update(Table, ParseRes) -> State. make_sql_upsert_insert(Table, ParseRes) -> - make_sql_upsert_insert_replace(Table, ParseRes, "INSERT"). - -make_sql_upsert_insert_replace(Table, ParseRes, Keyword) -> Vals = lists:map( fun({_Field, _, ST}) -> @@ -658,7 +674,7 @@ make_sql_upsert_insert_replace(Table, ParseRes, Keyword) -> end, ParseRes), State = concat_states( - [#state{'query' = [{str, Keyword ++" INTO "}, {str, Table}, {str, "("}]}, + [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, join_states(Fields, ", "), #state{'query' = [{str, ") VALUES ("}]}, join_states(Vals, ", "), @@ -666,15 +682,35 @@ make_sql_upsert_insert_replace(Table, ParseRes, Keyword) -> ]), State. -make_sql_upsert_replace(Table, ParseRes) -> - make_sql_upsert_insert_replace(Table, ParseRes, "REPLACE"). - make_sql_upsert_mysql(Table, ParseRes) -> - Replace = make_sql_query(make_sql_upsert_replace(Table, ParseRes)), + Vals = + lists:map( + fun({_Field, _, ST}) -> + ST + end, ParseRes), + {Fields, Set} = + lists:foldr( + fun({Field, key, _ST}, {F, S}) -> + {[#state{'query' = [{str, Field}]} | F], S}; + ({Field, {false}, _ST}, {F, S}) -> + {[#state{'query' = [{str, Field}]} | F], S}; + ({Field, {true}, _ST}, {F, S}) -> + {[#state{'query' = [{str, Field}]} | F], + [#state{'query' = [{str, Field}, {str, "=VALUES("}, {str, Field}, {str, ")"}]} | S]} + end, {[], []}, ParseRes), + Insert = + concat_states( + [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, + join_states(Fields, ", "), + #state{'query' = [{str, ") VALUES ("}]}, + join_states(Vals, ", "), + #state{'query' = [{str, ") ON DUPLICATE KEY UPDATE "}]}, + join_states(Set, ", ") + ]), erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), - [Replace]). + [make_sql_query(Insert)]). make_sql_upsert_pgsql901(Table, ParseRes0) -> ParseRes = lists:map( @@ -707,7 +743,58 @@ make_sql_upsert_pgsql901(Table, ParseRes0) -> #state{'query' = [{str, " RETURNING *) "}]}, Insert ]), - Upsert = make_sql_query(State), + Upsert = make_sql_query(State, pgsql), + erl_syntax:application( + erl_syntax:atom(ejabberd_sql), + erl_syntax:atom(sql_query_t), + [Upsert]). + +make_sql_upsert_pgsql905(Table, ParseRes0) -> + ParseRes = lists:map( + fun({"family", A2, A3}) -> {"\"family\"", A2, A3}; + (Other) -> Other + end, ParseRes0), + Vals = + lists:map( + fun({_Field, _, ST}) -> + ST + end, ParseRes), + Fields = + lists:map( + fun({Field, _, _ST}) -> + #state{'query' = [{str, Field}]} + end, ParseRes), + SPairs = + lists:flatmap( + fun({_Field, key, _ST}) -> + []; + ({_Field, {false}, _ST}) -> + []; + ({Field, {true}, ST}) -> + [ST#state{ + 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' + }] + end, ParseRes), + Set = join_states(SPairs, ", "), + KeyFields = + lists:flatmap( + fun({Field, key, _ST}) -> + [#state{'query' = [{str, Field}]}]; + ({_Field, _, _ST}) -> + [] + end, ParseRes), + State = + concat_states( + [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, + join_states(Fields, ", "), + #state{'query' = [{str, ") VALUES ("}]}, + join_states(Vals, ", "), + #state{'query' = [{str, ") ON CONFLICT ("}]}, + join_states(KeyFields, ", "), + #state{'query' = [{str, ") DO UPDATE SET "}]}, + Set + ]), + Upsert = make_sql_query(State, pgsql), erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), @@ -831,12 +918,12 @@ resolve_vars(ST1, ST2) -> end, ST1#state.params), NewQuery = lists:map( - fun({var, Var}) -> + fun({var, Var, Type}) -> case dict:find(Var, Map) of {ok, New} -> - {var, New}; + {var, New, Type}; error -> - {var, Var} + {var, Var, Type} end; (S) -> S end, ST1#state.'query'), diff --git a/src/ejabberd_sql_sup.erl b/src/ejabberd_sql_sup.erl index d4af0244b..659189c33 100644 --- a/src/ejabberd_sql_sup.erl +++ b/src/ejabberd_sql_sup.erl @@ -5,7 +5,7 @@ %%% Created : 22 Dec 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_stun.erl b/src/ejabberd_stun.erl index 118af6eac..b41baca12 100644 --- a/src/ejabberd_stun.erl +++ b/src/ejabberd_stun.erl @@ -5,7 +5,7 @@ %%% Created : 8 May 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2021 ProcessOne +%%% ejabberd, Copyright (C) 2013-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 364c8a058..e15e658c4 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -66,7 +66,8 @@ init([]) -> supervisor(ejabberd_gen_mod_sup, gen_mod), worker(ejabberd_acme), worker(ejabberd_auth), - worker(ejabberd_oauth)]}}. + worker(ejabberd_oauth), + worker(ejabberd_batch)]}}. -spec stop_child(atom()) -> ok. stop_child(Name) -> diff --git a/src/ejabberd_system_monitor.erl b/src/ejabberd_system_monitor.erl index 208909366..d03e3bbd8 100644 --- a/src/ejabberd_system_monitor.erl +++ b/src/ejabberd_system_monitor.erl @@ -5,7 +5,7 @@ %%% Created : 21 Mar 2007 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_tmp_sup.erl b/src/ejabberd_tmp_sup.erl index 64316d4c9..f23b51289 100644 --- a/src/ejabberd_tmp_sup.erl +++ b/src/ejabberd_tmp_sup.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jul 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_update.erl b/src/ejabberd_update.erl index ff6c2712d..0ec129944 100644 --- a/src/ejabberd_update.erl +++ b/src/ejabberd_update.erl @@ -5,7 +5,7 @@ %%% Created : 27 Jan 2006 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_web.erl b/src/ejabberd_web.erl index 5cfbb93aa..431ae8ef3 100644 --- a/src/ejabberd_web.erl +++ b/src/ejabberd_web.erl @@ -6,7 +6,7 @@ %%% Created : 28 Feb 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 7dc11b571..46598e1ef 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -5,7 +5,7 @@ %%% Created : 9 Apr 2004 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -52,8 +52,8 @@ %%%================================== %%%% get_acl_access -%% @spec (Path::[string()], Method) -> {HostOfRule, [AccessRule]} -%% where Method = 'GET' | 'POST' +-spec get_acl_rule(Path::[binary()], 'GET' | 'POST') -> + {HostOfRule::binary(), [AccessRule::atom()]}. %% All accounts can access those URLs get_acl_rule([], _) -> {<<"localhost">>, [all]}; @@ -275,10 +275,13 @@ get_auth_account2(HostOfRule, AccessRule, User, Server, make_xhtml(Els, Host, Lang, JID, Level) -> make_xhtml(Els, Host, cluster, Lang, JID, Level). -%% @spec (Els, Host, Node, Lang, JID, Level::integer()) -> {200, [html], xmlelement()} -%% where Host = global | string() -%% Node = cluster | atom() -%% JID = jid() +-spec make_xhtml([xmlel()], + Host::global | binary(), + Node::cluster | atom(), + Lang::binary(), + jid(), + Level::integer()) -> + {200, [html], xmlel()}. make_xhtml(Els, Host, Node, Lang, JID, Level) -> Base = get_base_path_sum(0, 0, Level), MenuItems = make_navigation(Host, Node, Lang, JID, Level), @@ -333,7 +336,7 @@ make_xhtml(Els, Host, Node, Lang, JID, Level) -> [?XE(<<"p">>, [?AC(<<"https://www.ejabberd.im/">>, <<"ejabberd">>), ?C(<<" ">>), ?C(ejabberd_option:version()), - ?C(<<" (c) 2002-2021 ">>), + ?C(<<" (c) 2002-2022 ">>), ?AC(<<"https://www.process-one.net/">>, <<"ProcessOne, leader in messaging and push solutions">>)] )])])])]}}. @@ -602,12 +605,14 @@ list_vhosts2(Lang, Hosts) -> [?AC(<<"../server/", Host/binary, "/">>, Host)]), - ?XAC(<<"td">>, + ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(RegisteredUsers))), - ?XAC(<<"td">>, + [?AC(<<"../server/", Host/binary, "/users/">>, + pretty_string_int(RegisteredUsers))]), + ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], - (pretty_string_int(OnlineUsers)))]) + [?AC(<<"../server/", Host/binary, "/online-users/">>, + pretty_string_int(OnlineUsers))])]) end, SHosts)))])]. @@ -1855,11 +1860,11 @@ build_elements_pages_list(Size, PageNumber, PageSize) -> ?C(<<" [", (integer_to_binary(PageNumber))/binary, "] ">>)). calculate_pages_number(Size, PageSize) -> - Remainer = case Size rem PageSize of + Remainder = case Size rem PageSize of 0 -> 0; _ -> 1 end, - case (Size div PageSize) + Remainer of + case (Size div PageSize) + Remainder of 1 -> 0; Res -> Res end. @@ -1875,18 +1880,15 @@ get_table_content(Node, Table, _Type, PageNumber, PageSize) -> %%%================================== %%%% navigation menu -%% @spec (Host, Node, Lang, JID::jid(), Level::integer()) -> [LI] make_navigation(Host, Node, Lang, JID, Level) -> Menu = make_navigation_menu(Host, Node, Lang, JID, Level), make_menu_items(Lang, Menu). -%% @spec (Host, Node, Lang, JID::jid(), Level::integer()) -> Menu -%% where Host = global | string() -%% Node = cluster | string() -%% Lang = string() -%% Menu = {URL, Title} | {URL, Title, [Menu]} -%% URL = string() -%% Title = string() +-spec make_navigation_menu(Host::global | binary(), + Node::cluster | atom(), + Lang::binary(), JID::jid(), Level::integer()) -> + Menu::{URL::binary(), Title::binary()} + | {URL::binary(), Title::binary(), [Menu::any()]}. make_navigation_menu(Host, Node, Lang, JID, Level) -> HostNodeMenu = make_host_node_menu(Host, Node, Lang, JID, Level), @@ -1895,7 +1897,6 @@ make_navigation_menu(Host, Node, Lang, JID, Level) -> NodeMenu = make_node_menu(Host, Node, Lang, Level), make_server_menu(HostMenu, NodeMenu, Lang, JID, Level). -%% @spec (Host, Node, Base, Lang) -> [LI] make_menu_items(global, cluster, Base, Lang) -> HookItems = get_menu_items_hook(server, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); @@ -1976,9 +1977,11 @@ get_menu_items_hook({node, Node}, Lang) -> get_menu_items_hook(server, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]). -%% @spec (Lang::string(), Menu) -> [LI] -%% where Menu = {MURI::string(), MName::string(), Items::[Item]} -%% Item = {IURI::string(), IName::string()} | {IURI::string(), IName::string(), Menu} +-spec make_menu_items(Lang::binary(), + {MURI::binary(), MName::binary(), + Items::[{IURI::binary(), IName::binary()} + | {IURI::binary(), IName::binary(), Menu::any()}]}) -> + [xmlel()]. make_menu_items(Lang, Menu) -> lists:reverse(make_menu_items2(Lang, 1, Menu)). diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl index 5f9d284a6..966242331 100644 --- a/src/ejabberd_websocket.erl +++ b/src/ejabberd_websocket.erl @@ -33,7 +33,7 @@ %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE %%% POSSIBILITY OF SUCH DAMAGE. %%% ========================================================================================================== -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%%---------------------------------------------------------------------- -module(ejabberd_websocket). diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 1a1ec63f4..9e587add8 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -5,7 +5,7 @@ %%% Created : 21 Aug 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl index ad0cc5e88..469457048 100644 --- a/src/ejd2sql.erl +++ b/src/ejd2sql.erl @@ -5,7 +5,7 @@ %%% Created : 22 Aug 2005 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -73,11 +73,16 @@ export(Server, Output) -> end, Modules), close_output(Output, IO). -export(Server, Output, Module1) -> - Module = case Module1 of - mod_pubsub -> pubsub_db; - _ -> Module1 - end, +export(Server, Output, mod_mam = M1) -> + MucServices = gen_mod:get_module_opt_hosts(Server, mod_muc), + [export2(MucService, Output, M1, M1) || MucService <- MucServices], + export2(Server, Output, M1, M1); +export(Server, Output, mod_pubsub = M1) -> + export2(Server, Output, M1, pubsub_db); +export(Server, Output, M1) -> + export2(Server, Output, M1, M1). + +export2(Server, Output, Module1, Module) -> SQLMod = gen_mod:db_mod(sql, Module), LServer = jid:nameprep(iolist_to_binary(Server)), IO = prepare_output(Output), @@ -160,12 +165,18 @@ import_info(Mod) -> %%%---------------------------------------------------------------------- export(LServer, Table, IO, ConvertFun) -> DbType = ejabberd_option:sql_type(LServer), + LServerConvert = case Table of + archive_msg -> + [LServer | mod_muc_admin:find_hosts(LServer)]; + _ -> + LServer + end, F = fun () -> mnesia:read_lock_table(Table), {_N, SQLs} = mnesia:foldl( fun(R, {N, SQLs} = Acc) -> - case ConvertFun(LServer, R) of + case ConvertFun(LServerConvert, R) of [] -> Acc; SQL1 -> diff --git a/src/eldap_filter.erl b/src/eldap_filter.erl index ab5b8eb6f..4e554572d 100644 --- a/src/eldap_filter.erl +++ b/src/eldap_filter.erl @@ -6,7 +6,7 @@ %%% Author: Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/eldap_pool.erl b/src/eldap_pool.erl index dd3b9e665..148095783 100644 --- a/src/eldap_pool.erl +++ b/src/eldap_pool.erl @@ -5,7 +5,7 @@ %%% Created : 12 Nov 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/eldap_utils.erl b/src/eldap_utils.erl index dae9b563b..efbd78550 100644 --- a/src/eldap_utils.erl +++ b/src/eldap_utils.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/elixir_logger_backend.erl b/src/elixir_logger_backend.erl index d466652e1..4db60789d 100644 --- a/src/elixir_logger_backend.erl +++ b/src/elixir_logger_backend.erl @@ -5,7 +5,7 @@ %%% Created : 9 March 2016 by Mickael Remond %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,7 @@ -module(elixir_logger_backend). -ifdef(ELIXIR_ENABLED). +-ifdef(LAGER). -behaviour(gen_event). @@ -123,3 +124,4 @@ severity_to_level(alert) -> error; severity_to_level(emergency) -> error. -endif. +-endif. diff --git a/src/ext_mod.erl b/src/ext_mod.erl index 5353019bb..eeb310207 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -5,7 +5,7 @@ %%% Created : 19 Feb 2015 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2006-2021 ProcessOne +%%% ejabberd, Copyright (C) 2006-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -36,13 +36,17 @@ config_dir/0, get_commands_spec/0]). -export([modules_configs/0, module_ebin_dir/1]). -export([compile_erlang_file/2, compile_elixir_file/2]). +-export([web_menu_node/3, web_page_node/5, get_page/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("ejabberd_commands.hrl"). +-include("ejabberd_web_admin.hrl"). -include("logger.hrl"). +-include("translate.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). -define(REPOS, "https://github.com/processone/ejabberd-contrib"). @@ -57,6 +61,8 @@ init([]) -> application:start(inets), inets:start(httpc, [{profile, ext_mod}]), ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50), {ok, #state{}}. add_paths() -> @@ -76,6 +82,8 @@ handle_info(Info, State) -> {noreply, State}. terminate(_Reason, _State) -> + ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50), ejabberd_commands:unregister_commands(get_commands_spec()). code_change(_OldVsn, State, _Extra) -> @@ -84,14 +92,14 @@ code_change(_OldVsn, State, _Extra) -> %% -- ejabberd commands get_commands_spec() -> [#ejabberd_commands{name = modules_update_specs, - tags = [admin,modules], + tags = [modules], desc = "Update the module source code from Git", longdesc = "A connection to Internet is required", module = ?MODULE, function = update, args = [], result = {res, rescode}}, #ejabberd_commands{name = modules_available, - tags = [admin,modules], + tags = [modules], desc = "List the contributed modules available to install", module = ?MODULE, function = available_command, result_desc = "List of tuples with module name and description", @@ -103,7 +111,7 @@ get_commands_spec() -> [{name, atom}, {summary, string}]}}}}}, #ejabberd_commands{name = modules_installed, - tags = [admin,modules], + tags = [modules], desc = "List the contributed modules already installed", module = ?MODULE, function = installed_command, result_desc = "List of tuples with module name and description", @@ -115,7 +123,7 @@ get_commands_spec() -> [{name, atom}, {summary, string}]}}}}}, #ejabberd_commands{name = module_install, - tags = [admin,modules], + tags = [modules], desc = "Compile, install and start an available contributed module", module = ?MODULE, function = install, args_desc = ["Module name"], @@ -123,7 +131,7 @@ get_commands_spec() -> args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_uninstall, - tags = [admin,modules], + tags = [modules], desc = "Uninstall a contributed module", module = ?MODULE, function = uninstall, args_desc = ["Module name"], @@ -131,7 +139,7 @@ get_commands_spec() -> args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_upgrade, - tags = [admin,modules], + tags = [modules], desc = "Upgrade the running code of an installed module", longdesc = "In practice, this uninstalls and installs the module", module = ?MODULE, function = upgrade, @@ -140,7 +148,7 @@ get_commands_spec() -> args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_check, - tags = [admin,modules], + tags = [modules], desc = "Check the contributed module repository compliance", module = ?MODULE, function = check, args_desc = ["Module name"], @@ -223,6 +231,7 @@ install(Package) when is_binary(Package) -> ok -> code:add_patha(module_ebin_dir(Module)), ejabberd_config:reload(), + copy_commit_json(Package, Attrs), case erlang:function_exported(Module, post_install, 0) of true -> Module:post_install(); _ -> ok @@ -330,7 +339,8 @@ geturl(Url) -> [U, Pass] -> [{proxy_auth, {U, Pass}}]; _ -> [] end, - case httpc:request(get, {Url, []}, User, [{body_format, binary}], ext_mod) of + UA = {"User-Agent", "ejabberd/ext_mod"}, + case httpc:request(get, {Url, [UA]}, User, [{body_format, binary}], ext_mod) of {ok, {{_, 200, _}, Headers, Response}} -> {ok, Headers, Response}; {ok, {{_, Code, _}, _Headers, Response}} -> @@ -380,7 +390,8 @@ extract_github_master(Repos, DestDir) -> case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of ok -> RepDir = filename:join(DestDir, module_name(Repos)), - file:rename(RepDir++"-master", RepDir); + file:rename(RepDir++"-master", RepDir), + maybe_write_commit_json(Url, RepDir); Error -> Error end. @@ -722,3 +733,429 @@ format({Key, Val}) when is_binary(Val) -> {Key, binary_to_list(Val)}; format({Key, Val}) -> % TODO: improve Yaml parsing {Key, Val}. + +%% -- COMMIT.json + +maybe_write_commit_json(Url, RepDir) -> + case (os:getenv("GITHUB_ACTIONS") == "true") of + true -> + ok; + false -> + write_commit_json(Url, RepDir) + end. + +write_commit_json(Url, RepDir) -> + Url2 = string_replace(Url, "https://github.com", "https://api.github.com/repos"), + BranchUrl = lists:flatten(Url2 ++ "/branches/master"), + {ok, _Headers, Body} = geturl(BranchUrl), + {ok, F} = file:open(filename:join(RepDir, "COMMIT.json"), [raw, write]), + file:write(F, Body), + file:close(F). + +find_commit_json(Attrs) -> + {_, FromPath} = lists:keyfind(path, 1, Attrs), + case {find_commit_json_path(FromPath), + find_commit_json_path(filename:join(FromPath, ".."))} + of + {{ok, FromFile}, _} -> + FromFile; + {_, {ok, FromFile}} -> + FromFile; + _ -> + not_found + end. + +-ifdef(HAVE_URI_STRING). %% Erlang/OTP 20 or higher can use this: +string_replace(Subject, Pattern, Replacement) -> + string:replace(Subject, Pattern, Replacement). + +find_commit_json_path(Path) -> + filelib:find_file("COMMIT.json", Path). +-else. % Workaround for Erlang/OTP older than 20: +string_replace(Subject, Pattern, Replacement) -> + B = binary:replace(list_to_binary(Subject), + list_to_binary(Pattern), + list_to_binary(Replacement)), + binary_to_list(B). + +find_commit_json_path(Path) -> + case filelib:wildcard("COMMIT.json", Path) of + [] -> + {error, commit_json_not_found}; + ["COMMIT.json"] = File -> + {ok, filename:join(Path, File)} + end. +-endif. + +copy_commit_json(Package, Attrs) -> + DestPath = module_lib_dir(Package), + case find_commit_json(Attrs) of + not_found -> + ok; + FromFile -> + file:copy(FromFile, filename:join(DestPath, "COMMIT.json")) + end. + +get_commit_details(Dirname) -> + RepDir = filename:join(sources_dir(), Dirname), + get_commit_details2(filename:join(RepDir, "COMMIT.json")). + +get_commit_details2(Path) -> + case file:read_file(Path) of + {ok, Body} -> + parse_details(Body); + _ -> + #{sha => unknown_sha, + date => <<>>, + message => <<>>, + html => <<>>, + author_name => <<>>, + commit_html_url => <<>>} + end. + +parse_details(Body) -> + {Contents} = jiffy:decode(Body), + + {_, {Commit}} = lists:keyfind(<<"commit">>, 1, Contents), + {_, Sha} = lists:keyfind(<<"sha">>, 1, Commit), + {_, CommitHtmlUrl} = lists:keyfind(<<"html_url">>, 1, Commit), + + {_, {Commit2}} = lists:keyfind(<<"commit">>, 1, Commit), + {_, Message} = lists:keyfind(<<"message">>, 1, Commit2), + {_, {Author}} = lists:keyfind(<<"author">>, 1, Commit2), + {_, AuthorName} = lists:keyfind(<<"name">>, 1, Author), + {_, {Committer}} = lists:keyfind(<<"committer">>, 1, Commit2), + {_, Date} = lists:keyfind(<<"date">>, 1, Committer), + + {_, {Links}} = lists:keyfind(<<"_links">>, 1, Contents), + {_, Html} = lists:keyfind(<<"html">>, 1, Links), + + #{sha => Sha, + date => Date, + message => Message, + html => Html, + author_name => AuthorName, + commit_html_url => CommitHtmlUrl}. + +%% -- Web Admin + +-define(AXC(URL, Attributes, Text), + ?XAE(<<"a">>, [{<<"href">>, URL} | Attributes], [?C(Text)]) + ). + +-define(INPUTCHECKED(Type, Name, Value), + ?XA(<<"input">>, + [{<<"type">>, Type}, + {<<"name">>, Name}, + {<<"disabled">>, <<"true">>}, + {<<"checked">>, <<"true">>}, + {<<"value">>, Value} + ] + ) + ). + +web_menu_node(Acc, _Node, Lang) -> + Acc ++ [{<<"contrib">>, translate:translate(Lang, ?T("Contrib Modules"))}]. + +web_page_node(_, Node, [<<"contrib">>], Query, Lang) -> + Res = rpc:call(Node, ?MODULE, get_page, [Node, Query, Lang]), + {stop, Res}; +web_page_node(Acc, _, _, _, _) -> + Acc. + +get_page(Node, Query, Lang) -> + QueryRes = list_modules_parse_query(Query), + Title = ?H1GL(translate:translate(Lang, ?T("Contrib Modules")), + <<"../../developer/extending-ejabberd/modules/#ejabberd-contrib">>, + <<"ejabberd-contrib">>), + Contents = get_content(Node, Query, Lang), + Result = case QueryRes of + ok -> [?XREST(?T("Submitted"))]; + nothing -> [] + end, + Title ++ Result ++ Contents. + +get_module_home(Module, Attrs) -> + case element(2, lists:keyfind(home, 1, Attrs)) of + "https://github.com/processone/ejabberd-contrib/tree/master/" = P1 -> + P1 ++ atom_to_list(Module); + Other -> + Other + end. + +get_module_summary(Attrs) -> + element(2, lists:keyfind(summary, 1, Attrs)). + +get_module_author(Attrs) -> + element(2, lists:keyfind(author, 1, Attrs)). + +get_installed_module_el({ModAtom, Attrs}, Lang) -> + Mod = misc:atom_to_binary(ModAtom), + Home = list_to_binary(get_module_home(ModAtom, Attrs)), + Summary = list_to_binary(get_module_summary(Attrs)), + Author = list_to_binary(get_module_author(Attrs)), + {_, FromPath} = lists:keyfind(path, 1, Attrs), + FromFile = case find_commit_json_path(FromPath) of + {ok, FF} -> FF; + {error, _} -> "dummypath" + end, + #{sha := CommitSha, + date := CommitDate, + message := CommitMessage, + author_name := CommitAuthorName, + commit_html_url := CommitHtmlUrl} = get_commit_details2(FromFile), + + [SourceSpec] = [S || {M, S} <- available(), M == ModAtom], + SourceFile = find_commit_json(SourceSpec), + #{sha := SourceSha, + date := SourceDate, + message := SourceMessage, + author_name := SourceAuthorName, + commit_html_url := SourceHtmlUrl} = get_commit_details2(SourceFile), + + UpgradeEls = + case CommitSha == SourceSha of + true -> + []; + false -> + SourceTitleEl = make_title_el(SourceDate, SourceMessage, SourceAuthorName), + [?XE(<<"td">>, + [?INPUT(<<"checkbox">>, <<"selected_upgrade">>, Mod), + ?C(<<" ">>), + ?AXC(SourceHtmlUrl, [SourceTitleEl], binary:part(SourceSha, {0, 8})) + ] + ) + ] + end, + + Started = + case gen_mod:is_loaded(hd(ejabberd_option:hosts()), ModAtom) of + false -> + [?C(<<" ">>)]; + true -> + [] + end, + TitleEl = make_title_el(CommitDate, CommitMessage, CommitAuthorName), + Status = case lists:member({mod_status, 0}, ModAtom:module_info(exports)) of + true -> + [?C(<<" ">>), + ?C(ModAtom:mod_status())]; + false -> [] + end, + HomeTitleEl = make_home_title_el(Summary, Author), + ?XE(<<"tr">>, + [?XE(<<"td">>, [?AXC(Home, [HomeTitleEl], Mod)]), + ?XE(<<"td">>, + [?INPUTTD(<<"checkbox">>, <<"selected_uninstall">>, Mod), + ?C(<<" ">>), + get_commit_link(CommitHtmlUrl, TitleEl, CommitSha), + ?C(<<" - ">>)] + ++ Started + ++ Status) + | UpgradeEls]). + +get_available_module_el({ModAtom, Attrs}) -> + Installed = installed(), + Mod = misc:atom_to_binary(ModAtom), + Home = list_to_binary(get_module_home(ModAtom, Attrs)), + Summary = list_to_binary(get_module_summary(Attrs)), + Author = list_to_binary(get_module_author(Attrs)), + HomeTitleEl = make_home_title_el(Summary, Author), + InstallCheckbox = + case lists:keymember(ModAtom, 1, Installed) of + false -> [?INPUT(<<"checkbox">>, <<"selected_install">>, Mod)]; + true -> [?INPUTCHECKED(<<"checkbox">>, <<"selected_install">>, Mod)] + end, + ?XE(<<"tr">>, + [?XE(<<"td">>, InstallCheckbox ++ [?C(<<" ">>), ?AXC(Home, [HomeTitleEl], Mod)]), + ?XE(<<"td">>, [?C(Summary)])]). + +get_installed_modules_table(Lang) -> + Modules = installed(), + Tail = [?XE(<<"tr">>, + [?XE(<<"td">>, []), + ?XE(<<"td">>, + [?INPUTTD(<<"submit">>, <<"uninstall">>, ?T("Uninstall"))] + ), + ?XE(<<"td">>, + [?INPUTT(<<"submit">>, <<"upgrade">>, ?T("Upgrade"))] + ) + ] + ) + ], + TBody = [get_installed_module_el(Module, Lang) || Module <- lists:sort(Modules)], + ?XAE(<<"table">>, + [], + [?XE(<<"tbody">>, TBody ++ Tail)] + ). + +get_available_modules_table(Lang) -> + Modules = get_available_notinstalled(), + Tail = [?XE(<<"tr">>, + [?XE(<<"td">>, + [?INPUTT(<<"submit">>, <<"install">>, ?T("Install"))] + ) + ] + ) + ], + TBody = [get_available_module_el(Module) || Module <- lists:sort(Modules)], + ?XAE(<<"table">>, + [], + [?XE(<<"tbody">>, TBody ++ Tail)] + ). + +make_title_el(Date, Message, AuthorName) -> + LinkTitle = <>, + {<<"title">>, LinkTitle}. + +make_home_title_el(Summary, Author) -> + LinkTitle = <>, + {<<"title">>, LinkTitle}. + +get_commit_link(_CommitHtmlUrl, _TitleErl, unknown_sha) -> + ?C(<<"Please Update Specs">>); +get_commit_link(CommitHtmlUrl, TitleEl, CommitSha) -> + ?AXC(CommitHtmlUrl, [TitleEl], binary:part(CommitSha, {0, 8})). + +get_content(Node, Query, Lang) -> + {{_CommandCtl}, _Res} = + case catch parse_and_execute(Query, Node) of + {'EXIT', _} -> {{""}, <<"">>}; + Result_tuple -> Result_tuple + end, + + AvailableModulesEls = get_available_modules_table(Lang), + InstalledModulesEls = get_installed_modules_table(Lang), + + Sources = get_sources_list(), + SourceEls = (?XAE(<<"table">>, + [], + [?XE(<<"tbody">>, + (lists:map( + fun(Dirname) -> + #{sha := CommitSha, + date := CommitDate, + message := CommitMessage, + html := Html, + author_name := AuthorName, + commit_html_url := CommitHtmlUrl + } = get_commit_details(Dirname), + TitleEl = make_title_el(CommitDate, CommitMessage, AuthorName), + ?XE(<<"tr">>, + [?XE(<<"td">>, [?AC(Html, Dirname)]), + ?XE(<<"td">>, + [get_commit_link(CommitHtmlUrl, TitleEl, CommitSha)] + ), + ?XE(<<"td">>, [?C(CommitMessage)]) + ]) + end, + lists:sort(Sources) + )) + ) + ] + )), + + [?XC(<<"p">>, + translate:translate( + Lang, ?T("Update specs to get modules source, then install desired ones.") + ) + ), + ?XAE(<<"form">>, + [{<<"method">>, <<"post">>}], + [?XCT(<<"h3">>, ?T("Sources Specs:")), + SourceEls, + ?BR, + ?INPUTT(<<"submit">>, + <<"updatespecs">>, + translate:translate(Lang, ?T("Update Specs"))), + + ?XCT(<<"h3">>, ?T("Installed Modules:")), + InstalledModulesEls, + ?BR, + + ?XCT(<<"h3">>, ?T("Other Modules Available:")), + AvailableModulesEls + ] + ) + ]. + +get_sources_list() -> + case file:list_dir(sources_dir()) of + {ok, Filenames} -> Filenames; + {error, enoent} -> [] + end. + +get_available_notinstalled() -> + Installed = installed(), + lists:filter( + fun({Mod, _}) -> + not lists:keymember(Mod, 1, Installed) + end, + available() + ). + +parse_and_execute(Query, Node) -> + {[Exec], _} = lists:partition( + fun(ExType) -> + lists:keymember(ExType, 1, Query) + end, + [<<"updatespecs">>] + ), + Commands = {get_val(<<"updatespecs">>, Query)}, + {_, R} = parse1_command(Exec, Commands, Node), + {Commands, R}. + +get_val(Val, Query) -> + {value, {_, R}} = lists:keysearch(Val, 1, Query), + binary_to_list(R). + +parse1_command(<<"updatespecs">>, {_}, _Node) -> + Res = update(), + {oook, io_lib:format("~p", [Res])}. + +list_modules_parse_query(Query) -> + case {lists:keysearch(<<"install">>, 1, Query), + lists:keysearch(<<"upgrade">>, 1, Query), + lists:keysearch(<<"uninstall">>, 1, Query)} + of + {{value, _}, _, _} -> list_modules_parse_install(Query); + {_, {value, _}, _} -> list_modules_parse_upgrade(Query); + {_, _, {value, _}} -> list_modules_parse_uninstall(Query); + _ -> nothing + end. + +list_modules_parse_install(Query) -> + lists:foreach( + fun({Mod, _}) -> + ModBin = misc:atom_to_binary(Mod), + case lists:member({<<"selected_install">>, ModBin}, Query) of + true -> install(Mod); + _ -> ok + end + end, + get_available_notinstalled()), + ok. + +list_modules_parse_upgrade(Query) -> + lists:foreach( + fun({Mod, _}) -> + ModBin = misc:atom_to_binary(Mod), + case lists:member({<<"selected_upgrade">>, ModBin}, Query) of + true -> upgrade(Mod); + _ -> ok + end + end, + installed()), + ok. + +list_modules_parse_uninstall(Query) -> + lists:foreach( + fun({Mod, _}) -> + ModBin = misc:atom_to_binary(Mod), + case lists:member({<<"selected_uninstall">>, ModBin}, Query) of + true -> uninstall(Mod); + _ -> ok + end + end, + installed()), + ok. diff --git a/src/extauth.erl b/src/extauth.erl index 6462df1cb..64c1b9ebb 100644 --- a/src/extauth.erl +++ b/src/extauth.erl @@ -2,7 +2,7 @@ %%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -99,10 +99,7 @@ worker_name(Pool, N) -> -spec pool_size(binary()) -> pos_integer(). pool_size(Host) -> case ejabberd_option:extauth_pool_size(Host) of - undefined -> - try erlang:system_info(logical_processors) - catch _:_ -> 1 - end; + undefined -> misc:logical_processors(); Size -> Size end. diff --git a/src/extauth_sup.erl b/src/extauth_sup.erl index 7537e8aef..c10bbf3bf 100644 --- a/src/extauth_sup.erl +++ b/src/extauth_sup.erl @@ -2,7 +2,7 @@ %%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/gen_iq_handler.erl b/src/gen_iq_handler.erl index 29057435d..313061ae0 100644 --- a/src/gen_iq_handler.erl +++ b/src/gen_iq_handler.erl @@ -5,7 +5,7 @@ %%% Created : 22 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/gen_mod.erl b/src/gen_mod.erl index 212afb4ff..1d9d26b40 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -5,7 +5,7 @@ %%% Created : 24 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl index 5bdebdfc6..df6c61944 100644 --- a/src/gen_pubsub_node.erl +++ b/src/gen_pubsub_node.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -122,12 +122,21 @@ {result, {default, broadcast}} | {error, stanza_error()}. +-callback remove_extra_items(NodeIdx :: nodeIdx(), + Max_Items :: unlimited | non_neg_integer()) -> + {result, {[itemId()], [itemId()]} + }. + -callback remove_extra_items(NodeIdx :: nodeIdx(), Max_Items :: unlimited | non_neg_integer(), ItemIds :: [itemId()]) -> {result, {[itemId()], [itemId()]} }. +-callback remove_expired_items(NodeIdx :: nodeIdx(), + Seconds :: infinity | non_neg_integer()) -> + {result, [itemId()]}. + -callback get_node_affiliations(NodeIdx :: nodeIdx()) -> {result, [{ljid(), affiliation()}]}. diff --git a/src/gen_pubsub_nodetree.erl b/src/gen_pubsub_nodetree.erl index 5a24db2c4..190051fd7 100644 --- a/src/gen_pubsub_nodetree.erl +++ b/src/gen_pubsub_nodetree.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -67,6 +67,9 @@ -callback get_nodes(Host :: host())-> [pubsubNode()]. +-callback get_all_nodes(Host :: host()) -> + [pubsubNode()]. + -callback get_parentnodes(Host :: host(), NodeId :: nodeId(), From :: jid:jid()) -> diff --git a/src/jd2ejd.erl b/src/jd2ejd.erl index 279dd6e0d..b7fd5f66c 100644 --- a/src/jd2ejd.erl +++ b/src/jd2ejd.erl @@ -5,7 +5,7 @@ %%% Created : 2 Feb 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/misc.erl b/src/misc.erl index 474fad615..232e0ea33 100644 --- a/src/misc.erl +++ b/src/misc.erl @@ -8,7 +8,7 @@ %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -42,7 +42,8 @@ is_mucsub_message/1, best_match/2, pmap/2, peach/2, format_exception/4, get_my_ipv4_address/0, get_my_ipv6_address/0, parse_ip_mask/1, crypto_hmac/3, crypto_hmac/4, uri_parse/1, - match_ip_mask/3, format_hosts_list/1, format_cycle/1, delete_dir/1]). + match_ip_mask/3, format_hosts_list/1, format_cycle/1, delete_dir/1, + logical_processors/0]). %% Deprecated functions -export([decode_base64/1, encode_base64/1]). @@ -55,23 +56,30 @@ -type distance_cache() :: #{{string(), string()} => non_neg_integer()}. +-spec uri_parse(binary()|string()) -> {ok, string(), string(), string(), number(), string(), string()} | {error, term()}. -ifdef(USE_OLD_HTTP_URI). uri_parse(URL) when is_binary(URL) -> uri_parse(binary_to_list(URL)); uri_parse(URL) -> - {ok, {Scheme, _UserInfo, Host, Port, Path, _Query}} = http_uri:parse(URL), - {ok, Scheme, Host, Port, Path}. + case http_uri:parse(URL) of + {ok, {Scheme, UserInfo, Host, Port, Path, Query}} -> + {ok, atom_to_list(Scheme), UserInfo, Host, Port, Path, Query}; + {error, _} = E -> + E + end. -else. uri_parse(URL) when is_binary(URL) -> uri_parse(binary_to_list(URL)); uri_parse(URL) -> case uri_string:parse(URL) of - #{scheme := Scheme, host := Host, port := Port, path := Path} -> - {ok, Scheme, Host, Port, Path}; - #{scheme := "https", host := Host, path := Path} -> - {ok, "https", Host, 443, Path}; - #{scheme := "http", host := Host, path := Path} -> - {ok, "http", Host, 80, Path} + #{scheme := Scheme, host := Host, port := Port, path := Path} = M1 -> + {ok, Scheme, maps:get(userinfo, M1, ""), Host, Port, Path, maps:get(query, M1, "")}; + #{scheme := "https", host := Host, path := Path} = M2 -> + {ok, "https", maps:get(userinfo, M2, ""), Host, 443, Path, maps:get(query, M2, "")}; + #{scheme := "http", host := Host, path := Path} = M3 -> + {ok, "http", maps:get(userinfo, M3, ""), Host, 80, Path, maps:get(query, M3, "")}; + {error, Atom, _} -> + {error, Atom} end. -endif. @@ -212,13 +220,13 @@ encode_base64(Data) -> -spec ip_to_list(inet:ip_address() | undefined | {inet:ip_address(), inet:port_number()}) -> binary(). -ip_to_list({local, _}) -> - <<"unix">>; ip_to_list({IP, _Port}) -> ip_to_list(IP); %% This function clause could use inet_parse too: ip_to_list(undefined) -> <<"unknown">>; +ip_to_list(local) -> + <<"unix">>; ip_to_list(IP) -> list_to_binary(inet_parse:ntoa(IP)). @@ -451,9 +459,16 @@ best_match(Pattern, Opts) -> end, #{}, Opts), element(2, lists:min(Ds)). +-spec logical_processors() -> non_neg_integer(). +logical_processors() -> + case erlang:system_info(logical_processors) of + V when is_integer(V), V >= 2 -> V; + _ -> 1 + end. + -spec pmap(fun((T1) -> T2), [T1]) -> [T2]. pmap(Fun, [_,_|_] = List) -> - case erlang:system_info(logical_processors) of + case logical_processors() of 1 -> lists:map(Fun, List); _ -> Self = self(), @@ -477,7 +492,7 @@ pmap(Fun, List) -> -spec peach(fun((T) -> any()), [T]) -> ok. peach(Fun, [_,_|_] = List) -> - case erlang:system_info(logical_processors) of + case logical_processors() of 1 -> lists:foreach(Fun, List); _ -> Self = self(), diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index 73c7d82af..1a2d63038 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -5,7 +5,7 @@ %%% Created : 15 Nov 2005 by Magnus Henoch %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 04cb39cfb..12e775cfb 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -5,7 +5,7 @@ %%% Created : 10 Aug 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -92,7 +92,7 @@ %%% start(_Host, _Opts) -> - ejabberd_commands:register_commands(get_commands_spec()). + ejabberd_commands:register_commands(?MODULE, get_commands_spec()). stop(Host) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of @@ -695,7 +695,7 @@ get_commands_spec() -> args_example = [<<"group3">>, <<"myserver.com">>], args_desc = ["Group identifier", "Group server name"], result_example = [{<<"name">>, "Group 3"}, {<<"displayed_groups">>, "group1"}], - result_desc = "List of group informations, as key and value", + result_desc = "List of group information, as key and value", result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}}, #ejabberd_commands{name = srg_get_members, tags = [shared_roster_group], desc = "Get members of a Shared Roster Group", @@ -1149,12 +1149,35 @@ set_vcard(User, Host, Name, SomeContent) -> set_vcard(User, Host, Name, Subname, SomeContent) -> set_vcard_content(User, Host, [Name, Subname], SomeContent). +%% +%% Room vcard + +is_muc_service(Domain) -> + try mod_muc_admin:get_room_serverhost(Domain) of + Domain -> false; + Service when is_binary(Service) -> true + catch _:{unregistered_route, _} -> + throw(error_wrong_hostname) + end. + +get_room_vcard(Name, Service) -> + case mod_muc_admin:get_room_options(Name, Service) of + [] -> + throw(error_no_vcard_found); + Opts -> + case lists:keyfind(<<"vcard">>, 1, Opts) of + false -> + throw(error_no_vcard_found); + {_, VCardRaw} -> + [fxml_stream:parse_element(VCardRaw)] + end + end. %% %% Internal vcard get_vcard_content(User, Server, Data) -> - case mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) of + case get_vcard_element(User, Server) of [El|_] -> case get_vcard(Data, El) of [false] -> throw(error_no_value_found_in_vcard); @@ -1166,6 +1189,14 @@ get_vcard_content(User, Server, Data) -> throw(database_failure) end. +get_vcard_element(User, Server) -> + case is_muc_service(Server) of + true -> + get_room_vcard(User, Server); + false -> + mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) + end. + get_vcard([<<"TEL">>, TelType], {_, _, _, OldEls}) -> {TakenEl, _NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), [TakenEl]; @@ -1302,16 +1333,15 @@ get_roster(User, Server) -> %% several times, each one in a different group. make_roster_xmlrpc(Roster) -> lists:foldl( - fun(Item, Res) -> - JIDS = jid:encode(Item#roster.jid), - Nick = Item#roster.name, - Subs = atom_to_list(Item#roster.subscription), - Ask = atom_to_list(Item#roster.ask), - Groups = case Item#roster.groups of + fun(#roster_item{jid = JID, name = Nick, subscription = Sub, ask = Ask} = Item, Res) -> + JIDS = jid:encode(JID), + Subs = atom_to_list(Sub), + Asks = atom_to_list(Ask), + Groups = case Item#roster_item.groups of [] -> [<<>>]; Gs -> Gs end, - ItemsX = [{JIDS, Nick, Subs, Ask, Group} || Group <- Groups], + ItemsX = [{JIDS, Nick, Subs, Asks, Group} || Group <- Groups], ItemsX ++ Res end, [], @@ -1379,7 +1409,7 @@ push_roster_item(LU, LS, R, U, S, Action) -> xmpp:set_from_to(ResIQ, jid:remove_resource(LJID), LJID)). build_roster_item(U, S, {add, Nick, Subs, Group}) -> - Groups = binary:split(Group,<<";">>, [global]), + Groups = binary:split(Group,<<";">>, [global, trim]), #roster_item{jid = jid:make(U, S), name = Nick, subscription = misc:binary_to_atom(Subs), @@ -1507,7 +1537,8 @@ srg_user_del(User, Host, Group, GroupHost) -> %%% %% @doc Send a message to an XMPP account. -%% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok +-spec send_message(Type::binary(), From::binary(), To::binary(), + Subject::binary(), Body::binary()) -> ok. send_message(Type, From, To, Subject, Body) -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode( @@ -1652,14 +1683,6 @@ mod_doc() -> ?T("If you want to put a group Name with blankspaces, use the " "characters \"\' and \'\" to define when the Name starts and " "ends. See an example below.")], - opts => - [{module_resource, - #{value => ?T("Resource"), - desc => - ?T("Indicate the resource that the XMPP stanzas must use " - "in the FROM or TO JIDs. This is only useful in the " - "'get_vcard*' and 'set_vcard*' commands. The default " - "value is 'mod_admin_extra'.")}}], example => [{?T("With this configuration, vCards can only be modified with " "mod_admin_extra commands:"), @@ -1670,8 +1693,7 @@ mod_doc() -> " vcard_set:", " - allow: adminextraresource", "modules:", - " mod_admin_extra:", - " module_resource: \"modadminextraf8x,31ad\"", + " mod_admin_extra: {}", " mod_vcard:", " access_set: vcard_set"]}, {?T("Content of roster file for 'pushroster' command:"), diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl index 0215a5172..3f17deefc 100644 --- a/src/mod_admin_update_sql.erl +++ b/src/mod_admin_update_sql.erl @@ -5,7 +5,7 @@ %%% Created : 9 Aug 2017 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -46,7 +46,7 @@ %%% start(_Host, _Opts) -> - ejabberd_commands:register_commands(get_commands_spec()). + ejabberd_commands:register_commands(?MODULE, get_commands_spec()). stop(_Host) -> ejabberd_commands:unregister_commands(get_commands_spec()). @@ -140,6 +140,7 @@ update_tables(State) -> add_sh_column(State, "sr_group"), add_pkey(State, "sr_group", ["server_host", "name"]), + create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]), drop_sh_default(State, "sr_group"), add_sh_column(State, "sr_user"), @@ -147,6 +148,7 @@ update_tables(State) -> drop_index(State, "i_sr_user_jid"), drop_index(State, "i_sr_user_grp"), add_pkey(State, "sr_user", ["server_host", "jid", "grp"]), + create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]), create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]), create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]), drop_sh_default(State, "sr_user"), @@ -162,10 +164,12 @@ update_tables(State) -> drop_index(State, "i_timestamp"), drop_index(State, "i_peer"), drop_index(State, "i_bare_peer"), + drop_index(State, "i_username_peer"), + drop_index(State, "i_username_bare_peer"), create_index(State, "archive", "i_archive_sh_username_timestamp", ["server_host", "username", "timestamp"]), create_index(State, "archive", "i_archive_sh_timestamp", ["server_host", "timestamp"]), - create_index(State, "archive", "i_archive_sh_peer", ["server_host", "peer"]), - create_index(State, "archive", "i_archive_sh_bare_peer", ["server_host", "bare_peer"]), + create_index(State, "archive", "i_archive_sh_username_peer", ["server_host", "username", "peer"]), + create_index(State, "archive", "i_archive_sh_username_bare_peer", ["server_host", "username", "bare_peer"]), drop_sh_default(State, "archive"), add_sh_column(State, "archive_prefs"), @@ -253,20 +257,30 @@ update_tables(State) -> create_index(State, "sm", "i_sm_sh_username", ["server_host", "username"]), drop_sh_default(State, "sm"), - add_sh_column(State, "carboncopy"), - drop_index(State, "i_carboncopy_ur"), - drop_index(State, "i_carboncopy_user"), - add_pkey(State, "carboncopy", ["server_host", "username", "resource"]), - create_index(State, "carboncopy", "i_carboncopy_sh_user", ["server_host", "username"]), - drop_sh_default(State, "carboncopy"), - add_sh_column(State, "push_session"), drop_index(State, "i_push_usn"), drop_index(State, "i_push_ut"), add_pkey(State, "push_session", ["server_host", "username", "timestamp"]), - create_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]), + create_unique_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]), drop_sh_default(State, "push_session"), + add_sh_column(State, "mix_pam"), + drop_index(State, "i_mix_pam"), + drop_index(State, "i_mix_pam_us"), + create_unique_index(State, "mix_pam", "i_mix_pam", ["username", "server_host", "channel", "service"]), + create_index(State, "mix_pam", "i_mix_pam_us", ["username", "server_host"]), + drop_sh_default(State, "mix_pam"), + + add_sh_column(State, "route"), + drop_index(State, "i_route"), + create_unique_index(State, "route", "i_route", ["domain", "server_host", "node", "pid"]), + drop_sh_default(State, "route"), + + add_sh_column(State, "mqtt_pub"), + drop_index(State, "i_mqtt_topic"), + create_unique_index(State, "mqtt_pub", "i_mqtt_topic_server", ["topic", "server_host"]), + drop_sh_default(State, "mqtt_pub"), + ok. add_sh_column(#state{dbtype = pgsql} = State, Table) -> @@ -364,6 +378,6 @@ mod_doc() -> #{desc => ?T("This module can be used to update existing SQL database " "from the default to the new schema. Check the section " - "http://../database-ldap/#default-and-new-schemas[Default and New Schemas] for details. " + "http://../database/#default-and-new-schemas[Default and New Schemas] for details. " "Please note that only PostgreSQL is supported. " - "When the module is loaded use 'update_sql' ejabberdctl command.")}. + "When the module is loaded use _`update_sql`_ API.")}. diff --git a/src/mod_announce.erl b/src/mod_announce.erl index e0501d96b..8698a8a3b 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -5,7 +5,7 @@ %%% Created : 11 Aug 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -930,7 +930,7 @@ mod_doc() -> "should be disabled for instances of ejabberd with hundreds of " "thousands users."), "", ?T("The Ad-hoc Commands are listed in the Server Discovery. " - "For this feature to work, 'mod_adhoc' must be enabled."), "", + "For this feature to work, _`mod_adhoc`_ must be enabled."), "", ?T("The specific JIDs where messages can be sent are listed below. " "The first JID in each entry will apply only to the specified " "virtual host example.org, while the JID between brackets " @@ -940,7 +940,7 @@ mod_doc() -> "online and connected to several resources, only the resource " "with the highest priority will receive the message. " "If the registered user is not connected, the message will be " - "stored offline in assumption that offline storage (see 'mod_offline') " + "stored offline in assumption that offline storage (see _`mod_offline`_) " "is enabled."), "- example.org/announce/online (example.org/announce/all-hosts/online)::", ?T("The message is sent to all connected users. If the user is " @@ -965,20 +965,20 @@ mod_doc() -> {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_announce_mnesia.erl b/src/mod_announce_mnesia.erl index 068406e01..d56d036ad 100644 --- a/src/mod_announce_mnesia.erl +++ b/src/mod_announce_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_announce_sql.erl b/src/mod_announce_sql.erl index bc79fc77a..5ef3a7900 100644 --- a/src/mod_announce_sql.erl +++ b/src/mod_announce_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_avatar.erl b/src/mod_avatar.erl index 3ad55f6d0..0db65c253 100644 --- a/src/mod_avatar.erl +++ b/src/mod_avatar.erl @@ -3,7 +3,7 @@ %%% Created : 13 Sep 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -469,13 +469,13 @@ mod_doc() -> "[XEP-0398: User Avatar to vCard-Based Avatars Conversion]."), "", ?T("Also, the module supports conversion between avatar " "image formats on the fly."), "", - ?T("The module depends on 'mod_vcard', 'mod_vcard_xupdate' and " - "'mod_pubsub'.")], + ?T("The module depends on _`mod_vcard`_, _`mod_vcard_xupdate`_ and " + "_`mod_pubsub`_.")], opts => [{convert, #{value => "{From: To}", desc => - ?T("Defines image convertion rules: the format in 'From' " + ?T("Defines image conversion rules: the format in 'From' " "will be converted to format in 'To'. The value of 'From' " "can also be 'default', which is match-all rule. NOTE: " "the list of supported formats is detected at compile time " @@ -489,4 +489,4 @@ mod_doc() -> desc => ?T("Limit any given JID by the number of avatars it is able " "to convert per minute. This is to protect the server from " - "image convertion DoS. The default value is '10'.")}}]}. + "image conversion DoS. The default value is '10'.")}}]}. diff --git a/src/mod_block_strangers.erl b/src/mod_block_strangers.erl index 92f52236d..437df2307 100644 --- a/src/mod_block_strangers.erl +++ b/src/mod_block_strangers.erl @@ -5,7 +5,7 @@ %%% Created : 25 Dec 2016 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 7d4b1bff5..06aa4b195 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -5,7 +5,7 @@ %%% Created : 24 Aug 2008 by Stephan Maka %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_bosh.erl b/src/mod_bosh.erl index 1770d27a7..8f2690d60 100644 --- a/src/mod_bosh.erl +++ b/src/mod_bosh.erl @@ -7,7 +7,7 @@ %%% Created : 20 Jul 2011 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -207,7 +207,7 @@ mod_doc() -> "It makes it possible to simulate long lived connections " "required by XMPP over the HTTP protocol. In practice, " "this module makes it possible to use XMPP in a browser without " - "Websocket support and more generally to have a way to use " + "WebSocket support and more generally to have a way to use " "XMPP while having to get through an HTTP proxy."), opts => [{json, @@ -240,27 +240,28 @@ mod_doc() -> {queue_type, #{value => "ram | file", desc => - ?T("Same as top-level 'queue_type' option, but applied to this module only.")}}, + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, {ram_db_type, #{value => "mnesia | sql | redis", desc => - ?T("Same as 'default_ram_db' but applied to this module only.")}}, + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => ["listen:", " -", diff --git a/src/mod_bosh_mnesia.erl b/src/mod_bosh_mnesia.erl index 3f9f32345..01a64d67e 100644 --- a/src/mod_bosh_mnesia.erl +++ b/src/mod_bosh_mnesia.erl @@ -2,7 +2,7 @@ %%% Created : 12 Jan 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_bosh_redis.erl b/src/mod_bosh_redis.erl index c288f6c06..12c0a925d 100644 --- a/src/mod_bosh_redis.erl +++ b/src/mod_bosh_redis.erl @@ -5,7 +5,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_bosh_sql.erl b/src/mod_bosh_sql.erl index 7dba8fc4b..80369facc 100644 --- a/src/mod_bosh_sql.erl +++ b/src/mod_bosh_sql.erl @@ -5,7 +5,7 @@ %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 92869fc7d..3c8b16b0b 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -5,7 +5,7 @@ %%% Created : 7 Oct 2006 by Magnus Henoch %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -49,7 +49,8 @@ handle_cast/2, terminate/2, code_change/3]). -export([user_send_packet/1, user_receive_packet/1, - c2s_presence_in/2, mod_opt_type/1, mod_options/1, mod_doc/0]). + c2s_presence_in/2, c2s_copy_session/2, + mod_opt_type/1, mod_options/1, mod_doc/0]). -include("logger.hrl"). @@ -274,6 +275,13 @@ c2s_presence_in(C2SState, C2SState#{caps_resources => NewRs} end. +-spec c2s_copy_session(ejabberd_c2s:state(), ejabberd_c2s:state()) + -> ejabberd_c2s:state(). +c2s_copy_session(C2SState, #{caps_resources := Rs}) -> + C2SState#{caps_resources => Rs}; +c2s_copy_session(C2SState, _) -> + C2SState. + -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. @@ -304,6 +312,8 @@ init([Host|_]) -> caps_stream_features, 75), ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE, caps_stream_features, 75), + ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 75), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 75), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, @@ -341,6 +351,8 @@ terminate(_Reason, State) -> ?MODULE, caps_stream_features, 75), ejabberd_hooks:delete(s2s_in_post_auth_features, Host, ?MODULE, caps_stream_features, 75), + ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, + c2s_copy_session, 75), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 75), ejabberd_hooks:delete(disco_local_identity, Host, @@ -595,25 +607,25 @@ mod_doc() -> "https://xmpp.org/extensions/xep-0115.html" "[XEP-0115: Entity Capabilities]."), ?T("The main purpose of the module is to provide " - "PEP functionality (see 'mod_pubsub').")], + "PEP functionality (see _`mod_pubsub`_).")], opts => [{db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_caps_mnesia.erl b/src/mod_caps_mnesia.erl index 965c3dbfd..db3ac8351 100644 --- a/src/mod_caps_mnesia.erl +++ b/src/mod_caps_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_caps_sql.erl b/src/mod_caps_sql.erl index 7fb3cf8fa..7ab39b489 100644 --- a/src/mod_caps_sql.erl +++ b/src/mod_caps_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index a4e3a7999..3013ab749 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -7,7 +7,7 @@ %%% {mod_carboncopy, []} %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index 3585ec3a4..14a49fdba 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -5,7 +5,7 @@ %%% Created : 11 Sep 2014 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2014-2021 ProcessOne +%%% ejabberd, Copyright (C) 2014-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 99a20ef3a..385c6d657 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -5,7 +5,7 @@ %%% Created : 19 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -171,9 +171,14 @@ get_local_identity(Acc, _From, _To, Node, Lang) -> ?INFO_COMMAND(?T("Get User Last Login Time"), Lang); ?NS_ADMINL(<<"user-stats">>) -> ?INFO_COMMAND(?T("Get User Statistics"), Lang); + ?NS_ADMINL(<<"get-registered-users-list">>) -> + ?INFO_COMMAND(?T("Get List of Registered Users"), + Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?INFO_COMMAND(?T("Get Number of Registered Users"), Lang); + ?NS_ADMINL(<<"get-online-users-list">>) -> + ?INFO_COMMAND(?T("Get List of Online Users"), Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> ?INFO_COMMAND(?T("Get Number of Online Users"), Lang); _ -> Acc @@ -252,8 +257,12 @@ get_local_features(Acc, From, ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"user-stats">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); + ?NS_ADMINL(<<"get-registered-users-list">>) -> + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); + ?NS_ADMINL(<<"get-online-users-list">>) -> + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc @@ -476,8 +485,12 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"user-stats">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); + ?NS_ADMINL(<<"get-registered-users-list">>) -> + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); + ?NS_ADMINL(<<"get-online-users-list">>) -> + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-online-users-num">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); _ -> Acc @@ -515,8 +528,12 @@ get_local_items(_Host, [<<"user">>], Server, Lang) -> (?NS_ADMINX(<<"get-user-lastlogin">>))), ?NODE(?T("Get User Statistics"), (?NS_ADMINX(<<"user-stats">>))), + ?NODE(?T("Get List of Registered Users"), + (?NS_ADMINX(<<"get-registered-users-list">>))), ?NODE(?T("Get Number of Registered Users"), (?NS_ADMINX(<<"get-registered-users-num">>))), + ?NODE(?T("Get List of Online Users"), + (?NS_ADMINX(<<"get-online-users-list">>))), ?NODE(?T("Get Number of Online Users"), (?NS_ADMINX(<<"get-online-users-num">>)))]}; get_local_items(_Host, [<<"http:">> | _], _Server, @@ -1065,6 +1082,16 @@ get_form(_Host, ?NS_ADMINL(<<"user-stats">>), Lang) -> label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; +get_form(Host, ?NS_ADMINL(<<"get-registered-users-list">>), Lang) -> + Values = [jid:encode(jid:make(U, Host)) + || {U, _} <- ejabberd_auth:get_users(Host)], + {result, completed, + #xdata{type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-multi', + label = tr(Lang, ?T("The list of all users")), + var = <<"registereduserjids">>, + values = Values}]}}; get_form(Host, ?NS_ADMINL(<<"get-registered-users-num">>), Lang) -> Num = integer_to_binary(ejabberd_auth:count_users(Host)), @@ -1075,6 +1102,17 @@ get_form(Host, label = tr(Lang, ?T("Number of registered users")), var = <<"registeredusersnum">>, values = [Num]}]}}; +get_form(Host, ?NS_ADMINL(<<"get-online-users-list">>), Lang) -> + Accounts = [jid:encode(jid:make(U, Host)) + || {U, _, _} <- ejabberd_sm:get_vh_session_list(Host)], + Values = lists:usort(Accounts), + {result, completed, + #xdata{type = form, + fields = [?HFIELD(), + #xdata_field{type = 'jid-multi', + label = tr(Lang, ?T("The list of all online users")), + var = <<"onlineuserjids">>, + values = Values}]}}; get_form(Host, ?NS_ADMINL(<<"get-online-users-num">>), Lang) -> Num = integer_to_binary(ejabberd_sm:get_vh_session_number(Host)), @@ -1564,4 +1602,4 @@ mod_doc() -> ?T("The module provides server configuration functionality via " "https://xmpp.org/extensions/xep-0050.html" "[XEP-0050: Ad-Hoc Commands]. This module requires " - "'mod_adhoc' to be loaded.")}. + "_`mod_adhoc`_ to be loaded.")}. diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl new file mode 100644 index 000000000..61cec2322 --- /dev/null +++ b/src/mod_conversejs.erl @@ -0,0 +1,357 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_conversejs.erl +%%% Author : Alexey Shchepin +%%% Purpose : Serve simple page for Converse.js XMPP web browser client +%%% Created : 8 Nov 2021 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_conversejs). + +-author('alexey@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, process/2, depends/2, + mod_opt_type/1, mod_options/1, mod_doc/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_http.hrl"). +-include("translate.hrl"). +-include("ejabberd_web_admin.hrl"). + +start(_Host, _Opts) -> + ok. + +stop(_Host) -> + ok. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +process([], #request{method = 'GET', host = Host, raw_path = RawPath}) -> + ExtraOptions = get_auth_options(Host) + ++ get_register_options(Host) + ++ get_extra_options(Host), + DomainRaw = gen_mod:get_module_opt(Host, ?MODULE, default_domain), + Domain = misc:expand_keyword(<<"@HOST@">>, DomainRaw, Host), + Script = get_file_url(Host, conversejs_script, + <>, + <<"https://cdn.conversejs.org/dist/converse.min.js">>), + CSS = get_file_url(Host, conversejs_css, + <>, + <<"https://cdn.conversejs.org/dist/converse.min.css">>), + Init = [{<<"discover_connection_methods">>, false}, + {<<"default_domain">>, Domain}, + {<<"domain_placeholder">>, Domain}, + {<<"registration_domain">>, Domain}, + {<<"assets_path">>, RawPath}, + {<<"i18n">>, ejabberd_option:language(Host)}, + {<<"view_mode">>, <<"fullscreen">>} + | ExtraOptions], + Init2 = + case mod_host_meta:get_url(?MODULE, websocket, any, Host) of + undefined -> Init; + WSURL -> [{<<"websocket_url">>, WSURL} | Init] + end, + Init3 = + case mod_host_meta:get_url(?MODULE, bosh, any, Host) of + undefined -> Init2; + BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2] + end, + {200, [html], + [<<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>]}; +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; +is_served_file([<<"converse.min.css.map">>]) -> true; +is_served_file([<<"emojis.js">>]) -> true; +is_served_file([<<"locales">>, _]) -> true; +is_served_file([<<"locales">>, <<"dayjs">>, _]) -> true; +is_served_file([<<"webfonts">>, _]) -> true; +is_served_file(_) -> false. + +serve(Host, LocalPath) -> + case get_conversejs_resources(Host) of + undefined -> ejabberd_web:error(not_found); + 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], + MainPath = binary_to_list(MainPathBin), + FileName = filename:join(filename:split(MainPath) ++ LocalPath), + case file:read_file(FileName) of + {ok, FileContents} -> + ?DEBUG("Delivering content.", []), + {200, + [{<<"Content-Type">>, content_type(FileName)}], + FileContents}; + {error, eisdir} -> + {403, [], "Forbidden"}; + {error, Error} -> + ?DEBUG("Delivering error: ~p", [Error]), + case Error of + eacces -> {403, [], "Forbidden"}; + enoent -> {404, [], "Not found"}; + _Else -> {404, [], atom_to_list(Error)} + 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" + 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 + {false, false} -> + [{<<"authentication">>, <<"login">>}]; + {true, false} -> + [{<<"authentication">>, <<"external">>}]; + {_, true} -> + [{<<"authentication">>, <<"anonymous">>}, + {<<"jid">>, Domain}] + end. + +get_register_options(Server) -> + AuthSupportsRegister = + lists:any( + fun(ejabberd_auth_mnesia) -> true; + (ejabberd_auth_external) -> true; + (ejabberd_auth_sql) -> true; + (_) -> false + end, + ejabberd_auth:auth_modules(Server)), + Modules = get_register_modules(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) + 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]), + [] + end. + +get_extra_options(Host) -> + RawOpts = gen_mod:get_module_opt(Host, ?MODULE, conversejs_options), + lists:map(fun({Name, <<"true">>}) -> {Name, true}; + ({Name, <<"false">>}) -> {Name, false}; + ({<<"locked_domain">> = Name, Value}) -> + {Name, misc:expand_keyword(<<"@HOST@">>, Value, Host)}; + ({Name, Value}) -> + {Name, Value} + 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); + F -> F + end, + misc:expand_keyword(<<"@HOST@">>, FileRaw, Host). + +get_auto_file_url(Host, Filename, Default) -> + case get_conversejs_resources(Host) of + undefined -> Default; + _ -> Filename + end. + +%%---------------------------------------------------------------------- +%% +%%---------------------------------------------------------------------- + +mod_opt_type(bosh_service_url) -> + econf:either(auto, econf:binary()); +mod_opt_type(websocket_url) -> + econf:either(auto, econf:binary()); +mod_opt_type(conversejs_resources) -> + econf:either(undefined, econf:directory()); +mod_opt_type(conversejs_options) -> + econf:map(econf:binary(), econf:either(econf:binary(), econf:int())); +mod_opt_type(conversejs_script) -> + econf:binary(); +mod_opt_type(conversejs_css) -> + econf:binary(); +mod_opt_type(default_domain) -> + econf:binary(). + +mod_options(_) -> + [{bosh_service_url, auto}, + {websocket_url, auto}, + {default_domain, <<"@HOST@">>}, + {conversejs_resources, undefined}, + {conversejs_options, []}, + {conversejs_script, auto}, + {conversejs_css, auto}]. + +mod_doc() -> + #{desc => + [?T("This module serves a simple page for the " + "https://conversejs.org/[Converse] XMPP web browser client."), "", + ?T("This module is available since ejabberd 21.12."), + ?T("Several options were improved in ejabberd 22.05."), "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "http://../listen-options/#request-handlers[request_handlers]."), "", + ?T("Make sure either 'mod_bosh' or 'ejabberd_http_ws' " + "http://../listen-options/#request-handlers[request_handlers] " + "are enabled."), "", + ?T("When 'conversejs_css' and 'conversejs_script' are 'auto', " + "by default they point to the public Converse client.") + ], + example => + [{?T("Manually setup WebSocket url, and use the public Converse client:"), + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /bosh: mod_bosh", + " /websocket: ejabberd_http_ws", + " /conversejs: mod_conversejs", + "", + "modules:", + " mod_bosh: {}", + " mod_conversejs:", + " websocket_url: \"ws://@HOST@:5280/websocket\""]}, + {?T("Host Converse locally and let auto detection of WebSocket and Converse URLs:"), + ["listen:", + " -", + " port: 443", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " /websocket: ejabberd_http_ws", + " /conversejs: mod_conversejs", + "", + "modules:", + " mod_conversejs:", + " conversejs_resources: \"/home/ejabberd/conversejs-9.0.0/package/dist\""]}, + {?T("Configure some additional options for Converse"), + ["modules:", + " mod_conversejs:", + " websocket_url: auto", + " conversejs_options:", + " auto_away: 30", + " clear_cache_on_logout: true", + " i18n: \"pt\"", + " locked_domain: \"@HOST@\"", + " message_archiving: always", + " theme: dracula"]} + ], + opts => + [{websocket_url, + #{value => ?T("auto | WebSocketURL"), + desc => + ?T("A WebSocket 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 WebSocket request handler. " + "The default value is 'auto'.")}}, + {bosh_service_url, + #{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'.")}}, + {default_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@'.")}}, + {conversejs_resources, + #{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.")}}, + {conversejs_options, + #{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.")}}, + {conversejs_script, + #{value => ?T("auto | URL"), + desc => + ?T("Converse main script URL. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is 'auto'.")}}, + {conversejs_css, + #{value => ?T("auto | URL"), + desc => + ?T("Converse CSS URL. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is 'auto'.")}}] + }. diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl new file mode 100644 index 000000000..c8132bfab --- /dev/null +++ b/src/mod_conversejs_opt.erl @@ -0,0 +1,55 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_conversejs_opt). + +-export([bosh_service_url/1]). +-export([conversejs_css/1]). +-export([conversejs_options/1]). +-export([conversejs_resources/1]). +-export([conversejs_script/1]). +-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()}]. +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_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 a588e81ef..19a1adcc6 100644 --- a/src/mod_delegation.erl +++ b/src/mod_delegation.erl @@ -4,7 +4,7 @@ %%% Purpose : XEP-0355: Namespace Delegation %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -95,7 +95,7 @@ mod_doc() -> ?T("WARNING: Security issue: Namespace delegation gives components " "access to sensitive data, so permission should be granted " "carefully, only if you trust the component."), "", - ?T("NOTE: This module is complementary to 'mod_privilege' but can " + ?T("NOTE: This module is complementary to _`mod_privilege`_ but can " "also be used separately.")], opts => [{namespaces, diff --git a/src/mod_disco.erl b/src/mod_disco.erl index cf888bc4e..8a74d85df 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -5,7 +5,7 @@ %%% Created : 1 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -464,11 +464,11 @@ mod_doc() -> " -", " modules: all", " name: abuse-addresses", - " urls: [mailto:abuse@shakespeare.lit]", + " urls: [\"mailto:abuse@shakespeare.lit\"]", " -", " modules: [mod_muc]", " name: \"Web chatroom logs\"", - " urls: [http://www.example.org/muc-logs]", + " urls: [\"http://www.example.org/muc-logs\"]", " -", " modules: [mod_disco]", " name: feedback-addresses", diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index 99ff3d127..d7b2963f4 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -5,7 +5,7 @@ %%% Created : 15 Aug 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2021 ProcessOne +%%% ejabberd, Copyright (C) 2014-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -107,7 +107,7 @@ c2s_stream_started(#{ip := {Addr, _}} = State, _) -> start(Host, Opts) -> catch ets:new(failed_auth, [named_table, public, {heir, erlang:group_leader(), none}]), - ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> diff --git a/src/mod_host_meta.erl b/src/mod_host_meta.erl new file mode 100644 index 000000000..abbca332a --- /dev/null +++ b/src/mod_host_meta.erl @@ -0,0 +1,256 @@ +%%%------------------------------------------------------------------- +%%% File : mod_host_meta.erl +%%% Author : Badlop +%%% Purpose : Serve host-meta files as described in XEP-0156 +%%% Created : 25 March 2022 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2022 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_host_meta). + +-author('badlop@process-one.net'). + +-protocol({xep, 156, '1.4.0'}). + +-behaviour(gen_mod). + +-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]). + +-include("logger.hrl"). + +-include_lib("xmpp/include/xmpp.hrl"). + +-include("ejabberd_http.hrl"). + +-include("ejabberd_web_admin.hrl"). + +-include("translate.hrl"). + +%%%---------------------------------------------------------------------- +%%% 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">> -> + file_xml(Host); + <<"host-meta.json">> -> + file_json(Host) + end; +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}] + )] + end, + WsList = case get_url(?MODULE, websocket, true, Host) of + undefined -> []; + WsUrl -> + [?XA(<<"Link">>, + [{<<"rel">>, <<"urn:xmpp:alt-connections:websocket">>}, + {<<"href">>, WsUrl}] + )] + end, + {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) + )]}. + +file_json(Host) -> + BoshList = case get_url(?MODULE, bosh, true, Host) of + undefined -> []; + 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}] + end, + {200, [html, + {<<"Content-Type">>, <<"application/json">>}, + {<<"Access-Control-Allow-Origin">>, <<"*">>}], + [jiffy: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; + auto -> get_auto_url(Tls, Module); + <<"auto">> -> get_auto_url(Tls, Module); + U when is_binary(U) -> U + end. + +get_auto_url(Tls, Module) -> + case find_handler_port_path(Tls, Module) of + [] -> undefined; + [{ThisTls, Port, Path} | _] -> + Protocol = case {ThisTls, Module} of + {false, mod_bosh} -> <<"http">>; + {true, mod_bosh} -> <<"https">>; + {false, ejabberd_http_ws} -> <<"ws">>; + {true, ejabberd_http_ws} -> <<"wss">> + end, + <>))/binary>> + end. + +find_handler_port_path(Tls, Module) -> + 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)). + +report_hostmeta_listener() -> + case {find_handler_port_path(false, ?MODULE), + find_handler_port_path(true, ?MODULE)} of + {[], []} -> + ?CRITICAL_MSG("It seems you enabled ~p in 'modules' but forgot to " + "add it as a request_handler in an ejabberd_http " + "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 => + [?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]."), "", + ?T("This module is available since ejabberd 22.05."), "", + ?T("To use this module, in addition to adding it to the 'modules' " + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "http://../listen-options/#request-handlers[request_handlers]."), "", + ?T("Notice it only works if ejabberd_http has tls enabled.")], + example => + ["listen:", + " -", + " port: 443", + " module: ejabberd_http", + " tls: true", + " request_handlers:", + " /bosh: mod_bosh", + " /ws: ejabberd_http_ws", + " /.well-known/host-meta: mod_host_meta", + " /.well-known/host-meta.json: mod_host_meta", + "", + "modules:", + " mod_bosh: {}", + " mod_host_meta:", + " bosh_service_url: \"https://@HOST@:5443/bosh\"", + " websocket_url: \"wss://@HOST@:5443/ws\""], + + opts => + [{websocket_url, + #{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'.")}}, + {bosh_service_url, + #{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'.")}}] + }. diff --git a/src/mod_host_meta_opt.erl b/src/mod_host_meta_opt.erl new file mode 100644 index 000000000..965e95cf8 --- /dev/null +++ b/src/mod_host_meta_opt.erl @@ -0,0 +1,20 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_host_meta_opt). + +-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 0a3942024..8bd9522ce 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -5,7 +5,7 @@ %%% Created : 15 Sep 2014 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,6 +30,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, + format_arg/2, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). @@ -524,13 +525,24 @@ mod_options(_) -> mod_doc() -> #{desc => - [?T("This module provides a ReST API to call ejabberd commands " - "using JSON data."), "", + [?T("This module provides a ReST interface to call " + "https://docs.ejabberd.im/developer/ejabberd-api[ejabberd API] " + "commands using JSON data."), "", ?T("To use this module, in addition to adding it to the 'modules' " - "section, you must also add it to 'request_handlers' of some " - "listener."), "", + "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " + "http://../listen-options/#request-handlers[request_handlers]."), "", ?T("To use a specific API version N, when defining the URL path " "in the request_handlers, add a 'vN'. " "For example: '/api/v2: mod_http_api'"), "", ?T("To run a command, send a POST request to the corresponding " - "URL: 'http://localhost:5280/api/'")]}. + "URL: 'http://localhost:5280/api/'")], + example => + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /api: mod_http_api", + "", + "modules:", + " mod_http_api: {}"]}. diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index a5c6ebbfd..9177552a3 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -5,7 +5,7 @@ %%% Created : %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -151,7 +151,9 @@ initialize(Host, Opts) -> content_types = ContentTypes, user_access = UserAccess}. -%% @spec (AdminCTs::[CT], Default::[CT]) -> [CT] +-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. @@ -265,7 +267,8 @@ code_change(_OldVsn, State, _Extra) -> %% request_handlers callbacks %%==================================================================== -%% @spec (LocalPath, Request) -> {HTTPCode::integer(), [Header], 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. diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index 0cc8d8488..bda66253e 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -5,7 +5,7 @@ %%% Created : 20 Aug 2015 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2015-2021 ProcessOne +%%% ejabberd, Copyright (C) 2015-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -232,8 +232,9 @@ mod_doc() -> "[XEP-0363: HTTP File Upload]. If the request is accepted, " "the client receives a URL for uploading the file and " "another URL from which that file can later be downloaded."), "", - ?T("In order to use this module, it must be configured as " - "a 'request_handler' for 'ejabberd_http' listener.")], + ?T("In order to use this module, it must be enabled " + "in 'listen' -> 'ejabberd_http' -> " + "http://../listen-options/#request-handlers[request_handlers].")], opts => [{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, @@ -320,17 +321,18 @@ mod_doc() -> "used for file uploads. The keyword @HOST@ is replaced " "with the virtual host name. NOTE: different virtual " "hosts cannot use the same PUT URL. " - "The default value is \"https://@HOST@:5443\".")}}, + "The default value is \"https://@HOST@:5443/upload\".")}}, {get_url, #{value => ?T("URL"), desc => ?T("This option specifies the initial part of the GET URLs " - "used for downloading the files. By default, it is set " + "used for downloading the files. The default value is 'undefined'. " + "When this option is 'undefined', this option is set " "to the same value as 'put_url'. The keyword @HOST@ is " "replaced with the virtual host name. NOTE: if GET requests " "are handled by 'mod_http_upload', the 'get_url' must match the " "'put_url'. Setting it to a different value only makes " - "sense if an external web server or 'mod_http_fileserver' " + "sense if an external web server or _`mod_http_fileserver`_ " "is used to serve the uploaded files.")}}, {service_url, #{desc => ?T("Deprecated.")}}, @@ -711,7 +713,7 @@ get_proc_name(ServerHost, ModuleName) -> -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. - {ok, _Scheme, Host0, _Port, Path0} = + {ok, _Scheme, _UserInfo, Host0, _Port, Path0, _Query} = misc:uri_parse(expand_host(PutURL, ServerHost)), Host = jid:nameprep(iolist_to_binary(Host0)), Path = str:strip(iolist_to_binary(Path0), right, $/), diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl index 48b7b1958..2d8a0b0de 100644 --- a/src/mod_http_upload_quota.erl +++ b/src/mod_http_upload_quota.erl @@ -5,7 +5,7 @@ %%% Created : 15 Oct 2015 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2015-2021 ProcessOne +%%% ejabberd, Copyright (C) 2015-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,7 +27,6 @@ -author('holger@zedat.fu-berlin.de'). -define(TIMEOUT, timer:hours(24)). --define(INITIAL_TIMEOUT, timer:minutes(10)). -define(FORMAT(Error), file:format_error(Error)). -behaviour(gen_server). @@ -64,7 +63,7 @@ max_days :: pos_integer() | infinity, docroot :: binary(), disk_usage = #{} :: disk_usage(), - timers :: [timer:tref()]}). + timer :: reference() | undefined}). -type disk_usage() :: #{{binary(), binary()} => non_neg_integer()}. -type state() :: #state{}. @@ -166,12 +165,11 @@ init([ServerHost|_]) -> DocRoot1 = mod_http_upload_opt:docroot(ServerHost), DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)), DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost), - Timers = if MaxDays == infinity -> []; - true -> - {ok, T1} = timer:send_after(?INITIAL_TIMEOUT, sweep), - {ok, T2} = timer:send_interval(?TIMEOUT, sweep), - [T1, T2] - end, + Timer = if MaxDays == infinity -> undefined; + true -> + Timeout = p1_rand:uniform(?TIMEOUT div 2), + erlang:send_after(Timeout, self(), sweep) + end, ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE, handle_slot_request, 50), {ok, #state{server_host = ServerHost, @@ -179,7 +177,7 @@ init([ServerHost|_]) -> access_hard_quota = AccessHardQuota, max_days = MaxDays, docroot = DocRoot3, - timers = Timers}}. + timer = Timer}}. -spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. handle_call(Request, From, State) -> @@ -249,6 +247,7 @@ handle_info(sweep, #state{server_host = ServerHost, max_days = MaxDays} = State) when is_integer(MaxDays), MaxDays > 0 -> ?DEBUG("Got 'sweep' message for ~ts", [ServerHost]), + Timer = erlang:send_after(?TIMEOUT, self(), sweep), case file:list_dir(DocRoot) of {ok, Entries} -> BackThen = secs_since_epoch() - (MaxDays * 86400), @@ -264,17 +263,17 @@ handle_info(sweep, #state{server_host = ServerHost, ?ERROR_MSG("Cannot open document root ~ts: ~ts", [DocRoot, ?FORMAT(Error)]) end, - {noreply, State}; + {noreply, State#state{timer = Timer}}; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. -terminate(Reason, #state{server_host = ServerHost, timers = Timers}) -> +terminate(Reason, #state{server_host = ServerHost, timer = Timer}) -> ?DEBUG("Stopping upload quota process for ~ts: ~p", [ServerHost, Reason]), ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, handle_slot_request, 50), - lists:foreach(fun timer:cancel/1, Timers). + misc:cancel_timer(Timer). -spec code_change({down, _} | _, state(), _) -> {ok, state()}. code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> diff --git a/src/mod_jidprep.erl b/src/mod_jidprep.erl index c666f8544..605ca53b1 100644 --- a/src/mod_jidprep.erl +++ b/src/mod_jidprep.erl @@ -5,7 +5,7 @@ %%% Created : 11 Sep 2019 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2019-2021 ProcessOne +%%% ejabberd, Copyright (C) 2019-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_last.erl b/src/mod_last.erl index 295a546f2..c13e4d22f 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -5,7 +5,7 @@ %%% Created : 24 Oct 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -344,20 +344,20 @@ mod_doc() -> [{db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_last_mnesia.erl b/src/mod_last_mnesia.erl index a2549dfcf..c081ba039 100644 --- a/src/mod_last_mnesia.erl +++ b/src/mod_last_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_last_sql.erl b/src/mod_last_sql.erl index 07b9a62b3..121c96045 100644 --- a/src/mod_last_sql.erl +++ b/src/mod_last_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_legacy_auth.erl b/src/mod_legacy_auth.erl index 2b29b4cd2..0053d88a6 100644 --- a/src/mod_legacy_auth.erl +++ b/src/mod_legacy_auth.erl @@ -2,7 +2,7 @@ %%% Created : 11 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 12542bfa5..7d9e308f5 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -5,7 +5,7 @@ %%% Created : 4 Jul 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2013-2021 ProcessOne +%%% ejabberd, Copyright (C) 2013-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,6 +28,7 @@ -protocol({xep, 313, '0.6.1'}). -protocol({xep, 334, '0.2'}). -protocol({xep, 359, '0.5.0'}). +-protocol({xep, 441, '0.2.0'}). -behaviour(gen_mod). @@ -42,7 +43,8 @@ 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]). + process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2, select/7, + delete_old_messages_batch/5, delete_old_messages_status/1, delete_old_messages_abort/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). @@ -87,7 +89,18 @@ {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | {error, db_failure}. --optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/6, select/6, select/7]). +-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()}. + +-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 @@ -148,7 +161,7 @@ start(Host, Opts) -> ejabberd_hooks:add(check_create_room, Host, ?MODULE, check_create_room, 50) end, - ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), ok; Err -> Err @@ -567,6 +580,70 @@ message_is_archived(false, #{lserver := LServer}, Pkt) -> false end. +delete_old_messages_batch(Server, Type, Days, BatchSize, Rate) when Type == <<"chat">>; + Type == <<"groupchat">>; + Type == <<"all">> -> + CurrentTime = make_id(), + Diff = Days * 24 * 60 * 60 * 1000000, + TimeStamp = misc:usec_to_now(CurrentTime - Diff), + TypeA = misc:binary_to_atom(Type), + 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"} + 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, + 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" + end. + delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; TypeBin == <<"groupchat">>; TypeBin == <<"all">> -> @@ -739,7 +816,7 @@ should_archive(#message{body = Body, subject = Subject, #message{} = Msg -> should_archive(Msg, LServer); _ -> - false + misc:is_mucsub_message(Pkt) end end end @@ -1378,6 +1455,42 @@ get_commands_spec() -> args_example = [<<"all">>, 31], args = [{type, binary}, {days, integer}], result = {res, rescode}}, + #ejabberd_commands{name = delete_old_mam_messages_batch, tags = [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 = [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 = [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, @@ -1456,7 +1569,7 @@ mod_doc() -> #{value => "true | false", desc => ?T("This option determines how ejabberd's " - "stream management code (see 'mod_stream_mgmt') " + "stream management code (see _`mod_stream_mgmt`_) " "handles unacknowledged messages when the " "connection is lost. Usually, such messages are " "either bounced or resent. However, neither is " @@ -1495,28 +1608,28 @@ mod_doc() -> #{value => "true | false", desc => ?T("Whether to destroy message archive of a room " - "(see 'mod_muc') when it gets destroyed. " + "(see _`mod_muc`_) when it gets destroyed. " "The default value is 'true'.")}}, {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, {user_mucsub_from_muc_archive, #{value => "true | false", desc => diff --git a/src/mod_mam_mnesia.erl b/src/mod_mam_mnesia.erl index d21d0e6ce..8b8a4b91e 100644 --- a/src/mod_mam_mnesia.erl +++ b/src/mod_mam_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,7 +29,7 @@ %% API -export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/6, remove_from_archive/3, - is_empty_for_user/2, is_empty_for_room/3]). + is_empty_for_user/2, is_empty_for_room/3, delete_old_messages_batch/5]). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). @@ -131,6 +131,43 @@ delete_old_user_messages(User, TimeStamp, Type) -> Err end. +delete_batch('$end_of_table', _LServer, _TS, _Type, Num) -> + {Num, '$end_of_table'}; +delete_batch(LastUS, _LServer, _TS, _Type, 0) -> + {0, LastUS}; +delete_batch(none, LServer, TS, Type, Num) -> + delete_batch(mnesia:first(archive_msg), LServer, TS, Type, Num); +delete_batch({_, LServer2} = LastUS, LServer, TS, Type, Num) when LServer /= LServer2 -> + 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})), + case Left of + 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), + case R of + {atomic, {Num, State}} -> + {ok, State, Num}; + {aborted, Err} -> + {error, Err} + end. + extended_fields() -> []. diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl index e5069b9a2..09175d83f 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,7 +30,8 @@ %% API -export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, extended_fields/0, store/8, 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]). + is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6, + delete_old_messages_batch/4, count_messages_to_delete/3]). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). @@ -71,8 +72,58 @@ remove_from_archive(LUser, LServer, WithJid) -> _ -> 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 Res of + {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, + ?SQL("delete from archive" + " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d")); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H limit %(Batch)d")) + end, + case Res of + {updated, Count} -> + {ok, Count}; + {error, _} = Error -> + Error + end. + delete_old_messages(ServerHost, TimeStamp, Type) -> - TS = now_to_usec(TimeStamp), + TS = misc:now_to_usec(TimeStamp), case Type of all -> ejabberd_sql:sql_query( @@ -311,11 +362,11 @@ export(_Server) -> [] end}, {archive_msg, - fun(Host, #archive_msg{us ={LUser, LServer}, + fun([Host | HostTail], #archive_msg{us ={LUser, LServer}, id = _ID, timestamp = TS, peer = Peer, type = Type, nick = Nick, packet = Pkt}) - when LServer == Host -> - TStmp = now_to_usec(TS), + when (LServer == Host) or ([LServer] == HostTail) -> + TStmp = misc:now_to_usec(TS), SUser = case Type of chat -> LUser; groupchat -> jid:encode({LUser, LServer, <<>>}) @@ -372,16 +423,6 @@ is_empty_for_room(LServer, LName, LHost) -> %%%=================================================================== %%% Internal functions %%%=================================================================== -now_to_usec({MSec, Sec, USec}) -> - (MSec*1000000 + Sec)*1000000 + USec. - -usec_to_now(Int) -> - Secs = Int div 1000000, - USec = Int rem 1000000, - MSec = Secs div 1000000, - Sec = Secs rem 1000000, - {MSec, Sec, USec}. - make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> Start = proplists:get_value(start, MAMQuery), End = proplists:get_value('end', MAMQuery), @@ -432,14 +473,14 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> StartClause = case Start of {_, _, _} -> [<<" and timestamp >= ">>, - integer_to_binary(now_to_usec(Start))]; + integer_to_binary(misc:now_to_usec(Start))]; _ -> [] end, EndClause = case End of {_, _, _} -> [<<" and timestamp <= ">>, - integer_to_binary(now_to_usec(End))]; + integer_to_binary(misc:now_to_usec(End))]; _ -> [] end, @@ -526,7 +567,7 @@ make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchi TSInt -> try jid:decode(Peer) of PeerJID -> - Now = usec_to_now(TSInt), + Now = misc:usec_to_now(TSInt), PeerLJID = jid:tolower(PeerJID), T = case Kind of <<"">> -> chat; diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl index d42f79112..d1f24c700 100644 --- a/src/mod_metrics.erl +++ b/src/mod_metrics.erl @@ -5,7 +5,7 @@ %%% Created : 22 Oct 2015 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_mix.erl b/src/mod_mix.erl index 82b5e41a4..315c7b80d 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -106,12 +106,12 @@ mod_doc() -> "experimental feature, updated in 19.02, and is not " "yet ready to use in production. It's asserted that " "the MIX protocol is going to replace the MUC protocol " - "in the future (see 'mod_muc')."), "", + "in the future (see _`mod_muc`_)."), "", ?T("To learn more about how to use that feature, you can refer to " "our tutorial: https://docs.ejabberd.im/tutorials/mix-010/" "[Getting started with XEP-0369: Mediated Information " "eXchange (MIX) v0.1]."), "", - ?T("The module depends on 'mod_mam'.")], + ?T("The module depends on _`mod_mam`_.")], opts => [{access_create, #{value => ?T("AccessName"), @@ -136,7 +136,7 @@ mod_doc() -> {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}]}. -spec route(stanza()) -> ok. route(#iq{} = IQ) -> @@ -168,9 +168,10 @@ process_disco_info(#iq{type = get, to = #jid{luser = <<>>} = To, 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], + 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], xmpp:make_iq_result( IQ, #disco_info{features = Features, identities = [Identity], @@ -186,7 +187,7 @@ process_disco_info(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, Identity = #identity{category = <<"conference">>, type = <<"mix">>}, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, - ?NS_MIX_CORE_0, ?NS_MAM_2], + ?NS_MIX_CORE_0, ?NS_MIX_CORE_1, ?NS_MAM_2], xmpp:make_iq_result( IQ, #disco_info{node = Node, features = Features, @@ -350,7 +351,7 @@ format_status(_Opt, Status) -> %%%=================================================================== -spec process_mix_create(iq()) -> iq(). process_mix_create(#iq{to = To, from = From, - sub_els = [#mix_create{channel = Chan}]} = IQ) -> + 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), @@ -374,7 +375,7 @@ process_mix_create(#iq{to = To, from = From, end, case Ret of ok -> - xmpp:make_iq_result(IQ, #mix_create{channel = Chan1}); + xmpp:make_iq_result(IQ, #mix_create{channel = Chan1, xmlns = XmlNs}); {error, conflict} -> xmpp:make_error(IQ, channel_exists_error(IQ)); {error, db_failure} -> @@ -384,7 +385,7 @@ process_mix_create(#iq{to = To, from = From, -spec process_mix_destroy(iq()) -> iq(). process_mix_destroy(#iq{to = To, from = #jid{luser = U, lserver = S}, - sub_els = [#mix_destroy{channel = Chan}]} = IQ) -> + 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), @@ -392,7 +393,7 @@ process_mix_destroy(#iq{to = To, {ok, {#jid{luser = U, lserver = S}, _, _}} -> case Mod:del_channel(ServerHost, Chan, Host) of ok -> - xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan}); + xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan, xmlns = XmlNs}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; @@ -406,7 +407,7 @@ process_mix_destroy(#iq{to = To, -spec process_mix_join(iq()) -> iq(). process_mix_join(#iq{to = To, from = From, - sub_els = [#mix_join{} = JoinReq]} = IQ) -> + sub_els = [#mix_join{xmlns = XmlNs} = JoinReq]} = IQ) -> Chan = To#jid.luser, Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), @@ -423,7 +424,9 @@ process_mix_join(#iq{to = To, from = From, notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), xmpp:make_iq_result(IQ, #mix_join{id = ID, subscribe = Nodes, - nick = Nick}) + jid = make_channel_id(To, ID), + nick = Nick, + xmlns = XmlNs}) catch _:{badmatch, {error, db_failure}} -> xmpp:make_error(IQ, db_error(IQ)) end; @@ -435,7 +438,7 @@ process_mix_join(#iq{to = To, from = From, -spec process_mix_leave(iq()) -> iq(). process_mix_leave(#iq{to = To, from = From, - sub_els = [#mix_leave{}]} = IQ) -> + 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), @@ -453,7 +456,7 @@ process_mix_leave(#iq{to = To, from = From, xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> - xmpp:make_iq_result(IQ, #mix_leave{}); + xmpp:make_iq_result(IQ, #mix_leave{xmlns = XmlNs}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; @@ -465,7 +468,7 @@ process_mix_leave(#iq{to = To, from = From, -spec process_mix_setnick(iq()) -> iq(). process_mix_setnick(#iq{to = To, from = From, - sub_els = [#mix_setnick{nick = Nick}]} = IQ) -> + 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), @@ -474,12 +477,12 @@ process_mix_setnick(#iq{to = To, from = From, {ok, _} -> case Mod:get_participant(ServerHost, Chan, Host, BFrom) of {ok, {_, Nick}} -> - xmpp:make_iq_result(IQ, #mix_setnick{nick = 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}); + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick, xmlns = XmlNs}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; @@ -590,10 +593,8 @@ known_nodes() -> -spec filter_nodes([binary()]) -> [binary()]. filter_nodes(Nodes) -> - lists:filter( - fun(Node) -> - lists:member(Node, Nodes) - end, known_nodes()). + KnownNodes = known_nodes(), + [Node || KnownNode <- KnownNodes, Node <- Nodes, KnownNode == Node]. -spec multicast(module(), binary(), binary(), binary(), binary(), fun((jid()) -> message())) -> ok. @@ -644,6 +645,11 @@ 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). + %%%=================================================================== %%% Error generators %%%=================================================================== @@ -688,12 +694,16 @@ register_iq_handlers(Host) -> ?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, @@ -704,8 +714,10 @@ unregister_iq_handlers(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_MIX_CORE_0), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_1), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_1), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl index c6348b92f..7bd6f2a71 100644 --- a/src/mod_mix_pam.erl +++ b/src/mod_mix_pam.erl @@ -31,11 +31,17 @@ -export([bounce_sm_packet/1, disco_sm_features/5, remove_user/2, - process_iq/1]). + process_iq/1, + get_mix_roster_items/2, + webadmin_user/4, + webadmin_page/3]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). +-include("mod_roster.hrl"). -include("translate.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). -define(MIX_PAM_CACHE, mix_pam_cache). @@ -61,8 +67,11 @@ start(Host, Opts) -> ejabberd_hooks:add(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0, - ?MODULE, process_iq); + ejabberd_hooks:add(roster_get, Host, ?MODULE, get_mix_roster_items, 50), + ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50), + ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0, ?MODULE, process_iq), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2, ?MODULE, process_iq); Err -> Err end. @@ -71,7 +80,11 @@ stop(Host) -> ejabberd_hooks:delete(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0). + ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_mix_roster_items, 50), + ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50), + ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_2). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), @@ -120,23 +133,23 @@ mod_doc() -> [{db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. -spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}. bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To, @@ -168,7 +181,7 @@ bounce_sm_packet(Acc) -> disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; disco_sm_features(Acc, _From, _To, <<"">>, _Lang) -> - {result, [?NS_MIX_PAM_0 | + {result, [?NS_MIX_PAM_0, ?NS_MIX_PAM_2 | case Acc of {result, Features} -> Features; empty -> [] @@ -200,6 +213,26 @@ process_iq(#iq{type = set, 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{ + jid = ItemJID, + name = <<>>, + subscription = both, + ask = undefined, + groups = [<<"Channels">>], + mix_channel = #mix_roster_channel{participant_id = Id} + } + end, Channels); + _ -> + [] + end ++ Acc. + -spec remove_user(binary(), binary()) -> ok | {error, db_failure}. remove_user(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), @@ -229,13 +262,25 @@ remove_user(LUser, LServer) -> %%% Internal functions %%%=================================================================== -spec process_join(iq()) -> ignore. -process_join(#iq{from = From, +process_join(#iq{from = From, lang = Lang, sub_els = [#mix_client_join{channel = Channel, - join = Join}]} = IQ) -> + join = Join}]} = IQ) -> ejabberd_router:route_iq( #iq{from = jid:remove_resource(From), to = Channel, type = set, sub_els = [Join]}, - fun(ResIQ) -> process_join_result(ResIQ, IQ) end), + 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 + end), ignore. -spec process_leave(iq()) -> iq() | error. @@ -254,24 +299,40 @@ process_leave(#iq{from = From, end. -spec process_join_result(iq(), iq()) -> ok. -process_join_result(#iq{from = Channel, - type = result, sub_els = [#mix_join{id = ID} = Join]}, +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}), + 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(Err, IQ) -> +process_join_result(#iq{type = error} = Err, IQ) -> process_iq_error(Err, IQ). -spec process_leave_result(iq(), iq()) -> ok. -process_leave_result(#iq{type = result, sub_els = [#mix_leave{} = Leave]}, IQ) -> - ResIQ = xmpp:make_iq_result(IQ, #mix_client_leave{leave = Leave}), +process_leave_result(#iq{from = Channel, type = result, sub_els = [#mix_leave{xmlns = XmlNs} = Leave]}, + #iq{to = User} = IQ) -> + % Do roster push + mod_roster:push_item(User, + #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). @@ -339,6 +400,11 @@ get_channel(JID, Channel) -> 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 @@ -399,3 +465,55 @@ delete_cache(Mod, JID, Channel) -> false -> ok end. + +%%%=================================================================== +%%% Webadmin interface +%%%=================================================================== +webadmin_user(Acc, User, Server, Lang) -> + QueueLen = case get_channels({jid:nodeprep(User), jid:nameprep(Server), <<>>}) of + {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]. + +webadmin_page(_, Host, + #request{us = _US, path = [<<"user">>, U, <<"mix_channels">>], + lang = Lang} = _Request) -> + Res = web_mix_channels(U, Host, Lang), + {stop, Res}; +webadmin_page(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, + 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, + 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. + +us_to_list({User, Server}) -> + jid:encode({User, Server, <<"">>}). diff --git a/src/mod_mqtt.erl b/src/mod_mqtt.erl index 8734c778d..01950d64a 100644 --- a/src/mod_mqtt.erl +++ b/src/mod_mqtt.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. @@ -278,8 +278,9 @@ listen_options() -> %%%=================================================================== mod_doc() -> #{desc => - ?T("This module adds support for the MQTT protocol " - "version '3.1.1' and '5.0'. Remember to configure " + ?T("This module adds " + "https://docs.ejabberd.im/admin/guide/mqtt/[support for the MQTT] " + "protocol version '3.1.1' and '5.0'. Remember to configure " "'mod_mqtt' in 'modules' and 'listen' sections."), opts => [{access_subscribe, @@ -326,37 +327,37 @@ mod_doc() -> {queue_type, #{value => "ram | file", desc => - ?T("Same as top-level 'queue_type' option, " + ?T("Same as top-level _`queue_type`_ option, " "but applied to this module only.")}}, {ram_db_type, #{value => "mnesia", desc => - ?T("Same as top-level 'default_ram_db' option, " + ?T("Same as top-level _`default_ram_db`_ option, " "but applied to this module only.")}}, {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, " + ?T("Same as top-level _`default_db`_ option, " "but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, " + ?T("Same as top-level _`use_cache`_ option, " "but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, " + ?T("Same as top-level _`cache_size`_ option, " "but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, " + ?T("Same as top-level _`cache_missed`_ option, " "but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, " + ?T("Same as top-level _`cache_life_time`_ option, " "but applied to this module only.")}}]}. %%%=================================================================== diff --git a/src/mod_mqtt_mnesia.erl b/src/mod_mqtt_mnesia.erl index 92c43d2ee..1d8e7aa5b 100644 --- a/src/mod_mqtt_mnesia.erl +++ b/src/mod_mqtt_mnesia.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ -export([init/2, publish/6, delete_published/2, lookup_published/2]). -export([list_topics/1, use_cache/1]). -export([init/0]). --export([subscribe/4, unsubscribe/2, find_subscriber/2]). +-export([subscribe/4, unsubscribe/2, find_subscriber/2, mqtree_match/1]). -export([open_session/1, close_session/1, lookup_session/1, get_sessions/2]). -include("logger.hrl"). @@ -103,7 +103,7 @@ lookup_published({_, S, _}, Topic) -> correlation_data = CorrelationData, content_type = ContentType, user_properties = UserProps}] -> - Props = #{payload_format => PayloadFormat, + Props = #{payload_format_indicator => PayloadFormat, response_topic => ResponseTopic, correlation_data => CorrelationData, content_type => ContentType, @@ -249,9 +249,16 @@ unsubscribe({U, S, R} = USR, Topic) -> Reason, [jid:encode(USR), Topic]) end. -find_subscriber(S, Topic) when is_binary(Topic) -> +mqtree_match(Topic) -> Tree = mqtree:whereis(mqtt_sub_index), - case mqtree:match(Tree, Topic) of + 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] -> find_subscriber(S, {Filters, {Filter, S, '_', '_'}}); [] -> diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl index ca025e3d2..9b8c6ed44 100644 --- a/src/mod_mqtt_session.erl +++ b/src/mod_mqtt_session.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ -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(), @@ -47,7 +48,8 @@ in_flight :: undefined | publish() | pubrel(), codec :: mqtt_codec:state(), queue :: undefined | p1_queue:queue(publish()), - tls :: boolean()}). + tls :: boolean(), + tls_verify :: boolean()}). -type acks() :: #{non_neg_integer() => pubrec()}. -type subscriptions() :: #{binary() => {sub_opts(), non_neg_integer()}}. @@ -162,6 +164,7 @@ init([SockMod, Socket, ListenOpts]) -> 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), @@ -433,7 +436,7 @@ upgrade_state(State) -> upgrade_state(setelement(2, State1, VSN+1)) end. --spec upgrade_state(tuple(), 1..?VSN) -> tuple(). +-spec upgrade_state(tuple(), integer()) -> tuple(). upgrade_state(OldState, 1) -> %% Appending 'tls' field erlang:append_element(OldState, false); @@ -553,7 +556,7 @@ unregister_session(_, _) -> {error, state(), error_reason()}. handle_connect(#connect{clean_start = CleanStart} = Pkt, #state{jid = undefined, peername = IP} = State) -> - case authenticate(Pkt, IP) of + case authenticate(Pkt, IP, State) of {ok, JID} -> case validate_will(Pkt, JID) of ok -> @@ -939,7 +942,12 @@ check_sock_result({_, Sock}, {error, Why}) -> starttls(#state{socket = {gen_tcp, Socket}, tls = true}) -> case ejabberd_pkix:get_certfile() of {ok, Cert} -> - case fast_tls:tcp_to_tls(Socket, [{certfile, Cert}]) of + 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} -> @@ -1134,8 +1142,8 @@ is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> %%% Authentication %%%=================================================================== -spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}. -parse_credentials(#connect{client_id = <<>>}) -> - parse_credentials(#connect{client_id = p1_rand:get_string()}); +parse_credentials(#connect{client_id = <<>>} = C) -> + parse_credentials(C#connect{client_id = p1_rand:get_string()}); parse_credentials(#connect{username = <<>>, client_id = ClientID}) -> Host = ejabberd_config:get_myname(), JID = case jid:make(ClientID, Host) of @@ -1172,9 +1180,9 @@ parse_credentials(JID, ClientID) -> end end. --spec authenticate(connect(), peername()) -> {ok, jid:jid()} | {error, reason_code()}. -authenticate(Pkt, IP) -> - case authenticate(Pkt) of +-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), @@ -1185,8 +1193,8 @@ authenticate(Pkt, IP) -> Err end. --spec authenticate(connect()) -> {ok, jid:jid(), module()} | {error, reason_code()}. -authenticate(#connect{password = Pass, properties = Props} = Pkt) -> +-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 @@ -1200,16 +1208,82 @@ authenticate(#connect{password = Pass, properties = Props} = Pkt) -> {ok, _} -> {error, 'bad-authentication-method'}; error -> - case ejabberd_auth:check_password_with_authmodule( - LUser, <<>>, LServer, Pass) of - {true, AuthModule} -> {ok, JID, AuthModule}; - false -> {error, 'not-authorized'} - end + case Pass of + <<>> -> + case tls_auth(JID, State) of + true -> + {ok, JID, pkix}; + false -> + {error, 'not-authorized'} + end; + _ -> + case ejabberd_auth:check_password_with_authmodule( + LUser, <<>>, LServer, Pass) of + {true, AuthModule} -> {ok, JID, AuthModule}; + false -> {error, 'not-authorized'} + end + end end; {error, _} = Err -> Err end. +-spec tls_auth(jid:jid(), state()) -> boolean(). +tls_auth(_JID, #state{tls_verify = false}) -> + false; +tls_auth(JID, State) -> + case State#state.socket of + {fast_tls, Sock} -> + case fast_tls:get_peer_certificate(Sock, otp) of + {ok, Cert} -> + case fast_tls:get_verify_result(Sock) of + 0 -> + case get_cert_jid(Cert) of + {ok, JID2} -> + jid:remove_resource(jid:tolower(JID)) == + jid:remove_resource(jid:tolower(JID2)); + error -> + false + end; + VerifyRes -> + Reason = fast_tls:get_cert_verify_string(VerifyRes, Cert), + ?WARNING_MSG("TLS verify failed: ~s", [Reason]), + false + end; + error -> + false + end; + _ -> + false + 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'{value = {utf8String, Val}} -> + try jid:decode(Val) of + #jid{luser = <<>>} -> + case jid:make(Val, ejabberd_config:get_myname()) of + error -> + error; + JID -> + {ok, JID} + end; + JID -> + {ok, JID} + catch _:{bad_jid, _} -> + error + end; + _ -> + error + end; + _ -> + error + end. + %%%=================================================================== %%% Validators %%%=================================================================== diff --git a/src/mod_mqtt_sql.erl b/src/mod_mqtt_sql.erl index fefd000cd..dd40771f5 100644 --- a/src/mod_mqtt_sql.erl +++ b/src/mod_mqtt_sql.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. diff --git a/src/mod_mqtt_ws.erl b/src/mod_mqtt_ws.erl index 01f182d30..1c9c8de7a 100644 --- a/src/mod_mqtt_ws.erl +++ b/src/mod_mqtt_ws.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 46bfd20e5..c1c1c7f2a 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -40,6 +40,7 @@ room_destroyed/4, store_room/4, store_room/5, + store_changes/4, restore_room/3, forget_room/3, create_room/3, @@ -68,6 +69,7 @@ get_online_rooms_by_user/3, can_use_nick/4, get_subscribed_rooms/2, + remove_user/2, procname/2, route/1, unhibernate_room/3]). @@ -91,6 +93,7 @@ -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}. +-callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}. -callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error. -callback forget_room(binary(), binary(), binary()) -> {atomic, any()}. -callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean(). @@ -111,7 +114,8 @@ -callback get_subscribed_rooms(binary(), binary(), jid()) -> {ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}. --optional_callbacks([get_subscribed_rooms/3]). +-optional_callbacks([get_subscribed_rooms/3, + store_changes/4]). %%==================================================================== %% API @@ -119,6 +123,8 @@ start(Host, Opts) -> case mod_muc_sup:start(Host) of {ok, _} -> + ejabberd_hooks:add(remove_user, Host, ?MODULE, + remove_user, 50), MyHosts = gen_mod:get_opt_hosts(Opts), Mod = gen_mod:db_mod(Opts, ?MODULE), RMod = gen_mod:ram_db_mod(Opts, ?MODULE), @@ -130,6 +136,8 @@ start(Host, Opts) -> end. stop(Host) -> + ejabberd_hooks:delete(remove_user, Host, ?MODULE, + remove_user, 50), Proc = mod_muc_sup:procname(Host), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). @@ -158,7 +166,7 @@ reload(ServerHost, NewOpts, OldOpts) -> fun(I) -> ?GEN_SERVER:cast(procname(ServerHost, I), {reload, AddHosts, DelHosts, NewHosts}) - end, lists:seq(1, erlang:system_info(logical_processors))), + end, lists:seq(1, misc:logical_processors())), load_permanent_rooms(AddHosts, ServerHost, NewOpts), shutdown_rooms(ServerHost, DelHosts, OldRMod), lists:foreach( @@ -185,7 +193,7 @@ procname(Host, I) when is_integer(I) -> <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary, "_", (integer_to_binary(I))/binary>>, utf8); procname(Host, RoomHost) -> - Cores = erlang:system_info(logical_processors), + Cores = misc:logical_processors(), I = erlang:phash2(RoomHost, Cores) + 1, procname(Host, I). @@ -313,6 +321,11 @@ store_room(ServerHost, Host, Name, Opts, ChangesHints) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:store_room(LServer, Host, Name, Opts, ChangesHints). +store_changes(ServerHost, Host, Name, ChangesHints) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store_changes(LServer, Host, Name, ChangesHints). + restore_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), @@ -389,10 +402,10 @@ init([Host, Worker]) -> {stop, normal, ok, state()}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; -handle_call({unhibernate, Room, Host}, _From, +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), State}; + {reply, load_room(RMod, Host, ServerHost, Room, ResetHibernationTime), State}; handle_call({create, Room, Host, Opts}, _From, #{server_host := ServerHost} = State) -> ?DEBUG("MUC: create new room '~ts'~n", [Room]), @@ -566,11 +579,15 @@ extract_password(#iq{} = IQ) -> -spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | error. unhibernate_room(ServerHost, Host, Room) -> + unhibernate_room(ServerHost, Host, Room, true). + +-spec unhibernate_room(binary(), binary(), binary(), boolean()) -> {ok, pid()} | error. +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}), - case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host}) of + case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host, ResetHibernationTime}, 20000) of {ok, _} = R -> R; _ -> error end; @@ -592,7 +609,7 @@ route_to_room(Packet, ServerHost) -> Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error(Packet, Err); StartType -> - case load_room(RMod, Host, ServerHost, Room) of + case load_room(RMod, Host, ServerHost, Room, true) of {error, notfound} when StartType == start -> case check_create_room(ServerHost, Host, Room, From) of true -> @@ -836,28 +853,36 @@ load_permanent_rooms(Hosts, ServerHost, Opts) -> lists:foreach( fun(R) -> {Room, _} = R#muc_room.name_host, - unhibernate_room(ServerHost, Host, Room) + unhibernate_room(ServerHost, Host, Room, false) end, get_rooms(ServerHost, Host)) end, Hosts); false -> ok end. --spec load_room(module(), binary(), binary(), binary()) -> {ok, pid()} | - {error, notfound | term()}. -load_room(RMod, Host, ServerHost, Room) -> +-spec load_room(module(), binary(), binary(), binary(), boolean()) -> + {ok, pid()} | {error, notfound | term()}. +load_room(RMod, Host, ServerHost, Room, ResetHibernationTime) -> case restore_room(ServerHost, Host, Room) of error -> {error, notfound}; Opts0 -> + Mod = gen_mod:db_mod(ServerHost, mod_muc), case proplists:get_bool(persistent, Opts0) of true -> ?DEBUG("Restore room: ~ts", [Room]), - start_room(RMod, Host, ServerHost, Room, Opts0); + 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), - Mod = gen_mod:db_mod(ServerHost, mod_muc), case erlang:function_exported(Mod, get_subscribed_rooms, 3) of true -> ok; @@ -1114,6 +1139,32 @@ count_online_rooms(ServerHost, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:count_online_rooms(ServerHost, Host). +-spec remove_user(binary(), binary()) -> ok. +remove_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case erlang:function_exported(Mod, remove_user, 2) of + true -> + Mod:remove_user(LUser, LServer); + false -> + ok + end, + JID = jid:make(User, Server), + lists:foreach( + fun(Host) -> + lists:foreach( + fun({_, _, Pid}) -> + mod_muc_room:change_item_async( + Pid, JID, affiliation, none, <<"User removed">>), + mod_muc_room:change_item_async( + Pid, JID, role, none, <<"User removed">>) + end, + get_online_rooms(LServer, Host)) + end, + gen_mod:get_module_opt_hosts(LServer, mod_muc)), + ok. + opts_to_binary(Opts) -> lists:map( fun({title, Title}) -> @@ -1126,7 +1177,7 @@ opts_to_binary(Opts) -> {subject, iolist_to_binary(Subj)}; ({subject_author, Author}) -> {subject_author, iolist_to_binary(Author)}; - ({affiliations, Affs}) -> + ({AffOrRole, Affs}) when (AffOrRole == affiliation) or (AffOrRole == role) -> {affiliations, lists:map( fun({{U, S, R}, Aff}) -> NewAff = @@ -1217,6 +1268,8 @@ mod_opt_type(user_message_shaper) -> econf:atom(); mod_opt_type(user_presence_shaper) -> econf:atom(); +mod_opt_type(cleanup_affiliations_on_start) -> + econf:bool(); mod_opt_type(default_room_options) -> econf:options( #{allow_change_subj => econf:bool(), @@ -1228,8 +1281,11 @@ mod_opt_type(default_room_options) -> 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(), @@ -1245,7 +1301,11 @@ mod_opt_type(default_room_options) -> econf:enum([moderator, participant, visitor])), public => econf:bool(), public_list => econf:bool(), - title => econf:binary()}); + 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) -> @@ -1294,6 +1354,7 @@ mod_options(Host) -> {preload_rooms, true}, {hibernation_timeout, infinity}, {vcard, undefined}, + {cleanup_affiliations_on_start, false}, {default_room_options, [{allow_change_subj,true}, {allow_private_messages,true}, @@ -1358,19 +1419,19 @@ mod_doc() -> desc => ?T("To configure who is allowed to create new rooms at the " "Multi-User Chat service, this option can be used. " - "By default any account in the local ejabberd server is " + "The default value is 'all', which means everyone is " "allowed to create rooms.")}}, {access_persistent, #{value => ?T("AccessName"), desc => ?T("To configure who is allowed to modify the 'persistent' room option. " - "By default any account in the local ejabberd server is allowed to " + "The default value is 'all', which means everyone is allowed to " "modify that option.")}}, {access_mam, #{value => ?T("AccessName"), desc => ?T("To configure who is allowed to modify the 'mam' room option. " - "By default any account in the local ejabberd server is allowed to " + "The default value is 'all', which means everyone is allowed to " "modify that option.")}}, {access_register, #{value => ?T("AccessName"), @@ -1382,14 +1443,13 @@ mod_doc() -> {db_type, #{value => "mnesia | sql", desc => - ?T("Define the type of persistent storage where the module will " - "store room information. The default is the storage defined " - "by the global option 'default_db', or 'mnesia' if omitted.")}}, + ?T("Same as top-level _`default_db`_ option, " + "but applied to this module only.")}}, {ram_db_type, #{value => "mnesia | sql", desc => - ?T("Define the type of volatile (in-memory) storage where the module " - "will store room information ('muc_online_room' and 'muc_online_users').")}}, + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, {hibernation_timeout, #{value => "infinity | Seconds", desc => @@ -1486,7 +1546,8 @@ mod_doc() -> ?T("This option defines after how many users in the room, " "it is considered overcrowded. When a MUC room is considered " "overcrowed, presence broadcasts are limited to reduce load, " - "traffic and excessive presence \"storm\" received by participants.")}}, + "traffic and excessive presence \"storm\" received by participants. " + "The default value is '1000'.")}}, {min_message_interval, #{value => ?T("Number"), desc => @@ -1517,7 +1578,7 @@ mod_doc() -> {queue_type, #{value => "ram | file", desc => - ?T("Same as top-level 'queue_type' option, but applied to this module only.")}}, + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, {regexp_room_id, #{value => "string()", desc => @@ -1571,8 +1632,15 @@ mod_doc() -> " -", " work: true", " street: Elm Street"]}]}}, + {cleanup_affiliations_on_start, + #{value => "true | false", + note => "added in 22.05", + desc => + ?T("Remove affiliations for non-existing local users on startup. " + "The default value is 'false'.")}}, {default_room_options, #{value => ?T("Options"), + note => "improved in 22.05", desc => ?T("This option allows to define the desired " "default room options. Note that the creator of a room " @@ -1609,6 +1677,11 @@ mod_doc() -> "If disallowed, the status text is stripped before broadcasting " "the presence update to all the room occupants. " "The default value is 'true'.")}}, + {allow_voice_requests, + #{value => "true | false", + desc => + ?T("Allow visitors in a moderated room to request voice. " + "The default value is 'true'.")}}, {anonymous, #{value => "true | false", desc => @@ -1625,6 +1698,16 @@ mod_doc() -> "https://docs.ejabberd.im/admin/configuration/#captcha[CAPTCHA] " "in order to accept their join in the room. " "The default value is 'false'.")}}, + {description, + #{value => ?T("Room Description"), + desc => + ?T("Short description of the room. " + "The default value is an empty string.")}}, + {enable_hats, + #{value => "true | false", + desc => + ?T("Allow extended roles as defined in XEP-0317 Hats. " + "The default value is 'false'.")}}, {lang, #{value => ?T("Language"), desc => @@ -1634,7 +1717,7 @@ mod_doc() -> {logging, #{value => "true | false", desc => - ?T("The public messages are logged using 'mod_muc_log'. " + ?T("The public messages are logged using _`mod_muc_log`_. " "The default value is 'false'.")}}, {members_by_default, #{value => "true | false", @@ -1680,6 +1763,21 @@ mod_doc() -> desc => ?T("The list of participants is public, without requiring " "to enter the room. The default value is 'true'.")}}, + {pubsub, + #{value => ?T("PubSub Node"), + desc => + ?T("XMPP URI of associated Publish/Subscribe node. " + "The default value is an empty string.")}}, + {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.")}}, + {voice_request_min_interval, + #{value => ?T("Number"), + desc => + ?T("Minimum interval between voice requests, in seconds. " + "The default value is '1800'.")}}, {mam, #{value => "true | false", desc => diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 72a29a45b..5f96801c6 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -5,7 +5,7 @@ %%% Created : 8 Sep 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -40,8 +40,13 @@ change_room_option/4, get_room_options/2, set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3, web_menu_main/2, web_page_main/2, web_menu_host/3, - subscribe_room/4, unsubscribe_room/2, get_subscribers/2, - web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]). + subscribe_room/4, subscribe_room_many/3, + unsubscribe_room/2, get_subscribers/2, + get_room_serverhost/1, + web_page_host/3, + mod_opt_type/1, mod_options/1, + get_commands_spec/0, find_hosts/1, room_diagnostics/2, + get_room_pid/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). @@ -57,7 +62,7 @@ %%---------------------------- start(Host, _Opts) -> - ejabberd_commands:register_commands(get_commands_spec()), + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), @@ -281,7 +286,7 @@ get_commands_spec() -> #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], desc = "Send a direct invitation to several destinations", - longdesc = "Since ejabberd 20.10, this command is " + longdesc = "Since ejabberd 20.12, this command is " "asynchronous: the API call may return before the " "server has send all the invitations.\n\n" "Password and Message can also be: none. " @@ -331,6 +336,27 @@ get_commands_spec() -> args = [{user, binary}, {nick, binary}, {room, binary}, {nodes, binary}], result = {nodes, {list, {node, string}}}}, + #ejabberd_commands{name = subscribe_room_many, tags = [muc_room], + desc = "Subscribe several users to a MUC conference", + note = "added in 22.05", + longdesc = "This command accept up to 50 users at once (this is configurable with `subscribe_room_many_max_users` option)", + module = ?MODULE, function = subscribe_room_many, + args_desc = ["Users JIDs and nicks", + "the room to subscribe", + "nodes separated by commas: ,"], + args_example = [[{"tom@localhost", "Tom"}, + {"jerry@localhost", "Jerry"}], + "room1@conference.localhost", + "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], + args = [{users, {list, + {user, {tuple, + [{jid, binary}, + {nick, binary} + ]}} + }}, + {room, binary}, + {nodes, binary}], + result = {res, rescode}}, #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], desc = "Unsubscribe from a MUC conference", module = ?MODULE, function = unsubscribe_room, @@ -670,8 +696,7 @@ justcreated_to_binary(J) when is_atom(J) -> %% Create/Delete Room %%---------------------------- -%% @spec (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, []). @@ -710,7 +735,7 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) -> maybe_store_room(ServerHost, Host, Name, RoomOpts) -> case proplists:get_bool(persistent, RoomOpts) of true -> - {atomic, ok} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts), + {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts), ok; false -> ok @@ -722,8 +747,7 @@ 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 (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. @@ -835,7 +859,7 @@ rooms_report(Method, Action, Service, Days) -> muc_unused(Method, Action, Service, Last_allowed) -> %% Get all required info about all existing rooms - Rooms_all = get_all_rooms(Service), + 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), @@ -860,7 +884,14 @@ get_online_rooms(ServiceArg) -> || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)] end, Hosts). -get_all_rooms(Host) -> +get_all_rooms(ServiceArg, Timestamp) -> + Hosts = find_services(ServiceArg), + lists:flatmap( + fun(Host) -> + get_all_rooms2(Host, Timestamp) + end, Hosts). + +get_all_rooms2(Host, Timestamp) -> ServerHost = ejabberd_router:host_of_route(Host), OnlineRooms = get_online_rooms(Host), OnlineMap = lists:foldl( @@ -870,8 +901,11 @@ get_all_rooms(Host) -> Mod = gen_mod:db_mod(ServerHost, mod_muc), DbRooms = - case erlang:function_exported(Mod, get_rooms_without_subscribers, 2) of - true -> + 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) @@ -926,6 +960,8 @@ decide_room(unused, {_Room_name, _Host, ServerHost, Room_pid}, Last_allowed) -> case lists:keyfind(hibernation_time, 1, Opts) of false -> {NodeStartTime, 0}; + {_, undefined} -> + {NodeStartTime, 0}; {_, T} -> {T, 0} end @@ -1015,8 +1051,8 @@ get_room_occupants(Pid) -> get_room_occupants_number(Room, Host) -> case get_room_pid(Room, Host) of Pid when is_pid(Pid )-> - S = get_room_state(Pid), - maps:size(S#state.users); + {ok, #{occupants_number := N}} = mod_muc_room:get_info(Pid), + N; _ -> throw({error, room_not_found}) end. @@ -1079,8 +1115,8 @@ send_direct_invitation(FromJid, UserJid, Msg) -> %% Change Room Option %%---------------------------- -%% @spec(Name::string(), Service::string(), Option::string(), Value) -> ok -%% Value = atom() | integer() | string() +-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), @@ -1149,6 +1185,28 @@ get_room_pid(Name, Service) -> invalid_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 + catch + error:{invalid_domain, _} -> + invalid_service; + error:{unregistered_route, _} -> + invalid_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. @@ -1324,6 +1382,18 @@ subscribe_room(User, Nick, Room, Nodes) -> throw({error, "Malformed room JID"}) end. +subscribe_room_many(Users, Room, Nodes) -> + MaxUsers = mod_muc_admin_opt:subscribe_room_many_max_users(global), + if + length(Users) > MaxUsers -> + throw({error, "Too many users in subscribe_room_many command"}); + true -> + lists:foreach( + fun({User, Nick}) -> + subscribe_room(User, Nick, Room, Nodes) + end, Users) + end. + unsubscribe_room(User, Room) -> try jid:decode(Room) of #jid{luser = Name, lserver = Host} when Name /= <<"">> -> @@ -1406,11 +1476,23 @@ find_hosts(ServerHost) -> [] end. -mod_options(_) -> []. +mod_opt_type(subscribe_room_many_max_users) -> + econf:int(). + +mod_options(_) -> + [{subscribe_room_many_max_users, 50}]. mod_doc() -> #{desc => [?T("This module provides commands to administer local MUC " "services and their MUC rooms. It also provides simple " "WebAdmin pages to view the existing rooms."), "", - ?T("This module depends on 'mod_muc'.")]}. + ?T("This module depends on _`mod_muc`_.")], + opts => + [{subscribe_room_many_max_users, + #{value => ?T("Number"), + note => "added in 22.05", + desc => + ?T("How many users can be subscribed to a room at once using " + "the 'subscribe_room_many' command. " + "The default value is '50'.")}}]}. diff --git a/src/mod_muc_admin_opt.erl b/src/mod_muc_admin_opt.erl new file mode 100644 index 000000000..18ca64af7 --- /dev/null +++ b/src/mod_muc_admin_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_muc_admin_opt). + +-export([subscribe_room_many_max_users/1]). + +-spec subscribe_room_many_max_users(gen_mod:opts() | global | binary()) -> integer(). +subscribe_room_many_max_users(Opts) when is_map(Opts) -> + gen_mod:get_opt(subscribe_room_many_max_users, Opts); +subscribe_room_many_max_users(Host) -> + gen_mod:get_module_opt(Host, mod_muc_admin, subscribe_room_many_max_users). + diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 53223a8eb..0ee493b94 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -5,7 +5,7 @@ %%% Created : 12 Mar 2006 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -1021,7 +1021,7 @@ mod_doc() -> ?T("- URLs on messages and subjects are converted to hyperlinks."), "", ?T("- Timezone used on timestamps is shown on the log files."), "", ?T("- A custom link can be added on top of each page."), "", - ?T("The module depends on 'mod_muc'.")], + ?T("The module depends on _`mod_muc`_.")], opts => [{access_log, #{value => ?T("AccessName"), diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl index c822acc68..18b1e74ef 100644 --- a/src/mod_muc_mnesia.erl +++ b/src/mod_muc_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl index 760a5d7c8..c22a9594d 100644 --- a/src/mod_muc_opt.erl +++ b/src/mod_muc_opt.erl @@ -9,6 +9,7 @@ -export([access_mam/1]). -export([access_persistent/1]). -export([access_register/1]). +-export([cleanup_affiliations_on_start/1]). -export([db_type/1]). -export([default_room_options/1]). -export([hibernation_timeout/1]). @@ -73,13 +74,19 @@ access_register(Opts) when is_map(Opts) -> access_register(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_register). +-spec cleanup_affiliations_on_start(gen_mod:opts() | global | binary()) -> boolean(). +cleanup_affiliations_on_start(Opts) when is_map(Opts) -> + gen_mod:get_opt(cleanup_affiliations_on_start, Opts); +cleanup_affiliations_on_start(Host) -> + gen_mod:get_module_opt(Host, mod_muc, cleanup_affiliations_on_start). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); 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' | 'true' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer()}]. +-spec default_room_options(gen_mod:opts() | global | binary()) -> [{atom(),'anyone' | 'false' | 'moderators' | 'nobody' | '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) -> diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index fc03e50ef..79d7da928 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,8 @@ -author('alexey@process-one.net'). +-protocol({xep, 317, '0.1'}). + -behaviour(p1_fsm). %% External exports @@ -47,7 +49,9 @@ 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, @@ -74,7 +78,12 @@ -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). --define(DEFAULT_MAX_USERS_PRESENCE,1000). +-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>). +-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>). +-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>). +-define(MAX_HATS_USERS, 100). +-define(MAX_HATS_PER_USER, 10). +-define(CLEAN_ROOM_TIMEOUT, 30000). %-define(DBGFSM, true). @@ -194,6 +203,11 @@ change_item(Pid, JID, Type, AffiliationOrRole, Reason) -> {error, notfound} end. +-spec change_item_async(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> ok. +change_item_async(Pid, JID, Type, AffiliationOrRole, Reason) -> + p1_fsm:send_all_state_event( + Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}). + -spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}. get_state(Pid) -> try p1_fsm:sync_send_all_state_event(Pid, get_state) @@ -203,6 +217,17 @@ get_state(Pid) -> {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} + 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}) @@ -283,6 +308,8 @@ init([Host, ServerHost, Access, Room, HistorySize, 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(), + close_room_if_temporary_and_empty), {ok, normal_state, reset_hibernate_timer(State1)}; init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) -> process_flag(trap_exit, true), @@ -298,7 +325,10 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) room_shaper = Shaper}), add_to_log(room_existence, started, State), ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), - {ok, normal_state, reset_hibernate_timer(State)}. + State1 = cleanup_affiliations(State), + erlang:send_after(?CLEAN_ROOM_TIMEOUT, self(), + close_room_if_temporary_and_empty), + {ok, normal_state, reset_hibernate_timer(State1)}. normal_state({route, <<"">>, #message{from = From, type = Type, lang = Lang} = Packet}, @@ -446,6 +476,8 @@ normal_state({route, <<"">>, process_iq_mucsub(From, IQ, StateData); #xcaptcha{} -> process_iq_captcha(From, IQ, StateData); + #adhoc_command{} -> + process_iq_adhoc(From, IQ, StateData); _ -> Txt = ?T("The feature requested is not " "supported by the conference"), @@ -627,7 +659,7 @@ normal_state({route, ToNick, normal_state(hibernate, StateData) -> case maps:size(StateData#state.users) of 0 -> - store_room_no_checks(StateData, []), + 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}}; _ -> @@ -641,7 +673,7 @@ handle_event({service_message, Msg}, _StateName, MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)}, send_wrapped_multiple( StateData#state.jid, - get_users_and_subscribers(StateData), + get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), MessagePkt, ?NS_MUCSUB_NODES_MESSAGES, StateData), @@ -664,6 +696,16 @@ handle_event({set_affiliations, Affiliations}, StateName, StateData) -> NewStateData = set_affiliations(Affiliations, StateData), {next_state, StateName, NewStateData}; +handle_event({process_item_change, Item, UJID}, StateName, StateData) -> + case process_item_change(Item, StateData, UJID) of + {error, _} -> + {next_state, StateName, StateData}; + StateData -> + {next_state, StateName, StateData}; + NSD -> + store_room(NSD), + {next_state, StateName, NSD} + end; handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. @@ -694,6 +736,10 @@ handle_sync_event(get_config, _From, StateName, handle_sync_event(get_state, _From, StateName, StateData) -> {reply, {ok, StateData}, 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) -> {result, undefined, NSD} = change_config(Config, StateData), @@ -705,20 +751,24 @@ handle_sync_event({change_state, NewStateData}, _From, true -> ok; _ -> - erlang:put(muc_subscribers, NewStateData#state.subscribers) + erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers) end, {reply, {ok, NewStateData}, StateName, NewStateData}; handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> case process_item_change(Item, StateData, UJID) of {error, _} = Err -> {reply, Err, StateName, StateData}; + StateData -> + {reply, {ok, StateData}, StateName, StateData}; NSD -> store_room(NSD), {reply, {ok, NSD}, StateName, NSD} end; handle_sync_event(get_subscribers, _From, StateName, StateData) -> - JIDs = lists:map(fun jid:make/1, - maps:keys(StateData#state.subscribers)), + JIDs = muc_subscribers_fold( + fun(_LBareJID, #subscriber{jid = JID}, Acc) -> + [JID | Acc] + end, [], StateData#state.muc_subscribers), {reply, {ok, JIDs}, StateName, StateData}; handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, StateName, StateData) -> @@ -762,7 +812,8 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName, {reply, {error, get_error_text(Err)}, StateName, StateData} end; handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> - IsSubs = try maps:get(jid:split(From), StateData#state.subscribers) of + IsSubs = try muc_subscribers_get( + jid:split(From), StateData#state.muc_subscribers) of #subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes} catch _:{badkey, _} -> false end, @@ -843,6 +894,8 @@ handle_info({captcha_failed, From}, normal_state, 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}, @@ -899,7 +952,8 @@ terminate(Reason, _StateName, _ -> ok end, tab_remove_online_user(JID, StateData) - end, [], get_users_and_subscribers(StateData)), + end, [], get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), disable_hibernate_timer(StateData), case StateData#state.hibernate_timer of @@ -991,7 +1045,7 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData end, send_wrapped_multiple( jid:replace_resource(StateData#state.jid, FromNick), - get_users_and_subscribers(StateData), + get_users_and_subscribers_with_node(Node, StateData), NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> @@ -1197,8 +1251,8 @@ get_participant_data(From, StateData) -> #user{nick = FromNick, role = Role} -> {FromNick, Role} catch _:{badkey, _} -> - try maps:get(jid:tolower(jid:remove_resource(From)), - StateData#state.subscribers) of + try muc_subscribers_get(jid:tolower(jid:remove_resource(From)), + StateData#state.muc_subscribers) of #subscriber{nick = FromNick} -> {FromNick, none} catch _:{badkey, _} -> @@ -1329,7 +1383,7 @@ maybe_strip_status_from_presence(From, Packet, StateData) -> close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent andalso maps:size(StateData1#state.users) == 0 - andalso maps:size(StateData1#state.subscribers) == 0 of + andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of true -> ?INFO_MSG("Destroyed MUC room ~ts because it's temporary " "and empty", @@ -1342,6 +1396,17 @@ close_room_if_temporary_and_empty(StateData1) -> -spec get_users_and_subscribers(state()) -> users(). get_users_and_subscribers(StateData) -> + get_users_and_subscribers_aux( + StateData#state.muc_subscribers#muc_subscribers.subscribers, + StateData). + +-spec get_users_and_subscribers_with_node(binary(), state()) -> users(). +get_users_and_subscribers_with_node(Node, StateData) -> + get_users_and_subscribers_aux( + muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers), + StateData). + +get_users_and_subscribers_aux(Subscribers, StateData) -> OnlineSubscribers = maps:fold( fun(LJID, _, Acc) -> LBareJID = jid:remove_resource(LJID), @@ -1365,7 +1430,7 @@ get_users_and_subscribers(StateData) -> true -> Acc end - end, StateData#state.users, StateData#state.subscribers). + end, StateData#state.users, Subscribers). -spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> @@ -1375,7 +1440,7 @@ is_user_online(JID, StateData) -> -spec is_subscriber(jid(), state()) -> boolean(). is_subscriber(JID, StateData) -> LJID = jid:tolower(jid:remove_resource(JID)), - maps:is_key(LJID, StateData#state.subscribers). + muc_subscribers_is_key(LJID, StateData#state.muc_subscribers). %% Check if the user is occupant of the room, or at least is an admin or owner. -spec is_occupant_or_admin(jid(), state()) -> boolean(). @@ -1390,6 +1455,12 @@ is_occupant_or_admin(JID, StateData) -> _ -> false end. +%% Check if the user is an admin or owner. +-spec is_admin(jid(), state()) -> boolean(). +is_admin(JID, StateData) -> + FAffiliation = get_affiliation(JID, StateData), + FAffiliation == admin orelse FAffiliation == owner. + %% Decide the fate of the message and its sender %% Returns: continue_delivery | forget_message | {expulse_sender, Reason} -spec decide_fate_message(message(), jid(), state()) -> @@ -1587,7 +1658,7 @@ do_get_affiliation_fallback(JID, StateData) -> -spec get_affiliations(state()) -> affiliations(). get_affiliations(#state{config = #config{persistent = false}} = StateData) -> - get_affiliations_callback(StateData); + get_affiliations_fallback(StateData); get_affiliations(StateData) -> Room = StateData#state.room, Host = StateData#state.host, @@ -1595,13 +1666,13 @@ get_affiliations(StateData) -> Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:get_affiliations(ServerHost, Room, Host) of {error, _} -> - get_affiliations_callback(StateData); + get_affiliations_fallback(StateData); {ok, Affiliations} -> Affiliations end. --spec get_affiliations_callback(state()) -> affiliations(). -get_affiliations_callback(StateData) -> +-spec get_affiliations_fallback(state()) -> affiliations(). +get_affiliations_fallback(StateData) -> StateData#state.affiliations. -spec get_service_affiliation(jid(), state()) -> owner | none. @@ -1659,7 +1730,17 @@ set_role(JID, Role, StateData) -> end, StateData#state.users, LJIDs), StateData#state.nicks} end, - StateData#state{users = Users, nicks = Nicks}. + 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 + none -> + StateData#state.roles; + NewRole -> + maps:put(jid:remove_resource(LJID), + NewRole, + StateData#state.roles) + end, + StateData#state{users = Users, nicks = Nicks, roles = Roles}. -spec get_role(jid(), state()) -> role(). get_role(JID, StateData) -> @@ -1869,19 +1950,33 @@ set_subscriber(JID, Nick, Nodes, #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> BareJID = jid:remove_resource(JID), LBareJID = jid:tolower(BareJID), - Subscribers = maps:put(LBareJID, - #subscriber{jid = BareJID, - nick = Nick, - nodes = Nodes}, - StateData#state.subscribers), - Nicks = maps:put(Nick, [LBareJID], StateData#state.subscriber_nicks), - NewStateData = StateData#state{subscribers = Subscribers, - subscriber_nicks = Nicks}, + MUCSubscribers = + muc_subscribers_put( + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + StateData#state.muc_subscribers), + NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]), - case not maps:is_key(LBareJID, StateData#state.subscribers) of + case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of true -> - send_subscriptions_change_notifications(BareJID, Nick, subscribe, NewStateData), - ejabberd_hooks:run(muc_subscribed, ServerHost, [ServerHost, Room, Host, BareJID]); + 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, @@ -1921,7 +2016,7 @@ filter_presence(Presence) -> XMLNS = xmpp:get_ns(El), case catch binary:part(XMLNS, 0, size(?NS_MUC)) of ?NS_MUC -> false; - _ -> true + _ -> XMLNS /= ?NS_HATS end end, xmpp:get_els(Presence)), xmpp:set_els(Presence, Els). @@ -1956,7 +2051,8 @@ add_user_presence_un(JID, Presence, StateData) -> -spec find_jids_by_nick(binary(), state()) -> [jid()]. find_jids_by_nick(Nick, StateData) -> Users = case maps:get(Nick, StateData#state.nicks, []) of - [] -> maps:get(Nick, StateData#state.subscriber_nicks, []); + [] -> muc_subscribers_get_by_nick( + Nick, StateData#state.muc_subscribers); Us -> Us end, [jid:make(LJID) || LJID <- Users]. @@ -2020,10 +2116,10 @@ is_nick_change(JID, Nick, StateData) -> nick_collision(User, Nick, StateData) -> UserOfNick = case find_jid_by_nick(Nick, StateData) of false -> - try maps:get(Nick, StateData#state.subscriber_nicks) of - [J] -> J - catch _:{badkey, _} -> false - end; + case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of + [J] -> J; + [] -> false + end; J -> J end, (UserOfNick /= false andalso @@ -2048,15 +2144,15 @@ add_new_user(From, Nick, Packet, StateData) -> 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) + case {ServiceAffiliation == owner orelse + ((((Affiliation == admin orelse Affiliation == owner) andalso NUsers < MaxAdminUsers) orelse NUsers < MaxUsers) - andalso NConferences < MaxConferences, + andalso NConferences < MaxConferences), Collision, mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick), - get_default_role(Affiliation, StateData)} + get_occupant_initial_role(From, Affiliation, StateData)} of {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> Txt = ?T("Too many users in this conference"), @@ -2433,6 +2529,11 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> false -> {none, #presence{type = unavailable}} end, Affiliation = get_affiliation(LJID, StateData), + Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of + true -> ?NS_MUCSUB_NODES_AFFILIATIONS; + false -> ?NS_MUCSUB_NODES_PRESENCE + end, + Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, UserMap = case is_room_overcrowded(StateData) orelse (not (presence_broadcast_allowed(NJID, StateData) orelse @@ -2440,7 +2541,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> true -> #{LNJID => UserInfo}; false -> - get_users_and_subscribers(StateData) + %% TODO: optimize further + UM1 = get_users_and_subscribers_with_node(Node1, StateData), + UM2 = get_users_and_subscribers_with_node(Node2, StateData), + maps:merge(UM1, UM2) end, maps:fold( fun(LUJID, Info, _) -> @@ -2462,13 +2566,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> Pres = if Presence == undefined -> #presence{}; true -> Presence end, - Packet = xmpp:set_subtag( - Pres, #muc_user{items = [Item], - status_codes = StatusCodes}), - Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of - true -> ?NS_MUCSUB_NODES_AFFILIATIONS; - false -> ?NS_MUCSUB_NODES_PRESENCE - end, + Packet = xmpp:set_subtag( + add_presence_hats(NJID, Pres, StateData), + #muc_user{items = [Item], + status_codes = StatusCodes}), send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node1, StateData), Type = xmpp:get_type(Packet), @@ -2476,7 +2577,6 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) and (IsInitialPresence or (Type == unavailable)) -> - Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node2, StateData); true -> @@ -2518,7 +2618,9 @@ send_existing_presences1(ToJID, StateData) -> false -> Item0 end, Packet = xmpp:set_subtag( - Presence, #muc_user{items = [Item]}), + add_presence_hats( + FromJID, Presence, StateData), + #muc_user{items = [Item]}), send_wrapped(jid:replace_resource(StateData#state.jid, FromNick), RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData) end @@ -2607,11 +2709,13 @@ send_nick_changing(JID, OldNick, StateData, end; (_, _, _) -> ok - end, ok, get_users_and_subscribers(StateData)). + end, ok, get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PRESENCE, StateData)). -spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. maybe_send_affiliation(JID, Affiliation, StateData) -> LJID = jid:tolower(JID), + %% TODO: there should be a better way to check IsOccupant Users = get_users_and_subscribers(StateData), IsOccupant = case LJID of {LUser, LServer, <<"">>} -> @@ -2637,7 +2741,8 @@ send_affiliation(JID, Affiliation, StateData) -> role = none}, Message = #message{id = p1_rand:get_string(), sub_els = [#muc_user{items = [Item]}]}, - Users = get_users_and_subscribers(StateData), + Users = get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), Recipients = case (StateData#state.config)#config.anonymous of true -> maps:filter(fun(_, #user{role = moderator}) -> @@ -2917,22 +3022,26 @@ process_item_change(Item, SD, UJID) -> send_kickban_presence(UJID, JID, Reason, 307, SD), set_role(JID, none, SD); {JID, affiliation, none, Reason} -> - case (SD#state.config)#config.members_only of - true -> - send_kickban_presence(UJID, JID, Reason, 321, none, SD), - maybe_send_affiliation(JID, none, SD), - SD1 = set_affiliation(JID, none, SD), - set_role(JID, none, SD1); - _ -> - SD1 = set_affiliation(JID, none, SD), - SD2 = case (SD1#state.config)#config.moderated of - true -> set_role(JID, visitor, SD1); - false -> set_role(JID, participant, SD1) - end, - send_update_presence(JID, Reason, SD2, SD), - maybe_send_affiliation(JID, none, SD2), - SD2 - end; + case get_affiliation(JID, SD) of + none -> SD; + _ -> + case (SD#state.config)#config.members_only of + true -> + send_kickban_presence(UJID, JID, Reason, 321, none, SD), + maybe_send_affiliation(JID, none, SD), + SD1 = set_affiliation(JID, none, SD), + set_role(JID, none, SD1); + _ -> + SD1 = set_affiliation(JID, none, SD), + SD2 = case (SD1#state.config)#config.moderated of + true -> set_role(JID, visitor, SD1); + false -> set_role(JID, participant, SD1) + end, + send_update_presence(JID, Reason, SD2, SD), + maybe_send_affiliation(JID, none, SD2), + SD2 + end + end; {JID, affiliation, outcast, Reason} -> send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), maybe_send_affiliation(JID, outcast, SD), @@ -3271,6 +3380,13 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users), ActorNick = get_actor_nick(MJID, StateData), + %% TODO: optimize further + UserMap = + maps:merge( + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), maps:fold( fun(LJID, Info, _) -> IsSelfPresence = jid:tolower(UJID) == LJID, @@ -3304,7 +3420,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, true -> ok end - end, ok, get_users_and_subscribers(StateData)). + end, ok, UserMap). -spec get_actor_nick(undefined | jid(), state()) -> binary(). get_actor_nick(undefined, _StateData) -> @@ -3551,7 +3667,8 @@ get_config(Lang, StateData, From) -> {allow_voice_requests, Config#config.allow_voice_requests}, {allow_subscription, Config#config.allow_subscription}, {voice_request_min_interval, Config#config.voice_request_min_interval}, - {pubsub, Config#config.pubsub}] + {pubsub, Config#config.pubsub}, + {enable_hats, Config#config.enable_hats}] ++ case ejabberd_captcha:is_feature_available() of true -> @@ -3639,6 +3756,7 @@ set_config(Opts, Config, ServerHost, Lang) -> ({maxusers, V}, C) -> C#config{max_users = V}; ({enablelogging, V}, C) -> C#config{logging = V}; ({pubsub, V}, C) -> C#config{pubsub = V}; + ({enable_hats, V}, C) -> C#config{enable_hats = V}; ({lang, L}, C) -> C#config{lang = L}; ({captcha_whitelist, Js}, C) -> LJIDs = [jid:tolower(J) || J <- Js], @@ -3720,7 +3838,8 @@ send_config_change_info(New, #state{config = Old} = StateData) -> id = p1_rand:get_string(), sub_els = [#muc_user{status_codes = Codes}]}, send_wrapped_multiple(StateData#state.jid, - get_users_and_subscribers(StateData), + get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_CONFIG, StateData), Message, ?NS_MUCSUB_NODES_CONFIG, StateData); @@ -3744,6 +3863,11 @@ remove_nonmembers(StateData) -> -spec set_opts([{atom(), any()}], state()) -> state(). set_opts([], StateData) -> set_vcard_xupdate(StateData); +set_opts([{vcard, Val} | Opts], StateData) + when is_record(Val, vcard_temp) -> + %% default_room_options is setting a default room vcard + ValRaw = fxml:element_to_binary(xmpp:encode(Val)), + set_opts([{vcard, ValRaw} | Opts], StateData); set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> @@ -3868,32 +3992,34 @@ set_opts([{Opt, Val} | Opts], StateData) -> allow_subscription -> StateData#state{config = (StateData#state.config)#config{allow_subscription = Val}}; + enable_hats -> + StateData#state{config = + (StateData#state.config)#config{enable_hats = Val}}; lang -> StateData#state{config = (StateData#state.config)#config{lang = Val}}; subscribers -> - {Subscribers, Nicks} = - lists:foldl( - fun({JID, Nick, Nodes}, {SubAcc, NickAcc}) -> - BareJID = case JID of - #jid{} -> jid:remove_resource(JID); - _ -> - ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]), - jid:remove_resource(jid:make(JID)) - end, - LBareJID = jid:tolower(BareJID), - {maps:put( - LBareJID, - #subscriber{jid = BareJID, - nick = Nick, - nodes = Nodes}, - SubAcc), - maps:put(Nick, [LBareJID], NickAcc)} - end, {#{}, #{}}, Val), - StateData#state{subscribers = Subscribers, - subscriber_nicks = Nicks}; + MUCSubscribers = + lists:foldl( + fun({JID, Nick, Nodes}, MUCSubs) -> + BareJID = + case JID of + #jid{} -> jid:remove_resource(JID); + _ -> + ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]), + jid:remove_resource(jid:make(JID)) + end, + muc_subscribers_put( + #subscriber{jid = BareJID, + nick = Nick, + nodes = Nodes}, + MUCSubs) + end, muc_subscribers_new(), Val), + StateData#state{muc_subscribers = MUCSubscribers}; affiliations -> StateData#state{affiliations = maps:from_list(Val)}; + roles -> + StateData#state{roles = maps:from_list(Val)}; subject -> Subj = if Val == <<"">> -> []; is_binary(Val) -> [#text{data = Val}]; @@ -3901,7 +4027,14 @@ set_opts([{Opt, Val} | Opts], StateData) -> end, StateData#state{subject = Subj}; subject_author -> StateData#state{subject_author = Val}; - _ -> StateData + hats_users -> + Hats = maps:from_list( + lists:map(fun({U, H}) -> {U, maps:from_list(H)} end, + Val)), + StateData#state{hats_users = Hats}; + Other -> + ?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]), + StateData end, set_opts(Opts, NSD). @@ -3920,18 +4053,30 @@ set_vcard_xupdate(#state{config = 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 + true -> + get_occupant_stored_role(Jid, Roles, DefaultRole); + false -> + 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)}). --spec make_opts(state()) -> [{atom(), any()}]. -make_opts(StateData) -> +-spec make_opts(state(), boolean()) -> [{atom(), any()}]. +make_opts(StateData, Hibernation) -> Config = StateData#state.config, - Subscribers = maps:fold( + Subscribers = muc_subscribers_fold( fun(_LJID, Sub, Acc) -> [{Sub#subscriber.jid, Sub#subscriber.nick, Sub#subscriber.nodes}|Acc] - end, [], StateData#state.subscribers), + end, [], StateData#state.muc_subscribers), [?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description), ?MAKE_CONFIG_OPT(#config.allow_change_subj), ?MAKE_CONFIG_OPT(#config.allow_query_users), @@ -3957,14 +4102,19 @@ make_opts(StateData) -> ?MAKE_CONFIG_OPT(#config.vcard), ?MAKE_CONFIG_OPT(#config.vcard_xupdate), ?MAKE_CONFIG_OPT(#config.pubsub), + ?MAKE_CONFIG_OPT(#config.enable_hats), ?MAKE_CONFIG_OPT(#config.lang), {captcha_whitelist, (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, {affiliations, maps:to_list(StateData#state.affiliations)}, + {roles, maps:to_list(StateData#state.roles)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author}, - {hibernation_time, erlang:system_time(microsecond)}, + {hats_users, + lists:map(fun({U, H}) -> {U, maps:to_list(H)} end, + maps:to_list(StateData#state.hats_users))}, + {hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end}, {subscribers, Subscribers}]. expand_opts(CompactOpts) -> @@ -4013,7 +4163,8 @@ destroy_room(DEl, StateData) -> send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, ?NS_MUCSUB_NODES_CONFIG, StateData) - end, ok, get_users_and_subscribers(StateData)), + end, ok, get_users_and_subscribers_with_node( + ?NS_MUCSUB_NODES_CONFIG, StateData)), forget_room(StateData), {result, undefined, stop}. @@ -4053,6 +4204,7 @@ maybe_forget_room(StateData) -> make_disco_info(_From, StateData) -> Config = StateData#state.config, Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_COMMANDS, ?CONFIG_OPT_TO_FEATURE((Config#config.public), <<"muc_public">>, <<"muc_hidden">>), ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), @@ -4092,6 +4244,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang, DiscoInfo = make_disco_info(From, StateData), Extras = iq_disco_info_extras(Lang, StateData, false), {result, DiscoInfo#disco_info{xdata = [Extras]}}; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = ?NS_COMMANDS}]}, + StateData) -> + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + {result, + #disco_info{ + identities = [#identity{category = <<"automation">>, + type = <<"command-list">>, + name = translate:translate( + Lang, ?T("Commands"))}]}}; + false -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]}, + StateData) -> + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + {result, + #disco_info{ + identities = [#identity{category = <<"automation">>, + type = <<"command-node">>, + name = translate:translate( + Lang, ?T("Add a hat to a user"))}], + features = [?NS_COMMANDS]}}; + false -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]}, + StateData) -> + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + {result, + #disco_info{ + identities = [#identity{category = <<"automation">>, + type = <<"command-node">>, + name = translate:translate( + Lang, ?T("Remove a hat from a user"))}], + features = [?NS_COMMANDS]}}; + false -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]}, + StateData) -> + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + {result, + #disco_info{ + identities = [#identity{category = <<"automation">>, + type = <<"command-node">>, + name = translate:translate( + Lang, ?T("List users with hats"))}], + features = [?NS_COMMANDS]}}; + false -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = Node}]}, StateData) -> @@ -4123,6 +4346,7 @@ iq_disco_info_extras(Lang, StateData, Static) -> {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, AllowPM}, {lang, Config#config.lang}], Fs2 = case Config#config.pubsub of @@ -4172,6 +4396,46 @@ process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>> {result, #disco_items{}} end end; +process_iq_disco_items(From, #iq{type = get, lang = Lang, + sub_els = [#disco_items{node = ?NS_COMMANDS}]}, + StateData) -> + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + {result, + #disco_items{ + items = [#disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_ADD_CMD, + name = translate:translate( + Lang, ?T("Add a hat to a user"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_REMOVE_CMD, + name = translate:translate( + Lang, ?T("Remove a hat from a user"))}, + #disco_item{jid = StateData#state.jid, + node = ?MUC_HAT_LIST_CMD, + name = translate:translate( + Lang, ?T("List users with hats"))}]}}; + false -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end; +process_iq_disco_items(From, #iq{type = get, lang = Lang, + sub_els = [#disco_items{node = Node}]}, + StateData) + when Node == ?MUC_HAT_ADD_CMD; + Node == ?MUC_HAT_REMOVE_CMD; + Node == ?MUC_HAT_LIST_CMD -> + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + {result, #disco_items{}}; + false -> + Txt = ?T("Node not found"), + {error, xmpp:err_item_not_found(Txt, Lang)} + end; process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)}. @@ -4248,30 +4512,35 @@ process_iq_mucsub(From, sub_els = [#muc_subscribe{nick = Nick}]} = Packet, StateData) -> LBareJID = jid:tolower(jid:remove_resource(From)), - try maps:get(LBareJID, StateData#state.subscribers) of + try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of #subscriber{nick = Nick1} when Nick1 /= Nick -> Nodes = get_subscription_nodes(Packet), - case {nick_collision(From, Nick, StateData), - mod_muc:can_use_nick(StateData#state.server_host, - StateData#state.host, - From, Nick)} of - {true, _} -> + case nick_collision(From, Nick, StateData) of + true -> ErrText = ?T("That nickname is already in use by another occupant"), {error, xmpp:err_conflict(ErrText, Lang)}; - {_, false} -> - Err = case Nick of - <<>> -> - xmpp:err_jid_malformed(?T("Nickname can't be empty"), - Lang); - _ -> - xmpp:err_conflict(?T("That nickname is registered" - " by another person"), Lang) - end, - {error, Err}; - _ -> - NewStateData = set_subscriber(From, Nick, Nodes, StateData), - {result, subscribe_result(Packet), NewStateData} - end; + false -> + case mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, + From, Nick) of + false -> + Err = case Nick of + <<>> -> + xmpp:err_jid_malformed( + ?T("Nickname can't be empty"), + Lang); + _ -> + xmpp:err_conflict( + ?T("That nickname is registered" + " by another person"), Lang) + end, + {error, Err}; + true -> + NewStateData = + set_subscriber(From, Nick, Nodes, StateData), + {result, subscribe_result(Packet), NewStateData} + end + end; #subscriber{} -> Nodes = get_subscription_nodes(Packet), NewStateData = set_subscriber(From, Nick, Nodes, StateData), @@ -4298,15 +4567,27 @@ process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> BareJID = jid:remove_resource(From), LBareJID = jid:tolower(BareJID), - try maps:get(LBareJID, StateData#state.subscribers) of - #subscriber{nick = Nick} -> - Nicks = maps:remove(Nick, StateData#state.subscriber_nicks), - Subscribers = maps:remove(LBareJID, StateData#state.subscribers), - NewStateData = StateData#state{subscribers = Subscribers, - subscriber_nicks = Nicks}, + try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of + {MUCSubscribers, #subscriber{nick = Nick}} -> + NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, store_room(NewStateData, [{del_subscription, LBareJID}]), - send_subscriptions_change_notifications(BareJID, Nick, unsubscribe, StateData), - ejabberd_hooks:run(muc_unsubscribed, ServerHost, [ServerHost, Room, Host, BareJID]), + 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 @@ -4326,7 +4607,7 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang, true -> ShowJid = IsModerator orelse (StateData#state.config)#config.anonymous == false, - Subs = maps:fold( + Subs = muc_subscribers_fold( fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) -> case ShowJid of true -> @@ -4334,7 +4615,7 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang, _ -> [#muc_subscription{nick = N, events = Nodes}|Acc] end - end, [], StateData#state.subscribers), + end, [], StateData#state.muc_subscribers), {result, #muc_subscriptions{list = Subs}, StateData}; _ -> Txt = ?T("Moderator privileges required"), @@ -4347,8 +4628,7 @@ process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> -spec remove_subscriptions(state()) -> state(). remove_subscriptions(StateData) -> if not (StateData#state.config)#config.allow_subscription -> - StateData#state{subscribers = #{}, - subscriber_nicks = #{}}; + StateData#state{muc_subscribers = muc_subscribers_new()}; true -> StateData end. @@ -4413,6 +4693,271 @@ get_mucroom_disco_items(StateData) -> end, [], StateData#state.nicks), #disco_items{items = Items}. +-spec process_iq_adhoc(jid(), iq(), state()) -> + {result, adhoc_command()} | + {result, adhoc_command(), state()} | + {error, stanza_error()}. +process_iq_adhoc(_From, #iq{type = get}, _StateData) -> + {error, xmpp:err_bad_request()}; +process_iq_adhoc(From, #iq{type = set, lang = Lang1, + sub_els = [#adhoc_command{} = Request]}, + StateData) -> + % Ad-Hoc Commands are used only for Hats here + case (StateData#state.config)#config.enable_hats andalso + is_admin(From, StateData) + of + true -> + #adhoc_command{lang = Lang2, node = Node, + action = Action, xdata = XData} = Request, + Lang = case Lang2 of + <<"">> -> Lang1; + _ -> Lang2 + end, + case {Node, Action} of + {_, cancel} -> + {result, + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{status = canceled, lang = Lang, + node = Node})}; + {?MUC_HAT_ADD_CMD, execute} -> + Form = + #xdata{ + title = translate:translate( + Lang, ?T("Add a hat to a user")), + type = form, + fields = + [#xdata_field{ + type = 'jid-single', + label = translate:translate(Lang, ?T("Jabber ID")), + required = true, + var = <<"jid">>}, + #xdata_field{ + type = 'text-single', + label = translate:translate(Lang, ?T("Hat title")), + var = <<"hat_title">>}, + #xdata_field{ + type = 'text-single', + label = translate:translate(Lang, ?T("Hat URI")), + required = true, + var = <<"hat_uri">>} + ]}, + {result, + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{ + status = executing, + xdata = Form})}; + {?MUC_HAT_ADD_CMD, complete} when XData /= undefined -> + JID = try + jid:decode(hd(xmpp_util:get_xdata_values( + <<"jid">>, XData))) + catch _:_ -> error + end, + URI = try + hd(xmpp_util:get_xdata_values( + <<"hat_uri">>, XData)) + catch _:_ -> error + end, + Title = case xmpp_util:get_xdata_values( + <<"hat_title">>, XData) of + [] -> <<"">>; + [T] -> T + end, + if + (JID /= error) and (URI /= error) -> + case add_hat(JID, URI, Title, StateData) of + {ok, NewStateData} -> + store_room(NewStateData), + send_update_presence( + JID, NewStateData, StateData), + {result, + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{status = completed}), + NewStateData}; + {error, size_limit} -> + Txt = ?T("Hats limit exceeded"), + {error, xmpp:err_not_allowed(Txt, Lang)} + end; + true -> + {error, xmpp:err_bad_request()} + end; + {?MUC_HAT_ADD_CMD, complete} -> + {error, xmpp:err_bad_request()}; + {?MUC_HAT_ADD_CMD, _} -> + Txt = ?T("Incorrect value of 'action' attribute"), + {error, xmpp:err_bad_request(Txt, Lang)}; + {?MUC_HAT_REMOVE_CMD, execute} -> + Form = + #xdata{ + title = translate:translate( + Lang, ?T("Remove a hat from a user")), + type = form, + fields = + [#xdata_field{ + type = 'jid-single', + label = translate:translate(Lang, ?T("Jabber ID")), + required = true, + var = <<"jid">>}, + #xdata_field{ + type = 'text-single', + label = translate:translate(Lang, ?T("Hat URI")), + required = true, + var = <<"hat_uri">>} + ]}, + {result, + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{ + status = executing, + xdata = Form})}; + {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined -> + JID = try + jid:decode(hd(xmpp_util:get_xdata_values( + <<"jid">>, XData))) + catch _:_ -> error + end, + URI = try + hd(xmpp_util:get_xdata_values( + <<"hat_uri">>, XData)) + catch _:_ -> error + end, + if + (JID /= error) and (URI /= error) -> + NewStateData = del_hat(JID, URI, StateData), + store_room(NewStateData), + send_update_presence( + JID, NewStateData, StateData), + {result, + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{status = completed}), + NewStateData}; + true -> + {error, xmpp:err_bad_request()} + end; + {?MUC_HAT_REMOVE_CMD, complete} -> + {error, xmpp:err_bad_request()}; + {?MUC_HAT_REMOVE_CMD, _} -> + Txt = ?T("Incorrect value of 'action' attribute"), + {error, xmpp:err_bad_request(Txt, Lang)}; + {?MUC_HAT_LIST_CMD, execute} -> + Hats = get_all_hats(StateData), + Items = + lists:map( + fun({JID, URI, Title}) -> + [#xdata_field{ + var = <<"jid">>, + values = [jid:encode(JID)]}, + #xdata_field{ + var = <<"hat_title">>, + values = [URI]}, + #xdata_field{ + var = <<"hat_uri">>, + values = [Title]}] + end, Hats), + Form = + #xdata{ + title = translate:translate( + Lang, ?T("List of users with hats")), + type = result, + reported = + [#xdata_field{ + label = translate:translate(Lang, ?T("Jabber ID")), + var = <<"jid">>}, + #xdata_field{ + label = translate:translate(Lang, ?T("Hat title")), + var = <<"hat_title">>}, + #xdata_field{ + label = translate:translate(Lang, ?T("Hat URI")), + var = <<"hat_uri">>}], + items = Items}, + {result, + xmpp_util:make_adhoc_response( + Request, + #adhoc_command{ + status = completed, + xdata = Form})}; + {?MUC_HAT_LIST_CMD, _} -> + Txt = ?T("Incorrect value of 'action' attribute"), + {error, xmpp:err_bad_request(Txt, Lang)}; + _ -> + {error, xmpp:err_item_not_found()} + end; + _ -> + {error, xmpp:err_forbidden()} + end. + +-spec add_hat(jid(), binary(), binary(), state()) -> + {ok, state()} | {error, size_limit}. +add_hat(JID, URI, Title, StateData) -> + Hats = StateData#state.hats_users, + LJID = jid:remove_resource(jid:tolower(JID)), + UserHats = maps:get(LJID, Hats, #{}), + UserHats2 = maps:put(URI, Title, UserHats), + USize = maps:size(UserHats2), + if + USize =< ?MAX_HATS_PER_USER -> + Hats2 = maps:put(LJID, UserHats2, Hats), + Size = maps:size(Hats2), + if + Size =< ?MAX_HATS_USERS -> + {ok, StateData#state{hats_users = Hats2}}; + true -> + {error, size_limit} + end; + true -> + {error, size_limit} + end. + +-spec del_hat(jid(), binary(), state()) -> state(). +del_hat(JID, URI, StateData) -> + Hats = StateData#state.hats_users, + LJID = jid:remove_resource(jid:tolower(JID)), + UserHats = maps:get(LJID, Hats, #{}), + UserHats2 = maps:remove(URI, UserHats), + Hats2 = + case maps:size(UserHats2) of + 0 -> + maps:remove(LJID, Hats); + _ -> + maps:put(LJID, UserHats2, Hats) + end, + StateData#state{hats_users = Hats2}. + +-spec get_all_hats(state()) -> list({jid(), binary(), binary()}). +get_all_hats(StateData) -> + lists:flatmap( + fun({LJID, H}) -> + JID = jid:make(LJID), + lists:map(fun({URI, Title}) -> {JID, URI, Title} end, + maps:to_list(H)) + end, + maps:to_list(StateData#state.hats_users)). + +-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}. +add_presence_hats(JID, Pres, StateData) -> + case (StateData#state.config)#config.enable_hats of + true -> + Hats = StateData#state.hats_users, + LJID = jid:remove_resource(jid:tolower(JID)), + UserHats = maps:get(LJID, Hats, #{}), + case maps:size(UserHats) of + 0 -> Pres; + _ -> + Items = + lists:map(fun({URI, Title}) -> + #muc_hat{uri = URI, title = Title} + end, + maps:to_list(UserHats)), + xmpp:set_subtag(Pres, + #muc_hats{hats = Items}) + end; + false -> + Pres + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Voice request support @@ -4547,13 +5092,13 @@ add_to_log(Type, Data, StateData) when Type == roomconfig_change_disabledlogging -> mod_muc_log:add_to_log(StateData#state.server_host, roomconfig_change, Data, StateData#state.jid, - make_opts(StateData)); + make_opts(StateData, false)); add_to_log(Type, Data, StateData) -> case (StateData#state.config)#config.logging of true -> mod_muc_log:add_to_log(StateData#state.server_host, Type, Data, StateData#state.jid, - make_opts(StateData)); + make_opts(StateData, false)); false -> ok end. @@ -4597,7 +5142,7 @@ store_room(StateData, ChangesHints) -> true -> ok; _ -> - erlang:put(muc_subscribers, StateData#state.subscribers) + erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers) end, ShouldStore = case (StateData#state.config)#config.persistent of true -> @@ -4611,66 +5156,51 @@ store_room(StateData, ChangesHints) -> end end, if ShouldStore -> - store_room_no_checks(StateData, ChangesHints); + case erlang:function_exported(Mod, store_changes, 4) of + true when ChangesHints /= [] -> + mod_muc:store_changes( + StateData#state.server_host, + StateData#state.host, StateData#state.room, + ChangesHints); + _ -> + store_room_no_checks(StateData, ChangesHints, false), + ok + end; true -> ok end. -store_room_no_checks(StateData, ChangesHints) -> +-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), + make_opts(StateData, Hibernation), ChangesHints). --spec send_subscriptions_change_notifications(jid(), binary(), subscribe|unsubscribe, state()) -> ok. -send_subscriptions_change_notifications(From, Nick, Type, State) -> +-spec send_subscriptions_change_notifications(stanza(), stanza(), state()) -> ok. +send_subscriptions_change_notifications(Packet, PacketWithoutJid, State) -> {WJ, WN} = - maps:fold( - fun(_, #subscriber{nodes = Nodes, jid = JID}, {WithJid, WithNick} = Res) -> - case lists:member(?NS_MUCSUB_NODES_SUBSCRIBERS, Nodes) of - true -> - case (State#state.config)#config.anonymous == false orelse - get_role(JID, State) == moderator orelse - get_default_role(get_affiliation(JID, State), State) == moderator of - true -> - {[JID | WithJid], WithNick}; - _ -> - {WithJid, [JID | WithNick]} - end; - false -> - Res - end - end, {[], []}, State#state.subscribers), + maps:fold( + fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) -> + case (State#state.config)#config.anonymous == false orelse + get_role(JID, State) == moderator orelse + get_default_role(get_affiliation(JID, State), State) == moderator of + true -> + {[JID | WithJid], WithNick}; + _ -> + {WithJid, [JID | WithNick]} + end + end, {[], []}, + muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS, + State#state.muc_subscribers)), if WJ /= [] -> - Payload1 = case Type of - subscribe -> #muc_subscribe{jid = From, nick = Nick}; - _ -> #muc_unsubscribe{jid = From, nick = Nick} - end, - Packet1 = #message{ - sub_els = [#ps_event{ - items = #ps_items{ - node = ?NS_MUCSUB_NODES_SUBSCRIBERS, - items = [#ps_item{ - id = p1_rand:get_string(), - sub_els = [Payload1]}]}}]}, ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, - WJ, Packet1, true); + WJ, Packet, false); true -> ok end, if WN /= [] -> - Payload2 = case Type of - subscribe -> #muc_subscribe{nick = Nick}; - _ -> #muc_unsubscribe{nick = Nick} - end, - Packet2 = #message{ - sub_els = [#ps_event{ - items = #ps_items{ - node = ?NS_MUCSUB_NODES_SUBSCRIBERS, - items = [#ps_item{ - id = p1_rand:get_string(), - sub_els = [Payload2]}]}}]}, ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, - WN, Packet2, true); + WN, PacketWithoutJid, false); true -> ok end. @@ -4684,7 +5214,7 @@ send_wrapped(From, To, Packet, Node, State) -> _ -> false end, if IsOffline -> - try maps:get(LBareTo, State#state.subscribers) of + try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of #subscriber{nodes = Nodes, jid = JID} -> case lists:member(Node, Nodes) of true -> @@ -4751,12 +5281,13 @@ send_wrapped_multiple(From, Users, Packet, Node, State) -> IsOffline = LP == undefined, if IsOffline -> LBareTo = jid:tolower(jid:remove_resource(To)), - case maps:find(LBareTo, State#state.subscribers) of + case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of {ok, #subscriber{nodes = Nodes}} -> case lists:member(Node, Nodes) of true -> {Direct, [To | Wrapped]}; _ -> + %% TODO: check that this branch is never called Res end; _ -> @@ -4810,6 +5341,107 @@ send_wrapped_multiple(From, Users, Packet, Node, State) -> Wra, NewPacket2, true) end. +%%%---------------------------------------------------------------------- +%%% #muc_subscribers API +%%%---------------------------------------------------------------------- + +-spec muc_subscribers_new() -> #muc_subscribers{}. +muc_subscribers_new() -> + #muc_subscribers{}. + +-spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}. +muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) -> + maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_find(ljid(), #muc_subscribers{}) -> + {ok, #subscriber{}} | error. +muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) -> + maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean(). +muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) -> + maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_size(#muc_subscribers{}) -> integer(). +muc_subscribers_size(MUCSubscribers) -> + maps:size(MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when + Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc). +muc_subscribers_fold(Fun, Init, MUCSubscribers) -> + maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers). + +-spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}]. +muc_subscribers_get_by_nick(Nick, MUCSubscribers) -> + maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []). + +-spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers(). +muc_subscribers_get_by_node(Node, MUCSubscribers) -> + maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}). + +-spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) -> + {#muc_subscribers{}, #subscriber{}}. +muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) -> + #muc_subscribers{subscribers = Subs, + subscriber_nicks = SubNicks, + subscriber_nodes = SubNodes} = MUCSubscribers, + Subscriber = maps:get(LJID, Subs), + #subscriber{nick = Nick, nodes = Nodes} = Subscriber, + NewSubNicks = maps:remove(Nick, SubNicks), + NewSubs = maps:remove(LJID, Subs), + NewSubNodes = + lists:foldl( + fun(Node, Acc) -> + NodeSubs = maps:get(Node, Acc, #{}), + NodeSubs2 = maps:remove(LJID, NodeSubs), + maps:put(Node, NodeSubs2, Acc) + end, SubNodes, Nodes), + {#muc_subscribers{subscribers = NewSubs, + subscriber_nicks = NewSubNicks, + subscriber_nodes = NewSubNodes}, Subscriber}. + +-spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) -> + #muc_subscribers{}. +muc_subscribers_put(Subscriber, MUCSubscribers) -> + #subscriber{jid = JID, + nick = Nick, + nodes = Nodes} = Subscriber, + #muc_subscribers{subscribers = Subs, + subscriber_nicks = SubNicks, + subscriber_nodes = SubNodes} = MUCSubscribers, + LJID = jid:tolower(JID), + NewSubs = maps:put(LJID, Subscriber, Subs), + NewSubNicks = maps:put(Nick, [LJID], SubNicks), + NewSubNodes = + lists:foldl( + fun(Node, Acc) -> + NodeSubs = maps:get(Node, Acc, #{}), + NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs), + maps:put(Node, NodeSubs2, Acc) + end, SubNodes, Nodes), + #muc_subscribers{subscribers = NewSubs, + subscriber_nicks = NewSubNicks, + subscriber_nodes = NewSubNodes}. + + +cleanup_affiliations(State) -> + case mod_muc_opt:cleanup_affiliations_on_start(State#state.server_host) of + true -> + Affiliations = + maps:filter( + fun({LUser, LServer, _}, _) -> + case ejabberd_router:is_my_host(LServer) of + true -> + ejabberd_auth:user_exists(LUser, LServer); + false -> + true + end + end, State#state.affiliations), + State#state{affiliations = Affiliations}; + false -> + State + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaningful content -spec has_body_or_subject(message()) -> boolean(). diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl index 569cfac49..3fed0bf2b 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,7 +29,8 @@ -behaviour(mod_muc_room). %% API --export([init/2, store_room/5, restore_room/3, forget_room/3, +-export([init/2, store_room/5, store_changes/4, + restore_room/3, forget_room/3, can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4, import/3, export/1]). -export([register_online_room/4, unregister_online_room/4, find_online_room/3, @@ -37,7 +38,8 @@ register_online_user/4, unregister_online_user/4, count_online_rooms_by_user/3, get_online_rooms_by_user/3, get_subscribed_rooms/3, get_rooms_without_subscribers/2, - find_online_room_by_pid/2]). + 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]). @@ -63,13 +65,19 @@ store_room(LServer, Host, Name, Opts, ChangesHints) -> _ -> {[], 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"]), + "opts=%(SOpts)s", + "created_at=%(Timestamp)t"]), case ChangesHints of Changes when is_list(Changes) -> [change_room(Host, Name, Change) || Change <- Changes]; @@ -83,6 +91,12 @@ store_room(LServer, Host, Name, Opts, ChangesHints) -> end, ejabberd_sql:sql_transaction(LServer, F). +store_changes(LServer, Host, Name, Changes) -> + F = fun () -> + [change_room(Host, Name, Change) || Change <- Changes] + end, + ejabberd_sql:sql_transaction(LServer, F). + change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) -> SJID = jid:encode(JID), SNodes = misc:term_to_expr(Nodes), @@ -172,6 +186,23 @@ get_rooms_without_subscribers(LServer, Host) -> [] 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 -> + [] + end. + get_rooms(LServer, Host) -> case catch ejabberd_sql:sql_query( LServer, @@ -185,13 +216,20 @@ get_rooms(LServer, Host) -> {selected, Subs} -> SubsD = lists:foldl( fun({Room, Jid, Nick, Nodes}, Dict) -> - dict:append(Room, {jid:decode(Jid), - Nick, ejabberd_sql:decode_term(Nodes)}, Dict) - end, dict:new(), Subs), + Sub = {jid:decode(Jid), + Nick, ejabberd_sql:decode_term(Nodes)}, + maps:update_with( + Room, + fun(SubAcc) -> + [Sub | SubAcc] + end, + [Sub], + Dict) + end, maps:new(), Subs), lists:map( fun({Room, Opts}) -> OptsD = ejabberd_sql:decode_term(Opts), - OptsD2 = case {dict:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of + OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of {_, true} -> store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined), OptsD; @@ -451,6 +489,13 @@ get_subscribed_rooms(LServer, Host, Jid) -> {error, db_failure} end. +remove_user(LUser, LServer) -> + SJID = jid:encode(jid:make(LUser, LServer)), + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from muc_room_subscribers where jid=%(SJID)s")), + ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -476,3 +521,12 @@ clean_tables(ServerHost) -> ?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])) + end. diff --git a/src/mod_muc_sup.erl b/src/mod_muc_sup.erl index 62240c857..2c2d62313 100644 --- a/src/mod_muc_sup.erl +++ b/src/mod_muc_sup.erl @@ -2,7 +2,7 @@ %%% Created : 4 Jul 2019 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -51,7 +51,7 @@ procname(Host) -> %%% Supervisor callbacks %%%=================================================================== init([Host]) -> - Cores = erlang:system_info(logical_processors), + Cores = misc:logical_processors(), Specs = lists:foldl( fun(I, Acc) -> [#{id => mod_muc:procname(Host, I), diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index 161d3a4c4..a702f0f74 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -5,7 +5,7 @@ %%% Created : 29 May 2007 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -35,7 +35,7 @@ %% API -export([start/2, stop/1, reload/3, - user_send_packet/1]). + user_send_packet/1]). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, @@ -51,11 +51,6 @@ response, ts :: integer()}). --record(dest, {jid_string :: binary() | none, - jid_jid :: jid() | undefined, - type :: bcc | cc | noreply | ofrom | replyroom | replyto | to, - address :: address()}). - -type limit_value() :: {default | custom, integer()}. -record(limits, {message :: limit_value(), presence :: limit_value()}). @@ -63,14 +58,6 @@ -record(service_limits, {local :: #limits{}, remote :: #limits{}}). --type routing() :: route_single | {route_multicast, binary(), #service_limits{}}. - --record(group, {server :: binary(), - dests :: [#dest{}], - multicast :: routing() | undefined, - others :: [address()], - addresses :: [address()]}). - -record(state, {lserver :: binary(), lservice :: binary(), access :: atom(), @@ -79,8 +66,6 @@ %% All the elements are of type value() --define(VERSION_MULTICAST, <<"$Revision: 440 $ ">>). - -define(PURGE_PROCNAME, ejabberd_mod_multicast_purgeloop). @@ -92,8 +77,6 @@ -define(CACHE_PURGE_TIMER, 86400000). --define(DISCO_QUERY_TIMEOUT, 10000). - -define(DEFAULT_LIMIT_LOCAL_MESSAGE, 100). -define(DEFAULT_LIMIT_LOCAL_PRESENCE, 100). @@ -117,7 +100,7 @@ reload(LServerS, NewOpts, OldOpts) -> user_send_packet({#presence{} = Packet, C2SState} = Acc) -> case xmpp:get_subtag(Packet, #addresses{}) of #addresses{list = Addresses} -> - {ToDeliver, _Delivereds} = split_addresses_todeliver(Addresses), + {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses), NewState = lists:foldl( fun(Address, St) -> @@ -138,7 +121,7 @@ user_send_packet({#presence{} = Packet, C2SState} = Acc) -> undefined -> St end - end, C2SState, ToDeliver), + end, C2SState, CC ++ BCC), {Packet, NewState}; false -> Acc @@ -281,8 +264,6 @@ process_iq(#iq{type = T}, _) when T == set; T == get -> process_iq(_, _) -> reply. --define(FEATURE(Feat), Feat). - iq_disco_info(From, Lang, State) -> Name = mod_multicast_opt:name(State#state.lserver), #disco_info{ @@ -308,19 +289,10 @@ iq_vcard(Lang, State) -> %%%------------------------- -spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'. -route_trusted(LServiceS, LServerS, FromJID, - Destinations, Packet) -> - Packet_stripped = Packet, - Delivereds = [], - Dests2 = lists:map( - fun(D) -> - #dest{jid_string = jid:encode(D), - jid_jid = D, type = bcc, - address = #address{type = bcc, jid = D}} - end, Destinations), - Groups = group_dests(Dests2), - route_common(LServerS, LServiceS, FromJID, Groups, - Delivereds, Packet_stripped). +route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) -> + Addresses = [#address{type = bcc, jid = D} || D <- Destinations], + Groups = group_by_destinations(Addresses, #{}), + route_grouped(LServerS, LServiceS, FromJID, Groups, [], Packet). -spec route_untrusted(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'. route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) -> @@ -356,50 +328,88 @@ route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) -> route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) -> FromJID = xmpp:get_from(Packet), ok = check_access(LServerS, Access, FromJID), - {ok, Packet_stripped, Addresses} = strip_addresses_element(Packet), - {To_deliver, Delivereds} = split_addresses_todeliver(Addresses), - Dests = convert_dest_record(To_deliver), - {Dests2, Not_jids} = split_dests_jid(Dests), - report_not_jid(FromJID, Packet, Not_jids), - ok = check_limit_dests(SLimits, FromJID, Packet, Dests2), - Groups = group_dests(Dests2), + {ok, PacketStripped, Addresses} = strip_addresses_element(Packet), + {CC, BCC, NotJids, Rest} = partition_addresses(Addresses), + report_not_jid(FromJID, Packet, NotJids), + ok = check_limit_dests(SLimits, FromJID, Packet, length(CC) + length(BCC)), + Groups0 = group_by_destinations(CC, #{}), + Groups = group_by_destinations(BCC, Groups0), ok = check_relay(FromJID#jid.server, LServerS, Groups), - route_common(LServerS, LServiceS, FromJID, Groups, - Delivereds, Packet_stripped). + route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped). --spec route_common(binary(), binary(), jid(), [#group{}], - [address()], stanza()) -> 'ok'. -route_common(LServerS, LServiceS, FromJID, Groups, - Delivereds, Packet_stripped) -> - Groups2 = look_cached_servers(LServerS, LServiceS, Groups), - Groups3 = build_others_xml(Groups2), - Groups4 = add_addresses(Delivereds, Groups3), - AGroups = decide_action_groups(Groups4), - act_groups(FromJID, Packet_stripped, LServiceS, - AGroups). +-spec mark_as_delivered([address()]) -> [address()]. +mark_as_delivered(Addresses) -> + [A#address{delivered = true} || A <- Addresses]. --spec act_groups(jid(), stanza(), binary(), [{routing(), #group{}}]) -> 'ok'. -act_groups(FromJID, Packet_stripped, LServiceS, AGroups) -> +-spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok. +route_individual(From, CC, BCC, Other, Packet) -> + CCDelivered = mark_as_delivered(CC), + Addresses = CCDelivered ++ Other, + PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]), lists:foreach( - fun(AGroup) -> - perform(FromJID, Packet_stripped, LServiceS, - AGroup) - end, AGroups). - --spec perform(jid(), stanza(), binary(), - {routing(), #group{}}) -> 'ok'. -perform(From, Packet, _, - {route_single, Group}) -> + fun(#address{jid = To}) -> + ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)) + end, CC), lists:foreach( - fun(ToUser) -> - Group_others = strip_other_bcc(ToUser, Group#group.others), - route_packet(From, ToUser, Packet, - Group_others, Group#group.addresses) - end, Group#group.dests); -perform(From, Packet, _, - {{route_multicast, JID, RLimits}, Group}) -> - route_packet_multicast(From, JID, Packet, - Group#group.dests, Group#group.addresses, RLimits). + fun(#address{jid = To} = Address) -> + Packet2 = case Addresses of + [] -> + Packet; + _ -> + xmpp:append_subtags(Packet, [#addresses{list = [Address | Addresses]}]) + end, + ejabberd_router:route(xmpp:set_from_to(Packet2, From, To)) + end, BCC). + +-spec route_chunk(jid(), jid(), stanza(), [address()]) -> ok. +route_chunk(From, To, Packet, Addresses) -> + PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]), + ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)). + +-spec route_in_chunks(jid(), jid(), stanza(), integer(), [address()], [address()], [address()]) -> ok. +route_in_chunks(_From, _To, _Packet, _Limit, [], [], _) -> + ok; +route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(CC) > Limit -> + {Chunk, Rest} = lists:split(Limit, CC), + route_chunk(From, To, Packet, Chunk ++ RestOfAddresses), + route_in_chunks(From, To, Packet, Limit, Rest, BCC, RestOfAddresses); +route_in_chunks(From, To, Packet, Limit, [], BCC, RestOfAddresses) when length(BCC) > Limit -> + {Chunk, Rest} = lists:split(Limit, BCC), + route_chunk(From, To, Packet, Chunk ++ RestOfAddresses), + route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses); +route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(BCC) + length(CC) > Limit -> + {Chunk, Rest} = lists:split(Limit - length(CC), BCC), + route_chunk(From, To, Packet, CC ++ Chunk ++ RestOfAddresses), + route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses); +route_in_chunks(From, To, Packet, _Limit, CC, BCC, RestOfAddresses) -> + route_chunk(From, To, Packet, CC ++ BCC ++ RestOfAddresses). + +-spec route_multicast(jid(), jid(), [address()], [address()], [address()], stanza(), #limits{}) -> ok. +route_multicast(From, To, CC, BCC, RestOfAddresses, Packet, Limits) -> + {_Type, Limit} = get_limit_number(element(1, Packet), + Limits), + route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses). + +-spec route_grouped(binary(), binary(), jid(), #{}, [address()], stanza()) -> ok. +route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) -> + maps:fold( + fun(Server, {CC, BCC}, _) -> + OtherCC = maps:fold( + fun(Server2, _, Res) when Server2 == Server -> + Res; + (_, {CC2, _}, Res) -> + mark_as_delivered(CC2) ++ Res + end, [], Groups), + case search_server_on_cache(Server, + LServer, LService, + {?MAXTIME_CACHE_POSITIVE, + ?MAXTIME_CACHE_NEGATIVE}) of + route_single -> + route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet); + {route_multicast, Service, Limits} -> + route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) + end + end, ok, Groups). %%%------------------------- %%% Check access permission @@ -425,245 +435,89 @@ strip_addresses_element(Packet) -> throw(eadsele) end. -%%%------------------------- -%%% Strip third-party bcc 'addresses' -%%%------------------------- - -strip_other_bcc(#dest{jid_jid = ToUserJid}, Group_others) -> - lists:filter( - fun(#address{jid = JID, type = Type}) -> - case {JID, Type} of - {ToUserJid, bcc} -> true; - {_, bcc} -> false; - _ -> true - end - end, - Group_others). - %%%------------------------- %%% Split Addresses %%%------------------------- --spec split_addresses_todeliver([address()]) -> {[address()], [address()]}. -split_addresses_todeliver(Addresses) -> - lists:partition( - fun(#address{delivered = true}) -> - false; - (#address{type = Type}) -> - case Type of - to -> true; - cc -> true; - bcc -> true; - _ -> false - end - end, Addresses). +partition_addresses(Addresses) -> + lists:foldl( + fun(#address{delivered = true} = A, {C, B, I, D}) -> + {C, B, I, [A | D]}; + (#address{type = T, jid = undefined} = A, {C, B, I, D}) + when T == to; T == cc; T == bcc -> + {C, B, [A | I], D}; + (#address{type = T} = A, {C, B, I, D}) + when T == to; T == cc -> + {[A | C], B, I, D}; + (#address{type = bcc} = A, {C, B, I, D}) -> + {C, [A | B], I, D}; + (A, {C, B, I, D}) -> + {C, B, I, [A | D]} + end, {[], [], [], []}, Addresses). %%%------------------------- %%% Check does not exceed limit of destinations %%%------------------------- --spec check_limit_dests(#service_limits{}, jid(), stanza(), [address()]) -> ok. -check_limit_dests(SLimits, FromJID, Packet, - Addresses) -> +-spec check_limit_dests(#service_limits{}, jid(), stanza(), integer()) -> ok. +check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) -> SenderT = sender_type(FromJID), Limits = get_slimit_group(SenderT, SLimits), - Type_of_stanza = type_of_stanza(Packet), - {_Type, Limit_number} = get_limit_number(Type_of_stanza, - Limits), - case length(Addresses) > Limit_number of + StanzaType = type_of_stanza(Packet), + {_Type, Limit} = get_limit_number(StanzaType, + Limits), + case NumOfAddresses > Limit of false -> ok; true -> throw(etoorec) end. -%%%------------------------- -%%% Convert Destination XML to record -%%%------------------------- --spec convert_dest_record([address()]) -> [#dest{}]. -convert_dest_record(Addrs) -> - lists:map( - fun(#address{jid = undefined, type = Type} = Addr) -> - #dest{jid_string = none, - type = Type, address = Addr}; - (#address{jid = JID, type = Type} = Addr) -> - #dest{jid_string = jid:encode(JID), jid_jid = JID, - type = Type, address = Addr} - end, Addrs). - -%%%------------------------- -%%% Split destinations by existence of JID -%%% and send error messages for other dests -%%%------------------------- - --spec split_dests_jid([#dest{}]) -> {[#dest{}], [#dest{}]}. -split_dests_jid(Dests) -> - lists:partition(fun (Dest) -> - case Dest#dest.jid_string of - none -> false; - _ -> true - end - end, - Dests). - --spec report_not_jid(jid(), stanza(), [#dest{}]) -> any(). -report_not_jid(From, Packet, Dests) -> - Dests2 = [fxml:element_to_binary(xmpp:encode(Dest#dest.address)) - || Dest <- Dests], - [route_error( - xmpp:set_from_to(Packet, From, From), jid_malformed, - str:format(?T("This service can not process the address: ~s"), [D])) - || D <- Dests2]. +-spec report_not_jid(jid(), stanza(), [address()]) -> any(). +report_not_jid(From, Packet, Addresses) -> + lists:foreach( + fun(Address) -> + route_error( + xmpp:set_from_to(Packet, From, From), jid_malformed, + str:format(?T("This service can not process the address: ~s"), + [fxml:element_to_binary(xmpp:encode(Address))])) + end, Addresses). %%%------------------------- %%% Group destinations by their servers %%%------------------------- --spec group_dests([#dest{}]) -> [#group{}]. -group_dests(Dests) -> - D = lists:foldl(fun (Dest, Dict) -> - ServerS = (Dest#dest.jid_jid)#jid.server, - dict:append(ServerS, Dest, Dict) - end, - dict:new(), Dests), - Keys = dict:fetch_keys(D), - [#group{server = Key, dests = dict:fetch(Key, D), - addresses = [], others = []} - || Key <- Keys]. - -%%%------------------------- -%%% Look for cached responses -%%%------------------------- - -look_cached_servers(LServerS, LServiceS, Groups) -> - [look_cached(LServerS, LServiceS, Group) || Group <- Groups]. - -look_cached(LServerS, LServiceS, G) -> - Maxtime_positive = (?MAXTIME_CACHE_POSITIVE), - Maxtime_negative = (?MAXTIME_CACHE_NEGATIVE), - Cached_response = search_server_on_cache(G#group.server, - LServerS, LServiceS, - {Maxtime_positive, - Maxtime_negative}), - G#group{multicast = Cached_response}. - -%%%------------------------- -%%% Build delivered XML element -%%%------------------------- - -build_others_xml(Groups) -> - [Group#group{others = - build_other_xml(Group#group.dests)} - || Group <- Groups]. - -build_other_xml(Dests) -> - lists:foldl(fun (Dest, R) -> - XML = Dest#dest.address, - case Dest#dest.type of - to -> [add_delivered(XML) | R]; - cc -> [add_delivered(XML) | R]; - _ -> [XML | R] - end - end, - [], Dests). - --spec add_delivered(address()) -> address(). -add_delivered(Addr) -> - Addr#address{delivered = true}. - -%%%------------------------- -%%% Add preliminary packets -%%%------------------------- - -add_addresses(Delivereds, Groups) -> - Ps = [Group#group.others || Group <- Groups], - add_addresses2(Delivereds, Groups, [], [], Ps). - -add_addresses2(_, [], Res, _, []) -> Res; -add_addresses2(Delivereds, [Group | Groups], Res, Pa, - [Pi | Pz]) -> - Addresses = lists:append([Delivereds] ++ Pa ++ Pz), - Group2 = Group#group{addresses = Addresses}, - add_addresses2(Delivereds, Groups, [Group2 | Res], - [Pi | Pa], Pz). - -%%%------------------------- -%%% Decide action groups -%%%------------------------- - --spec decide_action_groups([#group{}]) -> [{routing(), #group{}}]. -decide_action_groups(Groups) -> - [{Group#group.multicast, Group} - || Group <- Groups]. +group_by_destinations(Addrs, Map) -> + lists:foldl( + fun + (#address{type = Type, jid = #jid{lserver = Server}} = Addr, Map2) when Type == to; Type == cc -> + maps:update_with(Server, + fun({CC, BCC}) -> + {[Addr | CC], BCC} + end, {[Addr], []}, Map2); + (#address{type = bcc, jid = #jid{lserver = Server}} = Addr, Map2) -> + maps:update_with(Server, + fun({CC, BCC}) -> + {CC, [Addr | BCC]} + end, {[], [Addr]}, Map2) + end, Map, Addrs). %%%------------------------- %%% Route packet %%%------------------------- --spec route_packet(jid(), #dest{}, stanza(), [addresses()], [addresses()]) -> 'ok'. -route_packet(From, ToDest, Packet, Others, Addresses) -> - Dests = case ToDest#dest.type of - bcc -> []; - _ -> [ToDest] - end, - route_packet2(From, ToDest#dest.jid_string, Dests, - Packet, {Others, Addresses}). - --spec route_packet_multicast(jid(), binary(), stanza(), [#dest{}], [address()], #limits{}) -> 'ok'. -route_packet_multicast(From, ToS, Packet, Dests, - Addresses, Limits) -> - Type_of_stanza = type_of_stanza(Packet), - {_Type, Limit_number} = get_limit_number(Type_of_stanza, - Limits), - Fragmented_dests = fragment_dests(Dests, Limit_number), - lists:foreach(fun(DFragment) -> - route_packet2(From, ToS, DFragment, Packet, - Addresses) - end, Fragmented_dests). - --spec route_packet2(jid(), binary(), [#dest{}], stanza(), {[address()], [address()]} | [address()]) -> 'ok'. -route_packet2(From, ToS, Dests, Packet, Addresses) -> - Els = case append_dests(Dests, Addresses) of - [] -> - xmpp:get_els(Packet); - ACs -> - [#addresses{list = ACs}|xmpp:get_els(Packet)] - end, - Packet2 = xmpp:set_els(Packet, Els), - ToJID = stj(ToS), - ejabberd_router:route(xmpp:set_from_to(Packet2, From, ToJID)). - --spec append_dests([#dest{}], {[address()], [address()]} | [address()]) -> [address()]. -append_dests(_Dests, {Others, Addresses}) -> - Addresses ++ Others; -append_dests([], Addresses) -> Addresses; -append_dests([Dest | Dests], Addresses) -> - append_dests(Dests, [Dest#dest.address | Addresses]). - %%%------------------------- %%% Check relay %%%------------------------- --spec check_relay(binary(), binary(), [#group{}]) -> ok. +-spec check_relay(binary(), binary(), #{}) -> ok. check_relay(RS, LS, Gs) -> - case check_relay_required(RS, LS, Gs) of - false -> ok; - true -> throw(edrelay) + case lists:suffix(str:tokens(LS, <<".">>), + str:tokens(RS, <<".">>)) orelse + (maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of + true -> ok; + _ -> throw(edrelay) end. --spec check_relay_required(binary(), binary(), [#group{}]) -> boolean(). -check_relay_required(RServer, LServerS, Groups) -> - case lists:suffix(str:tokens(LServerS, <<".">>), - str:tokens(RServer, <<".">>)) of - true -> false; - false -> check_relay_required(LServerS, Groups) - end. - --spec check_relay_required(binary(), [#group{}]) -> boolean(). -check_relay_required(LServerS, Groups) -> - lists:any(fun (Group) -> Group#group.server /= LServerS - end, - Groups). - %%%------------------------- %%% Check protocol support: Send request %%%------------------------- @@ -1060,20 +914,6 @@ get_slimit_group(local, SLimits) -> get_slimit_group(remote, SLimits) -> SLimits#service_limits.remote. -fragment_dests(Dests, Limit_number) -> - {R, _} = lists:foldl(fun (Dest, {Res, Count}) -> - case Count of - Limit_number -> - Head2 = [Dest], {[Head2 | Res], 0}; - _ -> - [Head | Tail] = Res, - Head2 = [Dest | Head], - {[Head2 | Tail], Count + 1} - end - end, - {[[]], 0}, Dests), - R. - %%%------------------------- %%% Limits: XEP-0128 Service Discovery Extensions %%%------------------------- diff --git a/src/mod_offline.erl b/src/mod_offline.erl index c3fda25db..a66ffb20d 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -5,7 +5,7 @@ %%% Created : 5 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -79,8 +79,6 @@ -include("translate.hrl"). --define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). - %% default value for the maximum number of user messages -define(MAX_USER_MESSAGES, infinity). @@ -106,9 +104,14 @@ -callback count_messages(binary(), binary()) -> {ets_cache:tag(), non_neg_integer()}. -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()}. +-callback remove_old_messages_batch(binary(), non_neg_integer(), pos_integer(), any()) -> + {ok, any(), non_neg_integer()} | {error, term()}. -optional_callbacks([remove_expired_messages/1, remove_old_messages/2, - use_cache/1, cache_nodes/1]). + use_cache/1, cache_nodes/1, remove_old_messages_batch/3, + remove_old_messages_batch/4]). depends(_Host, _Opts) -> []. @@ -572,6 +575,16 @@ check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) -> sub_els = [#xevent{id = ID, offline = true}]}, ejabberd_router:route(NewMsg), true; + % Don't store composing events + #xevent{id = V, composing = true} when V /= undefined -> + false; + % Nor composing stopped events + #xevent{id = V, composing = false, delivered = false, + displayed = false, offline = false} when V /= undefined -> + false; + % But store other received notifications + #xevent{id = V} when V /= undefined -> + true; _ -> false end. @@ -1284,20 +1297,18 @@ mod_doc() -> {use_mam_for_storage, #{value => "true | false", desc => - ?T("This is an experimental option. Enabling this option " - "will make 'mod_offline' not use the former spool " - "table for storing MucSub offline messages, but will " - "use the archive table instead. This use of the archive " - "table is cleaner and it makes it possible for clients " - "to slowly drop the former offline use case and rely on " - "message archive instead. It also further reduces the " - "storage required when you enabled MucSub. Enabling this " + ?T("This is an experimental option. Enabling this option, " + "'mod_offline' uses the 'mod_mam' archive table instead " + "of its own spool table to retrieve the messages received " + "when the user was offline. This allows client " + "developers to slowly drop XEP-0160 and rely on XEP-0313 " + "instead. It also further reduces the " + "storage required when you enable MucSub. Enabling this " "option has a known drawback for the moment: most of " "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 and " - "ensure this option is disabled.")}}, + "is 'false' to keep former behaviour as default.")}}, {bounce_groupchat, #{value => "true | false", desc => @@ -1315,19 +1326,19 @@ mod_doc() -> {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => [{?T("This example allows power users to have as much as 5000 " "offline messages, administrators up to 2000, and all the " diff --git a/src/mod_offline_mnesia.erl b/src/mod_offline_mnesia.erl index a3c902fb3..28a105dcf 100644 --- a/src/mod_offline_mnesia.erl +++ b/src/mod_offline_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,7 +29,8 @@ -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_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"). @@ -97,6 +98,47 @@ remove_old_messages(Days, _LServer) -> end, mnesia:transaction(F). +delete_batch('$end_of_table', _LServer, _TS, Num) -> + {Num, '$end_of_table'}; +delete_batch(LastUS, _LServer, _TS, 0) -> + {0, LastUS}; +delete_batch(none, LServer, TS, Num) -> + delete_batch(mnesia:first(offline_msg), LServer, TS, Num); +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})), + case Left of + 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), + case R of + {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, diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index 4e8441b7f..07978befd 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -4,7 +4,7 @@ %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,7 +30,7 @@ -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_all_messages/2, count_messages/2, import/1, export/1, remove_old_messages_batch/3]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_offline.hrl"). @@ -112,6 +112,31 @@ remove_old_messages(Days, LServer) -> 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} + end. + remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 483f2e834..825acf2cd 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jul 2009 by Brian Cully %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -132,35 +132,51 @@ 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, + {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]), 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) -> - Host = State#state.host, - From = jid:remove_resource(JID), - IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, - ejabberd_router:route_iq(IQ, JID, - gen_mod:get_module_proc(Host, ?MODULE), - State#state.ping_ack_timeout), - Timers = add_timer(JID, State#state.ping_interval, - State#state.timers), + Timers = case ejabberd_sm:get_session_pid(JID#jid.luser, + JID#jid.lserver, + JID#jid.lresource) of + none -> + del_timer(JID, State#state.timers); + _ -> + Host = State#state.host, + From = jid:make(Host), + IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, + ejabberd_router:route_iq(IQ, JID, + gen_mod:get_module_proc(Host, ?MODULE), + State#state.ping_ack_timeout), + 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]), @@ -187,7 +203,7 @@ user_offline(_SID, JID, _Info) -> case ejabberd_sm:get_session_pid(JID#jid.luser, JID#jid.lserver, JID#jid.lresource) of - none -> + PID when PID =:= none; node(PID) /= node() -> stop_ping(JID#jid.lserver, JID); _ -> ok @@ -243,10 +259,10 @@ unregister_iq_handlers(Host) -> 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 + {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). @@ -255,10 +271,10 @@ add_timer(JID, Interval, 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) -> @@ -300,7 +316,7 @@ mod_doc() -> desc => ?T("How long to wait before deeming that a client " "has not answered a given server ping request. " - "The default value is '32' seconds.")}}, + "The default value is 'undefined'.")}}, {send_pings, #{value => "true | false", desc => @@ -317,8 +333,8 @@ mod_doc() -> "server ping request in less than period defined " "in 'ping_ack_timeout' option: " "'kill' means destroying the underlying connection, " - "'none' means to do nothing. NOTE: when 'mod_stream_mgmt' " - "module is loaded and stream management is enabled by " + "'none' means to do nothing. NOTE: when _`mod_stream_mgmt`_ " + "is loaded and stream management is enabled by " "a client, killing the client connection doesn't mean " "killing the client session - the session will be kept " "alive in order to give the client a chance to resume it. " diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index 77f42961f..80b8fb85f 100644 --- a/src/mod_pres_counter.erl +++ b/src/mod_pres_counter.erl @@ -5,7 +5,7 @@ %%% Created : 23 Sep 2010 by Ahmed Omar %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index 4f15b80c4..4a4ebd5c5 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -5,7 +5,7 @@ %%% Created : 21 Jul 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -887,25 +887,25 @@ mod_doc() -> "https://xmpp.org/extensions/xep-0191.html" "[XEP-0191: Blocking Command] which is implemented by " "'mod_blocking' module. However, you still need " - "'mod_privacy' loaded in order for 'mod_blocking' to work.")], + "'mod_privacy' loaded in order for _`mod_blocking`_ to work.")], opts => [{db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_privacy_mnesia.erl b/src/mod_privacy_mnesia.erl index 2f9cb0027..f2c6879f1 100644 --- a/src/mod_privacy_mnesia.erl +++ b/src/mod_privacy_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl index b27442724..07c97ca7f 100644 --- a/src/mod_privacy_sql.erl +++ b/src/mod_privacy_sql.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_private.erl b/src/mod_private.erl index ad36c8494..f6cebbcda 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -5,7 +5,7 @@ %%% Created : 16 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -66,7 +66,7 @@ start(Host, Opts) -> ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE, ?MODULE, process_sm_iq), - ejabberd_commands:register_commands(get_commands_spec()). + ejabberd_commands:register_commands(?MODULE, get_commands_spec()). stop(Host) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), @@ -128,23 +128,23 @@ mod_doc() -> [{db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. -spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, jid(), jid(), binary(), binary()) -> diff --git a/src/mod_private_mnesia.erl b/src/mod_private_mnesia.erl index b5617d96e..5c789f0a7 100644 --- a/src/mod_private_mnesia.erl +++ b/src/mod_private_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_private_sql.erl b/src/mod_private_sql.erl index 08a13e8a4..d2efbe98d 100644 --- a/src/mod_private_sql.erl +++ b/src/mod_private_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl index a6d4ba446..9c8c9462e 100644 --- a/src/mod_privilege.erl +++ b/src/mod_privilege.erl @@ -4,7 +4,7 @@ %%% Purpose : XEP-0356: Privileged Entity %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -106,7 +106,7 @@ mod_doc() -> ?T("WARNING: Security issue: Privileged access gives components " "access to sensitive data, so permission should be granted " "carefully, only if you trust a component."), "", - ?T("NOTE: This module is complementary to 'mod_delegation', " + ?T("NOTE: This module is complementary to _`mod_delegation`_, " "but can also be used separately.")], opts => [{roster, diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl index ce1c67a36..1ccb31978 100644 --- a/src/mod_proxy65.erl +++ b/src/mod_proxy65.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -173,8 +173,8 @@ mod_doc() -> {ram_db_type, #{value => "mnesia | redis | sql", desc => - ?T("Define the type of volatile (in-memory) storage where the module " - "will store room information.")}}, + ?T("Same as top-level _`default_ram_db`_ option, " + "but applied to this module only.")}}, {ip, #{value => ?T("IPAddress"), desc => diff --git a/src/mod_proxy65_lib.erl b/src/mod_proxy65_lib.erl index f0fb40830..6298d7bf0 100644 --- a/src/mod_proxy65_lib.erl +++ b/src/mod_proxy65_lib.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_mnesia.erl b/src/mod_proxy65_mnesia.erl index cd1790f96..73f248d4c 100644 --- a/src/mod_proxy65_mnesia.erl +++ b/src/mod_proxy65_mnesia.erl @@ -2,7 +2,7 @@ %%% Created : 16 Jan 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_redis.erl b/src/mod_proxy65_redis.erl index ab1f3cf85..e93dce36a 100644 --- a/src/mod_proxy65_redis.erl +++ b/src/mod_proxy65_redis.erl @@ -3,7 +3,7 @@ %%% Created : 31 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index 78a8d85d2..c6995482d 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_sql.erl b/src/mod_proxy65_sql.erl index ece71c9c5..6e69e5a0f 100644 --- a/src/mod_proxy65_sql.erl +++ b/src/mod_proxy65_sql.erl @@ -3,7 +3,7 @@ %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index 505daf446..4e1eaf3fe 100644 --- a/src/mod_proxy65_stream.erl +++ b/src/mod_proxy65_stream.erl @@ -4,7 +4,7 @@ %%% Purpose : Bytestream process. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index b0dca4b0d..7b01c4af0 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -45,6 +45,7 @@ -include("mod_roster.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). +-include("ejabberd_commands.hrl"). -define(STDTREE, <<"tree">>). -define(STDNODE, <<"flat">>). @@ -93,6 +94,9 @@ handle_call/3, handle_cast/2, handle_info/2, mod_doc/0, terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]). +%% ejabberd commands +-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]). + -export([route/1]). %%==================================================================== @@ -210,7 +214,7 @@ pep_mapping :: [{binary(), binary()}], ignore_pep_from_offline :: boolean(), last_item_cache :: boolean(), - max_items_node :: non_neg_integer(), + max_items_node :: non_neg_integer()|unlimited, max_subscriptions_node :: non_neg_integer()|undefined, default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}], nodetree :: binary(), @@ -337,6 +341,7 @@ init([ServerHost|_]) -> false -> ok end, + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), NodeTree = config(ServerHost, nodetree), Plugins = config(ServerHost, plugins), PepMapping = config(ServerHost, pep_mapping), @@ -806,7 +811,13 @@ terminate(_Reason, gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), terminate_plugins(Host, ServerHost, Plugins, TreePlugin), ejabberd_router:unregister_route(Host) - end, Hosts). + end, Hosts), + case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -2251,10 +2262,11 @@ get_affiliations(Host, Node, JID) -> set_affiliations(Host, Node, From, Affs) -> Owner = jid:tolower(jid:remove_resource(From)), Action = - fun(#pubsub_node{type = Type, id = Nidx, owners = O} = N) -> + 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 @@ -2287,6 +2299,17 @@ set_affiliations(Host, Node, From, Affs) -> 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}; @@ -3391,15 +3414,10 @@ node_config(Node, ServerHost, [{RE, Opts}|NodeOpts]) -> node_config(_, _, []) -> []. -%% @spec (Host, Options) -> MaxItems -%% Host = host() -%% Options = [Option] -%% Option = {Key::atom(), Value::term()} -%% MaxItems = integer() | unlimited %% @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(). +-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer() | unlimited. max_items(Host, Options) -> case get_option(Options, persist_items) of true -> @@ -3420,6 +3438,14 @@ max_items(Host, Options) -> end end. +-spec item_expire(host(), [{atom(), any()}]) -> non_neg_integer() | infinity. +item_expire(Host, Options) -> + case get_option(Options, item_expire) of + I when is_integer(I), I < 0 -> 0; + I when is_integer(I) -> I; + _ -> get_max_item_expire_node(Host) + end. + -spec get_configure_xfields(_, pubsub_node_config:result(), binary(), [binary()]) -> [xdata_field()]. get_configure_xfields(_Type, Options, Lang, Groups) -> @@ -3493,17 +3519,24 @@ decode_node_config(undefined, _, _) -> decode_node_config(#xdata{fields = Fs}, Host, Lang) -> try Config = pubsub_node_config:decode(Fs), - Max = get_max_items_node(Host), - case {check_opt_range(max_items, Config, Max), + MaxItems = get_max_items_node(Host), + MaxExpiry = get_max_item_expire_node(Host), + case {check_opt_range(max_items, Config, MaxItems), + check_opt_range(item_expire, Config, MaxExpiry), check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of - {true, true} -> + {true, true, true} -> Config; - {true, false} -> + {true, true, false} -> erlang:error( {pubsub_node_config, {bad_var_value, <<"pubsub#max_payload_size">>, ?NS_PUBSUB_NODE_CONFIG}}); - {false, _} -> + {true, false, _} -> + erlang:error( + {pubsub_node_config, + {bad_var_value, <<"pubsub#item_expire">>, + ?NS_PUBSUB_NODE_CONFIG}}); + {false, _, _} -> erlang:error( {pubsub_node_config, {bad_var_value, <<"pubsub#max_items">>, @@ -3548,16 +3581,25 @@ decode_get_pending(#xdata{fields = Fs}, Lang) -> {error, xmpp:err_resource_constraint(Txt, Lang)} end. --spec check_opt_range(atom(), [proplists:property()], non_neg_integer()) -> boolean(). -check_opt_range(_Opt, _Opts, undefined) -> +-spec check_opt_range(atom(), [proplists:property()], + non_neg_integer() | unlimited | infinity) -> boolean(). +check_opt_range(_Opt, _Opts, unlimited) -> + true; +check_opt_range(_Opt, _Opts, infinity) -> true; check_opt_range(Opt, Opts, Max) -> - Val = proplists:get_value(Opt, Opts, Max), - Val =< Max. + case proplists:get_value(Opt, Opts, Max) of + max -> true; + Val -> Val =< Max + end. --spec get_max_items_node(host()) -> undefined | non_neg_integer(). +-spec get_max_items_node(host()) -> unlimited | non_neg_integer(). get_max_items_node(Host) -> - config(Host, max_items_node, undefined). + config(Host, max_items_node, ?MAXITEMS). + +-spec get_max_item_expire_node(host()) -> infinity | non_neg_integer(). +get_max_item_expire_node(Host) -> + config(Host, max_item_expire_node, infinity). -spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer(). get_max_subscriptions_node(Host) -> @@ -3707,6 +3749,7 @@ features() -> <<"access-whitelist">>, % OPTIONAL <<"collections">>, % RECOMMENDED <<"config-node">>, % RECOMMENDED + <<"config-node-max">>, <<"create-and-configure">>, % RECOMMENDED <<"item-ids">>, % RECOMMENDED <<"last-published">>, % RECOMMENDED @@ -4136,6 +4179,93 @@ purge_offline(Host, LJID, Node) -> {error, xmpp:err_internal_server_error(Txt, Lang)} end. +-spec delete_old_items(non_neg_integer()) -> ok | error. +delete_old_items(N) -> + Results = lists:flatmap( + fun(Host) -> + case tree_action(Host, get_all_nodes, [Host]) of + Nodes when is_list(Nodes) -> + lists:map( + fun(#pubsub_node{id = Nidx, type = Type}) -> + case node_action(Host, Type, + remove_extra_items, + [Nidx , N]) of + {result, _} -> + ok; + {error, _} -> + error + end + end, Nodes); + _ -> + [error] + end + end, ejabberd_option:hosts()), + case lists:member(error, Results) of + true -> + error; + false -> + ok + end. + +-spec delete_expired_items() -> ok | error. +delete_expired_items() -> + Results = lists:flatmap( + fun(Host) -> + case tree_action(Host, get_all_nodes, [Host]) of + Nodes when is_list(Nodes) -> + lists:map( + fun(#pubsub_node{id = Nidx, type = Type, + options = Options}) -> + case item_expire(Host, Options) of + infinity -> + ok; + Seconds -> + case node_action( + Host, Type, + remove_expired_items, + [Nidx, Seconds]) of + {result, []} -> + ok; + {result, [_|_]} -> + unset_cached_item( + Host, Nidx); + {error, _} -> + error + end + end + end, Nodes); + _ -> + [error] + end + end, ejabberd_option:hosts()), + case lists:member(error, Results) of + true -> + error; + false -> + ok + end. + +-spec get_commands_spec() -> [ejabberd_commands()]. +get_commands_spec() -> + [#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge], + desc = "Keep only NUMBER of PubSub items per node", + 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) -> econf:acl(); @@ -4146,7 +4276,9 @@ mod_opt_type(ignore_pep_from_offline) -> mod_opt_type(last_item_cache) -> econf:bool(); mod_opt_type(max_items_node) -> - econf:non_neg_int(); + econf:non_neg_int(unlimited); +mod_opt_type(max_item_expire_node) -> + econf:timeout(second, infinity); mod_opt_type(max_nodes_discoitems) -> econf:non_neg_int(infinity); mod_opt_type(max_subscriptions_node) -> @@ -4194,6 +4326,7 @@ mod_options(Host) -> {ignore_pep_from_offline, true}, {last_item_cache, false}, {max_items_node, ?MAXITEMS}, + {max_item_expire_node, infinity}, {max_nodes_discoitems, 100}, {nodetree, ?STDTREE}, {pep_mapping, []}, @@ -4212,7 +4345,7 @@ mod_doc() -> "(https://xmpp.org/extensions/xep-0163.html" "[XEP-0163: Personal Eventing via Pubsub]) " "is enabled in the default ejabberd configuration file, " - "and it requires 'mod_caps'.")], + "and it requires _`mod_caps`_.")], opts => [{access_createnode, #{value => "AccessName", @@ -4225,7 +4358,7 @@ mod_doc() -> {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to " + ?T("Same as top-level _`default_db`_ option, but applied to " "this module only.")}}, {default_node_config, #{value => "List of Key:Value", @@ -4272,11 +4405,17 @@ mod_doc() -> " so many nodes, caching last items speeds up pubsub " "and allows to raise user connection rate. The cost " "is memory usage, as every item is stored in memory.")}}, + {max_item_expire_node, + #{value => "timeout() | infinity", + note => "added in 21.12", + desc => + ?T("Specify the maximum item epiry time. Default value " + "is: 'infinity'.")}}, {max_items_node, - #{value => "MaxItems", + #{value => "non_neg_integer() | infinity", desc => ?T("Define the maximum number of items that can be " - "stored in a node. Default value is: '10'.")}}, + "stored in a node. Default value is: '1000'.")}}, {max_nodes_discoitems, #{value => "pos_integer() | infinity", desc => @@ -4342,7 +4481,7 @@ mod_doc() -> "to the 'create' stanza element."), ?T("- 'flat' plugin handles the default behaviour and " "follows standard XEP-0060 implementation."), - ?T("- 'pep' plugin adds extention to handle Personal " + ?T("- 'pep' plugin adds extension to handle Personal " "Eventing Protocol (XEP-0163) to the PubSub engine. " "Adding pep allows to handle PEP automatically.")]}}, {vcard, diff --git a/src/mod_pubsub_mnesia.erl b/src/mod_pubsub_mnesia.erl index b7730ce55..3d2d1e46b 100644 --- a/src/mod_pubsub_mnesia.erl +++ b/src/mod_pubsub_mnesia.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl index 8db5532f6..cb3c014b9 100644 --- a/src/mod_pubsub_opt.erl +++ b/src/mod_pubsub_opt.erl @@ -11,6 +11,7 @@ -export([hosts/1]). -export([ignore_pep_from_offline/1]). -export([last_item_cache/1]). +-export([max_item_expire_node/1]). -export([max_items_node/1]). -export([max_nodes_discoitems/1]). -export([max_subscriptions_node/1]). @@ -68,7 +69,13 @@ last_item_cache(Opts) when is_map(Opts) -> last_item_cache(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache). --spec max_items_node(gen_mod:opts() | global | binary()) -> non_neg_integer(). +-spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_item_expire_node(Opts) when is_map(Opts) -> + gen_mod:get_opt(max_item_expire_node, Opts); +max_item_expire_node(Host) -> + gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node). + +-spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer(). max_items_node(Opts) when is_map(Opts) -> gen_mod:get_opt(max_items_node, Opts); max_items_node(Host) -> diff --git a/src/mod_pubsub_sql.erl b/src/mod_pubsub_sql.erl index 0e670e1d5..41b7eed70 100644 --- a/src/mod_pubsub_sql.erl +++ b/src/mod_pubsub_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_push.erl b/src/mod_push.erl index b4dd48234..5389e3dfb 100644 --- a/src/mod_push.erl +++ b/src/mod_push.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -34,8 +34,8 @@ -export([mod_doc/0]). %% ejabberd_hooks callbacks. -export([disco_sm_features/5, c2s_session_pending/1, c2s_copy_session/2, - c2s_handle_cast/2, c2s_stanza/3, mam_message/7, offline_message/1, - remove_user/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]). @@ -57,19 +57,19 @@ -define(PUSH_CACHE, push_cache). -type c2s_state() :: ejabberd_c2s:state(). --type timestamp() :: erlang:timestamp(). --type push_session() :: {timestamp(), ljid(), binary(), xdata()}. +-type push_session_id() :: erlang:timestamp(). +-type push_session() :: {push_session_id(), ljid(), binary(), xdata()}. -type err_reason() :: notfound | db_failure. -type direction() :: send | recv | undefined. -callback init(binary(), gen_mod:opts()) -> any(). --callback store_session(binary(), binary(), timestamp(), jid(), binary(), +-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(), timestamp()) +-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()}. @@ -77,7 +77,7 @@ -> {ok, [push_session()]} | {error, err_reason()}. -callback lookup_sessions(binary()) -> {ok, [push_session()]} | {error, err_reason()}. --callback delete_session(binary(), binary(), timestamp()) +-callback delete_session(binary(), binary(), push_session_id()) -> ok | {error, err_reason()}. -callback delete_old_sessions(binary() | global, erlang:timestamp()) -> ok | {error, err_reason()}. @@ -98,7 +98,7 @@ start(Host, Opts) -> init_cache(Mod, Host, Opts), register_iq_handlers(Host), register_hooks(Host), - ejabberd_commands:register_commands(get_commands_spec()). + ejabberd_commands:register_commands(?MODULE, get_commands_spec()). -spec stop(binary()) -> ok. stop(Host) -> @@ -191,23 +191,23 @@ mod_doc() -> {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. %%-------------------------------------------------------------------- %% ejabberd command callback. @@ -259,6 +259,8 @@ register_hooks(Host) -> c2s_session_pending, 50), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), + ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, + c2s_session_resumed, 50), ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, c2s_handle_cast, 50), ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, @@ -278,6 +280,8 @@ unregister_hooks(Host) -> c2s_session_pending, 50), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), + ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, + c2s_session_resumed, 50), ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, c2s_handle_cast, 50), ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, @@ -361,12 +365,14 @@ process_iq(IQ) -> enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, PushJID, Node, XData) -> case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of - {TS, PID} -> - case store_session(LUser, LServer, TS, PushJID, Node, XData) 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); + 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)]), @@ -381,10 +387,11 @@ enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, -spec disable(jid(), jid(), binary() | undefined) -> ok | {error, err_reason()}. disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, PushJID, Node) -> - case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of - {_TS, PID} -> + 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", @@ -465,16 +472,29 @@ c2s_session_pending(State) -> State. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). -c2s_copy_session(State, #{push_enabled := true}) -> - State#{push_enabled => true}; +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) -> + 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) -> - {stop, State#{push_enabled => true}}; +c2s_handle_cast(State, {push_enable, ID}) -> + {stop, State#{push_enabled => true, + push_session_id => ID}}; c2s_handle_cast(State, push_disable) -> - {stop, maps:remove(push_enabled, State)}; + State1 = maps:remove(push_disable, State), + State2 = maps:remove(push_session_id, State1), + {stop, State2}; c2s_handle_cast(State, _Msg) -> State. @@ -489,10 +509,8 @@ remove_user(LUser, LServer) -> %% Generate push notifications. %%-------------------------------------------------------------------- -spec notify(c2s_state(), xmpp_element() | xmlel() | none, direction()) -> ok. -notify(#{jid := #jid{luser = LUser, lserver = LServer}, - sid := {TS, _}}, - Pkt, Dir) -> - case lookup_session(LUser, LServer, TS) of +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 -> @@ -503,7 +521,7 @@ notify(#{jid := #jid{luser = LUser, lserver = LServer}, xmpp_element() | xmlel() | none, direction()) -> ok. notify(LUser, LServer, Clients, Pkt, Dir) -> lists:foreach( - fun({TS, PushLJID, Node, XData}) -> + fun({ID, PushLJID, Node, XData}) -> HandleResponse = fun(#iq{type = result}) -> ?DEBUG("~ts accepted notification for ~ts@~ts (~ts)", @@ -517,7 +535,7 @@ notify(LUser, LServer, Clients, Pkt, Dir) -> LServer, Node, Reason]); {Type, Reason} -> spawn(?MODULE, delete_session, - [LUser, LServer, TS]), + [LUser, LServer, ID]), ?WARNING_MSG("~ts rejected notification for " "~ts@~ts (~ts), disabling push: ~ts " "(~ts)", @@ -565,9 +583,9 @@ is_incoming_chat_msg(_Stanza) -> %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- --spec store_session(binary(), binary(), timestamp(), jid(), binary(), xdata()) - -> {ok, push_session()} | {error, err_reason()}. -store_session(LUser, LServer, TS, PushJID, Node, XData) -> +-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 @@ -576,26 +594,26 @@ store_session(LUser, LServer, TS, PushJID, Node, XData) -> cache_nodes(Mod, LServer)), ets_cache:update( ?PUSH_CACHE, - {LUser, LServer, TS}, {ok, {TS, PushJID, Node, XData}}, + {LUser, LServer, ID}, {ok, {ID, PushJID, Node, XData}}, fun() -> - Mod:store_session(LUser, LServer, TS, PushJID, Node, + Mod:store_session(LUser, LServer, ID, PushJID, Node, XData) end, cache_nodes(Mod, LServer)); false -> - Mod:store_session(LUser, LServer, TS, PushJID, Node, XData) + Mod:store_session(LUser, LServer, ID, PushJID, Node, XData) end. --spec lookup_session(binary(), binary(), timestamp()) +-spec lookup_session(binary(), binary(), c2s_state()) -> {ok, push_session()} | error | {error, err_reason()}. -lookup_session(LUser, LServer, TS) -> +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, TS}, - fun() -> Mod:lookup_session(LUser, LServer, TS) end); + ?PUSH_CACHE, {LUser, LServer, ID}, + fun() -> Mod:lookup_session(LUser, LServer, ID) end); false -> - Mod:lookup_session(LUser, LServer, TS) + Mod:lookup_session(LUser, LServer, ID) end. -spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | {error, err_reason()}. @@ -610,16 +628,17 @@ lookup_sessions(LUser, LServer) -> Mod:lookup_sessions(LUser, LServer) end. --spec delete_session(binary(), binary(), timestamp()) -> ok | {error, db_failure}. -delete_session(LUser, LServer, TS) -> +-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, TS) of + 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, TS}, + ets_cache:delete(?PUSH_CACHE, {LUser, LServer, ID}, cache_nodes(Mod, LServer)); false -> ok @@ -632,8 +651,8 @@ delete_session(LUser, LServer, TS) -> delete_session(LUser, LServer, PushJID, Node) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:lookup_session(LUser, LServer, PushJID, Node) of - {ok, {TS, _, _, _}} -> - delete_session(LUser, LServer, TS); + {ok, {ID, _, _, _}} -> + delete_session(LUser, LServer, ID); error -> {error, notfound}; {error, _} = Err -> @@ -661,12 +680,12 @@ delete_sessions(LUser, LServer, LookupFun, Mod) -> ok end, lists:foreach( - fun({TS, _, _, _}) -> - ok = Mod:delete_session(LUser, LServer, TS), + fun({ID, _, _, _}) -> + ok = Mod:delete_session(LUser, LServer, ID), case use_cache(Mod, LServer) of true -> ets_cache:delete(?PUSH_CACHE, - {LUser, LServer, TS}, + {LUser, LServer, ID}, cache_nodes(Mod, LServer)); false -> ok @@ -679,9 +698,17 @@ delete_sessions(LUser, LServer, LookupFun, Mod) -> -spec drop_online_sessions(binary(), binary(), [push_session()]) -> [push_session()]. drop_online_sessions(LUser, LServer, Clients) -> - SessIDs = ejabberd_sm:get_session_sids(LUser, LServer), - [Client || {TS, _, _, _} = Client <- Clients, - lists:keyfind(TS, 1, SessIDs) == false]. + 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)]. -spec make_summary(binary(), xmpp_element() | xmlel() | none, direction()) -> xdata() | undefined. diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl index aba4fbfae..8a816aef1 100644 --- a/src/mod_push_keepalive.erl +++ b/src/mod_push_keepalive.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -94,13 +94,13 @@ mod_options(_Host) -> mod_doc() -> #{desc => [?T("This module tries to keep the stream management " - "session (see 'mod_stream_mgmt') of a disconnected " + "session (see _`mod_stream_mgmt`_) of a disconnected " "mobile client alive if the client enabled push " "notifications for that session. However, the normal " "session resumption timeout is restored once a push " "notification is issued, so the session will be closed " "if the client doesn't respond to push notifications."), "", - ?T("The module depends on 'mod_push'.")], + ?T("The module depends on _`mod_push`_.")], opts => [{resume_timeout, #{value => "timeout()", @@ -109,8 +109,8 @@ mod_doc() -> "the session of a disconnected push client times out. " "This timeout is only in effect as long as no push " "notification is issued. Once that happened, the " - "resumption timeout configured for the 'mod_stream_mgmt' " - "module is restored. " + "resumption timeout configured for _`mod_stream_mgmt`_ " + "is restored. " "The default value is '72' hours.")}}, {wake_on_start, #{value => "true | false", @@ -213,7 +213,7 @@ c2s_copy_session(State, _) -> State. -spec c2s_handle_cast(c2s_state(), any()) -> c2s_state(). -c2s_handle_cast(#{lserver := LServer} = State, push_enable) -> +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, diff --git a/src/mod_push_mnesia.erl b/src/mod_push_mnesia.erl index 665ab48f2..e265678e7 100644 --- a/src/mod_push_mnesia.erl +++ b/src/mod_push_mnesia.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl index f89b904c2..ae6774a51 100644 --- a/src/mod_push_sql.erl +++ b/src/mod_push_sql.erl @@ -5,7 +5,7 @@ %%% Created : 26 Oct 2017 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2017-2021 ProcessOne +%%% ejabberd, Copyright (C) 2017-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -52,7 +52,7 @@ store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> case ?SQL_UPSERT(LServer, "push_session", ["!username=%(LUser)s", "!server_host=%(LServer)s", - "!timestamp=%(TS)d", + "timestamp=%(TS)d", "!service=%(Service)s", "!node=%(Node)s", "xml=%(XML)s"]) of diff --git a/src/mod_register.erl b/src/mod_register.erl index a864c0df2..5f3d7de56 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -32,11 +32,13 @@ -behaviour(gen_mod). -export([start/2, stop/1, reload/3, stream_feature_register/2, - c2s_unauthenticated_packet/2, try_register/4, + c2s_unauthenticated_packet/2, try_register/4, try_register/5, process_iq/1, send_registration_notifications/3, mod_opt_type/1, mod_options/1, depends/2, format_error/1, mod_doc/0]). +-deprecated({try_register, 4}). + -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). @@ -283,7 +285,7 @@ try_register_or_set_password(User, Server, Password, _ when CaptchaSucceed -> case check_from(From, Server) of allow -> - case try_register(User, Server, Password, Source, Lang) of + case try_register(User, Server, Password, Source, ?MODULE, Lang) of ok -> xmpp:make_iq_result(IQ); {error, Error} -> @@ -328,6 +330,13 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) -> xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang)) end. +try_register(User, Server, Password, SourceRaw, Module) -> + Modules = mod_register_opt:allow_modules(Server), + case (Modules == all) orelse lists:member(Module, Modules) of + true -> try_register(User, Server, Password, SourceRaw); + false -> {error, eaccess} + end. + try_register(User, Server, Password, SourceRaw) -> case jid:is_nodename(User) of false -> @@ -363,8 +372,8 @@ try_register(User, Server, Password, SourceRaw) -> end end. -try_register(User, Server, Password, SourceRaw, Lang) -> - case try_register(User, Server, Password, SourceRaw) of +try_register(User, Server, Password, SourceRaw, Module, Lang) -> + case try_register(User, Server, Password, SourceRaw, Module) of ok -> JID = jid:make(User, Server), Source = may_remove_resource(SourceRaw), @@ -597,6 +606,8 @@ mod_opt_type(access_from) -> econf:acl(); mod_opt_type(access_remove) -> econf:acl(); +mod_opt_type(allow_modules) -> + econf:either(all, econf:list(econf:atom())); mod_opt_type(captcha_protected) -> econf:bool(); mod_opt_type(ip_access) -> @@ -623,6 +634,7 @@ mod_options(_Host) -> [{access, all}, {access_from, none}, {access_remove, all}, + {allow_modules, all}, {captcha_protected, false}, {ip_access, all}, {password_strength, 0}, @@ -638,9 +650,9 @@ mod_doc() -> ?T("* Register a new account on the server."), "", ?T("* Change the password from an existing account on the server."), "", ?T("* Delete an existing account on the server."), "", - ?T("This module reads also another option defined globally for the " - "server: 'registration_timeout'. Please check that option " - "documentation in the section with top-level options.")], + ?T("This module reads also the top-level _`registration_timeout`_ " + "option defined globally for the server, " + "so please check that option documentation too.")], opts => [{access, #{value => ?T("AccessName"), @@ -661,12 +673,18 @@ mod_doc() -> desc => ?T("Specify rules to restrict access for user unregistration. " "By default any user is able to unregister their account.")}}, + {allow_modules, + #{value => "all | [Module, ...]", + note => "added in 21.12", + desc => + ?T("List of modules that can register accounts, or 'all'. " + "The default value is 'all', which is equivalent to " + "something like '[mod_register, mod_register_web]'.")}}, {captcha_protected, #{value => "true | false", desc => - ?T("Protect registrations with CAPTCHA (see section " - "https://docs.ejabberd.im/admin/configuration/basic/#captcha[CAPTCHA] " - "of the Configuration Guide). The default is 'false'.")}}, + ?T("Protect registrations with http://../basic/#captcha[CAPTCHA]. " + "The default is 'false'.")}}, {ip_access, #{value => ?T("AccessName"), desc => @@ -680,7 +698,7 @@ mod_doc() -> "https://en.wikipedia.org/wiki/Entropy_(information_theory)" "[Shannon entropy] for passwords. The value 'Entropy' is a " "number of bits of entropy. The recommended minimum is 32 bits. " - "The default is 0, i.e. no checks are performed.")}}, + "The default is '0', i.e. no checks are performed.")}}, {registration_watchers, #{value => "[JID, ...]", desc => diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl index 53c6ca6ea..e7236424c 100644 --- a/src/mod_register_opt.erl +++ b/src/mod_register_opt.erl @@ -6,6 +6,7 @@ -export([access/1]). -export([access_from/1]). -export([access_remove/1]). +-export([allow_modules/1]). -export([captcha_protected/1]). -export([ip_access/1]). -export([password_strength/1]). @@ -31,6 +32,12 @@ access_remove(Opts) when is_map(Opts) -> access_remove(Host) -> gen_mod:get_module_opt(Host, mod_register, access_remove). +-spec allow_modules(gen_mod:opts() | global | binary()) -> 'all' | [atom()]. +allow_modules(Opts) when is_map(Opts) -> + gen_mod:get_opt(allow_modules, Opts); +allow_modules(Host) -> + gen_mod:get_module_opt(Host, mod_register, allow_modules). + -spec captcha_protected(gen_mod:opts() | global | binary()) -> boolean(). captcha_protected(Opts) when is_map(Opts) -> gen_mod:get_opt(captcha_protected, Opts); diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index 9c2179302..59bc855bc 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -5,7 +5,7 @@ %%% Created : 4 May 2008 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,32 +23,6 @@ %%% %%%---------------------------------------------------------------------- -%%% IDEAS: -%%% -%%% * Implement those options, already present in mod_register: -%%% + access -%%% + captcha_protected -%%% + password_strength -%%% + welcome_message -%%% + registration_timeout -%%% -%%% * Improve this module to allow each virtual host to have different -%%% options. See http://support.process-one.net/browse/EJAB-561 -%%% -%%% * Check that all the text is translatable. -%%% -%%% * Add option to use a custom CSS file, or custom CSS lines. -%%% -%%% * Don't hardcode the "register" path in URL. -%%% -%%% * Allow private email during register, and store in custom table. -%%% * Optionally require private email to register. -%%% * Optionally require email confirmation to register. -%%% * Allow to set a private email address anytime. -%%% * Allow to recover password using private email to confirm (mod_passrecover) -%%% * Optionally require invitation -%%% * Optionally register request is forwarded to admin, no account created. - -module(mod_register_web). -author('badlop@process-one.net'). @@ -111,7 +85,7 @@ process([Section], process([<<"new">>], #request{method = 'POST', q = Q, ip = {Ip, _Port}, lang = Lang, host = _HTTPHost}) -> - case form_new_post(Q) of + case form_new_post(Q, Ip) of {success, ok, {Username, Host, _Password}} -> Jid = jid:make(Username, Host), mod_register:send_registration_notifications(?MODULE, Jid, Ip), @@ -316,10 +290,10 @@ form_new_get2(Host, Lang, CaptchaEls) -> %%% Formulary new POST %%%---------------------------------------------------------------------- -form_new_post(Q) -> +form_new_post(Q, Ip) -> case catch get_register_parameters(Q) of [Username, Host, Password, Password, Id, Key] -> - form_new_post(Username, Host, Password, {Id, Key}); + form_new_post(Username, Host, Password, {Id, Key}, Ip); [_Username, _Host, _Password, _Password2, false, false] -> {error, passwords_not_identical}; [_Username, _Host, _Password, _Password2, Id, Key] -> @@ -338,13 +312,12 @@ get_register_parameters(Q) -> [<<"username">>, <<"host">>, <<"password">>, <<"password2">>, <<"id">>, <<"key">>]). -form_new_post(Username, Host, Password, - {false, false}) -> - register_account(Username, Host, Password); -form_new_post(Username, Host, Password, {Id, Key}) -> +form_new_post(Username, Host, Password, {false, false}, Ip) -> + register_account(Username, Host, Password, Ip); +form_new_post(Username, Host, Password, {Id, Key}, Ip) -> case ejabberd_captcha:check_captcha(Id, Key) of captcha_valid -> - register_account(Username, Host, Password); + register_account(Username, Host, Password, Ip); captcha_non_valid -> {error, captcha_non_valid}; captcha_not_found -> {error, captcha_non_valid} end. @@ -528,24 +501,27 @@ form_del_get(Host, Lang) -> {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. -%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} | +%% @spec(Username, Host, Password, Ip) -> {success, ok, {Username, Host, Password} | %% {success, exists, {Username, Host, Password}} | %% {error, not_allowed} | %% {error, invalid_jid} -register_account(Username, Host, Password) -> - Access = mod_register_opt:access(Host), - case jid:make(Username, Host) of - error -> {error, invalid_jid}; - JID -> - case acl:match_rule(Host, Access, JID) of - deny -> {error, not_allowed}; - allow -> register_account2(Username, Host, Password) - end +register_account(Username, Host, Password, Ip) -> + try mod_register_opt:access(Host) of + Access -> + case jid:make(Username, Host) of + error -> {error, invalid_jid}; + JID -> + case acl:match_rule(Host, Access, JID) of + deny -> {error, not_allowed}; + allow -> register_account2(Username, Host, Password, Ip) + end + end + catch _:{module_not_loaded, mod_register, _Host} -> + {error, host_unknown} end. -register_account2(Username, Host, Password) -> - case ejabberd_auth:try_register(Username, Host, - Password) +register_account2(Username, Host, Password, Ip) -> + case mod_register:try_register(Username, Host, Password, Ip, ?MODULE) of ok -> {success, ok, {Username, Host, Password}}; @@ -601,10 +577,8 @@ get_error_text({error, exists}) -> ?T("The account already exists"); get_error_text({error, password_incorrect}) -> ?T("Incorrect password"); -get_error_text({error, invalid_jid}) -> - ?T("The username is not valid"); -get_error_text({error, not_allowed}) -> - ?T("Not allowed"); +get_error_text({error, host_unknown}) -> + ?T("Host unknown"); get_error_text({error, account_doesnt_exist}) -> ?T("Account doesn't exist"); get_error_text({error, account_exists}) -> @@ -614,7 +588,9 @@ get_error_text({error, password_not_changed}) -> get_error_text({error, passwords_not_identical}) -> ?T("The passwords are different"); get_error_text({error, wrong_parameters}) -> - ?T("Wrong parameters in the web formulary"). + ?T("Wrong parameters in the web formulary"); +get_error_text({error, Why}) -> + mod_register:format_error(Why). mod_options(_) -> []. @@ -625,13 +601,27 @@ mod_doc() -> ?T("- Register a new account on the server."), "", ?T("- Change the password from an existing account on the server."), "", ?T("- Unregister an existing account on the server."), "", - ?T("This module supports CAPTCHA image to register a new account. " - "To enable this feature, configure the options 'captcha\_cmd' " - "and 'captcha\_url', which are documented in the section with " - "top-level options."), "", - ?T("As an example usage, the users of the host 'example.org' can " - "visit the page: 'https://example.org:5281/register/' It is " + ?T("This module supports http://../basic/#captcha[CAPTCHA] " + "to register a new account. " + "To enable this feature, configure the " + "top-level _`captcha_cmd`_ and " + "top-level _`captcha_url`_ options."), "", + ?T("As an example usage, the users of the host 'localhost' can " + "visit the page: 'https://localhost:5280/register/' It is " "important to include the last / character in the URL, " "otherwise the subpages URL will be incorrect."), "", - ?T("The module depends on 'mod_register' where all the configuration " - "is performed.")]}. + ?T("This module is enabled in 'listen' -> 'ejabberd_http' -> " + "http://../listen-options/#request-handlers[request_handlers], " + "no need to enable in 'modules'."), + ?T("The module depends on _`mod_register`_ where all the " + "configuration is performed.")], + example => + ["listen:", + " -", + " port: 5280", + " module: ejabberd_http", + " request_handlers:", + " /register: mod_register_web", + "", + "modules:", + " mod_register: {}"]}. diff --git a/src/mod_roster.erl b/src/mod_roster.erl index f204c9211..b6cf771f0 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -5,7 +5,7 @@ %%% Created : 11 Dec 2002 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -41,7 +41,7 @@ -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/2, + 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, @@ -64,6 +64,7 @@ -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). -type c2s_state() :: ejabberd_c2s:state(). -export_type([subscription/0]). @@ -91,7 +92,7 @@ start(Host, Opts) -> Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster, 50), + get_user_roster_items, 50), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, in_subscription, 50), ejabberd_hooks:add(roster_out_subscription, Host, @@ -113,7 +114,7 @@ start(Host, Opts) -> stop(Host) -> ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster, 50), + get_user_roster_items, 50), ejabberd_hooks:delete(roster_in_subscription, Host, ?MODULE, in_subscription, 50), ejabberd_hooks:delete(roster_out_subscription, Host, @@ -204,9 +205,8 @@ process_local_iq(#iq{lang = Lang} = IQ) -> -spec roster_hash([#roster{}]) -> binary(). roster_hash(Items) -> - str:sha(term_to_binary(lists:sort([R#roster{groups = - lists:sort(Grs)} - || R = #roster{groups = Grs} + 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. @@ -226,7 +226,6 @@ get_versioning_feature(Acc, Host) -> -spec roster_version(binary(), binary()) -> undefined | binary(). roster_version(LServer, LUser) -> - US = {LUser, LServer}, case mod_roster_opt:store_current_id(LServer) of true -> case read_roster_version(LUser, LServer) of @@ -234,8 +233,7 @@ roster_version(LServer, LUser) -> {ok, V} -> V end; false -> - roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, - [], [US])) + roster_hash(run_roster_get_hook(LUser, LServer)) end. -spec read_roster_version(binary(), binary()) -> {ok, binary()} | error. @@ -274,11 +272,10 @@ 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, - sub_els = [#roster_query{ver = RequestedVersion}]} = 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, - US = {LUser, LServer}, {ItemsToSend, VersionToSend} = case {mod_roster_opt:versioning(LServer), mod_roster_opt:store_current_id(LServer)} of @@ -286,36 +283,33 @@ process_iq_get(#iq{to = To, case read_roster_version(LUser, LServer) of error -> RosterVersion = write_roster_version(LUser, LServer), - {lists:map(fun encode_item/1, - ejabberd_hooks:run_fold( - roster_get, To#jid.lserver, [], [US])), - RosterVersion}; + {run_roster_get_hook(LUser, LServer), RosterVersion}; {ok, RequestedVersion} -> {false, false}; {ok, NewVersion} -> - {lists:map(fun encode_item/1, - ejabberd_hooks:run_fold( - roster_get, To#jid.lserver, [], [US])), - NewVersion} + {run_roster_get_hook(LUser, LServer), NewVersion} end; {true, false} when RequestedVersion /= undefined -> - RosterItems = ejabberd_hooks:run_fold( - roster_get, To#jid.lserver, [], [US]), + RosterItems = run_roster_get_hook(LUser, LServer), case roster_hash(RosterItems) of RequestedVersion -> {false, false}; New -> - {lists:map(fun encode_item/1, RosterItems), New} + {RosterItems, New} end; _ -> - {lists:map(fun encode_item/1, - ejabberd_hooks:run_fold( - roster_get, To#jid.lserver, [], [US])), - false} + {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, xmpp:make_iq_result( IQ, - case {ItemsToSend, VersionToSend} of + case {Items, VersionToSend} of {false, false} -> undefined; {Items, false} -> @@ -325,16 +319,21 @@ process_iq_get(#iq{to = To, ver = Version} end). --spec get_user_roster([#roster{}], {binary(), binary()}) -> [#roster{}]. -get_user_roster(Acc, {LUser, LServer}) -> - Items = get_roster(LUser, LServer), - lists:filter(fun (#roster{subscription = none, - ask = in}) -> - false; - (_) -> true - end, - Items) - ++ Acc. +-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)). + +-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) -> @@ -481,7 +480,7 @@ set_item_and_notify_clients(To, #roster_item{jid = PeerJID} = RosterItem, end, case transaction(LUser, LServer, [PeerLJID], F) of {atomic, {OldItem, NewItem}} -> - push_item(To, OldItem, NewItem), + push_item(To, encode_item(OldItem), encode_item(NewItem)), case NewItem#roster.subscription of remove -> send_unsubscribing_presence(To, OldItem); @@ -492,7 +491,7 @@ set_item_and_notify_clients(To, #roster_item{jid = PeerJID} = RosterItem, {error, Reason} end. --spec push_item(jid(), #roster{}, #roster{}) -> ok. +-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 @@ -505,21 +504,22 @@ push_item(To, OldItem, NewItem) -> push_item(To1, OldItem, NewItem, Ver) end, ejabberd_sm:get_user_resources(LUser, LServer)). --spec push_item(jid(), #roster{}, #roster{}, undefined | binary()) -> ok. +-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 = [encode_item(NewItem)]}]}, + items = [Item]}]}, ejabberd_router:route(IQ). --spec route_presence_change(jid(), #roster{}, #roster{}) -> ok. +-spec route_presence_change(jid(), #roster_item{}, #roster_item{}) -> ok. route_presence_change(From, OldItem, NewItem) -> - OldSub = OldItem#roster.subscription, - NewSub = NewItem#roster.subscription, - To = jid:make(NewItem#roster.jid), + OldSub = OldItem#roster_item.subscription, + NewSub = NewItem#roster_item.subscription, + To = NewItem#roster_item.jid, NewIsFrom = NewSub == both orelse NewSub == from, OldIsFrom = OldSub == both orelse OldSub == from, if NewIsFrom andalso not OldIsFrom -> @@ -643,7 +643,9 @@ process_subscription(Direction, User, Server, JID1, NewItem#roster.ask == in -> ok; true -> - push_item(jid:make(User, Server), OldItem, NewItem) + push_item(jid:make(User, Server), + encode_item(OldItem), + encode_item(NewItem)) end, true; none -> @@ -821,7 +823,7 @@ in_auto_reply(_, _, _) -> none. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - Items = get_user_roster([], {LUser, LServer}), + Items = get_filtered_roster(LUser, LServer), send_unsubscription_to_rosteritems(LUser, LServer, Items), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), @@ -865,6 +867,57 @@ send_unsubscribing_presence(From, Item) -> 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). + +-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 + 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 + end; +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 + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec set_items(binary(), binary(), roster_query()) -> {atomic, ok} | {aborted, any()}. @@ -1345,29 +1398,29 @@ mod_doc() -> "This option does not affect the client in any way. " "This option is only useful if option 'versioning' is " "set to 'true'. The default value is 'false'. " - "IMPORTANT: if you use 'mod_shared_roster' or " - "'mod_shared_roster_ldap', you must set the value " + "IMPORTANT: if you use _`mod_shared_roster`_ or " + " _`mod_shared_roster_ldap`_, you must set the value " "of the option to 'false'.")}}, {db_type, #{value => "mnesia | sql", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => ["modules:", " ...", diff --git a/src/mod_roster_mnesia.erl b/src/mod_roster_mnesia.erl index fa1d52f7c..19f602f1e 100644 --- a/src/mod_roster_mnesia.erl +++ b/src/mod_roster_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -194,7 +194,6 @@ process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> ), rosteritem_purge({Action, Subs, Asks, Users, Contacts}). -%% @spec ({Action::atom(), Subs::[atom()], Asks::[atom()], User::string(), Contact::string()}) -> {atomic, ok} rosteritem_purge(Options) -> Num_rosteritems = mnesia:table_info(roster, size), io:format("There are ~p roster items in total.~n", [Num_rosteritems]), diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index 76ddb29dd..1a8d812b6 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -80,9 +80,10 @@ get_roster(LUser, LServer) -> [] end, GroupsDict = lists:foldl(fun({J, G}, Acc) -> - dict:append(J, G, Acc) + Gs = maps:get(J, Acc, []), + maps:put(J, [G | Gs], Acc) end, - dict:new(), JIDGroups), + maps:new(), JIDGroups), {ok, lists:flatmap( fun(I) -> case raw_to_record(LServer, I) of @@ -90,10 +91,7 @@ get_roster(LUser, LServer) -> error -> []; R -> SJID = jid:encode(R#roster.jid), - Groups = case dict:find(SJID, GroupsDict) of - {ok, Gs} -> Gs; - error -> [] - end, + Groups = maps:get(SJID, GroupsDict, []), [R#roster{groups = Groups}] end end, Items)}; diff --git a/src/mod_s2s_dialback.erl b/src/mod_s2s_dialback.erl index 05747e3bc..5e966967d 100644 --- a/src/mod_s2s_dialback.erl +++ b/src/mod_s2s_dialback.erl @@ -2,7 +2,7 @@ %%% Created : 16 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl index f5f23eeb0..2f962a13f 100644 --- a/src/mod_service_log.erl +++ b/src/mod_service_log.erl @@ -5,7 +5,7 @@ %%% Created : 24 Aug 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 16cc96a75..69bbc520d 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -5,7 +5,7 @@ %%% Created : 5 Mar 2005 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -150,7 +150,8 @@ depends(_Host, _Opts) -> -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> - ets_cache:new(?SPECIAL_GROUPS_CACHE, [{max_size, 4}]), + 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), @@ -184,8 +185,8 @@ cache_nodes(Mod, Host) -> false -> ejabberd_cluster:get_nodes() end. --spec get_user_roster([#roster{}], {binary(), binary()}) -> [#roster{}]. -get_user_roster(Items, {U, S} = US) -> +-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) -> @@ -201,24 +202,23 @@ get_user_roster(Items, {U, S} = US) -> end, dict:new(), DisplayedGroups), {NewItems1, SRUsersRest} = lists:mapfoldl( - fun(Item, SRUsers1) -> - {_, _, {U1, S1, _}} = Item#roster.usj, - US1 = {U1, S1}, + fun(Item = #roster_item{jid = #jid{luser = User1, lserver = Server1}}, SRUsers1) -> + US1 = {User1, Server1}, case dict:find(US1, SRUsers1) of {ok, GroupLabels} -> - {Item#roster{subscription = both, - groups = Item#roster.groups ++ GroupLabels, - ask = none}, + {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{usj = {U, S, {U1, S1, <<"">>}}, - us = US, jid = {U1, S1, <<"">>}, - name = get_rosteritem_name(U1, S1), - subscription = both, ask = none, groups = GroupLabels} + 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. @@ -422,6 +422,7 @@ create_group(Host, Group, Opts) -> 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 @@ -511,7 +512,8 @@ get_group_opt_cached(Host, Group, Opt, Default, Cache) -> proplists:get_value(Opt, Opts, Default) end. -%% @spec (Host::string(), Group::string(), Opt::atom(), Default) -> OptValue | 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; @@ -686,7 +688,8 @@ is_user_in_group(US, Group, Host) -> true end. -%% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {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 @@ -852,16 +855,14 @@ displayed_to_groups(GroupName, LServer) -> push_item(User, Server, Item) -> mod_roster:push_item(jid:make(User, Server), - Item#roster{subscription = none}, + Item#roster_item{subscription = none}, Item). push_roster_item(User, Server, ContactU, ContactS, ContactN, GroupLabel, Subscription) -> - Item = #roster{usj = - {User, Server, {ContactU, ContactS, <<"">>}}, - us = {User, Server}, jid = {ContactU, ContactS, <<"">>}, - name = ContactN, subscription = Subscription, ask = none, - groups = [GroupLabel]}, + 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()}) @@ -870,12 +871,15 @@ c2s_self_presence(Acc) -> Acc. -spec unset_presence(binary(), binary(), binary(), binary()) -> ok. -unset_presence(LUser, LServer, Resource, Status) -> +unset_presence(User, Server, Resource, Status) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), Resources = ejabberd_sm:get_user_resources(LUser, LServer), ?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p " "(~p resources)", - [LUser, LServer, Resource, Status, length(Resources)]), + [LUser, LServer, LResource, Status, length(Resources)]), case length(Resources) of 0 -> lists:foreach( @@ -1266,33 +1270,30 @@ mod_doc() -> ?T("- Displayed: A list of groups that will be in the " "rosters of this group's members. A group of other vhost can " "be identified with 'groupid@vhost'."), "", - ?T("This module depends on 'mod_roster'. " + ?T("This module depends on _`mod_roster`_. " "If not enabled, roster queries will return 503 errors.")], opts => [{db_type, #{value => "mnesia | sql", desc => - ?T("Define the type of storage where the module will create " - "the tables and store user information. The default is " - "the storage defined by the global option 'default_db', " - "or 'mnesia' if omitted. If 'sql' value is defined, " - "make sure you have defined the database.")}}, - {use_cache, + ?T("Same as top-level _`default_db`_ option, " + "but applied to this module only.")}}, + {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}], + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => [{?T("Take the case of a computer club that wants all its members " "seeing each other in their rosters. To achieve this, they " diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index 93c08e0c3..ebcb77ab3 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -7,7 +7,7 @@ %%% Created : 5 Mar 2005 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -109,36 +109,25 @@ depends(_Host, _Opts) -> %%-------------------------------------------------------------------- %% Hooks %%-------------------------------------------------------------------- --spec get_user_roster([#roster{}], {binary(), binary()}) -> [#roster{}]. -get_user_roster(Items, {U, S} = US) -> +-spec get_user_roster([#roster_item{}], {binary(), binary()}) -> [#roster_item{}]. +get_user_roster(Items, US) -> SRUsers = get_user_to_groups_map(US, true), - {NewItems1, SRUsersRest} = lists:mapfoldl(fun (Item, - SRUsers1) -> - {_, _, {U1, S1, _}} = - Item#roster.usj, - US1 = {U1, S1}, - case dict:find(US1, - SRUsers1) - of - {ok, GroupNames} -> - {Item#roster{subscription - = - both, - groups = - Item#roster.groups ++ GroupNames, - ask = - none}, - dict:erase(US1, - SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), - SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, - us = US, jid = {U1, S1, <<"">>}, - name = get_user_name(U1, S1), subscription = both, - ask = none, groups = GroupNames} + {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)], SRItems ++ NewItems1. @@ -689,9 +678,9 @@ mod_doc() -> ?T("- Connection parameters: The module also accepts the " "connection parameters, all of which default to the top-level " "parameter of the same name, if unspecified. " - "See http://../database-ldap/#ldap-connection[LDAP Connection] " + "See http://../ldap/#ldap-connection[LDAP Connection] " "section for more information about them."), "", - ?T("Check also the http://../database-ldap/#configuration-examples" + ?T("Check also the http://../ldap/#ldap-examples" "[Configuration examples] section to get details about " "retrieving the roster, " "and configuration examples including Flat DIT and Deep DIT.")], @@ -721,13 +710,13 @@ mod_doc() -> "name of roster entries (usually full names of people in " "the roster). See also the parameters 'ldap_userdesc' and " "'ldap_useruid'. For more information check the LDAP " - "http://../database-ldap/#filters[Filters] section.")}}, + "http://../ldap/#filters[Filters] section.")}}, {ldap_filter, #{desc => ?T("Additional filter which is AND-ed together " "with \"User Filter\" and \"Group Filter\". " "For more information check the LDAP " - "http://../database-ldap/#filters[Filters] section.")}}, + "http://../ldap/#filters[Filters] section.")}}, %% Attributes: {ldap_groupattr, #{desc => @@ -785,7 +774,7 @@ mod_doc() -> #{desc => ?T("A regex for extracting user ID from the value of the " "attribute named by 'ldap_memberattr'. Check the LDAP " - "http://../database-ldap/#control-parameters" + "http://../ldap/#control-parameters" "[Control Parameters] section.")}}, {ldap_auth_check, #{value => "true | false", @@ -796,7 +785,7 @@ mod_doc() -> "disable the check. Default value is 'true'.")}}] ++ [{Opt, #{desc => - {?T("Same as top-level '~s' option, but " + {?T("Same as top-level _`~s`_ option, but " "applied to this module only."), [Opt]}}} || Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases, ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, diff --git a/src/mod_shared_roster_mnesia.erl b/src/mod_shared_roster_mnesia.erl index 484df0cd5..0504184ba 100644 --- a/src/mod_shared_roster_mnesia.erl +++ b/src/mod_shared_roster_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl index 0d44dce69..ca25314fd 100644 --- a/src/mod_shared_roster_sql.erl +++ b/src/mod_shared_roster_sql.erl @@ -4,7 +4,7 @@ %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_sic.erl b/src/mod_sic.erl index 4eeb8fd08..c21482d7f 100644 --- a/src/mod_sic.erl +++ b/src/mod_sic.erl @@ -5,7 +5,7 @@ %%% Created : 6 Mar 2010 by Karim Gemayel %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_sip.erl b/src/mod_sip.erl index 6ea2a408f..96e344569 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -5,7 +5,7 @@ %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2021 ProcessOne +%%% ejabberd, Copyright (C) 2014-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -357,9 +357,9 @@ mod_opt_type(via) -> (econf:and_then( econf:url([tls, tcp, udp]), fun(URI) -> - {ok, Type, Host, Port, _} = + {ok, Type, _UserInfo, Host, Port, _, _} = misc:uri_parse(URI), - {Type, {unicode:characters_to_binary(Host), Port}} + {list_to_atom(Type), {unicode:characters_to_binary(Host), Port}} end))(U) end, [unique]). diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index ddfefb1ad..8534766c4 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -5,7 +5,7 @@ %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2021 ProcessOne +%%% ejabberd, Copyright (C) 2014-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl index 5079aebf0..7c6ffef9e 100644 --- a/src/mod_sip_registrar.erl +++ b/src/mod_sip_registrar.erl @@ -5,7 +5,7 @@ %%% Created : 23 Apr 2014 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2014-2021 ProcessOne +%%% ejabberd, Copyright (C) 2014-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_stats.erl b/src/mod_stats.erl index c61fe6175..52798ce49 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl index b9443e5d2..67ab815d1 100644 --- a/src/mod_stream_mgmt.erl +++ b/src/mod_stream_mgmt.erl @@ -3,7 +3,7 @@ %%% Created : 25 Dec 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -268,7 +268,7 @@ c2s_handle_info(State, {timeout, _, Timeout}) when Timeout == ack_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 cancelation in the case when p1_server is used. + %% timer cancellation in the case when p1_server is used. {stop, State}; c2s_handle_info(State, _) -> State. @@ -962,12 +962,14 @@ mod_doc() -> {queue_type, #{value => "ram | file", desc => - ?T("Same as top-level 'queue_type' option, but applied to this module only.")}}, + ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, " + "but applied to this module only. " + "The default value is '48 hours'.")}}]}. diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl index bb701b96b..26a2646cc 100644 --- a/src/mod_stun_disco.erl +++ b/src/mod_stun_disco.erl @@ -5,7 +5,7 @@ %%% Created : 18 Apr 2020 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2020-2021 ProcessOne +%%% ejabberd, Copyright (C) 2020-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -176,7 +176,7 @@ mod_doc() -> "clients. If ejabberd's built-in TURN service is used, " "TURN relays allocated using temporary credentials will " "be terminated shortly after the credentials expired. The " - "default value is '12' hours. Note that restarting the " + "default value is '12 hours'. Note that restarting the " "ejabberd node invalidates any temporary credentials " "offered before the restart unless a 'secret' is " "specified (see below).")}}, @@ -646,7 +646,7 @@ get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) -> {undefined, get_turn_ipv6_addr(Opts)}; get_listener_ips(#{ip := {_, _, _, _} = IP}) -> {IP, undefined}; -get_listener_ips(#{ip := {_, _, _, _, _,_, _, _, _} = IP}) -> +get_listener_ips(#{ip := {_, _, _, _, _, _, _, _} = IP}) -> {undefined, IP}. -spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined. diff --git a/src/mod_time.erl b/src/mod_time.erl index 4ec3cd64e..9530967ec 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -6,7 +6,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index e7cfff819..3ac97b30e 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -5,7 +5,7 @@ %%% Created : 2 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -640,23 +640,23 @@ mod_doc() -> {db_type, #{value => "mnesia | sql | ldap", desc => - ?T("Same as top-level 'default_db' option, but applied to this module only.")}}, + ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, {vcard, #{value => ?T("vCard"), desc => diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index c81c058f5..f8e9a1d93 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -4,7 +4,7 @@ %%% Created : 29 Jul 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -571,7 +571,7 @@ mod_doc() -> }]}}] ++ [{Opt, #{desc => - {?T("Same as top-level '~s' option, but " + {?T("Same as top-level _`~s`_ option, but " "applied to this module only."), [Opt]}}} || Opt <- [ldap_base, ldap_servers, ldap_uids, ldap_deref_aliases, ldap_encrypt, ldap_password, diff --git a/src/mod_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl index ad4be701c..694333d65 100644 --- a/src/mod_vcard_mnesia.erl +++ b/src/mod_vcard_mnesia.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl index 620c1b4b5..842d68c80 100644 --- a/src/mod_vcard_sql.erl +++ b/src/mod_vcard_sql.erl @@ -4,7 +4,7 @@ %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index ab8df2c60..61d582437 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -5,7 +5,7 @@ %%% Created : 9 Mar 2007 by Igor Goryachev %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -228,26 +228,26 @@ mod_doc() -> "frequently their presence. However, the overhead is significantly " "reduced by the use of caching, so you probably don't want " "to set 'use_cache' to 'false'."), "", - ?T("The module depends on 'mod_vcard'."), "", + ?T("The module depends on _`mod_vcard`_."), "", ?T("NOTE: Nowadays https://xmpp.org/extensions/xep-0153.html" "[XEP-0153] is used mostly as \"read-only\", i.e. modern " "clients don't publish their avatars inside vCards. Thus " "in the majority of cases the module is only used along " - "with 'mod_avatar' module for providing backward compatibility.")], + "with _`mod_avatar`_ for providing backward compatibility.")], opts => [{use_cache, #{value => "true | false", desc => - ?T("Same as top-level 'use_cache' option, but applied to this module only.")}}, + ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => - ?T("Same as top-level 'cache_size' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => - ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}}, + ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => - ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}. + ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. diff --git a/src/mod_version.erl b/src/mod_version.erl index 9aa93829d..b842dcfd0 100644 --- a/src/mod_version.erl +++ b/src/mod_version.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mqtt_codec.erl b/src/mqtt_codec.erl index 74fed4eac..e09391ddf 100644 --- a/src/mqtt_codec.erl +++ b/src/mqtt_codec.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov -%%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. +%%% @copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. diff --git a/src/node_flat.erl b/src/node_flat.erl index 4a2a60971..b829395cc 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -39,7 +39,8 @@ -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, create_node/2, delete_node/1, purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, remove_extra_items/3, + publish_item/7, delete_item/4, + remove_extra_items/2, remove_extra_items/3, remove_expired_items/2, get_entity_affiliations/2, get_node_affiliations/1, get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, @@ -375,7 +376,8 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, or (Subscribed == true)) -> {error, xmpp:err_forbidden()}; true -> - if MaxItems > 0 -> + if MaxItems > 0; + MaxItems == unlimited -> Now = erlang:timestamp(), case get_item(Nidx, ItemId) of {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> @@ -402,6 +404,16 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, end end. +remove_extra_items(Nidx, MaxItems) -> + {result, States} = get_states(Nidx), + Records = States ++ mnesia:read({pubsub_orphan, Nidx}), + ItemIds = lists:flatmap(fun(#pubsub_state{items = Is}) -> + Is; + (#pubsub_orphan{items = Is}) -> + Is + end, Records), + remove_extra_items(Nidx, MaxItems, ItemIds). + %% @doc

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 @@ -420,6 +432,22 @@ remove_extra_items(Nidx, MaxItems, ItemIds) -> del_items(Nidx, OldItems), {result, {NewItems, OldItems}}. +remove_expired_items(_Nidx, infinity) -> + {result, []}; +remove_expired_items(Nidx, Seconds) -> + Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), + ExpT = misc:usec_to_now( + erlang:system_time(microsecond) - (Seconds * 1000000)), + ExpItems = lists:filtermap( + fun(#pubsub_item{itemid = {ItemId, _}, + modification = {ModT, _}}) when ModT < ExpT -> + {true, ItemId}; + (#pubsub_item{}) -> + false + end, Items), + del_items(Nidx, ExpItems), + {result, ExpItems}. + %% @doc

Triggers item deletion.

%%

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

@@ -945,15 +973,12 @@ rsm_page(Count, Index, Offset, Items) -> last = Last}. encode_stamp(Stamp) -> - case catch xmpp_util:decode_timestamp(Stamp) of - {MS,S,US} -> {MS,S,US}; - _ -> Stamp + try xmpp_util:decode_timestamp(Stamp) + catch _:{bad_timestamp, _} -> + Stamp % We should return a proper error to the client instead. end. decode_stamp(Stamp) -> - case catch xmpp_util:encode_timestamp(Stamp) of - TimeStamp when is_binary(TimeStamp) -> TimeStamp; - _ -> Stamp - end. + xmpp_util:encode_timestamp(Stamp). transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) -> {pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss}; diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 1e197a51d..a4142cb10 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -40,9 +40,10 @@ -include("translate.hrl"). -export([init/3, terminate/2, options/0, features/0, - create_node_permission/6, create_node/2, delete_node/1, - purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, remove_extra_items/3, + create_node_permission/6, create_node/2, delete_node/1, purge_node/2, + subscribe_node/8, unsubscribe_node/4, + publish_item/7, delete_item/4, + remove_extra_items/2, remove_extra_items/3, remove_expired_items/2, get_entity_affiliations/2, get_node_affiliations/1, get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, @@ -247,7 +248,8 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, or (Subscribed == true)) -> {error, xmpp:err_forbidden()}; true -> - if MaxItems > 0 -> + if MaxItems > 0; + MaxItems == unlimited -> Now = erlang:timestamp(), case get_item(Nidx, ItemId) of {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> @@ -258,20 +260,23 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, {result, _} -> {error, xmpp:err_forbidden()}; _ -> - Items = [ItemId | itemids(Nidx, GenKey)], - {result, {_NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), + OldIds = maybe_remove_extra_items(Nidx, MaxItems, + GenKey, ItemId), set_item(#pubsub_item{ itemid = {ItemId, Nidx}, creation = {Now, GenKey}, modification = {Now, SubKey}, payload = Payload}), - {result, {default, broadcast, OI}} + {result, {default, broadcast, OldIds}} end; true -> {result, {default, broadcast, []}} end end. +remove_extra_items(Nidx, MaxItems) -> + remove_extra_items(Nidx, MaxItems, itemids(Nidx)). + remove_extra_items(_Nidx, unlimited, ItemIds) -> {result, {ItemIds, []}}; remove_extra_items(Nidx, MaxItems, ItemIds) -> @@ -280,6 +285,23 @@ remove_extra_items(Nidx, MaxItems, ItemIds) -> del_items(Nidx, OldItems), {result, {NewItems, OldItems}}. +remove_expired_items(_Nidx, infinity) -> + {result, []}; +remove_expired_items(Nidx, Seconds) -> + ExpT = encode_now( + misc:usec_to_now( + erlang:system_time(microsecond) - (Seconds * 1000000))), + case ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s from pubsub_item where nodeid=%(Nidx)d " + "and creation < %(ExpT)s")) of + {selected, RItems} -> + ItemIds = [ItemId || {ItemId} <- RItems], + del_items(Nidx, ItemIds), + {result, ItemIds}; + _ -> + {result, []} + end. + delete_item(Nidx, Publisher, PublishModel, ItemId) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), @@ -862,6 +884,18 @@ first_in_list(Pred, [H | T]) -> _ -> first_in_list(Pred, T) end. +itemids(Nidx) -> + case catch + ejabberd_sql:sql_query_t( + ?SQL("select @(itemid)s from pubsub_item where " + "nodeid=%(Nidx)d order by modification desc")) + of + {selected, RItems} -> + [ItemId || {ItemId} <- RItems]; + _ -> + [] + end. + itemids(Nidx, {_U, _S, _R} = JID) -> SJID = encode_jid(JID), SJIDLike = <<(encode_jid_like(JID))/binary, "/%">>, @@ -933,6 +967,16 @@ update_subscription(Nidx, JID, Subscription) -> "-affiliation='n'" ]). +-spec maybe_remove_extra_items(mod_pubsub:nodeIdx(), + non_neg_integer() | unlimited, ljid(), + mod_pubsub:itemId()) -> [mod_pubsub:itemId()]. +maybe_remove_extra_items(_Nidx, unlimited, _GenKey, _ItemId) -> + []; +maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) -> + ItemIds = [ItemId | itemids(Nidx, GenKey)], + {result, {_NewIds, OldIds}} = remove_extra_items(Nidx, MaxItems, ItemIds), + OldIds. + -spec decode_jid(SJID :: binary()) -> ljid(). decode_jid(SJID) -> jid:tolower(jid:decode(SJID)). @@ -1037,15 +1081,14 @@ rsm_page(Count, Index, Offset, Items) -> last = Last}. encode_stamp(Stamp) -> - case catch xmpp_util:decode_timestamp(Stamp) of - {MS,S,US} -> encode_now({MS,S,US}); - _ -> Stamp + try xmpp_util:decode_timestamp(Stamp) of + Now -> + encode_now(Now) + catch _:{bad_timestamp, _} -> + Stamp % We should return a proper error to the client instead. end. decode_stamp(Stamp) -> - case catch xmpp_util:encode_timestamp(decode_now(Stamp)) of - TimeStamp when is_binary(TimeStamp) -> TimeStamp; - _ -> Stamp - end. + xmpp_util:encode_timestamp(decode_now(Stamp)). encode_now({T1, T2, T3}) -> <<(misc:i2l(T1, 6))/binary, ":", diff --git a/src/node_pep.erl b/src/node_pep.erl index 58c3050a0..c52db1b3e 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -35,7 +35,8 @@ -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, create_node/2, delete_node/1, purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, remove_extra_items/3, + publish_item/7, delete_item/4, + remove_extra_items/2, remove_extra_items/3, remove_expired_items/2, get_entity_affiliations/2, get_node_affiliations/1, get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, @@ -80,10 +81,12 @@ features() -> [<<"create-nodes">>, <<"auto-create">>, <<"auto-subscribe">>, + <<"config-node">>, <<"delete-nodes">>, <<"delete-items">>, <<"filtered-notifications">>, <<"modify-affiliations">>, + <<"multi-items">>, <<"outcast-affiliation">>, <<"persistent-items">>, <<"publish">>, @@ -135,9 +138,15 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts). +remove_extra_items(Nidx, MaxItems) -> + node_flat:remove_extra_items(Nidx, MaxItems). + remove_extra_items(Nidx, MaxItems, ItemIds) -> node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). +remove_expired_items(Nidx, Seconds) -> + node_flat:remove_expired_items(Nidx, Seconds). + delete_item(Nidx, Publisher, PublishModel, ItemId) -> node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index 7b21aa901..1d1a632f2 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -37,7 +37,8 @@ -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, create_node/2, delete_node/1, purge_node/2, subscribe_node/8, unsubscribe_node/4, - publish_item/7, delete_item/4, remove_extra_items/3, + publish_item/7, delete_item/4, + remove_extra_items/2, remove_extra_items/3, remove_expired_items/2, get_entity_affiliations/2, get_node_affiliations/1, get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, @@ -92,9 +93,15 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts). +remove_extra_items(Nidx, MaxItems) -> + node_flat_sql:remove_extra_items(Nidx, MaxItems). + remove_extra_items(Nidx, MaxItems, ItemIds) -> node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). +remove_expired_items(Nidx, Seconds) -> + node_flat_sql:remove_expired_items(Nidx, Seconds). + delete_item(Nidx, Publisher, PublishModel, ItemId) -> node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl index fe15f3323..df6e9cb60 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -46,7 +46,8 @@ -export([init/3, terminate/2, options/0, set_node/1, get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, + get_nodes/1, get_all_nodes/1, + get_parentnodes/3, get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/6, delete_node/2]). @@ -98,6 +99,14 @@ get_nodes(Host, Limit) -> {Nodes, _} -> Nodes end. +get_all_nodes({_U, _S, _R} = Owner) -> + Host = jid:tolower(jid:remove_resource(Owner)), + mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}); +get_all_nodes(Host) -> + mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) + ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'}, + _ = '_'}). + get_parentnodes(Host, Node, _From) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of [Record] when is_record(Record, pubsub_node) -> diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl index d68355202..6c3419948 100644 --- a/src/nodetree_tree_sql.erl +++ b/src/nodetree_tree_sql.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -45,7 +45,8 @@ -export([init/3, terminate/2, options/0, set_node/1, get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, + get_nodes/1, get_all_nodes/1, + get_parentnodes/3, get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/6, delete_node/2]). @@ -165,6 +166,34 @@ get_nodes(Host, Limit) -> [] end. +get_all_nodes({_U, _S, _R} = JID) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + EncKey = node_flat_sql:encode_jid(GenKey), + Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, + case ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(EncKey)s " + "or host like %(Pattern)s %ESCAPE")) of + {selected, RItems} -> + [raw_to_node(GenKey, Item) || Item <- RItems]; + _ -> + [] + end; +get_all_nodes(Host) -> + Pattern1 = <<"%@", Host/binary>>, + Pattern2 = <<"%@", Host/binary, "/%">>, + case ejabberd_sql:sql_query_t( + ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " + "from pubsub_node where host=%(Host)s " + "or host like %(Pattern1)s " + "or host like %(Pattern2)s %ESCAPE")) of + {selected, RItems} -> + [raw_to_node(Host, Item) || Item <- RItems]; + _ -> + [] + end. + get_parentnodes(Host, Node, _From) -> case get_node(Host, Node) of Record when is_record(Record, pubsub_node) -> diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl index 9cf7a80ca..988c0334c 100644 --- a/src/nodetree_virtual.erl +++ b/src/nodetree_virtual.erl @@ -5,7 +5,7 @@ %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -38,7 +38,8 @@ -export([init/3, terminate/2, options/0, set_node/1, get_node/3, get_node/2, get_node/1, get_nodes/2, - get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, + get_nodes/1, get_all_nodes/1, + get_parentnodes/3, get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/6, delete_node/2]). @@ -71,6 +72,9 @@ get_nodes(Host) -> get_nodes(_Host, _Limit) -> []. +get_all_nodes(_Host) -> + []. + get_parentnodes(_Host, _Node, _From) -> []. diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index 3992a4034..df7dedc9b 100644 --- a/src/prosody2ejabberd.erl +++ b/src/prosody2ejabberd.erl @@ -4,7 +4,7 @@ %%% Created : 20 Jan 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -118,7 +118,7 @@ eval_file(Path) -> case luerl:eval(NewData, State1) of {ok, _} = Res -> Res; - {error, Why} = Err -> + {error, Why, _} = Err -> ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]), Err end; @@ -130,7 +130,7 @@ eval_file(Path) -> maybe_get_scram_auth(Data) -> case proplists:get_value(<<"iteration_count">>, Data, no_ic) of - IC when is_float(IC) -> %% A float like 4096.0 is read + 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, <<"">>)), diff --git a/src/proxy_protocol.erl b/src/proxy_protocol.erl index 146135a69..5716fe2e6 100644 --- a/src/proxy_protocol.erl +++ b/src/proxy_protocol.erl @@ -5,7 +5,7 @@ %%% Created : 27 Nov 2018 by Paweł Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/pubsub_db_sql.erl b/src/pubsub_db_sql.erl index 6c1e3b162..99c758d43 100644 --- a/src/pubsub_db_sql.erl +++ b/src/pubsub_db_sql.erl @@ -5,7 +5,7 @@ %%% Created : 7 Aug 2009 by Pablo Polvorin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/pubsub_index.erl b/src/pubsub_index.erl index 5b8d17e95..370fa1967 100644 --- a/src/pubsub_index.erl +++ b/src/pubsub_index.erl @@ -5,7 +5,7 @@ %%% Created : 30 Apr 2009 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/pubsub_migrate.erl b/src/pubsub_migrate.erl index 35fa87b90..9436ca133 100644 --- a/src/pubsub_migrate.erl +++ b/src/pubsub_migrate.erl @@ -5,7 +5,7 @@ %%% Created : 26 Jul 2014 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/pubsub_subscription.erl b/src/pubsub_subscription.erl index 92d6d314d..0b212d21c 100644 --- a/src/pubsub_subscription.erl +++ b/src/pubsub_subscription.erl @@ -5,7 +5,7 @@ %%% Created : 29 May 2009 by Brian Cully %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/pubsub_subscription_sql.erl b/src/pubsub_subscription_sql.erl index c36934f50..1b5257891 100644 --- a/src/pubsub_subscription_sql.erl +++ b/src/pubsub_subscription_sql.erl @@ -6,7 +6,7 @@ %%% Created : 7 Aug 2009 by Pablo Polvorin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/rest.erl b/src/rest.erl index d724352f2..58e424700 100644 --- a/src/rest.erl +++ b/src/rest.erl @@ -5,7 +5,7 @@ %%% Created : 16 Oct 2014 by Christophe Romain %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -191,13 +191,26 @@ base_url(Server, Path) -> _ -> Url end. +-ifdef(HAVE_URI_STRING). +uri_hack(Str) -> + case uri_string:normalize("%25") of + "%" -> % This hack around bug in httpc >21 <23.2 + binary:replace(Str, <<"%25">>, <<"%2525">>, [global]); + _ -> Str + end. +-else. +uri_hack(Str) -> + Str. +-endif. + url(Url, []) -> Url; url(Url, Params) -> L = [<<"&", (iolist_to_binary(Key))/binary, "=", (misc:url_encode(Value))/binary>> || {Key, Value} <- Params], - <<$&, Encoded/binary>> = iolist_to_binary(L), + <<$&, Encoded0/binary>> = iolist_to_binary(L), + Encoded = uri_hack(Encoded0), <>. url(Server, Path, Params) -> case binary:split(base_url(Server, Path), <<"?">>) of diff --git a/src/str.erl b/src/str.erl index a12d2640e..cc7957f97 100644 --- a/src/str.erl +++ b/src/str.erl @@ -5,7 +5,7 @@ %%% Created : 23 Feb 2012 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/translate.erl b/src/translate.erl index f2ca7c759..b0034588a 100644 --- a/src/translate.erl +++ b/src/translate.erl @@ -5,7 +5,7 @@ %%% Created : 6 Jan 2003 by Alexey Shchepin %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/win32_dns.erl b/src/win32_dns.erl index 76b347e45..bd65cdc36 100644 --- a/src/win32_dns.erl +++ b/src/win32_dns.erl @@ -5,7 +5,7 @@ %%% Created : 5 Mar 2009 by Geoff Cant %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/announce_tests.erl b/test/announce_tests.erl index 4b685cb3c..e1258629b 100644 --- a/test/announce_tests.erl +++ b/test/announce_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/carbons_tests.erl b/test/carbons_tests.erl index a6cde55c1..e1717be90 100644 --- a/test/carbons_tests.erl +++ b/test/carbons_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/csi_tests.erl b/test/csi_tests.erl index a2fb413fb..f1f39b49d 100644 --- a/test/csi_tests.erl +++ b/test/csi_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index bf837d269..10b6aff69 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -3,7 +3,7 @@ %%% Created : 2 Jun 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -76,7 +76,7 @@ init_per_group(Group, Config) -> %% All backends enabled do_init_per_group(Group, Config); Backends -> - %% Skipped backends that were not explicitely enabled + %% Skipped backends that were not explicitly enabled case lists:member(Group, Backends) of true -> do_init_per_group(Group, Config); diff --git a/test/ejabberd_SUITE_data/openssl.cnf b/test/ejabberd_SUITE_data/openssl.cnf index 594653b79..7003c68fc 100644 --- a/test/ejabberd_SUITE_data/openssl.cnf +++ b/test/ejabberd_SUITE_data/openssl.cnf @@ -39,7 +39,7 @@ certs = $dir/certs # Where the issued certs are kept crl_dir = $dir/crl # Where the issued crl are kept database = $dir/index.txt # database index file. #unique_subject = no # Set to 'no' to allow creation of - # several ctificates with same subject. + # several certificates with same subject. new_certs_dir = $dir/newcerts # default place for new certs. certificate = $dir/cacert.pem # The CA certificate @@ -50,7 +50,7 @@ crl = $dir/crl.pem # The current CRL private_key = $dir/private/cakey.pem# The private key RANDFILE = $dir/private/.rand # private random number file -x509_extensions = usr_cert # The extentions to add to the cert +x509_extensions = usr_cert # The extensions to add to the cert # Comment out the following two lines for the "traditional" # (and highly broken) format. @@ -102,7 +102,7 @@ default_bits = 1024 default_keyfile = privkey.pem distinguished_name = req_distinguished_name attributes = req_attributes -x509_extensions = v3_ca # The extentions to add to the self signed cert +x509_extensions = v3_ca # The extensions to add to the self signed cert # Passwords for private keys if not present they will be prompted for # input_password = secret diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs index c359c49c3..8970e02e6 100644 --- a/test/elixir-config/config_test.exs +++ b/test/elixir-config/config_test.exs @@ -47,7 +47,7 @@ defmodule Ejabberd.ConfigTest do assert is_function(register_hook.fun) end - # TODO: When enalbed, this test causes the evaluation of a different config file, so + # TODO: When enabled, this test causes the evaluation of a different config file, so # the other tests, that uses the store, are compromised because the data is different. # So, until a good way is found, this test should remain disabed. # diff --git a/test/example_tests.erl b/test/example_tests.erl index a8fe50ad0..dab5836d7 100644 --- a/test/example_tests.erl +++ b/test/example_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/jidprep_tests.erl b/test/jidprep_tests.erl index 72bf312ae..efe7f711c 100644 --- a/test/jidprep_tests.erl +++ b/test/jidprep_tests.erl @@ -3,7 +3,7 @@ %%% Created : 11 Sep 2019 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2019-2021 ProcessOne +%%% ejabberd, Copyright (C) 2019-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/ldap_srv.erl b/test/ldap_srv.erl index 966c08a1d..63d91f6c7 100644 --- a/test/ldap_srv.erl +++ b/test/ldap_srv.erl @@ -3,7 +3,7 @@ %%% Created : 21 Jun 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/mam_tests.erl b/test/mam_tests.erl index 0de380351..ed7bbe97d 100644 --- a/test/mam_tests.erl +++ b/test/mam_tests.erl @@ -3,7 +3,7 @@ %%% Created : 14 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -454,7 +454,7 @@ recv_archived_messages(Config, From, To, QID, Range) -> MyJID = my_jid(Config), lists:foreach( fun(N) -> - ct:comment("Retreiving ~pth message in range ~p", + ct:comment("Retrieving ~pth message in range ~p", [N, Range]), Body = xmpp:mk_text(integer_to_binary(N)), #message{to = MyJID, diff --git a/test/muc_tests.erl b/test/muc_tests.erl index ea3fd1c74..f050e72ca 100644 --- a/test/muc_tests.erl +++ b/test/muc_tests.erl @@ -3,7 +3,7 @@ %%% Created : 15 Oct 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -191,7 +191,7 @@ service_disco_items(Config) -> service_vcard(Config) -> MUC = muc_jid(Config), - ct:comment("Retreiving vCard from ~s", [jid:encode(MUC)]), + 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{}]}), @@ -1474,7 +1474,7 @@ config_voice_request_interval_slave(Config) -> denied = get_event(Config), ct:comment("Requesting voice again"), send(Config, #message{to = Room, sub_els = [X]}), - ct:comment("Receving voice request error because we're sending to fast"), + ct:comment("Receiving voice request error because we're sending to fast"), #message{from = Room, type = error} = Err = recv_message(Config), #stanza_error{reason = 'resource-constraint'} = xmpp:get_error(Err), ct:comment("Waiting for 5 seconds"), @@ -1872,7 +1872,7 @@ set_vcard(Config, VCard) -> get_vcard(Config) -> Room = muc_room_jid(Config), - ct:comment("Retreiving vCard from ~s", [jid:encode(Room)]), + 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]} -> diff --git a/test/offline_tests.erl b/test/offline_tests.erl index 1021c86e8..5ffe50def 100644 --- a/test/offline_tests.erl +++ b/test/offline_tests.erl @@ -3,7 +3,7 @@ %%% Created : 7 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -489,6 +489,14 @@ wait_for_complete(Config, N) -> end end, error, [0, 100, 200, 2000, 5000, 10000]). +xevent_stored(#message{body = [], subject = []}, _) -> false; +xevent_stored(#message{type = T}, _) when T /= chat, T /= normal -> false; +xevent_stored(_, #xevent{id = undefined}) -> true; +xevent_stored(_, #xevent{offline = true}) -> true; +xevent_stored(_, #xevent{delivered = true}) -> true; +xevent_stored(_, #xevent{displayed = true}) -> true; +xevent_stored(_, _) -> false. + message_iterator(Config) -> ServerJID = server_jid(Config), ChatStates = [[#chatstate{type = composing}]], @@ -511,8 +519,14 @@ message_iterator(Config) -> fun(#message{type = error}) -> true; (#message{type = groupchat}) -> false; (#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true; + (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; (#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false; - (#message{sub_els = [_, #xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; + (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled -> + xevent_stored(Msg#message{body = body, type = chat}, Event); + (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled -> + xevent_stored(Msg, Event); + (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled -> + xevent_stored(Msg, Event); (#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; (#message{sub_els = [#hint{type = store}|_]}) -> true; (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; diff --git a/test/privacy_tests.erl b/test/privacy_tests.erl index f24893b16..39526e5b1 100644 --- a/test/privacy_tests.erl +++ b/test/privacy_tests.erl @@ -3,7 +3,7 @@ %%% Created : 18 Oct 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/private_tests.erl b/test/private_tests.erl index e17932946..fd961b8b8 100644 --- a/test/private_tests.erl +++ b/test/private_tests.erl @@ -3,7 +3,7 @@ %%% Created : 23 Nov 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/proxy65_tests.erl b/test/proxy65_tests.erl index 618252eb0..0a75f820e 100644 --- a/test/proxy65_tests.erl +++ b/test/proxy65_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -48,7 +48,7 @@ feature_enabled(Config) -> service_vcard(Config) -> JID = proxy_jid(Config), - ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), + 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{}]}), diff --git a/test/pubsub_tests.erl b/test/pubsub_tests.erl index 2bca35173..043545422 100644 --- a/test/pubsub_tests.erl +++ b/test/pubsub_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -87,7 +87,7 @@ test_features(Config) -> test_vcard(Config) -> JID = pubsub_jid(Config), - ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), + 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{}]}), diff --git a/test/push_tests.erl b/test/push_tests.erl index 2b367cba8..553f90968 100644 --- a/test/push_tests.erl +++ b/test/push_tests.erl @@ -3,7 +3,7 @@ %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/replaced_tests.erl b/test/replaced_tests.erl index 9a172107e..5f3224aea 100644 --- a/test/replaced_tests.erl +++ b/test/replaced_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/roster_tests.erl b/test/roster_tests.erl index a3b6009c9..f8d1999e6 100644 --- a/test/roster_tests.erl +++ b/test/roster_tests.erl @@ -3,7 +3,7 @@ %%% Created : 22 Oct 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -224,13 +224,21 @@ get_items(Config, Version) -> sub_els = [#roster_query{ver = Version}]}) of #iq{type = result, sub_els = [#roster_query{ver = NewVersion, items = Items}]} -> - {NewVersion, Items}; + {NewVersion, normalize_items(Items)}; #iq{type = result, sub_els = []} -> {empty, []}; #iq{type = error} = Err -> xmpp:get_error(Err) end. +normalize_items(Items) -> + Items2 = + lists:map( + fun(I) -> + I#roster_item{groups = lists:sort(I#roster_item.groups)} + end, Items), + lists:sort(Items2). + get_item(Config, JID) -> case get_items(Config) of {_Ver, Items} when is_list(Items) -> diff --git a/test/sm_tests.erl b/test/sm_tests.erl index 81fe39ed0..8e87c642a 100644 --- a/test/sm_tests.erl +++ b/test/sm_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/stundisco_tests.erl b/test/stundisco_tests.erl index f80652df4..65569ead8 100644 --- a/test/stundisco_tests.erl +++ b/test/stundisco_tests.erl @@ -3,7 +3,7 @@ %%% Created : 22 Apr 2020 by Holger Weiss %%% %%% -%%% ejabberd, Copyright (C) 2020-2021 ProcessOne +%%% ejabberd, Copyright (C) 2020-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/suite.erl b/test/suite.erl index de7d64873..7669e4fe4 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -3,7 +3,7 @@ %%% Created : 27 Jun 2013 by Evgeniy Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/test/upload_tests.erl b/test/upload_tests.erl index 434d7be11..3a5885111 100644 --- a/test/upload_tests.erl +++ b/test/upload_tests.erl @@ -3,7 +3,7 @@ %%% Created : 17 May 2018 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -55,7 +55,7 @@ feature_enabled(Config) -> service_vcard(Config) -> Upload = upload_jid(Config), - ct:comment("Retreiving vCard from ~s", [jid:encode(Upload)]), + 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{}]}), diff --git a/test/vcard_tests.erl b/test/vcard_tests.erl index 8e16f7ac0..791a2265c 100644 --- a/test/vcard_tests.erl +++ b/test/vcard_tests.erl @@ -3,7 +3,7 @@ %%% Created : 16 Nov 2016 by Evgeny Khramtsov %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -90,7 +90,7 @@ get_set(Config) -> service_vcard(Config) -> JID = server_jid(Config), - ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), + 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{}]}), diff --git a/test/webadmin_tests.erl b/test/webadmin_tests.erl index 53a670d4e..34e2c1b86 100644 --- a/test/webadmin_tests.erl +++ b/test/webadmin_tests.erl @@ -3,7 +3,7 @@ %%% Created : 23 Mar 2020 by Pawel Chmielowski %%% %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/tools/captcha-ng.sh b/tools/captcha-ng.sh index cbcb95407..bb57385c4 100755 --- a/tools/captcha-ng.sh +++ b/tools/captcha-ng.sh @@ -42,7 +42,8 @@ INTRUDER() { NUMBERS=$(echo "$INPUT" | grep -o . | tr '\n' ' ') SORTED_UNIQ_NUM=$(echo "${NUMBERS[@]}" | sort -u | tr '\n' ' ') -RANDOM_DIGITS=$(echo 123456789 | grep -o . | sort -R | tr '\n' ' ') +SORT_RANDOM_CMD="$( ( echo x|sort -R >&/dev/null && echo "sort -R" ) || ( echo x|shuf >&/dev/null && echo shuf ) || echo cat)" +RANDOM_DIGITS=$(echo 123456789 | grep -o . | eval "$SORT_RANDOM_CMD" | tr '\n' ' ') INTRUDER=-1 for i in $RANDOM_DIGITS diff --git a/tools/captcha.sh b/tools/captcha.sh index 9fa4a52c4..7885858a2 100755 --- a/tools/captcha.sh +++ b/tools/captcha.sh @@ -15,69 +15,62 @@ INPUT=$1 -if test -n ${BASH_VERSION:-''} ; then - get_random () - { - R=$RANDOM - } -else - for n in `od -A n -t u2 -N 48 /dev/urandom`; do RL="$RL$n "; done - get_random () - { - R=${RL%% *} - RL=${RL#* } - } -fi +for n in $(od -A n -t u2 -N 48 /dev/urandom); do RL="$RL$n "; done +get_random () +{ + R=${RL%% *} + RL=${RL#* } +} get_random -WAVE1_AMPLITUDE=$((2 + $R % 5)) +WAVE1_AMPLITUDE=$((2 + R % 5)) get_random -WAVE1_LENGTH=$((50 + $R % 25)) +WAVE1_LENGTH=$((50 + R % 25)) get_random -WAVE2_AMPLITUDE=$((2 + $R % 5)) +WAVE2_AMPLITUDE=$((2 + R % 5)) get_random -WAVE2_LENGTH=$((50 + $R % 25)) +WAVE2_LENGTH=$((50 + R % 25)) get_random -WAVE3_AMPLITUDE=$((2 + $R % 5)) +WAVE3_AMPLITUDE=$((2 + R % 5)) get_random -WAVE3_LENGTH=$((50 + $R % 25)) +WAVE3_LENGTH=$((50 + R % 25)) get_random -W1_LINE_START_Y=$((10 + $R % 40)) +W1_LINE_START_Y=$((10 + R % 40)) get_random -W1_LINE_STOP_Y=$((10 + $R % 40)) +W1_LINE_STOP_Y=$((10 + R % 40)) get_random -W2_LINE_START_Y=$((10 + $R % 40)) +W2_LINE_START_Y=$((10 + R % 40)) get_random -W2_LINE_STOP_Y=$((10 + $R % 40)) +W2_LINE_STOP_Y=$((10 + R % 40)) get_random -W3_LINE_START_Y=$((10 + $R % 40)) +W3_LINE_START_Y=$((10 + R % 40)) get_random -W3_LINE_STOP_Y=$((10 + $R % 40)) +W3_LINE_STOP_Y=$((10 + R % 40)) get_random -B1_LINE_START_Y=$(($R % 40)) +B1_LINE_START_Y=$((R % 40)) get_random -B1_LINE_STOP_Y=$(($R % 40)) +B1_LINE_STOP_Y=$((R % 40)) get_random -B2_LINE_START_Y=$(($R % 40)) +B2_LINE_START_Y=$((R % 40)) get_random -B2_LINE_STOP_Y=$(($R % 40)) -#B3_LINE_START_Y=$(($R % 40)) -#B3_LINE_STOP_Y=$(($R % 40)) +B2_LINE_STOP_Y=$((R % 40)) +#B3_LINE_START_Y=$((R % 40)) +#B3_LINE_STOP_Y=$((R % 40)) get_random -B1_LINE_START_X=$(($R % 20)) +B1_LINE_START_X=$((R % 20)) get_random -B1_LINE_STOP_X=$((100 + $R % 40)) +B1_LINE_STOP_X=$((100 + R % 40)) get_random -B2_LINE_START_X=$(($R % 20)) +B2_LINE_START_X=$((R % 20)) get_random -B2_LINE_STOP_X=$((100 + $R % 40)) -#B3_LINE_START_X=$(($R % 20)) -#B3_LINE_STOP_X=$((100 + $R % 40)) +B2_LINE_STOP_X=$((100 + R % 40)) +#B3_LINE_START_X=$((R % 20)) +#B3_LINE_STOP_X=$((100 + R % 40)) get_random -ROLL_X=$(($R % 40)) +ROLL_X=$((R % 40)) convert -size 180x60 xc:none -pointsize 40 \ \( -clone 0 -fill white \ diff --git a/tools/make-binaries b/tools/make-binaries new file mode 100755 index 000000000..20749da11 --- /dev/null +++ b/tools/make-binaries @@ -0,0 +1,886 @@ +#!/bin/sh + +# Build portable binary release tarballs for Linux/x64 and Linux/arm64. +# +# Author: Holger Weiss . +# +# Copyright (c) 2022 ProcessOne, SARL. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u + +export PATH='/usr/local/bin:/usr/bin:/bin' + +myself=${0##*/} + +info() +{ + echo "$myself: $*" +} + +error() +{ + echo >&2 "$myself: $*" +} + +usage() +{ + echo >&2 "Usage: $myself" + exit 2 +} + +mix_version() +{ + # Mix insists on SemVer format. + local vsn="$(printf '%s' "$1" | sed 's/\.0/./')" + + case $vsn in + *.*.*) printf '%s' "$vsn" ;; + *.*) printf '%s.0' "$vsn" ;; + esac +} + +if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ] +then + error "Please call this script from the repository's root directory." + exit 2 +elif [ $# -ne 0 ] +then + usage +fi + +rel_name='ejabberd' +rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]') +mix_vsn=$(mix_version "$rel_vsn") +crosstool_vsn='1.25.0' +termcap_vsn='1.3.1' +expat_vsn='2.4.9' +zlib_vsn='1.2.12' +yaml_vsn='0.2.5' +ssl_vsn='1.1.1q' +otp_vsn='24.3.4.5' +elixir_vsn='1.14.0' +pam_vsn='1.5.2' +png_vsn='1.6.38' +jpeg_vsn='9e' +webp_vsn='1.2.4' +gd_vsn='2.3.3' +odbc_vsn='2.3.11' +sqlite_vsn='3390300' +root_dir="${BUILD_DIR:-$HOME/build}" +bootstrap_dir="$root_dir/bootstrap" +ct_prefix_dir="$root_dir/x-tools" +build_dir="$root_dir/$rel_name" +crosstool_dir="crosstool-ng-$crosstool_vsn" +termcap_dir="termcap-$termcap_vsn" +expat_dir="expat-$expat_vsn" +zlib_dir="zlib-$zlib_vsn" +yaml_dir="yaml-$yaml_vsn" +ssl_dir="openssl-$ssl_vsn" +otp_dir="otp_src_$otp_vsn" +elixir_dir="elixir-$elixir_vsn" +pam_dir="Linux-PAM-$pam_vsn" +png_dir="libpng-$png_vsn" +jpeg_dir="jpeg-$jpeg_vsn" +webp_dir="libwebp-$webp_vsn" +gd_dir="libgd-$gd_vsn" +odbc_dir="unixODBC-$odbc_vsn" +sqlite_dir="sqlite-autoconf-$sqlite_vsn" +crosstool_tar="$crosstool_dir.tar.xz" +termcap_tar="$termcap_dir.tar.gz" +expat_tar="$expat_dir.tar.gz" +zlib_tar="$zlib_dir.tar.gz" +yaml_tar="$yaml_dir.tar.gz" +ssl_tar="$ssl_dir.tar.gz" +otp_tar="$otp_dir.tar.gz" +elixir_tar="v$elixir_vsn.tar.gz" +pam_tar="$pam_dir.tar.xz" +png_tar="$png_dir.tar.gz" +jpeg_tar="jpegsrc.v$jpeg_vsn.tar.gz" +webp_tar="$webp_dir.tar.gz" +gd_tar="$gd_dir.tar.gz" +sqlite_tar="$sqlite_dir.tar.gz" +odbc_tar="$odbc_dir.tar.gz" +rel_tar="$rel_name-$mix_vsn.tar.gz" +ct_jobs=$(nproc) +src_dir="$root_dir/src" +platform='x86_64-pc-linux-gnu' +targets='x86_64-linux-gnu aarch64-linux-gnu' +build_start=$(date '+%F %T') +have_current_deps='false' +dep_vsns_file="$build_dir/.dep_vsns" +dep_vsns='' +deps='crosstool + termcap + expat + zlib + yaml + ssl + otp + elixir + pam + png + jpeg + webp + gd + odbc + sqlite' + +umask 022 + +#' Try to find a browser for checking dependency versions. +have_browser() +{ + for browser in 'lynx' 'links' 'elinks' + do + $browser -dump 'https://ejabberd.im/' >'/dev/null' && return 0 + done + return 1 +} +#. + +#' Check whether the given dependency version is up-to-date. +check_vsn() +{ + local name="$1" + local our_vsn="$2" + local src_url="$3" + local reg_exp="$4" + local cur_vsn=$($browser -dump "$src_url" | + sed -n "s/.*$reg_exp.*/\\1/p" | + head -1) + + if [ "$our_vsn" = "$cur_vsn" ] + then + return 0 + else + error "Current $name version is: $cur_vsn" + error "But our $name version is: $our_vsn" + error "Update $0 or set CHECK_DEPS=false" + exit 1 + fi +} +#. + +#' Check whether our dependency versions are up-to-date. +check_configured_dep_vsns() +{ + check_vsn 'OpenSSL' "$ssl_vsn" \ + 'https://www.openssl.org/source/' \ + 'openssl-\(1\.[0-9][0-9a-z.]*\)\.tar\.gz' + check_vsn 'LibYAML' "$yaml_vsn" \ + 'https://pyyaml.org/wiki/LibYAML' \ + 'yaml-\([0-9][0-9.]*\)\.tar\.gz' + check_vsn 'zlib' "$zlib_vsn" \ + 'https://zlib.net/' \ + 'zlib-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'Expat' "$expat_vsn" \ + 'https://github.com/libexpat/libexpat/releases' \ + '[0-9]\]\([1-9][0-9.]*\)' + check_vsn 'Termcap' "$termcap_vsn" \ + 'https://ftp.gnu.org/gnu/termcap/' \ + 'termcap-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'SQLite' "$sqlite_vsn" \ + 'https://www.sqlite.org/download.html' \ + 'sqlite-autoconf-\([1-9][0-9]*\)\.tar\.gz' + check_vsn 'ODBC' "$odbc_vsn" \ + 'http://www.unixodbc.org/download.html' \ + 'unixODBC-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'Linux-PAM' "$pam_vsn" \ + 'https://github.com/linux-pam/linux-pam/releases' \ + '[0-9]\]Linux-PAM \([1-9][0-9.]*\)' + check_vsn 'libpng' "$png_vsn" \ + 'http://www.libpng.org/pub/png/libpng.html' \ + 'libpng-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'JPEG' "$jpeg_vsn" \ + 'https://www.ijg.org' \ + 'jpegsrc.v\([1-9][0-9a-z]*\)\.tar\.gz' + check_vsn 'WebP' "$webp_vsn" \ + 'https://developers.google.com/speed/webp/download' \ + 'libwebp-\([1-9][0-9.]*\)\.tar\.gz' + check_vsn 'LibGD' "$gd_vsn" \ + 'https://github.com/libgd/libgd/releases' \ + 'gd-\([1-9][0-9.]*\)' + check_vsn 'Elixir' "$elixir_vsn" \ + 'https://elixir-lang.org/install.html' \ + 'v\([1-9][0-9.]*\)\.tar\.gz' +} +#. + +#' Check whether existing dependencies are up-to-date. +check_built_dep_vsns() +{ + for dep in $deps + do + eval dep_vsns=\"\$dep_vsns\$${dep}_vsn\" + done + + if [ -e "$dep_vsns_file" ] + then + if [ "$dep_vsns" = "$(cat "$dep_vsns_file")" ] + then have_current_deps='true' + fi + rm "$dep_vsns_file" + fi +} +#. + +#' Save built dependency versions. +save_built_dep_vsns() +{ + echo "$dep_vsns" >"$dep_vsns_file" +} +#. + +#' Create common part of Crosstool-NG configuration file. +create_common_config() +{ + local file="$1" + + cat >"$file" <<-'EOF' + CT_CONFIG_VERSION="4" + CT_DOWNLOAD_AGENT_CURL=y + CT_OMIT_TARGET_VENDOR=y + CT_CC_LANG_CXX=y + CT_ARCH_64=y + CT_KERNEL_LINUX=y + CT_LINUX_V_3_16=y + CT_GLIBC_V_2_17=y + CT_GLIBC_KERNEL_VERSION_NONE=y + EOF +} +#. + +#' Create Crosstool-NG configuration file for x64. +create_x64_config() +{ + local file="$1" + + create_common_config "$file" + + cat >>"$file" <<-'EOF' + CT_ARCH_X86=y + EOF +} +#. + +#' Create Crosstool-NG configuration file for arm64. +create_arm64_config() +{ + local file="$1" + + create_common_config "$file" + + cat >>"$file" <<-'EOF' + CT_ARCH_ARM=y + EOF +} +#. + +#' Return our name for the given platform. +arch_name() +{ + local target="$1" + + case $target in + x86_64*) + printf 'x64' + ;; + aarch64*) + printf 'arm64' + ;; + *) + error "Unsupported target platform: $target" + exit 1 + ;; + esac +} +#. + +#' Add native Erlang/OTP "bin" directory to PATH (for bootstrapping and Mix). +add_otp_path() +{ + local mode="$1" + local prefix="$2" + + if [ "$mode" = 'native' ] + then native_otp_bin="$prefix/bin" + fi + export PATH="$native_otp_bin:$PATH" +} +#. + +#' Create and populate /opt/ejabberd directory. +create_data_dir() +{ + local code_dir="$1" + local data_dir="$2" + + mkdir "$data_dir" "$data_dir/database" "$data_dir/logs" + mv "$code_dir/conf" "$data_dir" + chmod 'o-rwx' "$data_dir/"* + curl -o "$data_dir/conf/cacert.pem" 'https://curl.se/ca/cacert.pem' + sed -i '/^loglevel:/a\ +\ +ca_file: /opt/ejabberd/conf/cacert.pem\ +\ +certfiles:\ + - /opt/ejabberd/conf/server.pem' "$data_dir/conf/$rel_name.yml" +} +#. + +#' Add systemd unit and init script. +add_systemd_unit() +{ + local code_dir="$1" + + sed -e "s|@ctlscriptpath@|/opt/$rel_name-$rel_vsn/bin|g" \ + -e "s|@installuser@|$rel_name|g" 'ejabberd.service.template' \ + >"$code_dir/bin/ejabberd.service" + sed -e "s|@ctlscriptpath@|/opt/$rel_name-$rel_vsn/bin|g" \ + -e "s|@installuser@|$rel_name|g" 'ejabberd.init.template' \ + >"$code_dir/bin/ejabberd.init" + chmod '+x' "$code_dir/bin/ejabberd.init" +} +#. + +#' Add CAPTCHA script(s). +add_captcha_script() +{ + local code_dir="$1" + + cp -p 'tools/captcha'*'.sh' "$code_dir/lib" +} +#. + +#' Use our VT100 to avoid depending on Terminfo, adjust options/paths. +edit_ejabberdctl() +{ + local code_dir="$1" + + sed -i \ + -e "2iexport TERM='internal'" \ + -e '/ERL_OPTIONS=/d' \ + -e 's|^ERLANG_NODE=ejabberd$|ERLANG_NODE=ejabberd@localhost|' \ + -e 's|_DIR:=".*}/|_DIR:="/opt/ejabberd/|' \ + -e 's|/database|/database/$ERLANG_NODE|' \ + "$code_dir/bin/${rel_name}ctl" +} +#. + +#' Delete unused files and directories, just to save some space. +remove_unused_files() +{ + local code_dir="$1" + + # Remove shared object file used only in test suite: + find "$code_dir/lib" -name 'otp_test_engine.so' -delete + + # Remove shared object files of statically linked NIFs: + find "$code_dir/lib/crypto-"* "$code_dir/lib/asn1-"* \ + '(' -name 'asn1rt_nif.so' -o \ + -name 'crypto.so' -o \ + -name 'lib' -o \ + -name 'priv' ')' \ + -delete + + # Remove unused ERTS binaries (see systools_make:erts_binary_filter/0): + find "$code_dir/erts-"*'/bin' \ + '(' -name 'ct_run' -o \ + -name 'dialyzer' -o \ + -name 'erlc' -o \ + -name 'typer' -o \ + -name 'yielding_c_fun' ')' \ + -delete + + # Remove unused Mix stuff: + find "$code_dir/bin" -name 'ejabberd' -delete + find "$code_dir/releases" -name 'COOKIE' -delete +} +#. + +#' Strip ERTS binaries, shared objects, and BEAM files. +strip_files() +{ + local code_dir="$1" + local strip_cmd="$2" + + find "$code_dir/lib" \ + -type f \ + -name '*.so' \ + -exec "$strip_cmd" -s '{}' '+' + find "$code_dir/erts-"*'/bin' "$code_dir/lib/"*'/priv/bin' \ + -type f \ + -perm '-u+x' \ + -exec "$strip_cmd" -s '{}' '+' 2>'/dev/null' || : + erl -noinput -eval \ + "{ok, _} = beam_lib:strip_release('$code_dir'), halt()" +} +#. + +#' Build toochain for a given target. +build_toolchain() +{ + local target="$1" + local prefix="$2" + local arch=$(arch_name "$target") + + if [ -d "$prefix" ] + then + info "Using existing toolchain in $prefix ..." + else + if ! [ -x "$bootstrap_dir/bin/ct-ng" ] + then + info "Extracting Crosstool-NG $crosstool_vsn ..." + cd "$src_dir" + tar -xJf "$crosstool_tar" + cd "$OLDPWD" + + info "Building Crosstool-NG $crosstool_vsn ..." + cd "$src_dir/$crosstool_dir" + ./configure --prefix="$bootstrap_dir" + make + make install + cd "$OLDPWD" + fi + + info "Building toolchain for $arch ..." + cd "$root_dir" + create_${arch}_config 'defconfig' + ct-ng defconfig + ct-ng build CT_PREFIX="$ct_prefix_dir" CT_JOBS="$ct_jobs" + rm -rf '.config' '.build' 'build.log' + cd "$OLDPWD" + fi +} +#. + +#' Build target dependencies. +build_deps() +{ + local mode="$1" + local target="$2" + local prefix="$3" + local arch="$(arch_name "$target")" + local target_src_dir="$prefix/src" + local saved_path="$PATH" + + if [ "$mode" = 'cross' ] + then configure="./configure --host=$target --build=$platform" + else configure='./configure' + fi + + mkdir "$prefix" + + info 'Extracting dependencies ...' + mkdir "$target_src_dir" + cd "$target_src_dir" + tar -xzf "$src_dir/$termcap_tar" + tar -xzf "$src_dir/$sqlite_tar" + tar -xzf "$src_dir/$odbc_tar" + tar -xzf "$src_dir/$expat_tar" + tar -xzf "$src_dir/$zlib_tar" + tar -xzf "$src_dir/$yaml_tar" + tar -xzf "$src_dir/$ssl_tar" + tar -xzf "$src_dir/$otp_tar" + tar -xzf "$src_dir/$elixir_tar" + tar -xzf "$src_dir/$png_tar" + tar -xzf "$src_dir/$jpeg_tar" + tar -xzf "$src_dir/$webp_tar" + tar -xzf "$src_dir/$gd_tar" + tar -xJf "$src_dir/$pam_tar" + cd "$OLDPWD" + + info "Building Termcap $termcap_vsn for $arch ..." + cd "$target_src_dir/$termcap_dir" + $configure --prefix="$prefix" + cat >'config.h' <<-'EOF' + #ifndef CONFIG_H + #define CONFIG_H + #define INTERNAL_TERMINAL "internal:\\\n" \ + "\t:am:bs:ms:xn:xo:\\\n" \ + "\t:co#80:it#8:li#24:vt#3:\\\n" \ + "\t:@8=\\EOM:DO=\\E[%dB:K1=\\EOq:K2=\\EOr:K3=\\EOs:K4=\\EOp:K5=\\EOn:\\\n" \ + "\t:LE=\\E[%dD:RA=\\E[?7l:RI=\\E[%dC:SA=\\E[?7h:UP=\\E[%dA:\\\n" \ + "\t:ac=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~:\\\n" \ + "\t:ae=^O:as=^N:bl=^G:cb=\\E[1K:cd=\\E[J:ce=\\E[K:cl=\\E[H\\E[J:\\\n" \ + "\t:cm=\\E[%i%d;%dH:cr=^M:cs=\\E[%i%d;%dr:ct=\\E[3g:do=^J:\\\n" \ + "\t:eA=\\E(B\\E)0:ho=\\E[H:k0=\\EOy:k1=\\EOP:k2=\\EOQ:k3=\\EOR:\\\n" \ + "\t:k4=\\EOS:k5=\\EOt:k6=\\EOu:k7=\\EOv:k8=\\EOl:k9=\\EOw:k;=\\EOx:\\\n" \ + "\t:kb=^H:kd=\\EOB:ke=\\E[?1l\\E>:kl=\\EOD:kr=\\EOC:ks=\\E[?1h\\E=:\\\n" \ + "\t:ku=\\EOA:le=^H:mb=\\E[5m:md=\\E[1m:me=\\E[m\\017:mr=\\E[7m:\\\n" \ + "\t:nd=\\E[C:rc=\\E8:rs=\\E>\\E[?3l\\E[?4l\\E[?5l\\E[?7h\\E[?8h:\\\n" \ + "\t:..sa=\\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t\\016%e\\017%;:\\\n" \ + "\t:sc=\\E7:se=\\E[m:sf=^J:so=\\E[7m:sr=\\EM:st=\\EH:ta=^I:ue=\\E[m:\\\n" \ + "\t:up=\\E[A:us=\\E[4m:" + #endif + EOF + make CPPFLAGS="$CPPFLAGS -DHAVE_CONFIG_H=1" + make install + cd "$OLDPWD" + + info "Building zlib $zlib_vsn for $arch ..." + cd "$target_src_dir/$zlib_dir" + CFLAGS="$CFLAGS -O3 -fPIC" ./configure --prefix="$prefix" --static + make + make install + cd "$OLDPWD" + + info "Building OpenSSL $ssl_vsn for $arch ..." + cd "$target_src_dir/$ssl_dir" + CFLAGS="$CFLAGS -O3 -fPIC" ./Configure no-shared no-ui-console \ + --prefix="$prefix" \ + --openssldir="$prefix" \ + "linux-${target%-linux-gnu}" + make build_libs + make install_dev + cd "$OLDPWD" + + info "Building Expat $expat_vsn for $arch ..." + cd "$target_src_dir/$expat_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + --without-docbook \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building LibYAML $yaml_vsn for $arch ..." + cd "$target_src_dir/$yaml_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building SQLite $sqlite_vsn for $arch ..." + cd "$target_src_dir/$sqlite_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building ODBC $odbc_vsn for $arch ..." + cd "$target_src_dir/$odbc_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building Linux-PAM $pam_vsn for $arch ..." + cd "$target_src_dir/$pam_dir" + $configure --prefix="$prefix" --includedir="$prefix/include/security" \ + --enable-static --disable-shared --disable-doc --enable-db=no \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building libpng $png_vsn for $arch ..." + cd "$target_src_dir/$png_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building JPEG $jpeg_vsn for $arch ..." + cd "$target_src_dir/$jpeg_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building WebP $webp_vsn for $arch ..." + cd "$target_src_dir/$webp_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building LibGD $gd_vsn for $arch ..." + cd "$target_src_dir/$gd_dir" + $configure --prefix="$prefix" --enable-static --disable-shared \ + --with-zlib="$prefix" \ + --with-webp="$prefix" \ + --with-jpeg="$prefix" \ + --with-png="$prefix" \ + --without-avif \ + --without-fontconfig \ + --without-freetype \ + --without-heif \ + --without-libiconv-prefix \ + --without-liq \ + --without-raqm \ + --without-tiff \ + --without-x \ + --without-xpm \ + CFLAGS="$CFLAGS -O3 -fPIC" + make + make install + cd "$OLDPWD" + + info "Building Erlang/OTP $otp_vsn for $arch ..." + if [ "$mode" = 'cross' ] + then + add_otp_path "$mode" "$prefix" + export erl_xcomp_sysroot="$prefix" + fi + cd "$target_src_dir/$otp_dir" + # Don't link against libnsl: https://github.com/erlang/otp/pull/5558 + sed -i -e '/LIBS="-lnsl/d' -e '/LIBS="-lsocket/d' \ + 'lib/erl_interface/configure' + # The additional CFLAGS/LIBS below are required by --enable-static-nifs. + # The "-ldl" flag specifically is only needed for ODBC, though. + $configure \ + --prefix="$prefix" \ + --with-ssl="$prefix" \ + --with-odbc="$prefix" \ + --without-javac \ + --disable-dynamic-ssl-lib \ + --enable-static-nifs \ + CFLAGS="$CFLAGS -Wl,-L$prefix/lib" \ + LIBS='-lcrypto -ldl' + make + make install + if [ "$mode" = 'native' ] + then add_otp_path "$mode" "$prefix" + else unset erl_xcomp_sysroot + fi + cd "$OLDPWD" + + info "Building Elixir $elixir_vsn for $arch ..." + cd "$target_src_dir/$elixir_dir" + make install PREFIX="$prefix" + cd "$OLDPWD" + + export PATH="$saved_path" +} +#. + +#' Build the actual release. +build_rel() +{ + local mode="$1" + local target="$2" + local prefix="$3" + local arch="$(arch_name "$target")" + local rel_dir="$PWD/_build/prod" + local target_data_dir="$prefix/$rel_name" + local target_dst_dir="$prefix/$rel_name-$rel_vsn" + local target_dst_tar="$rel_name-$rel_vsn-linux-$arch.tar.gz" + local saved_path="$PATH" + + # + # The "$ct_prefix_dir/$target/$target/bin" directory contains cross + # compilation tools without "$target-" prefix. We add it to the PATH, + # just in case tools are called without prefix somewhere. However, we + # try to use the prefixed tools everywhere, so it should be possible to + # omit this directory from the path if desired. See also: + # + # https://stackoverflow.com/a/24243789 + # + export PATH="$ct_prefix_dir/$target/bin:$ct_prefix_dir/$target/$target/bin:$PATH" + export CC="$target-gcc" + export CXX="$target-g++" + export CPP="$target-cpp" + export LD="$target-ld" + export AS="$target-as" + export AR="$target-ar" + export NM="$target-nm" + export RANLIB="$target-ranlib" + export OBJCOPY="$target-objcopy" + export STRIP="$target-strip" + export CPPFLAGS="-I$prefix/include" + export CFLAGS="-g0 -O2 -pipe -fomit-frame-pointer -static-libgcc $CPPFLAGS" + export CXXFLAGS="$CFLAGS -static-libstdc++" + export LDFLAGS="-L$prefix/lib -static-libgcc -static-libstdc++" + export ERL_COMPILER_OPTIONS='[deterministic, no_debug_info]' + + if [ "$mode" = 'cross' ] + then configure="./configure --host=$target --build=$platform" + else configure='./configure' + fi + + if [ $have_current_deps = false ] + then build_deps "$mode" "$target" "$prefix" + fi + + add_otp_path "$mode" "$prefix" + + if [ "$mode" = 'native' ] # In order to only do this once. + then + info "Fetching Mix dependencies" + mix local.hex --force + mix local.rebar --force + fi + + info "Removing old $rel_name builds" + rm -rf '_build' 'deps' + + info "Building $rel_name $rel_vsn for $arch ..." + ./autogen.sh + eimp_cflags='-fcommon' + eimp_libs='-lwebp -ljpeg -lpng -lz -lm' + export CC="$CC -Wl,-ldl" # Required by (statically linking) epam. + export LIBS="$eimp_libs -lcrypto -lpthread -ldl" + export CFLAGS="$CFLAGS $eimp_cflags" + export LDFLAGS="$LDFLAGS $eimp_libs" + if [ "$mode" = 'cross' ] + then + # Hand over --host/--build to configure scripts of dependencies. + export host_alias="$target" + export build_alias="$platform" + fi + # The cache variable makes cross compilation work. + ac_cv_erlang_root_dir="$prefix/lib/erlang" $configure \ + --with-rebar='mix' \ + --with-sqlite3="$prefix" \ + --enable-user="$rel_name" \ + --enable-all \ + --disable-erlang-version-check + make deps + sed -i 's/ *-lstdc++//g' 'deps/'*'/rebar.config'* # Link statically. + if [ "$mode" = 'cross' ] + then + ln -s "$prefix/lib/erlang" 'lib/erlang' + erts_dir=$(ls -1d 'lib/erlang/erts-'*) + ei_inc="$prefix/lib/erlang/lib/erl_interface-"*'/include' + ei_lib="$prefix/lib/erlang/lib/erl_interface-"*'/lib' + export LDLIBS='-lpthread' + export ERL_EI_INCLUDE_DIR=$(ls -1d $ei_inc) + export ERL_EI_LIBDIR=$(ls -1d $ei_lib) + sed -i "/include_executables/a\\ + include_erts: \"$erts_dir\"," 'mix.exs' + fi + make rel + if [ "$mode" = 'cross' ] + then + sed -i '/include_erts/d' 'mix.exs' + rm 'lib/erlang' + unset LDLIBS ERL_EI_INCLUDE_DIR ERL_EI_LIBDIR + unset host_alias build_alias + fi + + info "Putting together $rel_name $rel_vsn archive for $arch ..." + mkdir "$target_dst_dir" + tar -C "$target_dst_dir" -xzf "$rel_dir/$rel_tar" + create_data_dir "$target_dst_dir" "$target_data_dir" + add_systemd_unit "$target_dst_dir" + add_captcha_script "$target_dst_dir" + edit_ejabberdctl "$target_dst_dir" + remove_unused_files "$target_dst_dir" + strip_files "$target_dst_dir" "$STRIP" + tar -C "$prefix" --owner="$rel_name" --group="$rel_name" -cf - \ + "$rel_name" "$rel_name-$rel_vsn" | gzip -9 >"$target_dst_tar" + rm -rf "$target_dst_dir" "$target_data_dir" + + info "Created $target_dst_tar successfully." + + unset CC CXX CPP LD AS AR NM RANLIB OBJCOPY STRIP + unset CFLAGS CXXFLAGS LDFLAGS LIBS ERL_COMPILER_OPTIONS + export PATH="$saved_path" +} +#. + +if [ "${CHECK_DEPS:-true}" = 'true' ] +then + if have_browser + then + check_configured_dep_vsns + else + error 'Cannot check dependency versions.' + error 'Install a browser or set CHECK_DEPS=false' + exit 1 + fi +else + info "Won't check dependency versions." +fi + +if ! mkdir -p "$root_dir" +then + error 'Set BUILD_DIR to a usable build directory path.' + exit 1 +fi + +check_built_dep_vsns + +info 'Removing old bootstrap tools ...' +rm -rf "$bootstrap_dir" +mkdir "$bootstrap_dir" + +if [ $have_current_deps = true ] +then + info 'Dependencies are up-to-date ...' +else + # Keep existing toolchains but rebuild everything else. + info 'Removing old builds ...' + rm -rf "$build_dir" + mkdir "$build_dir" + + info 'Removing old source ...' + rm -rf "$src_dir" + mkdir "$src_dir" + + info 'Downloading dependencies ...' + cd "$src_dir" + curl -LO "http://crosstool-ng.org/download/crosstool-ng/$crosstool_tar" + curl -LO "https://ftp.gnu.org/gnu/termcap/$termcap_tar" + curl -LO "https://github.com/libexpat/libexpat/releases/download/R_$(printf '%s' "$expat_vsn" | sed 's/\./_/g')/$expat_tar" + curl -LO "https://zlib.net/$zlib_tar" + curl -LO "https://pyyaml.org/download/libyaml/$yaml_tar" + curl -LO "https://www.openssl.org/source/$ssl_tar" + curl -LO "https://github.com/erlang/otp/releases/download/OTP-$otp_vsn/$otp_tar" + curl -LO "https://github.com/elixir-lang/elixir/archive/v$elixir_vsn.tar.gz" + curl -LO "https://github.com/linux-pam/linux-pam/releases/download/v$pam_vsn/$pam_tar" + curl -LO "https://download.sourceforge.net/libpng/$png_tar" + curl -LO "https://www.ijg.org/files/$jpeg_tar" + curl -LO "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/$webp_tar" + curl -LO "https://github.com/libgd/libgd/releases/download/gd-$gd_vsn/$gd_tar" + curl -LO "http://www.unixodbc.org/$odbc_tar" + curl -LO "https://www.sqlite.org/$(date '+%Y')/$sqlite_tar" \ + || curl -LO "https://www.sqlite.org/$(date -d '1 year ago' '+%Y')/$sqlite_tar" \ + || curl -LO "https://www.sqlite.org/$(date -d '2 years ago' '+%Y')/$sqlite_tar" + cd "$OLDPWD" +fi + +mkdir "$bootstrap_dir/bin" +export PATH="$bootstrap_dir/bin:$PATH" # For ct-ng. +export LC_ALL='C.UTF-8' # Elixir insists on a UTF-8 environment. + +for target in $targets +do + prefix="$build_dir/$(arch_name "$target")" + toolchain_dir="$ct_prefix_dir/$target" + + if [ "$(uname -m)-linux-gnu" = "$target" ] + then mode='native' + else mode='cross' + fi + build_toolchain "$target" "$toolchain_dir" + build_rel "$mode" "$target" "$prefix" +done + +save_built_dep_vsns + +info "Build started: $build_start" +info "Build ended: $(date '+%F %T')" + +# vim:set foldmarker=#',#. foldmethod=marker: diff --git a/tools/make-installers b/tools/make-installers new file mode 100755 index 000000000..d54a90bd8 --- /dev/null +++ b/tools/make-installers @@ -0,0 +1,369 @@ +#!/bin/sh + +# Build installers for Linux/x64 and Linux/arm64. +# +# Author: Holger Weiss . +# +# Copyright (c) 2022 ProcessOne, SARL. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u + +myself=${0##*/} +architectures='x64 arm64' +iteration=1 + +usage() +{ + echo >&2 "Usage: $myself [-i ]" + exit 2 +} + +while getopts i: opt +do + case $opt in + i) + iteration="$OPTARG" + ;; + \?) + usage + ;; + esac +done +shift $((OPTIND - 1)) + +if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ] +then + echo >&2 "Please call this script from the repository's root directory." + exit 2 +elif [ $# -ne 0 ] +then + usage +fi +if type 'makeself' >'/dev/null' +then makeself='makeself' +elif type 'makeself.sh' >'/dev/null' +then makeself='makeself.sh' +else + echo >&2 'This script requires makeself: https://makeself.io' + exit 1 +fi + +rel_name='ejabberd' +rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]') +home_url='https://www.ejabberd.im' +doc_url='https://docs.ejabberd.im' +upgrade_url="$doc_url/admin/upgrade/#specific-version-upgrade-notes" +admin_url="$doc_url/admin/installation/#administration-account" +default_code_dir="/opt/$rel_name-$rel_vsn" +default_data_dir="/opt/$rel_name" +tmp_dir=$(mktemp -d "/tmp/.$rel_name.XXXXXX") + +trap 'rm -rf "$tmp_dir"' INT TERM EXIT +umask 022 + +create_help_file() +{ + local file="$1" + + cat >"$file" <<-EOF + This is the $rel_name $rel_vsn-$iteration installer for linux-$arch + + Visit: + $home_url + + ejabberd documentation site: + $doc_url + + EOF +} + +create_setup_script() +{ + local dir="$1" + + cat >"$dir/setup" <<-EOF + #!/bin/sh + + set -e + set -u + + export PATH='/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin' + + user_agrees() + { + local question="\$*" + + if [ -t 0 ] + then + read -p "\$question (y/n) [n] " response + case "\$response" in + [Yy]|[Yy][Ee][Ss]) + return 0 + ;; + [Nn]|[Nn][Oo]|'') + return 1 + ;; + *) + echo 'Please respond with "yes" or "no".' + user_agrees "\$question" + ;; + esac + else # Assume 'yes' if not running interactively. + return 0 + fi + } + + if [ \$(id -u) = 0 ] + then + is_superuser=true + else + is_superuser=false + echo "Running without superuser privileges (installer wasn't invoked" + echo 'with "sudo"), cannot perform system-wide installation this way.' + if ! user_agrees 'Continue anyway?' + then + echo 'Aborting installation.' + exit 1 + fi + fi + + if [ \$is_superuser = true ] + then + code_dir='$default_code_dir' + data_dir='$default_data_dir' + user_name='$rel_name' + group_name='$rel_name' + elif user_agrees "Install $rel_name below \$HOME/opt?" + then + code_dir="\$HOME/opt/$rel_name-$rel_vsn" + data_dir="\$HOME/opt/$rel_name" + user_name="\$(id -u -n)" + group_name="\$(id -g -n)" + else + read -p 'Installation prefix: ' prefix + if printf '%s' "\$prefix" | grep -q '^/' + then + code_dir="\$prefix/$rel_name-$rel_vsn" + data_dir="\$prefix/$rel_name" + user_name="\$(id -u -n)" + group_name="\$(id -g -n)" + else + echo >&2 'Prefix must be specified as an absolute path.' + echo >&2 'Aborting installation.' + exit 1 + fi + fi + + prefix="\$(dirname "\$code_dir")" + conf_dir="\$data_dir/conf" + pem_file="\$conf_dir/server.pem" + uninstall_file="\$code_dir/uninstall.txt" + + if [ -e '/run/systemd/system' ] + then is_systemd=true + else is_systemd=false + fi + if [ -e "\$data_dir" ] + then is_upgrade=true + else is_upgrade=false + fi + if id -u "\$user_name" >'/dev/null' 2>&1 + then user_exists=true + else user_exists=false + fi + + echo + echo 'The following installation paths will be used:' + echo "- \$code_dir" + if [ \$is_upgrade = true ] + then echo "- \$data_dir (existing files won't be modified)" + else echo "- \$data_dir (for configuration, database, and log files)" + fi + if [ \$is_superuser = true ] + then + if [ \$is_systemd = true ] + then + echo '- /etc/systemd/system/$rel_name.service' + if [ \$is_upgrade = false ] + then echo 'The $rel_name service is going to be enabled and started.' + fi + fi + if [ \$user_exists = false ] + then echo 'The $rel_name user is going to be created.' + fi + fi + if ! user_agrees 'Install $rel_name $rel_vsn now?' + then + echo 'Aborting installation.' + exit 1 + fi + echo + + if [ \$user_exists = false ] && [ \$is_superuser = true ] + then useradd -r -d "\$data_dir" "\$user_name" + fi + + host=\$(hostname --fqdn 2>'/dev/null' || :) + if [ -z "\$host" ] + then host='localhost' + fi + + mkdir -p "\$prefix" + tar -cf - '$rel_name' | tar --skip-old-files -C "\$prefix" -xf - + tar -cf - '$rel_name-$rel_vsn' | tar -C "\$prefix" -xf - + + if [ \$is_superuser = true ] + then + if [ \$is_upgrade = false ] + then chown -R -h "\$user_name:\$group_name" "\$data_dir" + fi + chown -R -h "\$(id -u -n):\$group_name" "\$code_dir" + chmod -R g+rX "\$code_dir" + chmod '4750' "\$code_dir/lib/epam-"*'/priv/bin/epam' + else + sed -i "s/^INSTALLUSER=.*/INSTALLUSER=\"\$user_name\"/" \ + "\$code_dir/bin/${rel_name}ctl" + sed -i "s/^USER=.*/USER=\$user_name/" \ + "\$code_dir/bin/$rel_name.init" + sed -i \ + -e "s/^User=.*/User=\$user_name/" \ + -e "s/^Group=.*/Group=\$group_name/" \ + "\$code_dir/bin/$rel_name.service" + fi + if [ "\$code_dir" != '$default_code_dir' ] + then + sed -i "s|$default_code_dir|\$code_dir|g" \ + "\$code_dir/bin/${rel_name}ctl" \ + "\$code_dir/bin/$rel_name.init" \ + "\$code_dir/bin/$rel_name.service" + fi + if [ "\$data_dir" != '$default_data_dir' ] + then + sed -i "s|$default_data_dir|\$data_dir|g" \ + "\$code_dir/bin/${rel_name}ctl" \ + "\$code_dir/bin/$rel_name.init" \ + "\$code_dir/bin/$rel_name.service" \ + "\$data_dir/conf/$rel_name.yml" \ + "\$data_dir/conf/${rel_name}ctl.cfg" + fi + + if [ \$is_upgrade = false ] + then + sed -i "s/ - localhost$/ - \$host/" "\$conf_dir/$rel_name.yml" + openssl req -x509 \ + -batch \ + -nodes \ + -newkey rsa:4096 \ + -keyout "\$pem_file" \ + -out "\$pem_file" \ + -days 3650 \ + -subj "/CN=\$host" >'/dev/null' 2>&1 || : + if ! [ -e "\$pem_file" ] + then + echo 'Failed to create a TLS certificate for $rel_name.' >&2 + elif [ \$is_superuser = true ] + then + chown "\$user_name:\$group_name" "\$pem_file" + fi + fi + + case \$is_systemd,\$is_superuser in + true,true) + cp "\$code_dir/bin/$rel_name.service" '/etc/systemd/system/' + systemctl -q daemon-reload + if [ \$is_upgrade = false ] + then systemctl -q --now enable '$rel_name' + fi + ;; + true,false) + echo 'You might want to install a systemd unit (see the' + echo "\$code_dir/bin directory for an example)." + ;; + false,*) + echo 'You might want to install an init script (see the' + echo "\$code_dir/bin directory for an example)." + ;; + esac + + echo + echo '$rel_name $rel_vsn has been installed successfully.' + echo + + cat >"\$uninstall_file" <<-_EOF + # To uninstall $rel_name, first remove the service. If you're using systemd: + systemctl --now disable $rel_name + rm -f /etc/systemd/system/$rel_name.service + + # Remove the binary files: + rm -rf \$code_dir + + # If you want to remove your configuration, database and logs: + rm -rf \$data_dir + _EOF + if [ \$is_superuser = true ] + then + cat >>"\$uninstall_file" <<-_EOF + + # To remove the user running $rel_name: + userdel \$user_name + _EOF + fi + + if [ \$is_upgrade = false ] + then + if [ \$is_systemd = true ] && [ \$is_superuser = true ] + then + echo 'Now you can check $rel_name is running correctly:' + echo ' systemctl status $rel_name' + echo + fi + echo 'Next you may want to edit $rel_name.yml to set up hosts,' + echo 'register an account and grant it admin rigts, see:' + echo + echo '$admin_url' + else + echo 'Please check the following web site for upgrade notes:' + echo + echo '$upgrade_url' + echo + if [ \$is_systemd = true ] && [ \$is_superuser = true ] + then + echo 'If everything looks fine, restart the $rel_name service:' + echo ' systemctl restart $rel_name' + else + echo 'If everything looks fine, restart the $rel_name service.' + fi + fi + EOF + chmod +x "$dir/setup" +} + +for arch in $architectures +do + tar_name="$rel_name-$rel_vsn-linux-$arch.tar.gz" + installer_name="$rel_name-$rel_vsn-$iteration-linux-$arch.run" + + test -e "$tar_name" || tools/make-binaries + echo "$myself: Putting together installer for $arch ..." + tar -C "$tmp_dir" -xzpf "$tar_name" + create_help_file "$tmp_dir/help.txt" + create_setup_script "$tmp_dir" + "$makeself" --help-header "$tmp_dir/help.txt" \ + "$tmp_dir" "$installer_name" "$rel_name $rel_vsn" './setup' + find "$tmp_dir" -mindepth 1 -delete +done +echo "$myself: Created installers successfully." diff --git a/tools/make-packages b/tools/make-packages new file mode 100755 index 000000000..3b3388ae8 --- /dev/null +++ b/tools/make-packages @@ -0,0 +1,240 @@ +#!/bin/sh + +# Build DEB and RPM packages for Linux/x64 and Linux/arm64. +# +# Author: Holger Weiss . +# +# Copyright (c) 2022 ProcessOne, SARL. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u + +myself=${0##*/} +architectures='x64 arm64' +iteration=1 + +usage() +{ + echo >&2 "Usage: $myself [-i ]" + exit 2 +} + +while getopts i: opt +do + case $opt in + i) + iteration="$OPTARG" + ;; + \?) + usage + ;; + esac +done +shift $((OPTIND - 1)) + +if ! [ -e 'mix.exs' ] || ! [ -e "tools/$myself" ] +then + echo >&2 "Please call this script from the repository's root directory." + exit 2 +elif [ $# -ne 0 ] +then + usage +fi +if ! type fpm >'/dev/null' +then + echo >&2 'This script requires fpm: https://fpm.readthedocs.io' + exit 1 +fi + +rel_name='ejabberd' +rel_vsn=$(git describe --tags | sed -e 's/-g.*//' -e 's/-/./' | tr -d '[:space:]') +conf_dir="/opt/$rel_name/conf" +pem_file="$conf_dir/server.pem" +tmp_dir=$(mktemp -d "/tmp/.$myself.XXXXXX") + +trap 'rm -rf "$tmp_dir"' INT TERM EXIT +umask 022 + +create_scripts() +{ + local dir="$1" + + cat >"$dir/before-install" <<-EOF + if ! getent group '$rel_name' >'/dev/null' + then groupadd -r '$rel_name' + fi + if ! getent passwd '$rel_name' >'/dev/null' + then useradd -r -m -d '/opt/$rel_name' -g '$rel_name' '$rel_name' + fi + if ! [ -e '$pem_file' ] + then + if ! [ -e '/opt/$rel_name' ] # Huh? + then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name' + fi + if ! [ -e '$conf_dir' ] + then install -o '$rel_name' -g '$rel_name' -m 750 -d '$conf_dir' + fi + host=\$(hostname --fqdn 2>'/dev/null' || :) + if [ -z "\$host" ] + then host='localhost' + fi + openssl req -x509 \ + -batch \ + -nodes \ + -newkey rsa:4096 \ + -keyout '$pem_file' \ + -out '$pem_file' \ + -days 3650 \ + -subj "/CN=\$host" >'/dev/null' 2>&1 || : + if [ -e '$pem_file' ] + then chown '$rel_name:$rel_name' '$pem_file' + else echo 'Failed to create a TLS certificate for ejabberd.' >&2 + fi + fi + if ! [ -e '/opt/$rel_name/database' ] + then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name/database' + fi + if ! [ -e '/opt/$rel_name/logs' ] + then install -o '$rel_name' -g '$rel_name' -m 750 -d '/opt/$rel_name/logs' + fi + EOF + + cat >"$dir/after-install" <<-EOF + host=\$(hostname --fqdn 2>'/dev/null' || :) + if [ -n "\$host" ] + then sed -i "s/ - localhost$/ - \$host/" '$conf_dir/$rel_name.yml' + fi + chown 'root:$rel_name' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + chmod '4750' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + chown -R -h '$rel_name:$rel_name' '/opt/$rel_name' + chmod 'o-rwx' '/opt/$rel_name/'* + EOF + + cat >"$dir/after-upgrade" <<-EOF + chown 'root:$rel_name' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + chmod '4750' '/opt/$rel_name-$rel_vsn/lib/epam-'*'/priv/bin/epam' + EOF + + cat >"$dir/after-remove" <<-EOF + rm -f '/opt/$rel_name/.erlang.cookie' + if getent passwd '$rel_name' >'/dev/null' + then userdel '$rel_name' + fi + if getent group '$rel_name' >'/dev/null' + then groupdel '$rel_name' + fi + EOF +} + +package_architecture() +{ + local target="$1" + local host_target="$(uname -m)-$target" + + case $host_target in + x86_64-x64) + printf 'native' + ;; + x86_64-arm64) + printf 'arm64' + ;; + *) + echo >&2 "Unsupported host/target combination: $host_target" + exit 1 + ;; + esac +} + +make_package() +{ + local output_type="$1" + local architecture="$(package_architecture "$2")" + local work_dir="$3" + local include_dirs="$4" + + cd "$work_dir" # FPM's "--chdir" option doesn't work (as I'd expect). + fpm --output-type "$output_type" \ + --input-type 'dir' \ + --name "$rel_name" \ + --version "$rel_vsn" \ + --iteration "$iteration" \ + --license 'GPL-2+' \ + --category 'net' \ + --provides 'stun-server' \ + --provides 'turn-server' \ + --provides 'xmpp-server' \ + --no-depends \ + --no-auto-depends \ + --deb-maintainerscripts-force-errorchecks \ + --deb-systemd-enable \ + --deb-systemd-auto-start \ + --deb-systemd "./$rel_name.service" \ + --deb-init "./$rel_name" \ + --rpm-init "./$rel_name" \ + --config-files "$conf_dir" \ + --directories "/opt/$rel_name" \ + --directories "/opt/$rel_name-$rel_vsn" \ + --architecture "$architecture" \ + --maintainer 'ejabberd Maintainers ' \ + --vendor 'ProcessOne, SARL' \ + --description 'Robust and scalable XMPP/MQTT/SIP server.' \ + --url 'https://ejabberd.im' \ + --before-install './before-install' \ + --after-install './after-install' \ + --before-upgrade './before-install' \ + --after-upgrade './after-upgrade' \ + --after-remove './after-remove' \ + $include_dirs + cd "$OLDPWD" +} + +for arch in $architectures +do + tar_name="$rel_name-$rel_vsn-linux-$arch.tar.gz" + arch_dir="$tmp_dir/$arch" + opt_dir="$arch_dir/opt" + etc_dir="$arch_dir/etc" + bin_dir="$arch_dir/usr/sbin" + dst_dir="$opt_dir/$rel_name-$rel_vsn" + + test -e "$tar_name" || tools/make-binaries + + echo "$myself: Putting together DEB and RPM packages for $arch ..." + + mkdir -p "$opt_dir" "$bin_dir" + tar -C "$opt_dir" -xzf "$tar_name" + + cat >"$bin_dir/${rel_name}ctl" <<-EOF + #!/bin/sh + exec '/opt/$rel_name-$rel_vsn/bin/${rel_name}ctl' "\$@" + EOF + chmod +x "$bin_dir/${rel_name}ctl" + + mkdir -p "$etc_dir/systemd/system" + mv "$dst_dir/bin/$rel_name.service" "$etc_dir/systemd/system" + mv "$dst_dir/bin/$rel_name.init" "$arch_dir/$rel_name" + sed -i \ + "s|opt/$rel_name-$rel_vsn/bin/${rel_name}ctl|usr/sbin/${rel_name}ctl|g" \ + "$etc_dir/systemd/system/$rel_name.service" "$arch_dir/$rel_name" + + create_scripts "$arch_dir" + make_package 'rpm' "$arch" "$arch_dir" './opt ./usr ./etc' + mv "$etc_dir/systemd/system/$rel_name.service" "$arch_dir" + rm -r "$etc_dir" + make_package 'deb' "$arch" "$arch_dir" './opt ./usr' + mv "$arch_dir/$rel_name"?$rel_vsn*.??? . +done +echo "$myself: Created DEB and RPM packages successfully." diff --git a/tools/opt_types.sh b/tools/opt_types.sh index 8de6d8945..71d69a5d4 100755 --- a/tools/opt_types.sh +++ b/tools/opt_types.sh @@ -483,8 +483,12 @@ options_required([Form]) -> proplists:get_value(required, Opts, []). format_file(Path, Form) -> + Line = case erl_syntax:get_pos(Form) of + {L, _} -> L; + L -> L + end, filename:rootname(filename:basename(Path)) ++ ".erl:" ++ - integer_to_list(erl_syntax:get_pos(Form)). + integer_to_list(Line). module(Path) -> list_to_atom(filename:rootname(filename:basename(Path))). @@ -524,6 +528,8 @@ fold_opt(File, Fun, Acc, AbsCode) -> Fun(File, {#state.defaults, Form}, Acc1); {attribute, {spec, {spec, {{options, 0}, Spec}}}} -> Fun(File, {#state.specs, hd(Spec)}, Acc1); + {attribute, {spec, {{options, 0}, Spec}}} -> + Fun(File, {#state.specs, hd(Spec)}, Acc1); _ -> Acc1 end @@ -539,6 +545,8 @@ fold_mod_opt(File, Fun, Acc, AbsCode) -> Fun(File, {#state.mod_defaults, Form}, Acc1); {attribute, {spec, {spec, {{mod_options, 1}, Spec}}}} -> Fun(File, {#state.mod_specs, hd(Spec)}, Acc1); + {attribute, {spec, {{mod_options, 1}, Spec}}} -> + Fun(File, {#state.mod_specs, hd(Spec)}, Acc1); _ -> Acc1 end @@ -572,6 +580,8 @@ is_behaviour(AbsCode, Mod) -> {attribute, {Attr, {_, Mod}}} when Attr == behaviour orelse Attr == behavior -> true; + {attribute, {behaviour, Mod}} -> + true; _ -> false end diff --git a/tools/update-deps-releases.pl b/tools/update-deps-releases.pl index 4f7e55972..b450bd69b 100755 --- a/tools/update-deps-releases.pl +++ b/tools/update-deps-releases.pl @@ -413,7 +413,7 @@ while (1) { my $cmd = show_commands($old_deps ? (U => "Update dependency") : (), $changed_deps ? (T => "Tag new release") : (), @operations ? (A => "Apply changes") : (), - R => "Refresh repositiories", + R => "Refresh repositories", H => "What release to Hex", E => "Exit"); last if $cmd eq "E"; diff --git a/tools/xml_compress_gen.erl b/tools/xml_compress_gen.erl index 5b43c5925..157e6cc8a 100644 --- a/tools/xml_compress_gen.erl +++ b/tools/xml_compress_gen.erl @@ -4,7 +4,7 @@ %% Created : 14 Sep 2018 Pawel Chmielowski %% %% -%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%% ejabberd, Copyright (C) 2002-2022 ProcessOne %% %% This program is free software; you can redistribute it and/or %% modify it under the terms of the GNU General Public License as diff --git a/vars.config.in b/vars.config.in index 9b3ac7585..e80e49782 100644 --- a/vars.config.in +++ b/vars.config.in @@ -1,6 +1,6 @@ %%%---------------------------------------------------------------------- %%% -%%% ejabberd, Copyright (C) 2002-2021 ProcessOne +%%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,7 +21,6 @@ %% Macros {roster_gateway_workaround, @roster_gateway_workaround@}. {full_xml, @full_xml@}. -{db_type, @db_type@}. {debug, @debug@}. {new_sql_schema, @new_sql_schema@}. @@ -30,6 +29,7 @@ %% Dependencies {odbc, @odbc@}. +{mssql, @mssql@}. {mysql, @mysql@}. {pgsql, @pgsql@}. {sqlite, @sqlite@}. @@ -51,13 +51,19 @@ {release, true}. {release_dir, "${SCRIPT_DIR%/*}"}. {sysconfdir, "{{release_dir}}/etc"}. +{erts_dir, "{{release_dir}}/erts-${ERTS_VSN#erts-}"}. {installuser, "@INSTALLUSER@"}. -{erl, "{{release_dir}}/{{erts_vsn}}/bin/erl"}. -{epmd, "{{release_dir}}/{{erts_vsn}}/bin/epmd"}. +{erl, "{{erts_dir}}/bin/erl"}. +{epmd, "{{erts_dir}}/bin/epmd"}. {localstatedir, "{{release_dir}}/var"}. {libdir, "{{release_dir}}/lib"}. {docdir, "{{release_dir}}/doc"}. +%% OTP release +{config_dir, "{{release_dir}}/conf"}. +{logs_dir, "{{release_dir}}/logs"}. +{spool_dir, "{{release_dir}}/database"}. + {latest_deps, @latest_deps@}. {system_deps, @system_deps@}.