Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d576902bd4 | |||
| a3da27e917 | |||
| 3f015c829c | |||
| 2732c8f6fc | |||
| 3192687334 | |||
| 126653e01b | |||
| ce7acafe37 | |||
| 216a0c97b9 | |||
| 3c8308bb8d | |||
| 1c6aa5e84e | |||
| 609a1d07cf | |||
| 8c026582ab | |||
| 81df1ae3af | |||
| 22435ca562 | |||
| f464189819 | |||
| 89e504c55f | |||
| c1d3d1318e | |||
| d120e0ad91 | |||
| c17ec50e3a | |||
| 368ba3fc55 | |||
| cd098c5adc | |||
| 221e58fff0 | |||
| 9b043ae276 | |||
| abc09054e5 | |||
| 07a193d4dc | |||
| cf09ed2df2 | |||
| 0cc1ae0a6a | |||
| e1efd29156 | |||
| 05feab35c4 | |||
| 2198fbce97 | |||
| b8ab80d1f3 | |||
| 30e5c9bd3e | |||
| 251756de00 | |||
| 3e987d3bae | |||
| 138cc25355 | |||
| a15f186253 | |||
| 59ec3d36f4 | |||
| d7250111ce | |||
| adfb924808 | |||
| 48f2adde98 | |||
| c378ea403e | |||
| 692ccd2e20 | |||
| 5882c9b456 | |||
| db41643bea | |||
| cb076924cc | |||
| cce4056040 | |||
| 7ad525b542 | |||
| 5bf64381cb | |||
| f435d0a103 | |||
| e4d21c1941 | |||
| 5414cbe821 | |||
| e6b1521b29 | |||
| 2a7d9d93c8 | |||
| dfd2045523 | |||
| 9e35af54e0 | |||
| d87151aee6 | |||
| 4ecd8a0780 | |||
| 803c31f760 | |||
| 978c92f5e1 | |||
| 32397aa0c3 | |||
| 1a58a201f8 | |||
| 2acbf4625b | |||
| 7566d254e4 | |||
| 68dee8cbb3 | |||
| f7e8d287d5 | |||
| 30bca124f4 | |||
| d7891a5562 | |||
| 1d396b4716 | |||
| 7f3fceb432 | |||
| 81581f7794 | |||
| 47175adc74 | |||
| 350827f8f4 | |||
| 05c2995c7a | |||
| 2fa6e2fd90 | |||
| ba9a79c89c | |||
| 13ad754ecc | |||
| 0b02d42836 | |||
| ee0a8d2966 | |||
| 793ca45dda | |||
| 6e20e9bcf9 | |||
| b8d2a72333 | |||
| 0760c7273c | |||
| 9bd099013f | |||
| 68fb12153e | |||
| e19d1e9571 | |||
| 64150cc7c5 |
+1
-1
@@ -27,7 +27,7 @@ before_install:
|
||||
- pip install --user coveralls-merge
|
||||
|
||||
install:
|
||||
- sudo apt-get -qq install libexpat1-dev libyaml-dev libpam0g-dev libsqlite3-dev
|
||||
- sudo apt-get -qq install libexpat1-dev libyaml-dev libpam0g-dev libsqlite3-dev libgd-dev libwebp-dev
|
||||
|
||||
before_script:
|
||||
# Ulimit: See Travis-CI issue report: https://github.com/travis-ci/travis-ci/issues/3328
|
||||
|
||||
+12
-5
@@ -9,7 +9,7 @@ ENV EJABBERD_BRANCH=17.08 \
|
||||
EJABBERD_HOME=/opt/ejabberd \
|
||||
EJABBERD_DEBUG_MODE=false \
|
||||
HOME=$EJABBERD_HOME \
|
||||
PATH=$EJABBERD_HOME/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
PATH=$EJABBERD_HOME/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/sbin \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
XMPP_DOMAIN=localhost \
|
||||
# Set default locale for the environment
|
||||
@@ -38,6 +38,7 @@ RUN set -x \
|
||||
erlang-src erlang-dev \
|
||||
' \
|
||||
&& requiredAptPackages=' \
|
||||
wget \
|
||||
locales \
|
||||
ldnsutils \
|
||||
python2.7 \
|
||||
@@ -47,7 +48,7 @@ RUN set -x \
|
||||
erlang-base erlang-snmp erlang-ssl erlang-ssh erlang-webtool \
|
||||
erlang-tools erlang-xmerl erlang-corba erlang-diameter erlang-eldap \
|
||||
erlang-eunit erlang-ic erlang-odbc erlang-os-mon \
|
||||
erlang-parsetools erlang-percept erlang-typer erlang-inets \
|
||||
erlang-parsetools erlang-percept erlang-typer \
|
||||
python-mysqldb \
|
||||
imagemagick \
|
||||
' \
|
||||
@@ -68,7 +69,6 @@ RUN set -x \
|
||||
&& chmod +x ./autogen.sh \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure --enable-user=$EJABBERD_USER \
|
||||
--prefix=/ \
|
||||
--enable-all \
|
||||
--disable-tools \
|
||||
--disable-pam \
|
||||
@@ -82,12 +82,19 @@ RUN set -x \
|
||||
&& mkdir $EJABBERD_HOME/module_source \
|
||||
&& cd $EJABBERD_HOME \
|
||||
&& rm -rf /tmp/ejabberd \
|
||||
&& rm -rf /etc/ejabberd \
|
||||
&& ln -sf $EJABBERD_HOME/conf /etc/ejabberd \
|
||||
&& rm -rf /usr/local/etc/ejabberd \
|
||||
&& ln -sf $EJABBERD_HOME/conf /usr/local/etc/ejabberd \
|
||||
&& chown -R $EJABBERD_USER: $EJABBERD_HOME \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get purge -y --auto-remove $buildDeps
|
||||
|
||||
RUN wget -P /usr/local/share/ca-certificates/cacert.org http://www.cacert.org/certs/root.crt http://www.cacert.org/certs/class3.crt; \
|
||||
update-ca-certificates
|
||||
|
||||
# Create logging directories
|
||||
RUN mkdir -p /var/log/ejabberd
|
||||
RUN touch /var/log/ejabberd/crash.log /var/log/ejabberd/error.log /var/log/ejabberd/erlang.log
|
||||
|
||||
# Wrapper for setting config on disk from environment
|
||||
# allows setting things like XMPP domain at runtime
|
||||
ADD ./docker/run.sh /sbin/run
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ xref: all
|
||||
|
||||
|
||||
translations:
|
||||
contrib/extract_translations/prepare-translation.sh -updateall
|
||||
tools/prepare-tr.sh
|
||||
|
||||
edoc:
|
||||
$(ERL) -noinput +B -eval \
|
||||
@@ -123,7 +123,7 @@ FILES_WILDCARD=$(call FILTER_DIRS,$(foreach w,$(1),$(wildcard $(w))))
|
||||
|
||||
ifeq ($(MAKECMDGOALS),copy-files-sub)
|
||||
|
||||
DEPS:=$(sort $(shell $(REBAR) list-deps|$(SED) -e '/^=/d;s/ .*//'))
|
||||
DEPS:=$(sort $(shell $(REBAR) -q list-deps|$(SED) -e '/[a-z0-9_-]+\s/d;s/ .*//'))
|
||||
|
||||
DEPS_FILES=$(call FILES_WILDCARD,$(foreach DEP,$(DEPS),deps/$(DEP)/ebin/*.beam deps/$(DEP)/ebin/*.app deps/$(DEP)/priv/* deps/$(DEP)/priv/lib/* deps/$(DEP)/priv/bin/* deps/$(DEP)/include/*.hrl deps/$(DEP)/COPY* deps/$(DEP)/LICENSE* deps/$(DEP)/lib/*/ebin/*.beam deps/$(DEP)/lib/*/ebin/*.app))
|
||||
DEPS_FILES_FILTERED=$(filter-out %/epam deps/elixir/ebin/elixir.app,$(DEPS_FILES))
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ defmodule Ejabberd.ConfigFile do
|
||||
|
||||
module :mod_client_state do
|
||||
@opts [
|
||||
drop_chat_states: true,
|
||||
queue_chat_states: true,
|
||||
queue_presence: false]
|
||||
end
|
||||
|
||||
|
||||
+1
-1
@@ -560,7 +560,7 @@ modules:
|
||||
mod_caps: {}
|
||||
mod_carboncopy: {}
|
||||
mod_client_state:
|
||||
drop_chat_states: true
|
||||
queue_chat_states: true
|
||||
queue_presence: false
|
||||
mod_configure: {} # requires mod_adhoc
|
||||
mod_disco: {}
|
||||
|
||||
@@ -236,6 +236,14 @@ AC_ARG_ENABLE(sip,
|
||||
*) AC_MSG_ERROR(bad value ${enableval} for --enable-sip) ;;
|
||||
esac],[if test "x$sip" = "x"; then sip=false; fi])
|
||||
|
||||
AC_ARG_ENABLE(graphics,
|
||||
[AC_HELP_STRING([--enable-graphics], [enable support for graphic images manipulation (default: yes)])],
|
||||
[case "${enableval}" in
|
||||
yes) graphics=true ;;
|
||||
no) graphics=false ;;
|
||||
*) AC_MSG_ERROR(bad value ${enableval} for --enable-graphics) ;;
|
||||
esac],[if test "x$graphics" = "x"; then graphics=true; fi])
|
||||
|
||||
AC_CONFIG_FILES([Makefile
|
||||
vars.config
|
||||
src/ejabberd.app.src])
|
||||
@@ -280,6 +288,7 @@ AC_SUBST(iconv)
|
||||
AC_SUBST(stun)
|
||||
AC_SUBST(sip)
|
||||
AC_SUBST(debug)
|
||||
AC_SUBST(graphics)
|
||||
AC_SUBST(tools)
|
||||
AC_SUBST(latest_deps)
|
||||
AC_SUBST(system_deps)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
extract_translations - auxiliary tool that extracts lines to be translated
|
||||
from ejabberd source tree.
|
||||
|
||||
Building:
|
||||
erlc extract_translations.erl
|
||||
|
||||
Invoking 1:
|
||||
erl -noinput -s extract_translations -extra dirname message_file
|
||||
|
||||
where dirname is the directory "src" in ejabberd's source tree root,
|
||||
message_file is a file with translated messages (src/msgs/*.msg).
|
||||
|
||||
Result is a list of messages from source files which aren't contained in
|
||||
message file.
|
||||
|
||||
Invoking 2:
|
||||
erl -noinput -s extract_translations -extra -unused dirname message_file
|
||||
|
||||
Result is a list of messages from message file which aren't in source
|
||||
files anymore.
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% File : extract_translations.erl
|
||||
%%% Author : Sergei Golovan <sgolovan@nes.ru>
|
||||
%%% Purpose : Auxiliary tool for interface/messages translators
|
||||
%%% Created : 23 Apr 2005 by Sergei Golovan <sgolovan@nes.ru>
|
||||
%%% Id : $Id$
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
-module(extract_translations).
|
||||
-author('sgolovan@nes.ru').
|
||||
|
||||
-export([start/0]).
|
||||
|
||||
-define(STATUS_SUCCESS, 0).
|
||||
-define(STATUS_ERROR, 1).
|
||||
-define(STATUS_USAGE, 2).
|
||||
|
||||
-include_lib("kernel/include/file.hrl").
|
||||
|
||||
|
||||
start() ->
|
||||
ets:new(translations, [named_table, public]),
|
||||
ets:new(translations_obsolete, [named_table, public]),
|
||||
ets:new(files, [named_table, public]),
|
||||
ets:new(vars, [named_table, public]),
|
||||
case init:get_plain_arguments() of
|
||||
["-srcmsg2po", Dir, File] ->
|
||||
print_po_header(File),
|
||||
Status = process(Dir, File, srcmsg2po),
|
||||
halt(Status);
|
||||
["-unused", Dir, File] ->
|
||||
Status = process(Dir, File, unused),
|
||||
halt(Status);
|
||||
[Dir, File] ->
|
||||
Status = process(Dir, File, used),
|
||||
halt(Status);
|
||||
_ ->
|
||||
print_usage(),
|
||||
halt(?STATUS_USAGE)
|
||||
end.
|
||||
|
||||
|
||||
process(Dir, File, Used) ->
|
||||
case load_file(File) of
|
||||
{error, Reason} ->
|
||||
io:format("~s: ~s~n", [File, file:format_error(Reason)]),
|
||||
?STATUS_ERROR;
|
||||
_ ->
|
||||
FileList = find_src_files(Dir),
|
||||
lists:foreach(
|
||||
fun(F) ->
|
||||
parse_file(Dir, F, Used)
|
||||
end, FileList),
|
||||
case Used of
|
||||
unused ->
|
||||
ets:foldl(fun({Key, _}, _) ->
|
||||
io:format("~p~n", [Key])
|
||||
end, ok, translations);
|
||||
srcmsg2po ->
|
||||
ets:foldl(fun({Key, Trans}, _) ->
|
||||
print_translation_obsolete(Key, Trans)
|
||||
end, ok, translations_obsolete);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
?STATUS_SUCCESS
|
||||
end.
|
||||
|
||||
parse_file(Dir, File, Used) ->
|
||||
ets:delete_all_objects(vars),
|
||||
case epp:parse_file(File, [Dir, filename:dirname(File) | code:get_path()], []) of
|
||||
{ok, Forms} ->
|
||||
lists:foreach(
|
||||
fun(F) ->
|
||||
parse_form(Dir, File, F, Used)
|
||||
end, Forms);
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
parse_form(Dir, File, Form, Used) ->
|
||||
case Form of
|
||||
%%{undefined, Something} ->
|
||||
%% io:format("Undefined: ~p~n", [Something]);
|
||||
{call,
|
||||
_,
|
||||
{remote, _, {atom, _, translate}, {atom, _, translate}},
|
||||
[_, {string, Line, Str}]
|
||||
} ->
|
||||
process_string(Dir, File, Line, Str, Used);
|
||||
{call,
|
||||
_,
|
||||
{remote, _, {atom, _, translate}, {atom, _, translate}},
|
||||
[_,
|
||||
{bin,_,
|
||||
[{bin_element,_,
|
||||
{string,Line,Str},
|
||||
default,default}]}]
|
||||
} ->
|
||||
process_string(Dir, File, Line, Str, Used);
|
||||
{call,
|
||||
_,
|
||||
{remote, _, {atom, _, translate}, {atom, _, translate}},
|
||||
[_, {var, _, Name}]
|
||||
} ->
|
||||
case ets:lookup(vars, Name) of
|
||||
[{_Name, Value, Line}] ->
|
||||
process_string(Dir, File, Line, Value, Used);
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
{match,
|
||||
_,
|
||||
{var, _, Name},
|
||||
{string, Line, Value}
|
||||
} ->
|
||||
ets:insert(vars, {Name, Value, Line});
|
||||
{match,
|
||||
_,
|
||||
{var, _, Name},
|
||||
{bin,Line,[{bin_element,_,{string,_,Value},_,_}]}
|
||||
} ->
|
||||
ets:insert(vars, {Name, Value, Line});
|
||||
L when is_list(L) ->
|
||||
lists:foreach(
|
||||
fun(F) ->
|
||||
parse_form(Dir, File, F, Used)
|
||||
end, L);
|
||||
T when is_tuple(T) ->
|
||||
lists:foreach(
|
||||
fun(F) ->
|
||||
parse_form(Dir, File, F, Used)
|
||||
end, tuple_to_list(T));
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
process_string(_Dir, _File, _Line, "", _Used) ->
|
||||
ok;
|
||||
|
||||
process_string(_Dir, File, Line, Str, Used) ->
|
||||
case {ets:lookup(translations, Str), Used} of
|
||||
{[{_Key, _Trans}], unused} ->
|
||||
ets:delete(translations, Str);
|
||||
{[{_Key, _Trans}], used} ->
|
||||
ok;
|
||||
{[{_Key, Trans}], srcmsg2po} ->
|
||||
ets:delete(translations_obsolete, Str),
|
||||
print_translation(File, Line, Str, Trans);
|
||||
{_, used} ->
|
||||
case ets:lookup(files, File) of
|
||||
[{_}] ->
|
||||
ok;
|
||||
_ ->
|
||||
io:format("~n% ~s~n", [File]),
|
||||
ets:insert(files, {File})
|
||||
end,
|
||||
case Str of
|
||||
[] -> ok;
|
||||
_ -> io:format("{~p, \"\"}.~n", [Str])
|
||||
end,
|
||||
ets:insert(translations, {Str, ""});
|
||||
{_, srcmsg2po} ->
|
||||
case ets:lookup(files, File) of
|
||||
[{_}] ->
|
||||
ok;
|
||||
_ ->
|
||||
ets:insert(files, {File})
|
||||
end,
|
||||
ets:insert(translations, {Str, ""}),
|
||||
print_translation(File, Line, Str, "");
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
load_file(File) ->
|
||||
case file:consult(File) of
|
||||
{ok, Terms} ->
|
||||
lists:foreach(
|
||||
fun({Orig, Trans}) ->
|
||||
case Trans of
|
||||
"" ->
|
||||
ok;
|
||||
_ ->
|
||||
ets:insert(translations, {Orig, Trans}),
|
||||
ets:insert(translations_obsolete, {Orig, Trans})
|
||||
end
|
||||
end, Terms);
|
||||
Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
find_src_files(Dir) ->
|
||||
case file:list_dir(Dir) of
|
||||
{ok, FileList} ->
|
||||
recurse_filelist(
|
||||
lists:map(
|
||||
fun(F) ->
|
||||
filename:join(Dir, F)
|
||||
end, FileList));
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
recurse_filelist(FileList) ->
|
||||
recurse_filelist(FileList, []).
|
||||
|
||||
recurse_filelist([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
|
||||
recurse_filelist([H | T], Acc) ->
|
||||
case file:read_file_info(H) of
|
||||
{ok, #file_info{type = directory}} ->
|
||||
recurse_filelist(T, lists:reverse(find_src_files(H)) ++ Acc);
|
||||
{ok, #file_info{type = regular}} ->
|
||||
case string:substr(H, string:len(H) - 3) of
|
||||
".erl" ->
|
||||
recurse_filelist(T, [H | Acc]);
|
||||
".hrl" ->
|
||||
recurse_filelist(T, [H | Acc]);
|
||||
_ ->
|
||||
recurse_filelist(T, Acc)
|
||||
end;
|
||||
_ ->
|
||||
recurse_filelist(T, Acc)
|
||||
end.
|
||||
|
||||
|
||||
print_usage() ->
|
||||
io:format(
|
||||
"Usage: extract_translations [-unused] dir file~n"
|
||||
"~n"
|
||||
"Example:~n"
|
||||
" extract_translations . ./msgs/ru.msg~n"
|
||||
).
|
||||
|
||||
|
||||
%%%
|
||||
%%% Gettext
|
||||
%%%
|
||||
|
||||
print_po_header(File) ->
|
||||
MsgProps = get_msg_header_props(File),
|
||||
{Language, [LastT | AddT]} = prepare_props(MsgProps),
|
||||
print_po_header(Language, LastT, AddT).
|
||||
|
||||
get_msg_header_props(File) ->
|
||||
{ok, F} = file:open(File, [read]),
|
||||
Lines = get_msg_header_props(F, []),
|
||||
file:close(F),
|
||||
Lines.
|
||||
|
||||
get_msg_header_props(F, Lines) ->
|
||||
String = io:get_line(F, ""),
|
||||
case io_lib:fread("% ", String) of
|
||||
{ok, [], RemString} ->
|
||||
case io_lib:fread("~s", RemString) of
|
||||
{ok, [Key], Value} when Value /= "\n" ->
|
||||
%% The first character in Value is a blankspace:
|
||||
%% And the last characters are 'slash n'
|
||||
ValueClean = string:substr(Value, 2, string:len(Value)-2),
|
||||
get_msg_header_props(F, Lines ++ [{Key, ValueClean}]);
|
||||
_ ->
|
||||
get_msg_header_props(F, Lines)
|
||||
end;
|
||||
_ ->
|
||||
Lines
|
||||
end.
|
||||
|
||||
prepare_props(MsgProps) ->
|
||||
Language = proplists:get_value("Language:", MsgProps),
|
||||
Authors = proplists:get_all_values("Author:", MsgProps),
|
||||
{Language, Authors}.
|
||||
|
||||
print_po_header(Language, LastTranslator, AdditionalTranslatorsList) ->
|
||||
AdditionalTranslatorsString = build_additional_translators(AdditionalTranslatorsList),
|
||||
HeaderString =
|
||||
"msgid \"\"\n"
|
||||
"msgstr \"\"\n"
|
||||
++ "\"X-Language: " ++ Language ++ "\\n\"\n"
|
||||
"\"Last-Translator: " ++ LastTranslator ++ "\\n\"\n"
|
||||
++ AdditionalTranslatorsString ++
|
||||
"\"MIME-Version: 1.0\\n\"\n"
|
||||
"\"Content-Type: text/plain; charset=UTF-8\\n\"\n"
|
||||
"\"Content-Transfer-Encoding: 8bit\\n\"\n",
|
||||
io:format("~s~n", [HeaderString]).
|
||||
|
||||
build_additional_translators(List) ->
|
||||
lists:foldl(
|
||||
fun(T, Str) ->
|
||||
Str ++ "\"X-Additional-Translator: " ++ T ++ "\\n\"\n"
|
||||
end,
|
||||
"",
|
||||
List).
|
||||
|
||||
print_translation(File, Line, Str, StrT) ->
|
||||
StrQ = ejabberd_regexp:greplace(list_to_binary(Str), <<"\\\"">>, <<"\\\\\"">>),
|
||||
StrTQ = ejabberd_regexp:greplace(list_to_binary(StrT), <<"\\\"">>, <<"\\\\\"">>),
|
||||
io:format("#: ~s:~p~nmsgid \"~s\"~nmsgstr \"~s\"~n~n", [File, Line, StrQ, StrTQ]).
|
||||
|
||||
print_translation_obsolete(Str, StrT) ->
|
||||
File = "unknown.erl",
|
||||
Line = 1,
|
||||
StrQ = ejabberd_regexp:greplace(Str, "\\\"", "\\\\\""),
|
||||
StrTQ = ejabberd_regexp:greplace(StrT, "\\\"", "\\\\\""),
|
||||
io:format("#: ~s:~p~n#~~ msgid \"~s\"~n#~~ msgstr \"~s\"~n~n", [File, Line, StrQ, StrTQ]).
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Frontend for ejabberd's extract_translations.erl
|
||||
# by Badlop
|
||||
|
||||
# How to create template files for a new language:
|
||||
# NEWLANG=zh
|
||||
# cp msgs/ejabberd.pot msgs/$NEWLANG.po
|
||||
# echo \{\"\",\"\"\}. > msgs/$NEWLANG.msg
|
||||
# ../../extract_translations/prepare-translation.sh -updateall
|
||||
|
||||
prepare_dirs ()
|
||||
{
|
||||
# Where is Erlang binary
|
||||
ERL=`which erl`
|
||||
|
||||
EJA_SRC_DIR=$EJA_DIR/src/
|
||||
EJA_MSGS_DIR=$EJA_DIR/priv/msgs/
|
||||
EXTRACT_DIR=$EJA_DIR/contrib/extract_translations/
|
||||
EXTRACT_ERL=$EXTRACT_DIR/extract_translations.erl
|
||||
EXTRACT_BEAM=$EXTRACT_DIR/extract_translations.beam
|
||||
|
||||
SRC_DIR=$RUN_DIR/src
|
||||
EBIN_DIR=$RUN_DIR/ebin
|
||||
MSGS_DIR=$EJA_DIR/priv/msgs
|
||||
|
||||
if !([[ -n $EJA_DIR ]])
|
||||
then
|
||||
echo "ejabberd dir does not exist: $EJA_DIR"
|
||||
fi
|
||||
|
||||
if !([[ -x $EXTRACT_BEAM ]])
|
||||
then
|
||||
sh -c "cd $EXTRACT_DIR; $ERL -compile $EXTRACT_ERL"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_lang ()
|
||||
{
|
||||
MSGS_FILE=$1
|
||||
MSGS_FILE2=$MSGS_FILE.translate
|
||||
MSGS_PATH=$MSGS_DIR/$MSGS_FILE
|
||||
MSGS_PATH2=$MSGS_DIR/$MSGS_FILE2
|
||||
|
||||
echo -n "Extracting language strings for '$MSGS_FILE':"
|
||||
|
||||
echo -n " new..."
|
||||
cd $SRC_DIR
|
||||
$ERL -pa $EXTRACT_DIR -noinput -noshell -s extract_translations -s init stop -extra . $MSGS_PATH >$MSGS_PATH.new
|
||||
sed -e 's/^% \.\//% /g;' $MSGS_PATH.new > $MSGS_PATH.new2
|
||||
mv $MSGS_PATH.new2 $MSGS_PATH.new
|
||||
|
||||
echo -n " old..."
|
||||
$ERL -pa $EXTRACT_DIR -noinput -noshell -s extract_translations -s init stop -extra -unused . $MSGS_PATH >$MSGS_PATH.unused
|
||||
find_unused_full $MSGS_FILE $MSGS_FILE.unused
|
||||
|
||||
echo "" >$MSGS_PATH2
|
||||
echo " ***** Translation file for ejabberd ***** " >>$MSGS_PATH2
|
||||
echo "" >>$MSGS_PATH2
|
||||
|
||||
echo "" >>$MSGS_PATH2
|
||||
echo " *** New strings: Can you please translate them? *** " >>$MSGS_PATH2
|
||||
cat $MSGS_PATH.new >>$MSGS_PATH2
|
||||
|
||||
echo "" >>$MSGS_PATH2
|
||||
echo "" >>$MSGS_PATH2
|
||||
echo " *** Unused strings: They will be removed automatically *** " >>$MSGS_PATH2
|
||||
cat $MSGS_PATH.unused.full >>$MSGS_PATH2
|
||||
|
||||
echo "" >>$MSGS_PATH2
|
||||
echo "" >>$MSGS_PATH2
|
||||
echo " *** Already translated strings: you can also modify any of them if you want *** " >>$MSGS_PATH2
|
||||
echo "" >>$MSGS_PATH2
|
||||
cat $MSGS_PATH.old_cleaned >>$MSGS_PATH2
|
||||
|
||||
echo " ok"
|
||||
|
||||
rm $MSGS_PATH.new
|
||||
rm $MSGS_PATH.old_cleaned
|
||||
rm $MSGS_PATH.unused.full
|
||||
}
|
||||
|
||||
extract_lang_all ()
|
||||
{
|
||||
cd $MSGS_DIR
|
||||
for i in $( ls *.msg ) ; do
|
||||
extract_lang $i;
|
||||
done
|
||||
|
||||
echo -e "File\tMissing\tLanguage\t\tLast translator"
|
||||
echo -e "----\t-------\t--------\t\t---------------"
|
||||
cd $MSGS_DIR
|
||||
for i in $( ls *.msg ) ; do
|
||||
MISSING=`cat $i.translate | grep "\", \"\"}." | wc -l`
|
||||
LANGUAGE=`grep "X-Language:" $i.translate | sed 's/% Language: //g'`
|
||||
LASTAUTH=`grep "Author:" $i.translate | head -n 1 | sed 's/% Author: //g'`
|
||||
echo -e "$i\t$MISSING\t$LANGUAGE\t$LASTAUTH"
|
||||
done
|
||||
|
||||
cd $MSGS_DIR
|
||||
REVISION=`git describe --always`
|
||||
zip $HOME/ejabberd-langs-$REVISION.zip *.translate;
|
||||
|
||||
rm *.translate
|
||||
}
|
||||
|
||||
find_unused_full ()
|
||||
{
|
||||
DATFILE=$1
|
||||
DATFILEI=$1.old_cleaned
|
||||
DELFILE=$2
|
||||
cd msgs
|
||||
|
||||
DATFILE1=$DATFILE.t1
|
||||
DATFILE2=$DATFILE.t2
|
||||
|
||||
DELFILE1=$DELFILE.t1
|
||||
DELFILE2=$DELFILE.t2
|
||||
DELFILEF=$DATFILE.unused.full
|
||||
echo "" >$DELFILEF
|
||||
|
||||
grep -v "\\\\" $DELFILE >$DELFILE2
|
||||
echo ENDFILEMARK >>$DELFILE2
|
||||
cp $DATFILE $DATFILEI
|
||||
cp $DATFILE $DATFILE2
|
||||
|
||||
cp $DELFILE2 $DELFILE1
|
||||
STRING=`head -1 $DELFILE1`
|
||||
until [[ $STRING == ENDFILEMARK ]]; do
|
||||
cp $DELFILE2 $DELFILE1
|
||||
cp $DATFILE2 $DATFILE1
|
||||
|
||||
STRING=`head -1 $DELFILE1`
|
||||
|
||||
cat $DATFILE1 | grep "$STRING" >>$DELFILEF
|
||||
cat $DATFILE1 | grep -v "$STRING" >$DATFILE2
|
||||
cat $DELFILE1 | grep -v "$STRING" >$DELFILE2
|
||||
done
|
||||
|
||||
mv $DATFILE2 $DATFILEI
|
||||
|
||||
rm -f $MSGS_PATH.t1
|
||||
rm $MSGS_PATH.unused
|
||||
rm -f $MSGS_PATH.unused.t1
|
||||
rm $MSGS_PATH.unused.t2
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
extract_lang_srcmsg2po ()
|
||||
{
|
||||
LANG=$1
|
||||
LANG_CODE=$LANG.$PROJECT
|
||||
MSGS_PATH=$MSGS_DIR/$LANG_CODE.msg
|
||||
PO_PATH=$MSGS_DIR/$LANG_CODE.po
|
||||
|
||||
echo $MSGS_PATH
|
||||
|
||||
cd $SRC_DIR
|
||||
$ERL -pa $EXTRACT_DIR -pa $EBIN_DIR -pa $EJA_SRC_DIR -pa ../include -noinput -noshell -s extract_translations -s init stop -extra -srcmsg2po . $MSGS_PATH >$PO_PATH.1
|
||||
sed -e 's/ \[\]$/ \"\"/g;' $PO_PATH.1 > $PO_PATH.2
|
||||
msguniq --sort-by-file $PO_PATH.2 --output-file=$PO_PATH
|
||||
|
||||
rm $PO_PATH.*
|
||||
}
|
||||
|
||||
extract_lang_src2pot ()
|
||||
{
|
||||
LANG_CODE=$PROJECT
|
||||
MSGS_PATH=$MSGS_DIR/$LANG_CODE.msg
|
||||
POT_PATH=$MSGS_DIR/$LANG_CODE.pot
|
||||
|
||||
echo -n "" >$MSGS_PATH
|
||||
echo "% Language: Language Name" >>$MSGS_PATH
|
||||
echo "% Author: Translator name and contact method" >>$MSGS_PATH
|
||||
echo "" >>$MSGS_PATH
|
||||
|
||||
cd $SRC_DIR
|
||||
$ERL -pa $EXTRACT_DIR -pa $EBIN_DIR -pa $EJA_SRC_DIR -pa ../include -noinput -noshell -s extract_translations -s init stop -extra -srcmsg2po . $MSGS_PATH >$POT_PATH.1
|
||||
sed -e 's/ \[\]$/ \"\"/g;' $POT_PATH.1 > $POT_PATH.2
|
||||
|
||||
#msguniq --sort-by-file $POT_PATH.2 $EJA_MSGS_DIR --output-file=$POT_PATH
|
||||
msguniq --sort-by-file $POT_PATH.2 --output-file=$POT_PATH
|
||||
|
||||
rm $POT_PATH.*
|
||||
rm $MSGS_PATH
|
||||
|
||||
# If the project is a specific module, not the main ejabberd
|
||||
if [[ $PROJECT != ejabberd ]] ; then
|
||||
# Remove from project.pot the strings that are already present in the general ejabberd
|
||||
EJABBERD_MSG_FILE=$EJA_MSGS_DIR/es.po # This is just some file with translated strings
|
||||
POT_PATH_TEMP=$POT_PATH.temp
|
||||
msgattrib --set-obsolete --only-file=$EJABBERD_MSG_FILE -o $POT_PATH_TEMP $POT_PATH
|
||||
mv $POT_PATH_TEMP $POT_PATH
|
||||
fi
|
||||
}
|
||||
|
||||
extract_lang_popot2po ()
|
||||
{
|
||||
LANG_CODE=$1
|
||||
PO_PATH=$MSGS_DIR/$LANG_CODE.po
|
||||
POT_PATH=$MSGS_DIR/$PROJECT.pot
|
||||
|
||||
msgmerge $PO_PATH $POT_PATH >$PO_PATH.translate 2>/dev/null
|
||||
mv $PO_PATH.translate $PO_PATH
|
||||
}
|
||||
|
||||
extract_lang_po2msg ()
|
||||
{
|
||||
LANG_CODE=$1
|
||||
PO_PATH=$LANG_CODE.po
|
||||
MS_PATH=$PO_PATH.ms
|
||||
MSGID_PATH=$PO_PATH.msgid
|
||||
MSGSTR_PATH=$PO_PATH.msgstr
|
||||
MSGS_PATH=$LANG_CODE.msg
|
||||
|
||||
cd $MSGS_DIR
|
||||
|
||||
# Check PO has correct ~
|
||||
# Let's convert to C format so we can use msgfmt
|
||||
PO_TEMP=$LANG_CODE.po.temp
|
||||
cat $PO_PATH | sed 's/%/perc/g' | sed 's/~/%/g' | sed 's/#:.*/#, c-format/g' >$PO_TEMP
|
||||
msgfmt $PO_TEMP --check-format
|
||||
result=$?
|
||||
rm $PO_TEMP
|
||||
if [ $result -ne 0 ] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msgattrib $PO_PATH --translated --no-fuzzy --no-obsolete --no-location --no-wrap | grep "^msg" | tail --lines=+3 >$MS_PATH
|
||||
grep "^msgid" $PO_PATH.ms | sed 's/^msgid //g' >$MSGID_PATH
|
||||
grep "^msgstr" $PO_PATH.ms | sed 's/^msgstr //g' >$MSGSTR_PATH
|
||||
echo "%% -*- coding: latin-1 -*-" >$MSGS_PATH
|
||||
paste $MSGID_PATH $MSGSTR_PATH --delimiter=, | awk '{print "{" $0 "}."}' | sort -g >>$MSGS_PATH
|
||||
|
||||
rm $MS_PATH
|
||||
rm $MSGID_PATH
|
||||
rm $MSGSTR_PATH
|
||||
}
|
||||
|
||||
extract_lang_updateall ()
|
||||
{
|
||||
echo "Generating POT"
|
||||
extract_lang_src2pot
|
||||
|
||||
cd $MSGS_DIR
|
||||
echo ""
|
||||
echo -e "File Missing Language Last translator"
|
||||
echo -e "---- ------- -------- ---------------"
|
||||
for i in $( ls *.msg ) ; do
|
||||
LANG_CODE=${i%.msg}
|
||||
echo -n $LANG_CODE | awk '{printf "%-6s", $1 }'
|
||||
|
||||
# Convert old MSG file to PO
|
||||
PO=$LANG_CODE.po
|
||||
[ -f $PO ] || extract_lang_srcmsg2po $LANG_CODE
|
||||
|
||||
extract_lang_popot2po $LANG_CODE
|
||||
extract_lang_po2msg $LANG_CODE
|
||||
|
||||
MISSING=`msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4 }'`
|
||||
echo -n " $MISSING"
|
||||
|
||||
LANGUAGE=`grep "X-Language:" $PO | sed 's/\"X-Language: //g' | sed 's/\\\\n\"//g' | awk '{printf "%-12s", $1}'`
|
||||
echo -n " $LANGUAGE"
|
||||
|
||||
LASTAUTH=`grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\\\n\"//g'`
|
||||
echo " $LASTAUTH"
|
||||
done
|
||||
echo ""
|
||||
rm messages.mo
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
translation_instructions ()
|
||||
{
|
||||
echo ""
|
||||
echo " A new file has been created for you, with the current, the new and the deprecated strings:"
|
||||
echo " $MSGS_PATH2"
|
||||
echo ""
|
||||
echo " At the end of that file you will find the strings you must update:"
|
||||
echo " - Untranslated strings are like this: {"March", ""}."
|
||||
echo " To translate the string, add the text inside the commas. Example:"
|
||||
echo " {"March", "Marzo"}."
|
||||
echo " - Old strings that are not used: "Woowoa""
|
||||
echo " Search the entire file for those strings and remove them"
|
||||
echo ""
|
||||
echo " Once you have translated all the strings and removed all the old ones,"
|
||||
echo " rename the file to overwrite the previous one:"
|
||||
echo " $MSGS_PATH"
|
||||
}
|
||||
|
||||
EJA_DIR=`pwd`
|
||||
RUN_DIR=`pwd`
|
||||
PROJECT=ejabberd
|
||||
|
||||
while [ $# -ne 0 ] ; do
|
||||
PARAM=$1
|
||||
shift
|
||||
case $PARAM in
|
||||
--) break ;;
|
||||
-project)
|
||||
PROJECT=$1
|
||||
shift
|
||||
;;
|
||||
-ejadir)
|
||||
EJA_DIR=$1
|
||||
shift
|
||||
;;
|
||||
-rundir)
|
||||
RUN_DIR=$1
|
||||
shift
|
||||
;;
|
||||
-lang)
|
||||
LANGU=$1
|
||||
prepare_dirs
|
||||
extract_lang $LANGU
|
||||
shift
|
||||
;;
|
||||
-langall)
|
||||
prepare_dirs
|
||||
extract_lang_all
|
||||
;;
|
||||
-srcmsg2po)
|
||||
LANG_CODE=$1
|
||||
prepare_dirs
|
||||
extract_lang_srcmsg2po $LANG_CODE
|
||||
shift
|
||||
;;
|
||||
-popot2po)
|
||||
LANG_CODE=$1
|
||||
prepare_dirs
|
||||
extract_lang_popot2po $LANG_CODE
|
||||
shift
|
||||
;;
|
||||
-src2pot)
|
||||
prepare_dirs
|
||||
extract_lang_src2pot
|
||||
;;
|
||||
-po2msg)
|
||||
LANG_CODE=$1
|
||||
prepare_dirs
|
||||
extract_lang_po2msg $LANG_CODE
|
||||
shift
|
||||
;;
|
||||
-updateall)
|
||||
prepare_dirs
|
||||
extract_lang_updateall
|
||||
;;
|
||||
*)
|
||||
echo "Options:"
|
||||
echo " -langall"
|
||||
echo " -lang LANGUAGE_FILE"
|
||||
echo " -srcmsg2po LANGUAGE Construct .msg file using source code to PO file"
|
||||
echo " -src2pot Generate template POT file from source code"
|
||||
echo " -popot2po LANGUAGE Update PO file with template POT file"
|
||||
echo " -po2msg LANGUAGE Export PO file to MSG file"
|
||||
echo " -updateall Generate POT and update all PO"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " ./prepare-translation.sh -lang es.msg"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
+83
-41
@@ -1,21 +1,23 @@
|
||||
ejabberd container
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Version](#version)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Usage](#usage)
|
||||
- [Persistence](#persistence)
|
||||
- [SSL Certificates](#ssl-certificates)
|
||||
- [Base Image](#base-image)
|
||||
- [Ejabberd Configuration](#ejabberd-configuration)
|
||||
- [Cluster Example](#cluster-example)
|
||||
- [Runtime Configuration](#runtime-configuration)
|
||||
- [Served Hostnames](#served-hostnames)
|
||||
- [Authentication](#authentication)
|
||||
- [Admins](#admins)
|
||||
- [Users](#users)
|
||||
- [SSL](#ssl)
|
||||
- [Erlang](#erlang)
|
||||
- [Modules](#modules)
|
||||
- [Logging](#logging)
|
||||
- [Mount Configurations](#mount-configurations)
|
||||
- [Erlang Configuration](#erlang-configuration)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Register Users](#register-users)
|
||||
- [Creating Backups](#creating-backups)
|
||||
@@ -28,11 +30,29 @@ ejabberd container
|
||||
|
||||
# Introduction
|
||||
|
||||
Dockerfile to build an [ejabberd](https://www.ejabberd.im/) container image.
|
||||
This [ejabberd][] docker container is based on the work done by [rroemhild][]. See more [in this blogpost][].
|
||||
This container includes the necessary files to build your own containerized ejabberd,
|
||||
but *IT IS NOT* used to generate official images on the docker [hub][].
|
||||
This container is not maintained by [ProcessOne][].
|
||||
|
||||
Docker Tag Names are based on ejabberd versions in git [tags][]. The image tag `:latest` is based on the master branch.
|
||||
[ProcessOne][] provides and maintain official containers on the docker [hub][], which targets developers for now and will becomes production ready in a near future.
|
||||
These [new containers] allow to build and run ejabberd in a simple and lightweight environment.
|
||||
|
||||
[tags]: https://github.com/rroemhild/ejabberd/tags
|
||||
[ejabberd]: https://www.ejabberd.im/
|
||||
[rroemhild]: https://github.com/rroemhild/docker-ejabberd/
|
||||
[in this blogpost]: https://blog.process-one.net/ejabberd-16-12/
|
||||
[hub]: https://hub.docker.com/r/ejabberd/ecs/
|
||||
[new containers]: https://github.com/processone/docker-ejabberd/
|
||||
[ProcessOne]: https://www.process-one.net/
|
||||
|
||||
## Version
|
||||
|
||||
Current Version: `17.08`
|
||||
|
||||
Docker Tag Names are based on ejabberd versions in git [branches][] and [tags][]. The image tag `:latest` is based on the master branch.
|
||||
|
||||
[tags]: https://github.com/rroemhild/docker-ejabberd/tags
|
||||
[branches]: https://github.com/rroemhild/docker-ejabberd/branches
|
||||
|
||||
# Quick Start
|
||||
|
||||
@@ -46,13 +66,20 @@ docker run -d \
|
||||
-p 5280:5280 \
|
||||
-h 'xmpp.example.de' \
|
||||
-e "XMPP_DOMAIN=example.de" \
|
||||
-e "ERLANG_NODE=nodename" \
|
||||
-e "ERLANG_NODE=ejabberd" \
|
||||
-e "EJABBERD_ADMINS=admin@example.de admin2@example.de" \
|
||||
-e "EJABBERD_USERS=admin@example.de:password1234 admin2@example.de" \
|
||||
-e "TZ=Europe/Berlin" \
|
||||
rroemhild/ejabberd
|
||||
```
|
||||
|
||||
or with the [docker-compose](examples/docker-compose/docker-compose.yml) example
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/rroemhild/docker-ejabberd/master/examples/docker-compose/docker-compose.yml
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
## Persistence
|
||||
@@ -100,7 +127,11 @@ ADD ./example.com.pem /opt/ejabberd/ssl/example.com.pem
|
||||
|
||||
If you need root privileges switch to `USER root` and go back to `USER ejabberd` when you're done.
|
||||
|
||||
# Ejabberd Configuration
|
||||
## Cluster Example
|
||||
|
||||
The [docker-compose-cluster](examples/docker-compose-cluster) example demonstrates how to extend this container image to setup a multi-master cluster.
|
||||
|
||||
# Runtime Configuration
|
||||
|
||||
You can additionally provide extra runtime configuration in a downstream image by replacing the config template `ejabberd.yml.tpl` with one based on this image's template and include extra interpolation of environment variables. The template is parsed by Jinja2 with the runtime environment (equivalent to Python's `os.environ` available as `env`).
|
||||
|
||||
@@ -138,6 +169,26 @@ EJABBERD_EXTAUTH_CACHE=600
|
||||
```
|
||||
**EJABBERD_EXTAUTH_INSTANCES** must be an integer with a minimum value of 1. **EJABBERD_EXTAUTH_CACHE** can be set to "false" or an integer value representing cache time in seconds. Note that caching should not be enabled if internal auth is also enabled.
|
||||
|
||||
### Password format
|
||||
|
||||
The variable `EJABBERD_AUTH_PASSWORD_FORMAT` controls in which format user passwords are
|
||||
stored. Possible values are `plain` and `scram`. The default is to store
|
||||
[SCRAM](https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism)bled
|
||||
passwords, meaning that it is impossible to obtain the original plain password from the
|
||||
stored information.
|
||||
|
||||
NOTE: SCRAM does not work with SIP/TURN foreign authentication methods. In this case, you
|
||||
may have to disable the option. More details can be found here:
|
||||
https://docs.ejabberd.im/admin/configuration/#internal
|
||||
|
||||
If using SCRAM with an SQL database that has plaintext passwords stored, use the command
|
||||
|
||||
```
|
||||
ejabberdctl convert_to_scram example.org
|
||||
```
|
||||
|
||||
to convert all your existing plaintext passwords to scrambled format.
|
||||
|
||||
### MySQL Authentication
|
||||
|
||||
Set `EJABBERD_AUTH_METHOD=external` and `EJABBERD_EXTAUTH_PROGRAM=/opt/ejabberd/scripts/lib/auth_mysql.py` to enable MySQL authentication. Use the following environment variables to configure the database connection and the layout of the database. Password changing, registration, and unregistration are optional features and are enabled only if the respective queries are provided.
|
||||
@@ -215,28 +266,38 @@ EJABBERD_USERS=admin@example.ninja:password1234 user1@test.com user1@xyz.io
|
||||
```
|
||||
|
||||
## SSL
|
||||
|
||||
- **EJABBERD_SKIP_MAKE_SSLCERT**: Skip generating ssl certificates. Default: false
|
||||
- **EJABBERD_SSLCERT_HOST**: SSL Certificate for the hostname.
|
||||
- **EJABBERD_SSLCERT_EXAMPLE_COM**: SSL Certificates for XMPP domains.
|
||||
- **EJABBERD_STARTTLS**: Set to `false` to disable StartTLS for client to server connections. Default: `true`.
|
||||
- **EJABBERD_S2S_SSL**: Set to `false` to disable SSL in server 2 server connections. Default: `true`.
|
||||
- **EJABBERD_HTTPS**: If your proxy terminates SSL you may want to disable HTTPS on port 5280 and 5443. Default: `true`.
|
||||
- **EJABBERD_PROTOCOL_OPTIONS_TLSV1**: Allow TLSv1 protocol. Default: `false`.
|
||||
- **EJABBERD_PROTOCOL_OPTIONS_TLSV1_1**: Allow TLSv1.1 protocol. Default: `true`.
|
||||
- **EJABBERD_CIPHERS**: Cipher suite. Default: `HIGH:!aNULL:!3DES`.
|
||||
- **EJABBERD_DHPARAM**: Set to `true` to use or generate custom DH parameters. Default: `false`.
|
||||
- **EJABBERD_STARTTLS**: Set to `false` to disable StartTLS for client to server connections. Defaults
|
||||
to `true`.
|
||||
- **EJABBERD_S2S_SSL**: Set to `false` to disable SSL in server 2 server connections. Defaults to `true`.
|
||||
- **EJABBERD_HTTPS**: If your proxy terminates SSL you may want to disable HTTPS on port 5280 and 5443. Defaults to `true`.
|
||||
- **EJABBERD_PROTOCOL_OPTIONS_TLSV1**: Allow TLSv1 protocol. Defaults to `false`.
|
||||
- **EJABBERD_PROTOCOL_OPTIONS_TLSV1_1**: Allow TLSv1.1 protocol. Defaults to `true`.
|
||||
- **EJABBERD_CIPHERS**: Cipher suite. Defaults to `HIGH:!aNULL:!3DES`.
|
||||
- **EJABBERD_DHPARAM**: Set to `true` to use or generate custom DH parameters. Defaults to `false`.
|
||||
- **EJABBERD_SKIP_MAKE_DHPARAM**: Skip generating DH params. Default: false
|
||||
|
||||
## Erlang
|
||||
- **ERLANG_NODE**: Allows to explicitly specify erlang node for ejabberd. Set to `ejabberd` lets erlang add the hostname. Defaults to `ejabberd@localhost`.
|
||||
- **ERLANG_COOKIE**: Set erlang cookie. Defaults to auto-generated cookie.
|
||||
- **ERLANG_OPTIONS**: Overwrite additional options passed to erlang while starting ejabberd.
|
||||
|
||||
## Modules
|
||||
|
||||
- **EJABBERD_SKIP_MODULES_UPDATE**: If you do not need to update ejabberd modules specs, skip the update task and speedup start. Default: `false`.
|
||||
- **EJABBERD_MOD_MUC_ADMIN**: Activate the mod_muc_admin module. Default: `false`.
|
||||
- **EJABBERD_MOD_ADMIN_EXTRA**: Activate the mod_muc_admin module. Default: `true`.
|
||||
- **EJABBERD_REGISTER_TRUSTED_NETWORK_ONLY**: Only allow user registration from the trusted_network access rule. Default: `true`.
|
||||
- **EJABBERD_MOD_VERSION**: Activate the mod_version module. Default: `true`.
|
||||
- **EJABBERD_SKIP_MODULES_UPDATE**: If you do not need to update ejabberd modules specs, skip the update task and speedup start. Defaults to `false`.
|
||||
- **EJABBERD_MOD_MUC_ADMIN**: Activate the mod_muc_admin module. Defaults to `false`.
|
||||
- **EJABBERD_MOD_ADMIN_EXTRA**: Activate the mod_muc_admin module. Defaults to `true`.
|
||||
- **EJABBERD_REGISTER_TRUSTED_NETWORK_ONLY**: Only allow user registration from the trusted_network access rule. Defaults to `true`.
|
||||
- **EJABBERD_MOD_VERSION**: Activate the mod_version module. Defaults to `true`.
|
||||
- **EJABBERD_SOURCE_MODULES**: List of modules, which will be installed from sources localized in ${EJABBERD_HOME}/module_source.
|
||||
- **EJABBERD_CONTRIB_MODULES**: List of modules, which will be installed from contrib repository.
|
||||
- **EJABBERD_RESTART_AFTER_MODULE_INSTALL**: If any modules were installed, restart the server, if the option is enabled.
|
||||
- **EJABBERD_CUSTOM_AUTH_MODULE_OVERRIDE**: If a custom module was defined for handling auth, we need to override the pre-defined auth methods in the config.
|
||||
|
||||
## Logging
|
||||
|
||||
Use the **EJABBERD_LOGLEVEL** environment variable to set verbosity. Default: `4` (Info).
|
||||
Use the **EJABBERD_LOGLEVEL** environment variable to set verbosity. Defaults to `4` (Info).
|
||||
|
||||
```
|
||||
loglevel: Verbosity of log files generated by ejabberd.
|
||||
@@ -276,25 +337,6 @@ Example configuration files can be downloaded from the ejabberd [github](https:/
|
||||
|
||||
When these files exist in ```/opt/ejabberd/conf```, the run script will ignore the configuration templates.
|
||||
|
||||
## Erlang Configuration
|
||||
|
||||
With the following environment variables you can configure options that are passed by ejabberdctl to the erlang runtime system when starting ejabberd.
|
||||
|
||||
- **POLL**: Set to `false` to disable Kernel polling. Default: `true`.
|
||||
- **SMP**: SMP support `enable`, `auto`, `disable`. Default: `auto`.
|
||||
- **ERL_MAX_PORTS**: Maximum number of simultaneously open Erlang ports. Default: `32000`.
|
||||
- **FIREWALL_WINDOW**: Range of allowed ports to pass through a firewall. Default: `not defined`.
|
||||
- **INET_DIST_INTERFACE**: IP address where this Erlang node listens other nodes. Default: `0.0.0.0`.
|
||||
- **ERL_EPMD_ADDRESS**: IP addresses where epmd listens for connections. Default: `0.0.0.0`.
|
||||
- **ERL_PROCESSES**: Maximum number of Erlang processes. Default: `250000`.
|
||||
- **ERL_MAX_ETS_TABLES**: Maximum number of Erlang processes. Default: `1400`.
|
||||
- **ERLANG_OPTIONS**: Overwrite additional options passed to erlang while starting ejabberd. Default: `-noshell`
|
||||
- **ERLANG_NODE**: Allows to explicitly specify erlang node for ejabberd. Set to `nodename` lets erlang add the hostname. Default: `ejabberd@localhost`.
|
||||
- **EJABBERD_CONFIG_PATH**: ejabberd configuration file. Default: `/opt/ejabberd/conf/ejabberd.yml`.
|
||||
- **CONTRIB_MODULES_PATH**: contributed ejabberd modules path. Default: `/opt/ejabberd/modules`.
|
||||
- **CONTRIB_MODULES_CONF_DIR**: configuration directory for contributed modules. Default: `/opt/ejabberd/modules/conf`.
|
||||
- **ERLANG_COOKIE**: Set erlang cookie. Default is to auto-generated cookie.
|
||||
|
||||
# Maintenance
|
||||
|
||||
The `ejabberdctl` command is in the search path and can be run by:
|
||||
|
||||
@@ -123,8 +123,10 @@ auth_method:
|
||||
- {{ auth_method }}
|
||||
{%- endfor %}
|
||||
|
||||
auth_password_format: {{ env.get('EJABBERD_AUTH_PASSWORD_FORMAT', 'scram') }}
|
||||
|
||||
{%- if 'anonymous' in env.get('EJABBERD_AUTH_METHOD', 'internal').split() %}
|
||||
anonymous_protocol: login_anon
|
||||
anonymous_protocol: both
|
||||
allow_multiple_connections: true
|
||||
{%- endif %}
|
||||
|
||||
@@ -347,6 +349,8 @@ modules:
|
||||
- "flat"
|
||||
- "hometree"
|
||||
- "pep" # pep requires mod_caps
|
||||
mod_push: {}
|
||||
mod_push_keepalive: {}
|
||||
mod_register:
|
||||
##
|
||||
## Protect In-Band account registrations with CAPTCHA.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM rroemhild/ejabberd
|
||||
ENV EJABBERD_HOME /opt/ejabberd
|
||||
COPY ./scripts $EJABBERD_HOME/scripts
|
||||
@@ -0,0 +1,23 @@
|
||||
# Ejabberd cluster with docker compose
|
||||
|
||||
This example uses [dnsdocker](https://github.com/tonistiigi/dnsdock) to discover other nodes and setup a multi-master cluster.
|
||||
|
||||
Build the ejabberd cluster image:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rroemhild/docker-ejabberd.git
|
||||
cd docker-ejabberd/examples/docker-compose-cluster
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
Start dnsdocker and the first ejabberd node:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Wait until the first ejabberd node is up and running `docker-compose logs ejabberd`, then add some ejabberd nodes to the cluster:
|
||||
|
||||
```bash
|
||||
docker-compose scale ejabberd=4
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
dnsdock:
|
||||
image: tonistiigi/dnsdock
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 172.17.42.1:53:53/udp
|
||||
|
||||
ejabberd:
|
||||
build: .
|
||||
ports:
|
||||
- 5222
|
||||
- 5269
|
||||
- 5280
|
||||
environment:
|
||||
- XMPP_DOMAIN=example.com
|
||||
- ERLANG_NODE=ejabberd
|
||||
- EJABBERD_ADMINS=admin@example.com
|
||||
- EJABBERD_USERS=admin@example.com:test321 user@example.com
|
||||
- ERLANG_COOKIE=testCluster
|
||||
- SKIP_MODULES_UPDATE=true
|
||||
- EJABBERD_CLUSTER=true
|
||||
- USE_DNS=true
|
||||
dns: 172.17.42.1
|
||||
domainname: dockercomposecluster_ejabberd.docker
|
||||
tty: true
|
||||
@@ -0,0 +1,37 @@
|
||||
# overwrite get_nodename to discover hostname from DNS
|
||||
get_nodename() {
|
||||
local hostname=${HOSTNAME}
|
||||
|
||||
# get hostname from dns
|
||||
if ( is_true ${USE_DNS} ); then
|
||||
# wait for dns registration
|
||||
sleep 1
|
||||
|
||||
nodename=$(discover_dns_hostname ${HOSTIP})
|
||||
|
||||
is_set ${nodename} \
|
||||
&& hostname=${nodename}
|
||||
fi
|
||||
|
||||
echo $hostname
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# discover hostname from dns with a reverse lookup
|
||||
discover_dns_hostname() {
|
||||
local hostip=$1
|
||||
|
||||
# try to get the hostname from dns
|
||||
local dnsname=$(drill -x ${hostip} \
|
||||
| grep PTR \
|
||||
| awk '{print $5}' \
|
||||
| grep -E "^[a-zA-Z0-9]+([-._]?[a-zA-Z0-9]+)*.[a-zA-Z]+\.$" \
|
||||
| cut -d '.' -f1 \
|
||||
| tail -1)
|
||||
|
||||
is_set ${dnsname} \
|
||||
&& echo ${dnsname}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source "${EJABBERD_HOME}/scripts/lib/base_config.sh"
|
||||
source "${EJABBERD_HOME}/scripts/lib/config.sh"
|
||||
source "${EJABBERD_HOME}/scripts/lib/base_functions.sh"
|
||||
source "${EJABBERD_HOME}/scripts/lib/functions.sh"
|
||||
|
||||
|
||||
get_cluster_node_from_dns() {
|
||||
local cluster_host=$(drill ${DOMAINNAME} \
|
||||
| grep ${DOMAINNAME} \
|
||||
| grep -v ${HOSTIP} \
|
||||
| awk '{print $5}' \
|
||||
| grep -v "^$" \
|
||||
| head -1)
|
||||
echo $(discover_dns_hostname ${cluster_host})
|
||||
}
|
||||
|
||||
|
||||
file_exist ${FIRST_START_DONE_FILE} \
|
||||
&& exit 0
|
||||
|
||||
|
||||
join_cluster $(get_cluster_node_from_dns)
|
||||
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1 @@
|
||||
# simple docker-compose example
|
||||
@@ -0,0 +1,11 @@
|
||||
ejabberd:
|
||||
image: rroemhild/ejabberd
|
||||
ports:
|
||||
- 5222:5222
|
||||
- 5269:5269
|
||||
- 5280:5280
|
||||
environment:
|
||||
- ERLANG_NODE=ejabberd
|
||||
- XMPP_DOMAIN=example.com xyz.io
|
||||
- EJABBERD_ADMINS=admin@example.com
|
||||
- EJABBERD_USERS=admin@example.com:password4321 user1@xyz.io
|
||||
@@ -3,7 +3,7 @@ readonly HOSTNAME=$(hostname -f)
|
||||
readonly DOMAINNAME=$(hostname -d)
|
||||
|
||||
readonly ERLANGCOOKIEFILE="${EJABBERD_HOME}/.erlang.cookie"
|
||||
readonly EJABBERDCTL="/sbin/ejabberdctl"
|
||||
readonly EJABBERDCTL="/usr/local/sbin/ejabberdctl"
|
||||
readonly CONFIGFILE="${EJABBERD_HOME}/conf/ejabberd.yml"
|
||||
readonly CONFIGTEMPLATE="${EJABBERD_HOME}/conf/ejabberd.yml.tpl"
|
||||
readonly CTLCONFIGFILE="${EJABBERD_HOME}/conf/ejabberdctl.cfg"
|
||||
@@ -11,7 +11,7 @@ readonly CTLCONFIGTEMPLATE="${EJABBERD_HOME}/conf/ejabberdctl.cfg.tpl"
|
||||
readonly SSLCERTDIR="${EJABBERD_HOME}/ssl"
|
||||
readonly SSLCERTHOST="${SSLCERTDIR}/host.pem"
|
||||
readonly SSLDHPARAM="${SSLCERTDIR}/dh.pem"
|
||||
readonly LOGDIR="/var/log/ejabberd"
|
||||
readonly LOGDIR="/usr/local/var/log/ejabberd"
|
||||
readonly FIRST_START_DONE_FILE="/${EJABBERD_HOME}/first-start-done"
|
||||
readonly CLUSTER_NODE_FILE="/${EJABBERD_HOME}/cluster-done"
|
||||
|
||||
|
||||
@@ -111,8 +111,10 @@ hosts:
|
||||
## 'CERTFILE': "/path/to/xmpp.pem"
|
||||
## 'CIPHERS': "ECDH:DH:!3DES:!aNULL:!eNULL:!MEDIUM@STRENGTH"
|
||||
## 'TLSOPTS':
|
||||
## - "no_sslv2"
|
||||
## - "no_sslv3"
|
||||
## - "no_tlsv1"
|
||||
## - "no_tlsv1_1"
|
||||
## - "cipher_server_preference"
|
||||
## - "no_compression"
|
||||
## 'DHFILE': "/path/to/dhparams.pem" # generated with: openssl dhparam -out dhparams.pem 2048
|
||||
@@ -231,7 +233,7 @@ listen:
|
||||
|
||||
##
|
||||
## s2s_use_starttls: Enable STARTTLS for S2S connections.
|
||||
## Allowed values are: false optional required required_trusted
|
||||
## Allowed values are: false, optional or required
|
||||
## You must specify a certificate file.
|
||||
##
|
||||
## s2s_use_starttls: required
|
||||
@@ -766,6 +768,11 @@ modules:
|
||||
mod_time: {}
|
||||
mod_vcard:
|
||||
search: false
|
||||
mod_vcard_xupdate: {}
|
||||
## Convert all avatars posted by Android clients from WebP to JPEG
|
||||
mod_avatar:
|
||||
convert:
|
||||
webp: jpeg
|
||||
mod_version: {}
|
||||
mod_stream_mgmt: {}
|
||||
## Non-SASL Authentication (XEP-0078) is now disabled by default
|
||||
|
||||
+50
-51
@@ -7,6 +7,7 @@ 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
|
||||
@@ -16,7 +17,7 @@ EPMD="{{epmd}}"
|
||||
INSTALLUSER="{{installuser}}"
|
||||
|
||||
# check the proper system user is used
|
||||
case `id -un` in
|
||||
case $(id -un) in
|
||||
"$INSTALLUSER")
|
||||
EXEC_CMD="as_current_user"
|
||||
;;
|
||||
@@ -40,7 +41,7 @@ esac
|
||||
|
||||
# parse command line parameters
|
||||
for arg; do
|
||||
case $1 in
|
||||
case $arg in
|
||||
-n|--node) ERLANG_NODE_ARG=$2; shift;;
|
||||
-s|--spool) SPOOL_DIR=$2; shift;;
|
||||
-l|--logs) LOGS_DIR=$2; shift;;
|
||||
@@ -55,25 +56,25 @@ for arg; do
|
||||
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}
|
||||
: "${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"}"
|
||||
[ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH"
|
||||
[ "$ERLANG_NODE_ARG" != "" ] && ERLANG_NODE=$ERLANG_NODE_ARG
|
||||
[ -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}
|
||||
: "${EJABBERD_DOC_PATH:="{{docdir}}"}"
|
||||
: "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}"
|
||||
|
||||
# define erl parameters
|
||||
ERLANG_OPTS="+K $POLL -smp $SMP +P $ERL_PROCESSES $ERL_OPTIONS"
|
||||
if [ "$FIREWALL_WINDOW" != "" ] ; then
|
||||
if [ -n "$FIREWALL_WINDOW" ] ; then
|
||||
ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}"
|
||||
fi
|
||||
if [ "$INET_DIST_INTERFACE" != "" ] ; then
|
||||
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 [ "$INET_DIST_INTERFACE2" != "" ] ; then
|
||||
if [ -n "$INET_DIST_INTERFACE2" ] ; then
|
||||
ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2"
|
||||
fi
|
||||
fi
|
||||
@@ -115,12 +116,12 @@ exec_cmd()
|
||||
exec_erl()
|
||||
{
|
||||
NODE=$1; shift
|
||||
exec_cmd "$ERL" ${S:--}name $NODE $ERLANG_OPTS "$@"
|
||||
exec_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@"
|
||||
}
|
||||
exec_iex()
|
||||
{
|
||||
NODE=$1; shift
|
||||
exec_cmd "$IEX" ${S:---}name $NODE --erl "$ERLANG_OPTS" "$@"
|
||||
exec_cmd "$IEX" -${S:--}name "$NODE" --erl "$ERLANG_OPTS" "$@"
|
||||
}
|
||||
|
||||
# usage
|
||||
@@ -143,7 +144,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 foo
|
||||
read -r
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
@@ -166,7 +167,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 foo
|
||||
read -r
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
@@ -196,12 +197,12 @@ help()
|
||||
uid()
|
||||
{
|
||||
uuid=$(uuidgen 2>/dev/null)
|
||||
[ -z "$uuid" -a -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid)
|
||||
[ -z "$uuid" ] && uuid=$(printf "%X" $RANDOM$(date +%M%S)$$)
|
||||
[ -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=${uuid%%-*}
|
||||
[ $# -eq 0 ] && echo ${uuid}-${ERLANG_NODE}
|
||||
[ $# -eq 1 ] && echo ${uuid}-${1}-${ERLANG_NODE}
|
||||
[ $# -eq 2 ] && echo ${uuid}-${1}@${2}
|
||||
[ $# -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
|
||||
@@ -215,19 +216,17 @@ stop_epmd()
|
||||
check_start()
|
||||
{
|
||||
"$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && {
|
||||
ps ux | grep -v grep | grep -q " $ERLANG_NODE " && {
|
||||
pgrep -f "$ERLANG_NODE" >/dev/null && {
|
||||
echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running."
|
||||
exit 4
|
||||
} || {
|
||||
ps ux | grep -v grep | grep -q beam && {
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,17 +235,17 @@ wait_status()
|
||||
{
|
||||
# args: status try delay
|
||||
# return: 0 OK, 1 KO
|
||||
timeout=$2
|
||||
timeout="$2"
|
||||
status=4
|
||||
while [ $status -ne $1 ] ; do
|
||||
sleep $3
|
||||
timeout=`expr $timeout - 1`
|
||||
while [ "$status" -ne "$1" ] ; do
|
||||
sleep "$3"
|
||||
timeout=$((timeout - 1))
|
||||
if [ $timeout -eq 0 ] ; then
|
||||
status=$1
|
||||
status="$1"
|
||||
else
|
||||
exec_erl $(uid ctl) -hidden -noinput -s ejabberd_ctl \
|
||||
-extra $ERLANG_NODE $NO_TIMEOUT status > /dev/null
|
||||
status=$?
|
||||
exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \
|
||||
-extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null
|
||||
status="$?"
|
||||
fi
|
||||
done
|
||||
[ $timeout -gt 0 ]
|
||||
@@ -263,38 +262,38 @@ cd "$SPOOL_DIR" || {
|
||||
case $1 in
|
||||
start)
|
||||
check_start
|
||||
exec_erl $ERLANG_NODE $EJABBERD_OPTS -noinput -detached
|
||||
exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput -detached
|
||||
;;
|
||||
foreground)
|
||||
check_start
|
||||
exec_erl $ERLANG_NODE $EJABBERD_OPTS -noinput
|
||||
exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput
|
||||
;;
|
||||
live)
|
||||
livewarning
|
||||
check_start
|
||||
exec_erl $ERLANG_NODE $EJABBERD_OPTS
|
||||
exec_erl "$ERLANG_NODE" $EJABBERD_OPTS
|
||||
;;
|
||||
debug)
|
||||
debugwarning
|
||||
exec_erl $(uid debug) -hidden -remsh $ERLANG_NODE
|
||||
exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE"
|
||||
;;
|
||||
etop)
|
||||
exec_erl $(uid top) -hidden -node $ERLANG_NODE -s etop \
|
||||
exec_erl "$(uid top)" -hidden -node "$ERLANG_NODE" -s etop \
|
||||
-s erlang halt -output text
|
||||
;;
|
||||
iexdebug)
|
||||
debugwarning
|
||||
exec_iex $(uid debug) --remsh "$ERLANG_NODE"
|
||||
exec_iex "$(uid debug)" --remsh "$ERLANG_NODE"
|
||||
;;
|
||||
iexlive)
|
||||
livewarning
|
||||
exec_iex $ERLANG_NODE --erl "$EJABBERD_OPTS" --app ejabberd
|
||||
exec_iex "$ERLANG_NODE" --erl "$EJABBERD_OPTS" --app ejabberd
|
||||
;;
|
||||
ping)
|
||||
PEER=${2:-$ERLANG_NODE}
|
||||
[ "$PEER" = "${PEER%.*}" ] && PS="-s"
|
||||
exec_cmd "$ERL" ${PS:--}name $(uid ping $(hostname $PS)) $ERLANG_OPTS \
|
||||
-noinput -hidden -eval 'io:format("~p~n",[net_adm:ping('"$PEER"')])' \
|
||||
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)
|
||||
@@ -304,8 +303,8 @@ case $1 in
|
||||
wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout
|
||||
;;
|
||||
*)
|
||||
exec_erl $(uid ctl) -hidden -noinput -s ejabberd_ctl \
|
||||
-extra $ERLANG_NODE $NO_TIMEOUT "$@"
|
||||
exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \
|
||||
-extra "$ERLANG_NODE" $NO_TIMEOUT "$@"
|
||||
result=$?
|
||||
case $result in
|
||||
2|3) help;;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-define(T(S), <<S>>).
|
||||
@@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do
|
||||
|
||||
def project do
|
||||
[app: :ejabberd,
|
||||
version: "17.8.0",
|
||||
version: "17.9.0",
|
||||
description: description(),
|
||||
elixir: "~> 1.4",
|
||||
elixirc_paths: ["lib"],
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
-module(override_opts).
|
||||
-export([preprocess/2]).
|
||||
|
||||
override_opts(override, Config, Opts) ->
|
||||
lists:foldl(fun({Opt, Value}, Conf) ->
|
||||
rebar_config:set(Conf, Opt, Value)
|
||||
end, Config, Opts);
|
||||
override_opts(add, Config, Opts) ->
|
||||
lists:foldl(fun({Opt, Value}, Conf) ->
|
||||
V = rebar_config:get_local(Conf, Opt, []),
|
||||
rebar_config:set(Conf, Opt, [Value | V])
|
||||
end, Config, Opts).
|
||||
|
||||
preprocess(Config, _Dirs) ->
|
||||
Overrides = rebar_config:get_local(Config, overrides, []),
|
||||
TopOverrides = case rebar_config:get_xconf(Config, top_overrides, []) of
|
||||
[] -> Overrides;
|
||||
Val -> Val
|
||||
end,
|
||||
Config2 = rebar_config:set_xconf(Config, top_overrides, TopOverrides),
|
||||
Config3 = case rebar_app_utils:load_app_file(Config2, _Dirs) of
|
||||
{ok, C, AppName, _AppData} ->
|
||||
lists:foldl(fun({Type, AppName2, Opts}, Conf1) when
|
||||
AppName2 == AppName ->
|
||||
override_opts(Type, Conf1, Opts);
|
||||
(_, Conf2) ->
|
||||
Conf2
|
||||
end, C, TopOverrides);
|
||||
_ ->
|
||||
Config2
|
||||
end,
|
||||
{ok, Config3, []}.
|
||||
+20
-16
@@ -20,30 +20,31 @@
|
||||
|
||||
{deps, [{lager, ".*", {git, "https://github.com/erlang-lager/lager",
|
||||
{tag, {if_version_above, "17", "3.4.2", "3.2.1"}}}},
|
||||
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.9"}}},
|
||||
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.10"}}},
|
||||
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.15"}}},
|
||||
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.9"}}},
|
||||
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.23"}}},
|
||||
{xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.1.14"}}},
|
||||
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.10"}}},
|
||||
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.10"}}},
|
||||
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.11"}}},
|
||||
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.16"}}},
|
||||
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.10"}}},
|
||||
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.24"}}},
|
||||
{xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.1.15"}}},
|
||||
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.11"}}},
|
||||
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}},
|
||||
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.2"}}},
|
||||
{luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}},
|
||||
{if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.14"}}}},
|
||||
{if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.15"}}}},
|
||||
{if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.15"}}}},
|
||||
{if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.16"}}}},
|
||||
{if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
|
||||
{tag, "1.0.3"}}}},
|
||||
{tag, "1.0.4"}}}},
|
||||
{if_var_true, pgsql, {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql",
|
||||
{tag, "1.1.3"}}}},
|
||||
{tag, "1.1.4"}}}},
|
||||
{if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3",
|
||||
{tag, "1.1.5"}}}},
|
||||
{if_var_true, pam, {epam, ".*", {git, "https://github.com/processone/epam",
|
||||
{tag, "1.0.3"}}}},
|
||||
{if_var_true, zlib, {ezlib, ".*", {git, "https://github.com/processone/ezlib",
|
||||
{tag, "1.0.2"}}}},
|
||||
{if_var_true, riak, {riakc, ".*", {git, "https://github.com/basho/riak-erlang-client",
|
||||
{tag, "2.4.1"}}}},
|
||||
{if_var_true, riak, {riakc, ".*", {git, "https://github.com/processone/riak-erlang-client.git",
|
||||
{tag, {if_version_above, "19", "develop", "2.5.3"}}}}},
|
||||
{if_var_true, graphics, {eimp, ".*", {git, "https://github.com/processone/eimp.git", {tag, "1.0.1"}}}},
|
||||
%% Elixir support, needed to run tests
|
||||
{if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir",
|
||||
{tag, {if_version_above, "17", "v1.4.4", "v1.1.1"}}}}},
|
||||
@@ -51,7 +52,7 @@
|
||||
{if_not_rebar3, {if_var_true, elixir, {rebar_elixir_plugin, ".*",
|
||||
{git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}},
|
||||
{if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv",
|
||||
{tag, "1.0.5"}}}},
|
||||
{tag, "1.0.6"}}}},
|
||||
{if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck",
|
||||
{tag, "0.8.4"}}}},
|
||||
{if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git",
|
||||
@@ -74,6 +75,7 @@
|
||||
p1_oauth2,
|
||||
epam,
|
||||
ezlib,
|
||||
eimp,
|
||||
iconv]}}.
|
||||
|
||||
{erl_first_files, ["src/ejabberd_config.erl", "src/gen_mod.erl", "src/mod_muc_room.erl", "src/mod_push.erl"]}.
|
||||
@@ -87,11 +89,13 @@
|
||||
{if_var_true, debug, debug_info},
|
||||
{if_var_true, sip, {d, 'SIP'}},
|
||||
{if_var_true, stun, {d, 'STUN'}},
|
||||
{if_var_true, graphics, {d, 'GRAPHICS'}},
|
||||
{if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}},
|
||||
{if_var_match, db_type, mssql, {d, 'mssql'}},
|
||||
{if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
|
||||
{if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}},
|
||||
{if_have_fun, {crypto, strong_rand_bytes, 1}, {d, 'STRONG_RAND_BYTES'}},
|
||||
{if_have_fun, {rand, uniform, 1}, {d, 'RAND_UNIFORM'}},
|
||||
{if_have_fun, {gb_sets, iterator_from, 2}, {d, 'GB_SETS_ITERATOR_FROM'}},
|
||||
{if_have_fun, {public_key, short_name_hash, 1}, {d, 'SHORT_NAME_HASH'}},
|
||||
{if_var_true, hipe, native},
|
||||
@@ -103,7 +107,7 @@
|
||||
|
||||
{if_rebar3, {plugins, [rebar3_hex, {provider_asn1, "0.2.0"}]}}.
|
||||
{if_not_rebar3, {plugins, [
|
||||
deps_erl_opts, override_deps_versions,
|
||||
deps_erl_opts, override_deps_versions, override_opts,
|
||||
{if_var_true, elixir, rebar_elixir_compiler},
|
||||
{if_var_true, elixir, rebar_exunit}
|
||||
]}}.
|
||||
@@ -153,12 +157,12 @@
|
||||
{"fast_xml", [{if_var_true, full_xml, "--enable-full-xml"}]},
|
||||
{if_var_true, pam, {"epam", []}},
|
||||
{if_var_true, zlib, {"ezlib", []}},
|
||||
{if_var_true, graphics, {"eimp", []}},
|
||||
{if_var_true, iconv, {"iconv", []}}]}.
|
||||
|
||||
{port_env, [{"CFLAGS", "-g -O2 -Wall"}]}.
|
||||
|
||||
{port_specs, [{"priv/lib/jid.so", ["c_src/jid.c"]}]}.
|
||||
|
||||
%% Local Variables:
|
||||
%% mode: erlang
|
||||
%% End:
|
||||
|
||||
+6
-6
@@ -255,9 +255,9 @@ CtParams = fun(CompileOpts) ->
|
||||
|
||||
GenDepConfigureLine =
|
||||
fun(DepPath, Flags) ->
|
||||
["sh -c 'if test ! -f ",DepPath,"config.status -o ",
|
||||
"config.status -nt ",DepPath,"config.status; ",
|
||||
"then (cd ", DepPath, " && ",
|
||||
["sh -c 'if test ! -f config.status -o ",
|
||||
"../../config.status -nt config.status; ",
|
||||
"then (",
|
||||
"CFLAGS=\"", CFlags,"\" ",
|
||||
"CPPFLAGS=\"", CPPFlags, "\" "
|
||||
"LDFLAGS=\"", LDFlags, "\"",
|
||||
@@ -269,8 +269,8 @@ GenDepsConfigure =
|
||||
fun(Hooks) ->
|
||||
lists:map(fun({Pkg, Flags}) ->
|
||||
DepPath = ResolveDepPath("deps/" ++ Pkg ++ "/"),
|
||||
{'compile',
|
||||
lists:flatten(GenDepConfigureLine(DepPath, Flags))}
|
||||
{add, list_to_atom(Pkg), [{pre_hooks, {'compile',
|
||||
lists:flatten(GenDepConfigureLine(DepPath, Flags))}}]}
|
||||
end, Hooks)
|
||||
end,
|
||||
|
||||
@@ -326,7 +326,7 @@ Rules = [
|
||||
AppendList([{coveralls, ".*", {git, "https://github.com/markusn/coveralls-erl.git", "master"}}]), []},
|
||||
{[post_hooks], [cover_enabled], os:getenv("TRAVIS") == "true",
|
||||
AppendList2(TravisPostHooks), [], false},
|
||||
{[pre_hooks], [post_hook_configure], true,
|
||||
{[overrides], [post_hook_configure], true,
|
||||
AppendList2(GenDepsConfigure), [], []},
|
||||
{[ct_extra_params], [eunit_compile_opts], true,
|
||||
AppendStr2(CtParams), "", []},
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ CREATE TABLE archive (
|
||||
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE FULLTEXT INDEX i_text ON archive(txt);
|
||||
CREATE INDEX i_username USING BTREE ON archive(username);
|
||||
CREATE INDEX i_username_timestamp USING BTREE ON archive(username,timestamp);
|
||||
CREATE INDEX i_timestamp USING BTREE ON archive(timestamp);
|
||||
CREATE INDEX i_peer USING BTREE ON archive(peer);
|
||||
CREATE INDEX i_bare_peer USING BTREE ON archive(bare_peer);
|
||||
|
||||
+10
-1
@@ -146,7 +146,8 @@ start_apps() ->
|
||||
ejabberd:start_app(fast_yaml),
|
||||
ejabberd:start_app(fast_tls),
|
||||
ejabberd:start_app(xmpp),
|
||||
ejabberd:start_app(cache_tab).
|
||||
ejabberd:start_app(cache_tab),
|
||||
start_eimp().
|
||||
|
||||
setup_if_elixir_conf_used() ->
|
||||
case ejabberd_config:is_using_elixir_config() of
|
||||
@@ -170,3 +171,11 @@ start_elixir_application() ->
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-ifdef(GRAPHICS).
|
||||
start_eimp() ->
|
||||
ejabberd:start_app(eimp).
|
||||
-else.
|
||||
start_eimp() ->
|
||||
ok.
|
||||
-endif.
|
||||
|
||||
@@ -303,9 +303,9 @@ tls_options(#{lserver := LServer, tls_options := DefaultOpts,
|
||||
{true, CertFile} when CertFile /= undefined -> DefaultOpts;
|
||||
{_, _} ->
|
||||
case ejabberd_config:get_option(
|
||||
{c2s_certfile, LServer},
|
||||
{domain_certfile, LServer},
|
||||
ejabberd_config:get_option(
|
||||
{domain_certfile, LServer})) of
|
||||
{c2s_certfile, LServer})) of
|
||||
undefined -> DefaultOpts;
|
||||
CertFile -> lists:keystore(certfile, 1, DefaultOpts,
|
||||
{certfile, CertFile})
|
||||
@@ -411,7 +411,7 @@ bind(R, #{user := U, server := S, access := Access, lang := Lang,
|
||||
ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]),
|
||||
?INFO_MSG("(~s) Forbidden c2s session for ~s",
|
||||
[SockMod:pp(Socket), jid:encode(JID)]),
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
{error, xmpp:err_not_allowed(Txt, Lang), State}
|
||||
end
|
||||
end.
|
||||
@@ -658,7 +658,7 @@ process_presence_out(#{user := User, server := Server, lserver := LServer,
|
||||
MyBareJID = jid:remove_resource(JID),
|
||||
case acl:match_rule(LServer, Access, MyBareJID) of
|
||||
deny ->
|
||||
ErrText = <<"Denied by ACL">>,
|
||||
ErrText = <<"Access denied by service policy">>,
|
||||
Err = xmpp:err_forbidden(ErrText, Lang),
|
||||
send_error(State, Pres, Err);
|
||||
allow ->
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
-export([start/0, load_file/1, reload_file/0, read_file/1,
|
||||
get_option/1, get_option/2, add_option/2, has_option/1,
|
||||
get_vh_by_auth_method/1, is_file_readable/1,
|
||||
get_vh_by_auth_method/1,
|
||||
get_version/0, get_myhosts/0, get_mylang/0, get_lang/1,
|
||||
get_ejabberd_config_path/0, is_using_elixir_config/0,
|
||||
prepare_opt_val/4, transform_options/1, collect_options/1,
|
||||
@@ -46,11 +46,12 @@
|
||||
get_global_option/2, get_local_option/2,
|
||||
get_global_option/3, get_local_option/3,
|
||||
get_option/3]).
|
||||
-export([is_file_readable/1]).
|
||||
|
||||
-deprecated([{add_global_option, 2}, {add_local_option, 2},
|
||||
{get_global_option, 2}, {get_local_option, 2},
|
||||
{get_global_option, 3}, {get_local_option, 3},
|
||||
{get_option, 3}]).
|
||||
{get_option, 3}, {is_file_readable, 1}]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
-include("logger.hrl").
|
||||
|
||||
@@ -68,6 +68,8 @@ init([]) ->
|
||||
_ -> ok
|
||||
end,
|
||||
ejabberd:start_app(mnesia, permanent),
|
||||
?DEBUG("Waiting for Mnesia tables synchronization...", []),
|
||||
mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity),
|
||||
Schema = read_schema_file(),
|
||||
{ok, #state{schema = Schema}};
|
||||
false ->
|
||||
|
||||
@@ -436,7 +436,7 @@ process(_Handlers,
|
||||
?INPUT(<<"hidden">>, <<"scope">>, Scope),
|
||||
?INPUT(<<"hidden">>, <<"state">>, State),
|
||||
?BR,
|
||||
?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
|
||||
?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?C(<<": ">>)]),
|
||||
?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
|
||||
[
|
||||
?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
|
||||
|
||||
+10
-4
@@ -199,9 +199,9 @@ dirty_get_connections() ->
|
||||
-spec tls_options(binary(), [proplists:property()]) -> [proplists:property()].
|
||||
tls_options(LServer, DefaultOpts) ->
|
||||
TLSOpts1 = case ejabberd_config:get_option(
|
||||
{s2s_certfile, LServer},
|
||||
{domain_certfile, LServer},
|
||||
ejabberd_config:get_option(
|
||||
{domain_certfile, LServer})) of
|
||||
{s2s_certfile, LServer})) of
|
||||
undefined -> DefaultOpts;
|
||||
CertFile -> lists:keystore(certfile, 1, DefaultOpts,
|
||||
{certfile, CertFile})
|
||||
@@ -369,7 +369,7 @@ do_route(Packet) ->
|
||||
<<"Server connections to local "
|
||||
"subdomains are forbidden">>, Lang);
|
||||
forbidden ->
|
||||
xmpp:err_forbidden(<<"Denied by ACL">>, Lang);
|
||||
xmpp:err_forbidden(<<"Access denied by service policy">>, Lang);
|
||||
internal_server_error ->
|
||||
xmpp:err_internal_server_error()
|
||||
end,
|
||||
@@ -726,7 +726,13 @@ opt_type(s2s_use_starttls) ->
|
||||
(false) -> false;
|
||||
(optional) -> optional;
|
||||
(required) -> required;
|
||||
(required_trusted) -> required_trusted
|
||||
(required_trusted) ->
|
||||
?WARNING_MSG("The value 'required_trusted' of option "
|
||||
"'s2s_use_starttls' is deprected and will be "
|
||||
"unsupported in future releases. Instead, "
|
||||
"set it to 'required' and make sure "
|
||||
"mod_s2s_dialback is *NOT* loaded", []),
|
||||
required_trusted
|
||||
end;
|
||||
opt_type(s2s_zlib) ->
|
||||
fun(B) when is_boolean(B) -> B end;
|
||||
|
||||
@@ -374,7 +374,7 @@ mk_bounce_error(_Lang, _State) ->
|
||||
-spec get_delay() -> non_neg_integer().
|
||||
get_delay() ->
|
||||
MaxDelay = ejabberd_config:get_option(s2s_max_retry_delay, 300),
|
||||
crypto:rand_uniform(1, MaxDelay).
|
||||
randoms:uniform(MaxDelay).
|
||||
|
||||
-spec set_idle_timeout(state()) -> state().
|
||||
set_idle_timeout(#{on_route := send, server := LServer} = State) ->
|
||||
|
||||
@@ -199,7 +199,7 @@ handle_info({route, Packet}, #{access := Access} = State) ->
|
||||
xmpp_stream_in:send(State, Packet);
|
||||
deny ->
|
||||
Lang = xmpp:get_lang(Packet),
|
||||
Err = xmpp:err_not_allowed(<<"Denied by ACL">>, Lang),
|
||||
Err = xmpp:err_not_allowed(<<"Access denied by service policy">>, Lang),
|
||||
ejabberd_router:route_error(Packet, Err),
|
||||
State
|
||||
end;
|
||||
|
||||
+2
-1
@@ -317,7 +317,8 @@ get_session_sids(User, Server) ->
|
||||
LUser = jid:nodeprep(User),
|
||||
LServer = jid:nameprep(Server),
|
||||
Mod = get_sm_backend(LServer),
|
||||
online(get_sessions(Mod, LUser, LServer)).
|
||||
Sessions = online(get_sessions(Mod, LUser, LServer)),
|
||||
[SID || #session{sid = SID} <- Sessions].
|
||||
|
||||
-spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok.
|
||||
|
||||
|
||||
+12
-16
@@ -33,7 +33,7 @@
|
||||
atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1,
|
||||
l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1,
|
||||
now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2,
|
||||
compile_exprs/2, join_atoms/2, try_read_file/1]).
|
||||
compile_exprs/2, join_atoms/2, try_read_file/1, have_eimp/0]).
|
||||
|
||||
%% Deprecated functions
|
||||
-export([decode_base64/1, encode_base64/1]).
|
||||
@@ -204,25 +204,21 @@ join_atoms(Atoms, Sep) ->
|
||||
%% in configuration validators only.
|
||||
-spec try_read_file(file:filename_all()) -> binary().
|
||||
try_read_file(Path) ->
|
||||
Res = case file:read_file_info(Path) of
|
||||
{ok, #file_info{type = Type, access = Access}} ->
|
||||
case {Type, Access} of
|
||||
{regular, read} -> ok;
|
||||
{regular, read_write} -> ok;
|
||||
{regular, _} -> {error, file:format_error(eaccess)};
|
||||
_ -> {error, "not a regular file"}
|
||||
end;
|
||||
{error, Why} ->
|
||||
{error, file:format_error(Why)}
|
||||
end,
|
||||
case Res of
|
||||
ok ->
|
||||
case file:open(Path, [read]) of
|
||||
{ok, Fd} ->
|
||||
file:close(Fd),
|
||||
iolist_to_binary(Path);
|
||||
{error, Reason} ->
|
||||
?ERROR_MSG("Failed to read ~s: ~s", [Path, Reason]),
|
||||
{error, Why} ->
|
||||
?ERROR_MSG("Failed to read ~s: ~s", [Path, file:format_error(Why)]),
|
||||
erlang:error(badarg)
|
||||
end.
|
||||
|
||||
-ifdef(GRAPHICS).
|
||||
have_eimp() -> true.
|
||||
-else.
|
||||
have_eimp() -> false.
|
||||
-endif.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
kick_session/4, status_num/2, status_num/1,
|
||||
status_list/2, status_list/1, connected_users_info/0,
|
||||
connected_users_vhost/1, set_presence/7,
|
||||
get_presence/2, user_sessions_info/2, get_last/2,
|
||||
get_presence/2, user_sessions_info/2, get_last/2, set_last/4,
|
||||
|
||||
% Accounts
|
||||
set_password/3, check_password_hash/4, delete_old_users/1,
|
||||
@@ -602,9 +602,9 @@ get_commands_spec() ->
|
||||
]}}},
|
||||
#ejabberd_commands{name = set_last, tags = [last],
|
||||
desc = "Set last activity information",
|
||||
longdesc = "Timestamp is the seconds since"
|
||||
longdesc = "Timestamp is the seconds since "
|
||||
"1970-01-01 00:00:00 UTC, for example: date +%s",
|
||||
module = mod_last, function = store_last_info,
|
||||
module = ?MODULE, function = set_last,
|
||||
args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}],
|
||||
args_example = [<<"user1">>,<<"myserver.com">>, 1500045311, <<"GoSleeping">>],
|
||||
args_desc = ["User name", "Server name", "Number of seconds since epoch", "Status message"],
|
||||
@@ -1437,6 +1437,12 @@ get_last(User, Server) ->
|
||||
end,
|
||||
{xmpp_util:encode_timestamp(Now), Status}.
|
||||
|
||||
set_last(User, Server, Timestamp, Status) ->
|
||||
case mod_last:store_last_info(User, Server, Timestamp, Status) of
|
||||
{ok, _} -> ok;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%
|
||||
%%% Private Storage
|
||||
%%%
|
||||
|
||||
+14
-6
@@ -237,7 +237,7 @@ disco_identity(Acc, _From, _To, Node, Lang) ->
|
||||
-define(INFO_RESULT(Allow, Feats, Lang),
|
||||
case Allow of
|
||||
deny ->
|
||||
{error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
allow ->
|
||||
{result, Feats}
|
||||
end).
|
||||
@@ -252,7 +252,7 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, Lang) -
|
||||
case {acl:match_rule(LServer, Access1, From),
|
||||
acl:match_rule(global, Access2, From)} of
|
||||
{deny, deny} ->
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
{error, xmpp:err_forbidden(Txt, Lang)};
|
||||
_ ->
|
||||
{result, []}
|
||||
@@ -303,7 +303,7 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) ->
|
||||
-define(ITEMS_RESULT(Allow, Items, Lang),
|
||||
case Allow of
|
||||
deny ->
|
||||
{error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
allow ->
|
||||
{result, Items}
|
||||
end).
|
||||
@@ -417,7 +417,7 @@ commands_result(Allow, From, To, Request) ->
|
||||
case Allow of
|
||||
deny ->
|
||||
Lang = Request#adhoc_command.lang,
|
||||
{error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
allow ->
|
||||
announce_commands(From, To, Request)
|
||||
end.
|
||||
@@ -843,7 +843,7 @@ add_store_hint(El) ->
|
||||
-spec route_forbidden_error(stanza()) -> ok.
|
||||
route_forbidden_error(Packet) ->
|
||||
Lang = xmpp:get_lang(Packet),
|
||||
Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
|
||||
Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang),
|
||||
ejabberd_router:route_error(Packet, Err).
|
||||
|
||||
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
|
||||
@@ -913,4 +913,12 @@ import(LServer, {sql, _}, DBType, Tab, List) ->
|
||||
|
||||
mod_opt_type(access) -> fun acl:access_rules_validator/1;
|
||||
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
|
||||
mod_opt_type(_) -> [access, db_type].
|
||||
mod_opt_type(O) when O == cache_life_time; O == cache_size ->
|
||||
fun (I) when is_integer(I), I > 0 -> I;
|
||||
(infinity) -> infinity
|
||||
end;
|
||||
mod_opt_type(O) when O == use_cache; O == cache_missed ->
|
||||
fun (B) when is_boolean(B) -> B end;
|
||||
mod_opt_type(_) ->
|
||||
[access, db_type, cache_life_time, cache_size,
|
||||
use_cache, cache_missed].
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
|
||||
%%% Created : 13 Sep 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2017 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_avatar).
|
||||
-behaviour(gen_mod).
|
||||
|
||||
%% gen_mod API
|
||||
-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1]).
|
||||
%% Hooks
|
||||
-export([pubsub_publish_item/6, vcard_iq_convert/1, vcard_iq_publish/1]).
|
||||
|
||||
-include("xmpp.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("pubsub.hrl").
|
||||
|
||||
-type convert_rules() :: {default | eimp:img_type(), eimp:img_type()}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
start(Host, _Opts) ->
|
||||
case misc:have_eimp() of
|
||||
true ->
|
||||
ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE,
|
||||
pubsub_publish_item, 50),
|
||||
ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE,
|
||||
vcard_iq_convert, 30),
|
||||
ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE,
|
||||
vcard_iq_publish, 100);
|
||||
false ->
|
||||
?CRITICAL_MSG("ejabberd is built without "
|
||||
"graphics support: reconfigure it with "
|
||||
"--enable-graphics or disable '~s'",
|
||||
[?MODULE]),
|
||||
{error, graphics_not_compiled}
|
||||
end.
|
||||
|
||||
stop(Host) ->
|
||||
ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE,
|
||||
pubsub_publish_item, 50),
|
||||
ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_convert, 30),
|
||||
ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_publish, 100).
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
ok.
|
||||
|
||||
depends(_Host, _Opts) ->
|
||||
[{mod_vcard, hard}, {mod_vcard_xupdate, hard}, {mod_pubsub, hard}].
|
||||
|
||||
%%%===================================================================
|
||||
%%% Hooks
|
||||
%%%===================================================================
|
||||
pubsub_publish_item(LServer, ?NS_AVATAR_METADATA,
|
||||
#jid{luser = LUser, lserver = LServer} = From,
|
||||
#jid{luser = LUser, lserver = LServer} = Host,
|
||||
ItemId, [Payload|_]) ->
|
||||
try xmpp:decode(Payload) of
|
||||
#avatar_meta{info = []} ->
|
||||
delete_vcard_avatar(From);
|
||||
#avatar_meta{info = Info} ->
|
||||
Rules = get_converting_rules(LServer),
|
||||
case get_meta_info(Info, Rules) of
|
||||
#avatar_info{type = MimeType, id = ID, url = <<"">>} = I ->
|
||||
case get_avatar_data(Host, ID) of
|
||||
{ok, Data} ->
|
||||
Meta = #avatar_meta{info = [I]},
|
||||
Photo = #vcard_photo{type = MimeType,
|
||||
binval = Data},
|
||||
set_vcard_avatar(From, Photo,
|
||||
#{avatar_meta => {ID, Meta}});
|
||||
{error, _} ->
|
||||
ok
|
||||
end;
|
||||
#avatar_info{type = MimeType, url = URL} ->
|
||||
Photo = #vcard_photo{type = MimeType,
|
||||
extval = URL},
|
||||
set_vcard_avatar(From, Photo, #{})
|
||||
end;
|
||||
_ ->
|
||||
?WARNING_MSG("invalid avatar metadata of ~s@~s published "
|
||||
"with item id ~s",
|
||||
[LUser, LServer, ItemId])
|
||||
catch _:{xmpp_codec, Why} ->
|
||||
?WARNING_MSG("failed to decode avatar metadata of ~s@~s: ~s",
|
||||
[LUser, LServer, xmpp:format_error(Why)])
|
||||
end;
|
||||
pubsub_publish_item(_, _, _, _, _, _) ->
|
||||
ok.
|
||||
|
||||
-spec vcard_iq_convert(iq()) -> iq() | {stop, stanza_error()}.
|
||||
vcard_iq_convert(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) ->
|
||||
#jid{luser = LUser, lserver = LServer} = From,
|
||||
case convert_avatar(LUser, LServer, VCard) of
|
||||
{ok, MimeType, Data} ->
|
||||
VCard1 = VCard#vcard_temp{
|
||||
photo = #vcard_photo{type = MimeType,
|
||||
binval = Data}},
|
||||
IQ#iq{sub_els = [VCard1]};
|
||||
pass ->
|
||||
IQ;
|
||||
{error, Reason} ->
|
||||
stop_with_error(Lang, Reason)
|
||||
end;
|
||||
vcard_iq_convert(Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec vcard_iq_publish(iq()) -> iq() | {stop, stanza_error()}.
|
||||
vcard_iq_publish(#iq{sub_els = [#vcard_temp{photo = undefined}]} = IQ) ->
|
||||
publish_avatar(IQ, #avatar_meta{}, <<>>, <<>>, <<>>);
|
||||
vcard_iq_publish(#iq{sub_els = [#vcard_temp{
|
||||
photo = #vcard_photo{
|
||||
type = MimeType,
|
||||
binval = Data}}]} = IQ)
|
||||
when is_binary(Data), Data /= <<>> ->
|
||||
SHA1 = str:sha(Data),
|
||||
M = get_avatar_meta(IQ),
|
||||
case M of
|
||||
{ok, SHA1, _} ->
|
||||
IQ;
|
||||
{ok, _ItemID, #avatar_meta{info = Info} = Meta} ->
|
||||
case lists:keyfind(SHA1, #avatar_info.id, Info) of
|
||||
#avatar_info{} ->
|
||||
IQ;
|
||||
false ->
|
||||
Info1 = lists:filter(
|
||||
fun(#avatar_info{url = URL}) -> URL /= <<"">> end,
|
||||
Info),
|
||||
Meta1 = Meta#avatar_meta{info = Info1},
|
||||
publish_avatar(IQ, Meta1, MimeType, Data, SHA1)
|
||||
end;
|
||||
{error, _} ->
|
||||
publish_avatar(IQ, #avatar_meta{}, MimeType, Data, SHA1)
|
||||
end;
|
||||
vcard_iq_publish(Acc) ->
|
||||
Acc.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
-spec get_meta_info([avatar_info()], convert_rules()) -> avatar_info().
|
||||
get_meta_info(Info, Rules) ->
|
||||
case lists:foldl(
|
||||
fun(_, #avatar_info{} = Acc) ->
|
||||
Acc;
|
||||
(#avatar_info{url = URL}, Acc) when URL /= <<"">> ->
|
||||
Acc;
|
||||
(#avatar_info{} = I, _) when Rules == [] ->
|
||||
I;
|
||||
(#avatar_info{type = MimeType} = I, Acc) ->
|
||||
T = decode_mime_type(MimeType),
|
||||
case lists:keymember(T, 2, Rules) of
|
||||
true ->
|
||||
I;
|
||||
false ->
|
||||
case convert_to_type(T, Rules) of
|
||||
undefined ->
|
||||
Acc;
|
||||
_ ->
|
||||
[I|Acc]
|
||||
end
|
||||
end
|
||||
end, [], Info) of
|
||||
#avatar_info{} = I -> I;
|
||||
[] -> hd(Info);
|
||||
Is -> hd(lists:reverse(Is))
|
||||
end.
|
||||
|
||||
-spec get_avatar_data(jid(), binary()) -> {ok, binary()} |
|
||||
{error,
|
||||
notfound | invalid_data | internal_error}.
|
||||
get_avatar_data(JID, ItemID) ->
|
||||
{LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)),
|
||||
case mod_pubsub:get_item(LBJID, ?NS_AVATAR_DATA, ItemID) of
|
||||
#pubsub_item{payload = [Payload|_]} ->
|
||||
try xmpp:decode(Payload) of
|
||||
#avatar_data{data = Data} ->
|
||||
{ok, Data};
|
||||
_ ->
|
||||
?WARNING_MSG("invalid avatar data detected "
|
||||
"for ~s@~s with item id ~s",
|
||||
[LUser, LServer, ItemID]),
|
||||
{error, invalid_data}
|
||||
catch _:{xmpp_codec, Why} ->
|
||||
?WARNING_MSG("failed to decode avatar data for "
|
||||
"~s@~s with item id ~s: ~s",
|
||||
[LUser, LServer, ItemID,
|
||||
xmpp:format_error(Why)]),
|
||||
{error, invalid_data}
|
||||
end;
|
||||
{error, #stanza_error{reason = 'item-not-found'}} ->
|
||||
{error, notfound};
|
||||
{error, Reason} ->
|
||||
?WARNING_MSG("failed to get item for ~s@~s at node ~s "
|
||||
"with item id ~s: ~p",
|
||||
[LUser, LServer, ?NS_AVATAR_METADATA, ItemID, Reason]),
|
||||
{error, internal_error}
|
||||
end.
|
||||
|
||||
-spec get_avatar_meta(iq()) -> {ok, binary(), avatar_meta()} |
|
||||
{error,
|
||||
notfound | invalid_metadata | internal_error}.
|
||||
get_avatar_meta(#iq{meta = #{avatar_meta := {ItemID, Meta}}}) ->
|
||||
{ok, ItemID, Meta};
|
||||
get_avatar_meta(#iq{from = JID}) ->
|
||||
{LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)),
|
||||
case mod_pubsub:get_items(LBJID, ?NS_AVATAR_METADATA) of
|
||||
[#pubsub_item{itemid = {ItemID, _}, payload = [Payload|_]}|_] ->
|
||||
try xmpp:decode(Payload) of
|
||||
#avatar_meta{} = Meta ->
|
||||
{ok, ItemID, Meta};
|
||||
_ ->
|
||||
?WARNING_MSG("invalid metadata payload detected "
|
||||
"for ~s@~s with item id ~s",
|
||||
[LUser, LServer, ItemID]),
|
||||
{error, invalid_metadata}
|
||||
catch _:{xmpp_codec, Why} ->
|
||||
?WARNING_MSG("failed to decode metadata for "
|
||||
"~s@~s with item id ~s: ~s",
|
||||
[LUser, LServer, ItemID,
|
||||
xmpp:format_error(Why)]),
|
||||
{error, invalid_metadata}
|
||||
end;
|
||||
{error, #stanza_error{reason = 'item-not-found'}} ->
|
||||
{error, notfound};
|
||||
{error, Reason} ->
|
||||
?WARNING_MSG("failed to get items for ~s@~s at node ~s: ~p",
|
||||
[LUser, LServer, ?NS_AVATAR_METADATA, Reason]),
|
||||
{error, internal_error}
|
||||
end.
|
||||
|
||||
-spec publish_avatar(iq(), avatar_meta(), binary(), binary(), binary()) ->
|
||||
iq() | {stop, stanza_error()}.
|
||||
publish_avatar(#iq{from = JID} = IQ, Meta, <<>>, <<>>, <<>>) ->
|
||||
{_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)),
|
||||
case mod_pubsub:publish_item(
|
||||
LBJID, LServer, ?NS_AVATAR_METADATA,
|
||||
JID, <<>>, [xmpp:encode(Meta)]) of
|
||||
{result, _} ->
|
||||
IQ;
|
||||
{error, StanzaErr} ->
|
||||
{stop, StanzaErr}
|
||||
end;
|
||||
publish_avatar(#iq{from = JID} = IQ, Meta, MimeType, Data, ItemID) ->
|
||||
#avatar_meta{info = Info} = Meta,
|
||||
{_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)),
|
||||
Payload = xmpp:encode(#avatar_data{data = Data}),
|
||||
case mod_pubsub:publish_item(
|
||||
LBJID, LServer, ?NS_AVATAR_DATA,
|
||||
JID, ItemID, [Payload]) of
|
||||
{result, _} ->
|
||||
{W, H} = case eimp:identify(Data) of
|
||||
{ok, ImgInfo} ->
|
||||
{proplists:get_value(width, ImgInfo),
|
||||
proplists:get_value(height, ImgInfo)};
|
||||
_ ->
|
||||
{undefined, undefined}
|
||||
end,
|
||||
I = #avatar_info{id = ItemID,
|
||||
width = W,
|
||||
height = H,
|
||||
type = MimeType,
|
||||
bytes = size(Data)},
|
||||
Meta1 = Meta#avatar_meta{info = [I|Info]},
|
||||
case mod_pubsub:publish_item(
|
||||
LBJID, LServer, ?NS_AVATAR_METADATA,
|
||||
JID, ItemID, [xmpp:encode(Meta1)]) of
|
||||
{result, _} ->
|
||||
IQ;
|
||||
{error, StanzaErr} ->
|
||||
?ERROR_MSG("Failed to publish avatar metadata for ~s: ~p",
|
||||
[jid:encode(JID), StanzaErr]),
|
||||
{stop, StanzaErr}
|
||||
end;
|
||||
{error, StanzaErr} ->
|
||||
?ERROR_MSG("Failed to publish avatar data for ~s: ~p",
|
||||
[jid:encode(JID), StanzaErr]),
|
||||
{stop, StanzaErr}
|
||||
end.
|
||||
|
||||
-spec convert_avatar(binary(), binary(), vcard_temp()) ->
|
||||
{ok, binary(), binary()} |
|
||||
{error, eimp:error_reason() | base64_error} |
|
||||
pass.
|
||||
convert_avatar(LUser, LServer, VCard) ->
|
||||
case get_converting_rules(LServer) of
|
||||
[] ->
|
||||
pass;
|
||||
Rules ->
|
||||
case VCard#vcard_temp.photo of
|
||||
#vcard_photo{binval = Data} when is_binary(Data) ->
|
||||
convert_avatar(LUser, LServer, Data, Rules);
|
||||
_ ->
|
||||
pass
|
||||
end
|
||||
end.
|
||||
|
||||
-spec convert_avatar(binary(), binary(), binary(), convert_rules()) ->
|
||||
{ok, eimp:img_type(), binary()} |
|
||||
{error, eimp:error_reason()} |
|
||||
pass.
|
||||
convert_avatar(LUser, LServer, Data, Rules) ->
|
||||
Type = get_type(Data),
|
||||
NewType = convert_to_type(Type, Rules),
|
||||
if NewType == undefined orelse Type == NewType ->
|
||||
pass;
|
||||
true ->
|
||||
?DEBUG("Converting avatar of ~s@~s: ~s -> ~s",
|
||||
[LUser, LServer, Type, NewType]),
|
||||
case eimp:convert(Data, NewType) of
|
||||
{ok, NewData} ->
|
||||
{ok, encode_mime_type(NewType), NewData};
|
||||
{error, Reason} = Err ->
|
||||
?ERROR_MSG("Failed to convert avatar of "
|
||||
"~s@~s (~s -> ~s): ~s",
|
||||
[LUser, LServer, Type, NewType,
|
||||
eimp:format_error(Reason)]),
|
||||
Err
|
||||
end
|
||||
end.
|
||||
|
||||
-spec set_vcard_avatar(jid(), vcard_photo() | undefined, map()) -> ok.
|
||||
set_vcard_avatar(JID, VCardPhoto, Meta) ->
|
||||
case get_vcard(JID) of
|
||||
{ok, #vcard_temp{photo = VCardPhoto}} ->
|
||||
ok;
|
||||
{ok, VCard} ->
|
||||
VCard1 = VCard#vcard_temp{photo = VCardPhoto},
|
||||
IQ = #iq{from = JID, to = JID, id = randoms:get_string(),
|
||||
type = set, sub_els = [VCard1], meta = Meta},
|
||||
LServer = JID#jid.lserver,
|
||||
ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []),
|
||||
ok;
|
||||
{error, _} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec delete_vcard_avatar(jid()) -> ok.
|
||||
delete_vcard_avatar(JID) ->
|
||||
set_vcard_avatar(JID, undefined, #{}).
|
||||
|
||||
-spec get_vcard(jid()) -> {ok, vcard_temp()} | {error, invalid_vcard}.
|
||||
get_vcard(#jid{luser = LUser, lserver = LServer}) ->
|
||||
VCardEl = case mod_vcard:get_vcard(LUser, LServer) of
|
||||
[El] -> El;
|
||||
_ -> #vcard_temp{}
|
||||
end,
|
||||
try xmpp:decode(VCardEl, ?NS_VCARD, []) of
|
||||
#vcard_temp{} = VCard ->
|
||||
{ok, VCard};
|
||||
_ ->
|
||||
?ERROR_MSG("invalid vCard of ~s@~s in the database",
|
||||
[LUser, LServer]),
|
||||
{error, invalid_vcard}
|
||||
catch _:{xmpp_codec, Why} ->
|
||||
?ERROR_MSG("failed to decode vCard of ~s@~s: ~s",
|
||||
[LUser, LServer, xmpp:format_error(Why)]),
|
||||
{error, invalid_vcard}
|
||||
end.
|
||||
|
||||
-spec stop_with_error(binary(), eimp:error_reason()) ->
|
||||
{stop, stanza_error()}.
|
||||
stop_with_error(Lang, Reason) ->
|
||||
Txt = eimp:format_error(Reason),
|
||||
{stop, xmpp:err_internal_server_error(Txt, Lang)}.
|
||||
|
||||
-spec get_converting_rules(binary()) -> convert_rules().
|
||||
get_converting_rules(LServer) ->
|
||||
gen_mod:get_module_opt(LServer, ?MODULE, convert, []).
|
||||
|
||||
-spec get_type(binary()) -> eimp:img_type() | unknown.
|
||||
get_type(Data) ->
|
||||
eimp:get_type(Data).
|
||||
|
||||
-spec convert_to_type(eimp:img_type() | unknown, convert_rules()) ->
|
||||
eimp:img_type() | undefined.
|
||||
convert_to_type(unknown, _Rules) ->
|
||||
undefined;
|
||||
convert_to_type(Type, Rules) ->
|
||||
case proplists:get_value(Type, Rules) of
|
||||
undefined ->
|
||||
proplists:get_value(default, Rules);
|
||||
T ->
|
||||
T
|
||||
end.
|
||||
|
||||
-spec decode_mime_type(binary()) -> eimp:img_type() | unknown.
|
||||
decode_mime_type(MimeType) ->
|
||||
case str:to_lower(MimeType) of
|
||||
<<"image/jpeg">> -> jpeg;
|
||||
<<"image/png">> -> png;
|
||||
<<"image/webp">> -> webp;
|
||||
<<"image/gif">> -> gif;
|
||||
_ -> unknown
|
||||
end.
|
||||
|
||||
-spec encode_mime_type(eimp:img_type()) -> binary().
|
||||
encode_mime_type(Type) ->
|
||||
<<"image/", (atom_to_binary(Type, latin1))/binary>>.
|
||||
|
||||
mod_opt_type({convert, png}) ->
|
||||
fun(jpeg) -> jpeg;
|
||||
(webp) -> webp;
|
||||
(gif) -> gif
|
||||
end;
|
||||
mod_opt_type({convert, webp}) ->
|
||||
fun(jpeg) -> jpeg;
|
||||
(png) -> png;
|
||||
(gif) -> gif
|
||||
end;
|
||||
mod_opt_type({convert, jpeg}) ->
|
||||
fun(png) -> png;
|
||||
(webp) -> webp;
|
||||
(gif) -> gif
|
||||
end;
|
||||
mod_opt_type({convert, gif}) ->
|
||||
fun(png) -> png;
|
||||
(jpeg) -> jpeg;
|
||||
(webp) -> webp
|
||||
end;
|
||||
mod_opt_type({convert, default}) ->
|
||||
fun(png) -> png;
|
||||
(webp) -> webp;
|
||||
(jpeg) -> jpeg;
|
||||
(gif) -> gif
|
||||
end;
|
||||
mod_opt_type(_) ->
|
||||
[{convert, default},
|
||||
{convert, webp},
|
||||
{convert, png},
|
||||
{convert, gif},
|
||||
{convert, jpeg}].
|
||||
+85
-38
@@ -32,7 +32,7 @@
|
||||
-export([start/2, stop/1, reload/3,
|
||||
depends/2, mod_opt_type/1]).
|
||||
|
||||
-export([filter_packet/1]).
|
||||
-export([filter_packet/1, filter_offline_msg/1]).
|
||||
|
||||
-include("xmpp.hrl").
|
||||
-include("ejabberd.hrl").
|
||||
@@ -43,54 +43,29 @@
|
||||
start(Host, _Opts) ->
|
||||
ejabberd_hooks:add(user_receive_packet, Host,
|
||||
?MODULE, filter_packet, 25),
|
||||
ok.
|
||||
ejabberd_hooks:add(offline_message_hook, Host,
|
||||
?MODULE, filter_offline_msg, 25).
|
||||
|
||||
stop(Host) ->
|
||||
ejabberd_hooks:delete(user_receive_packet, Host,
|
||||
?MODULE, filter_packet, 25),
|
||||
ok.
|
||||
ejabberd_hooks:delete(offline_message_hook, Host,
|
||||
?MODULE, filter_offline_msg, 25).
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
ok.
|
||||
|
||||
filter_packet({#message{} = Msg, State} = Acc) ->
|
||||
From = xmpp:get_from(Msg),
|
||||
filter_packet({#message{from = From} = Msg, State} = Acc) ->
|
||||
LFrom = jid:tolower(From),
|
||||
LBFrom = jid:remove_resource(LFrom),
|
||||
#{pres_a := PresA, jid := JID, lserver := LServer} = State,
|
||||
AllowLocalUsers =
|
||||
gen_mod:get_module_opt(LServer, ?MODULE, allow_local_users, true),
|
||||
case (Msg#message.body == [] andalso
|
||||
Msg#message.subject == [])
|
||||
orelse (AllowLocalUsers andalso
|
||||
ejabberd_router:is_my_route(From#jid.lserver))
|
||||
orelse (?SETS):is_element(LFrom, PresA)
|
||||
orelse (?SETS):is_element(LBFrom, PresA)
|
||||
#{pres_a := PresA} = State,
|
||||
case (?SETS):is_element(LFrom, PresA)
|
||||
orelse (?SETS):is_element(LBFrom, PresA)
|
||||
orelse sets_bare_member(LBFrom, PresA) of
|
||||
false ->
|
||||
{Sub, _} = ejabberd_hooks:run_fold(
|
||||
roster_get_jid_info, LServer,
|
||||
{none, []}, [JID#jid.luser, LServer, From]),
|
||||
case Sub of
|
||||
none ->
|
||||
Drop = gen_mod:get_module_opt(LServer, ?MODULE, drop, true),
|
||||
Log = gen_mod:get_module_opt(LServer, ?MODULE, log, false),
|
||||
if
|
||||
Log ->
|
||||
?INFO_MSG("Drop packet: ~s",
|
||||
[fxml:element_to_binary(
|
||||
xmpp:encode(Msg, ?NS_CLIENT))]);
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
if
|
||||
Drop ->
|
||||
{stop, {drop, State}};
|
||||
true ->
|
||||
Acc
|
||||
end;
|
||||
_ ->
|
||||
Acc
|
||||
case check_message(Msg) of
|
||||
allow -> Acc;
|
||||
deny -> {stop, {drop, State}}
|
||||
end;
|
||||
true ->
|
||||
Acc
|
||||
@@ -98,6 +73,76 @@ filter_packet({#message{} = Msg, State} = Acc) ->
|
||||
filter_packet(Acc) ->
|
||||
Acc.
|
||||
|
||||
filter_offline_msg({_Action, #message{} = Msg} = Acc) ->
|
||||
case check_message(Msg) of
|
||||
allow -> Acc;
|
||||
deny -> {stop, {drop, Msg}}
|
||||
end.
|
||||
|
||||
check_message(#message{from = From, to = To} = Msg) ->
|
||||
LServer = To#jid.lserver,
|
||||
AllowLocalUsers =
|
||||
gen_mod:get_module_opt(LServer, ?MODULE, allow_local_users, true),
|
||||
case (Msg#message.body == [] andalso
|
||||
Msg#message.subject == [])
|
||||
orelse ((AllowLocalUsers orelse From#jid.luser == <<"">>) andalso
|
||||
ejabberd_router:is_my_host(From#jid.lserver)) of
|
||||
false ->
|
||||
case check_subscription(From, To) of
|
||||
none ->
|
||||
Drop = gen_mod:get_module_opt(LServer, ?MODULE, drop, true),
|
||||
Log = gen_mod:get_module_opt(LServer, ?MODULE, log, false),
|
||||
if
|
||||
Log ->
|
||||
?INFO_MSG("~s message from stranger ~s to ~s",
|
||||
[if Drop -> "Dropping";
|
||||
true -> "Allow"
|
||||
end,
|
||||
jid:encode(From), jid:encode(To)]);
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
if
|
||||
Drop ->
|
||||
deny;
|
||||
true ->
|
||||
allow
|
||||
end;
|
||||
some ->
|
||||
allow
|
||||
end;
|
||||
true ->
|
||||
allow
|
||||
end.
|
||||
|
||||
-spec check_subscription(jid(), jid()) -> none | some.
|
||||
check_subscription(From, To) ->
|
||||
{LocalUser, LocalServer, _} = jid:tolower(To),
|
||||
{RemoteUser, RemoteServer, _} = jid:tolower(From),
|
||||
case ejabberd_hooks:run_fold(
|
||||
roster_get_jid_info, LocalServer,
|
||||
{none, []}, [LocalUser, LocalServer, From]) of
|
||||
{none, _} when RemoteUser == <<"">> ->
|
||||
none;
|
||||
{none, _} ->
|
||||
case gen_mod:get_module_opt(LocalServer, ?MODULE,
|
||||
allow_transports, true) of
|
||||
true ->
|
||||
%% Check if the contact's server is in the roster
|
||||
case ejabberd_hooks:run_fold(
|
||||
roster_get_jid_info, LocalServer,
|
||||
{none, []},
|
||||
[LocalUser, LocalServer, jid:make(RemoteServer)]) of
|
||||
{none, _} -> none;
|
||||
_ -> some
|
||||
end;
|
||||
false ->
|
||||
none
|
||||
end;
|
||||
_ ->
|
||||
some
|
||||
end.
|
||||
|
||||
sets_bare_member({U, S, <<"">>} = LBJID, Set) ->
|
||||
case ?SETS:next(sets_iterator_from(LBJID, Set)) of
|
||||
{{U, S, _}, _} -> true;
|
||||
@@ -133,4 +178,6 @@ mod_opt_type(log) ->
|
||||
fun (B) when is_boolean(B) -> B end;
|
||||
mod_opt_type(allow_local_users) ->
|
||||
fun (B) when is_boolean(B) -> B end;
|
||||
mod_opt_type(_) -> [drop, log, allow_local_users].
|
||||
mod_opt_type(allow_transports) ->
|
||||
fun (B) when is_boolean(B) -> B end;
|
||||
mod_opt_type(_) -> [drop, log, allow_local_users, allow_transports].
|
||||
|
||||
@@ -192,7 +192,7 @@ get_local_identity(Acc, _From, _To, Node, Lang) ->
|
||||
|
||||
-define(INFO_RESULT(Allow, Feats, Lang),
|
||||
case Allow of
|
||||
deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
deny -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
allow -> {result, Feats}
|
||||
end).
|
||||
|
||||
@@ -310,7 +310,7 @@ get_sm_items(Acc, From,
|
||||
Items ++ Nodes ++ get_user_resources(User, Server)};
|
||||
{allow, <<"config">>} -> {result, []};
|
||||
{_, <<"config">>} ->
|
||||
{error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
_ -> Acc
|
||||
end
|
||||
end.
|
||||
@@ -432,7 +432,7 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To,
|
||||
_ ->
|
||||
LNode = tokenize(Node),
|
||||
Allow = acl:match_rule(LServer, configure, From),
|
||||
Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
|
||||
Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang),
|
||||
case LNode of
|
||||
[<<"config">>] ->
|
||||
?ITEMS_RESULT(Allow, LNode, {error, Err});
|
||||
@@ -765,7 +765,7 @@ get_stopped_nodes(_Lang) ->
|
||||
-define(COMMANDS_RESULT(LServerOrGlobal, From, To,
|
||||
Request, Lang),
|
||||
case acl:match_rule(LServerOrGlobal, configure, From) of
|
||||
deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
deny -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
allow -> adhoc_local_commands(From, To, Request)
|
||||
end).
|
||||
|
||||
@@ -1737,7 +1737,7 @@ adhoc_sm_commands(_Acc, From,
|
||||
action = Action, xdata = XData} = Request) ->
|
||||
case acl:match_rule(LServer, configure, From) of
|
||||
deny ->
|
||||
{error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
allow ->
|
||||
ActionIsExecute = Action == execute orelse Action == complete,
|
||||
if Action == cancel ->
|
||||
|
||||
+119
-91
@@ -107,10 +107,11 @@
|
||||
get_url :: binary(),
|
||||
service_url :: binary() | undefined,
|
||||
thumbnail :: boolean(),
|
||||
custom_headers :: [{binary(), binary()}],
|
||||
slots = #{} :: map()}).
|
||||
|
||||
-record(media_info,
|
||||
{type :: binary(),
|
||||
{type :: atom(),
|
||||
height :: integer(),
|
||||
width :: integer()}).
|
||||
|
||||
@@ -226,6 +227,7 @@ init([ServerHost, Opts]) ->
|
||||
GetURL = gen_mod:get_opt(get_url, Opts, PutURL),
|
||||
ServiceURL = gen_mod:get_opt(service_url, Opts),
|
||||
Thumbnail = gen_mod:get_opt(thumbnail, Opts, true),
|
||||
CustomHeaders = gen_mod:get_opt(custom_headers, Opts, []),
|
||||
DocRoot1 = expand_home(str:strip(DocRoot, right, $/)),
|
||||
DocRoot2 = expand_host(DocRoot1, ServerHost),
|
||||
case DirMode of
|
||||
@@ -236,12 +238,14 @@ init([ServerHost, Opts]) ->
|
||||
end,
|
||||
case Thumbnail of
|
||||
true ->
|
||||
case string:str(os:cmd("identify"), "Magick") of
|
||||
0 ->
|
||||
?ERROR_MSG("Cannot find 'identify' command, please install "
|
||||
"ImageMagick or disable thumbnail creation", []);
|
||||
_ ->
|
||||
ok
|
||||
case misc:have_eimp() of
|
||||
false ->
|
||||
?ERROR_MSG("ejabberd is built without graphics support, "
|
||||
"please rebuild it with --enable-graphics or "
|
||||
"set 'thumbnail: false' for module '~s' in "
|
||||
"ejabberd.yml", [?MODULE]);
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
@@ -258,7 +262,8 @@ init([ServerHost, Opts]) ->
|
||||
docroot = DocRoot2,
|
||||
put_url = expand_host(str:strip(PutURL, right, $/), ServerHost),
|
||||
get_url = expand_host(str:strip(GetURL, right, $/), ServerHost),
|
||||
service_url = ServiceURL}}.
|
||||
service_url = ServiceURL,
|
||||
custom_headers = CustomHeaders}}.
|
||||
|
||||
-spec handle_call(_, {pid(), _}, state())
|
||||
-> {reply, {ok, pos_integer(), binary(),
|
||||
@@ -266,25 +271,30 @@ init([ServerHost, Opts]) ->
|
||||
pos_integer() | undefined}, state()} |
|
||||
{reply, {error, atom()}, state()} | {noreply, state()}.
|
||||
|
||||
handle_call({use_slot, Slot, Size}, _From, #state{file_mode = FileMode,
|
||||
dir_mode = DirMode,
|
||||
get_url = GetPrefix,
|
||||
thumbnail = Thumbnail,
|
||||
docroot = DocRoot} = State) ->
|
||||
handle_call({use_slot, Slot, Size}, _From,
|
||||
#state{file_mode = FileMode,
|
||||
dir_mode = DirMode,
|
||||
get_url = GetPrefix,
|
||||
thumbnail = Thumbnail,
|
||||
custom_headers = CustomHeaders,
|
||||
docroot = DocRoot} = State) ->
|
||||
case get_slot(Slot, State) of
|
||||
{ok, {Size, Timer}} ->
|
||||
timer:cancel(Timer),
|
||||
NewState = del_slot(Slot, State),
|
||||
Path = str:join([DocRoot | Slot], <<$/>>),
|
||||
{reply, {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail},
|
||||
{reply,
|
||||
{ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders},
|
||||
NewState};
|
||||
{ok, {_WrongSize, _Timer}} ->
|
||||
{reply, {error, size_mismatch}, State};
|
||||
error ->
|
||||
{reply, {error, invalid_slot}, State}
|
||||
end;
|
||||
handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) ->
|
||||
{reply, {ok, DocRoot}, State};
|
||||
handle_call(get_conf, _From,
|
||||
#state{docroot = DocRoot,
|
||||
custom_headers = CustomHeaders} = State) ->
|
||||
{reply, {ok, DocRoot, CustomHeaders}, State};
|
||||
handle_call(Request, From, State) ->
|
||||
?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]),
|
||||
{noreply, State}.
|
||||
@@ -353,44 +363,44 @@ process(LocalPath, #request{method = Method, host = Host, ip = IP})
|
||||
Method == 'HEAD' ->
|
||||
?DEBUG("Rejecting ~s request from ~s for ~s: Too few path components",
|
||||
[Method, ?ADDR_TO_STR(IP), Host]),
|
||||
http_response(Host, 404);
|
||||
http_response(404);
|
||||
process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP,
|
||||
data = Data} = Request) ->
|
||||
{Proc, Slot} = parse_http_request(Request),
|
||||
case catch gen_server:call(Proc, {use_slot, Slot, byte_size(Data)}) of
|
||||
{ok, Path, FileMode, DirMode, GetPrefix, Thumbnail} ->
|
||||
{ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} ->
|
||||
?DEBUG("Storing file from ~s for ~s: ~s",
|
||||
[?ADDR_TO_STR(IP), Host, Path]),
|
||||
case store_file(Path, Data, FileMode, DirMode,
|
||||
GetPrefix, Slot, Thumbnail) of
|
||||
ok ->
|
||||
http_response(Host, 201);
|
||||
http_response(201, CustomHeaders);
|
||||
{ok, Headers, OutData} ->
|
||||
http_response(Host, 201, Headers, OutData);
|
||||
http_response(201, Headers ++ CustomHeaders, OutData);
|
||||
{error, Error} ->
|
||||
?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p",
|
||||
[Path, ?ADDR_TO_STR(IP), Host, ?FORMAT(Error)]),
|
||||
http_response(Host, 500)
|
||||
http_response(500)
|
||||
end;
|
||||
{error, size_mismatch} ->
|
||||
?INFO_MSG("Rejecting file from ~s for ~s: Unexpected size (~B)",
|
||||
[?ADDR_TO_STR(IP), Host, byte_size(Data)]),
|
||||
http_response(Host, 413);
|
||||
http_response(413);
|
||||
{error, invalid_slot} ->
|
||||
?INFO_MSG("Rejecting file from ~s for ~s: Invalid slot",
|
||||
[?ADDR_TO_STR(IP), Host]),
|
||||
http_response(Host, 403);
|
||||
http_response(403);
|
||||
Error ->
|
||||
?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p",
|
||||
[?ADDR_TO_STR(IP), Host, Error]),
|
||||
http_response(Host, 500)
|
||||
http_response(500)
|
||||
end;
|
||||
process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request)
|
||||
when Method == 'GET';
|
||||
Method == 'HEAD' ->
|
||||
{Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request),
|
||||
case catch gen_server:call(Proc, get_docroot) of
|
||||
{ok, DocRoot} ->
|
||||
case catch gen_server:call(Proc, get_conf) of
|
||||
{ok, DocRoot, CustomHeaders} ->
|
||||
Path = str:join([DocRoot | Slot], <<$/>>),
|
||||
case file:read_file(Path) of
|
||||
{ok, Data} ->
|
||||
@@ -405,37 +415,47 @@ process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request)
|
||||
$", FileName/binary, $">>}]
|
||||
end,
|
||||
Headers2 = [{<<"Content-Type">>, ContentType} | Headers1],
|
||||
http_response(Host, 200, Headers2, Data);
|
||||
Headers3 = Headers2 ++ CustomHeaders,
|
||||
http_response(200, Headers3, Data);
|
||||
{error, eacces} ->
|
||||
?INFO_MSG("Cannot serve ~s to ~s: Permission denied",
|
||||
[Path, ?ADDR_TO_STR(IP)]),
|
||||
http_response(Host, 403);
|
||||
http_response(403);
|
||||
{error, enoent} ->
|
||||
?INFO_MSG("Cannot serve ~s to ~s: No such file",
|
||||
[Path, ?ADDR_TO_STR(IP)]),
|
||||
http_response(Host, 404);
|
||||
http_response(404);
|
||||
{error, eisdir} ->
|
||||
?INFO_MSG("Cannot serve ~s to ~s: Is a directory",
|
||||
[Path, ?ADDR_TO_STR(IP)]),
|
||||
http_response(Host, 404);
|
||||
http_response(404);
|
||||
{error, Error} ->
|
||||
?INFO_MSG("Cannot serve ~s to ~s: ~s",
|
||||
[Path, ?ADDR_TO_STR(IP), ?FORMAT(Error)]),
|
||||
http_response(Host, 500)
|
||||
http_response(500)
|
||||
end;
|
||||
Error ->
|
||||
?ERROR_MSG("Cannot handle ~s request from ~s for ~s: ~p",
|
||||
[Method, ?ADDR_TO_STR(IP), Host, Error]),
|
||||
http_response(Host, 500)
|
||||
http_response(500)
|
||||
end;
|
||||
process(_LocalPath, #request{method = 'OPTIONS', host = Host, ip = IP}) ->
|
||||
process(_LocalPath, #request{method = 'OPTIONS', host = Host,
|
||||
ip = IP} = Request) ->
|
||||
?DEBUG("Responding to OPTIONS request from ~s for ~s",
|
||||
[?ADDR_TO_STR(IP), Host]),
|
||||
http_response(Host, 200);
|
||||
{Proc, _Slot} = parse_http_request(Request),
|
||||
case catch gen_server:call(Proc, get_conf) of
|
||||
{ok, _DocRoot, CustomHeaders} ->
|
||||
http_response(200, CustomHeaders);
|
||||
Error ->
|
||||
?ERROR_MSG("Cannot handle OPTIONS request from ~s for ~s: ~p",
|
||||
[?ADDR_TO_STR(IP), Host, Error]),
|
||||
http_response(500)
|
||||
end;
|
||||
process(_LocalPath, #request{method = Method, host = Host, ip = IP}) ->
|
||||
?DEBUG("Rejecting ~s request from ~s for ~s",
|
||||
[Method, ?ADDR_TO_STR(IP), Host]),
|
||||
http_response(Host, 405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]).
|
||||
http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Exported utility functions.
|
||||
@@ -522,7 +542,7 @@ process_slot_request(#iq{lang = Lang, from = From} = IQ,
|
||||
deny ->
|
||||
?DEBUG("Denying HTTP upload slot request from ~s",
|
||||
[jid:encode(From)]),
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
|
||||
end.
|
||||
|
||||
@@ -658,7 +678,7 @@ make_rand_string(S, N) -> make_rand_string([make_rand_char() | S], N - 1).
|
||||
-spec make_rand_char() -> char().
|
||||
|
||||
make_rand_char() ->
|
||||
map_int_to_char(crypto:rand_uniform(0, 62)).
|
||||
map_int_to_char(randoms:uniform(0, 61)).
|
||||
|
||||
-spec map_int_to_char(0..61) -> char().
|
||||
|
||||
@@ -726,15 +746,15 @@ parse_http_request(#request{host = Host, path = Path}) ->
|
||||
store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) ->
|
||||
case do_store_file(Path, Data, FileMode, DirMode) of
|
||||
ok when Thumbnail ->
|
||||
case identify(Path) of
|
||||
case identify(Path, Data) of
|
||||
{ok, MediaInfo} ->
|
||||
case convert(Path, MediaInfo) of
|
||||
{ok, OutPath} ->
|
||||
case convert(Path, Data, MediaInfo) of
|
||||
{ok, OutPath, OutMediaInfo} ->
|
||||
[UserDir, RandDir | _] = Slot,
|
||||
FileName = filename:basename(OutPath),
|
||||
URL = str:join([GetPrefix, UserDir,
|
||||
RandDir, FileName], <<$/>>),
|
||||
ThumbEl = thumb_el(OutPath, URL),
|
||||
ThumbEl = thumb_el(OutMediaInfo, URL),
|
||||
{ok,
|
||||
[{<<"Content-Type">>,
|
||||
<<"text/xml; charset=utf-8">>}],
|
||||
@@ -790,30 +810,29 @@ guess_content_type(FileName) ->
|
||||
?DEFAULT_CONTENT_TYPE,
|
||||
?CONTENT_TYPES).
|
||||
|
||||
-spec http_response(binary(), 100..599)
|
||||
-spec http_response(100..599)
|
||||
-> {pos_integer(), [{binary(), binary()}], binary()}.
|
||||
|
||||
http_response(Host, Code) ->
|
||||
http_response(Host, Code, []).
|
||||
http_response(Code) ->
|
||||
http_response(Code, []).
|
||||
|
||||
-spec http_response(binary(), 100..599, [{binary(), binary()}])
|
||||
-spec http_response(100..599, [{binary(), binary()}])
|
||||
-> {pos_integer(), [{binary(), binary()}], binary()}.
|
||||
|
||||
http_response(Host, Code, ExtraHeaders) ->
|
||||
http_response(Code, ExtraHeaders) ->
|
||||
Message = <<(code_to_message(Code))/binary, $\n>>,
|
||||
http_response(Host, Code, ExtraHeaders, Message).
|
||||
http_response(Code, ExtraHeaders, Message).
|
||||
|
||||
-spec http_response(binary(), 100..599, [{binary(), binary()}], binary())
|
||||
-spec http_response(100..599, [{binary(), binary()}], binary())
|
||||
-> {pos_integer(), [{binary(), binary()}], binary()}.
|
||||
|
||||
http_response(Host, Code, ExtraHeaders, Body) ->
|
||||
CustomHeaders = gen_mod:get_module_opt(Host, ?MODULE, custom_headers, []),
|
||||
http_response(Code, ExtraHeaders, Body) ->
|
||||
Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of
|
||||
true ->
|
||||
ExtraHeaders;
|
||||
false ->
|
||||
[{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders]
|
||||
end ++ CustomHeaders,
|
||||
end,
|
||||
{Code, Headers, Body}.
|
||||
|
||||
-spec code_to_message(100..599) -> binary().
|
||||
@@ -830,59 +849,68 @@ code_to_message(_Code) -> <<"">>.
|
||||
%% Image manipulation stuff.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec identify(binary()) -> {ok, media_info()} | pass.
|
||||
-spec identify(binary(), binary()) -> {ok, media_info()} | pass.
|
||||
|
||||
identify(Path) ->
|
||||
Cmd = io_lib:format("identify -format 'ok %m %h %w' ~s", [Path]),
|
||||
Res = string:strip(os:cmd(Cmd), right, $\n),
|
||||
case string:tokens(Res, " ") of
|
||||
["ok", T, H, W] ->
|
||||
{ok, #media_info{type = list_to_binary(string:to_lower(T)),
|
||||
height = list_to_integer(H),
|
||||
width = list_to_integer(W)}};
|
||||
_ ->
|
||||
?DEBUG("Cannot identify type of ~s: ~s", [Path, Res]),
|
||||
identify(Path, Data) ->
|
||||
case misc:have_eimp() of
|
||||
true ->
|
||||
case eimp:identify(Data) of
|
||||
{ok, Info} ->
|
||||
{ok, #media_info{
|
||||
type = proplists:get_value(type, Info),
|
||||
width = proplists:get_value(width, Info),
|
||||
height = proplists:get_value(height, Info)}};
|
||||
{error, Why} ->
|
||||
?DEBUG("Cannot identify type of ~s: ~s",
|
||||
[Path, eimp:format_error(Why)]),
|
||||
pass
|
||||
end;
|
||||
false ->
|
||||
pass
|
||||
end.
|
||||
|
||||
-spec convert(binary(), media_info()) -> {ok, binary()} | pass.
|
||||
-spec convert(binary(), binary(), media_info()) -> {ok, binary(), media_info()} | pass.
|
||||
|
||||
convert(Path, #media_info{type = T, width = W, height = H}) ->
|
||||
convert(Path, Data, #media_info{type = T, width = W, height = H} = Info) ->
|
||||
if W * H >= 25000000 ->
|
||||
?DEBUG("The image ~s is more than 25 Mpix", [Path]),
|
||||
pass;
|
||||
W =< 300, H =< 300 ->
|
||||
{ok, Path};
|
||||
T == <<"gif">>; T == <<"jpeg">>; T == <<"png">>; T == <<"webp">> ->
|
||||
Dir = filename:dirname(Path),
|
||||
FileName = <<(randoms:get_string())/binary, $., T/binary>>,
|
||||
OutPath = filename:join(Dir, FileName),
|
||||
Cmd = io_lib:format("convert -resize 300 ~s ~s", [Path, OutPath]),
|
||||
case os:cmd(Cmd) of
|
||||
"" ->
|
||||
{ok, OutPath};
|
||||
Err ->
|
||||
?ERROR_MSG("Failed to convert ~s to ~s: ~s",
|
||||
[Path, OutPath, string:strip(Err, right, $\n)]),
|
||||
pass
|
||||
end;
|
||||
{ok, Path, Info};
|
||||
true ->
|
||||
?DEBUG("Won't call 'convert' for unknown type ~s", [T]),
|
||||
pass
|
||||
Dir = filename:dirname(Path),
|
||||
Ext = atom_to_binary(T, latin1),
|
||||
FileName = <<(randoms:get_string())/binary, $., Ext/binary>>,
|
||||
OutPath = filename:join(Dir, FileName),
|
||||
{W1, H1} = if W > H -> {300, round(H*300/W)};
|
||||
H > W -> {round(W*300/H), 300};
|
||||
true -> {300, 300}
|
||||
end,
|
||||
OutInfo = #media_info{type = T, width = W1, height = H1},
|
||||
case eimp:convert(Data, T, [{scale, {W1, H1}}]) of
|
||||
{ok, OutData} ->
|
||||
case file:write_file(OutPath, OutData) of
|
||||
ok ->
|
||||
{ok, OutPath, OutInfo};
|
||||
{error, Why} ->
|
||||
?ERROR_MSG("Failed to write to ~s: ~s",
|
||||
[OutPath, file:format_error(Why)]),
|
||||
pass
|
||||
end;
|
||||
{error, Why} ->
|
||||
?ERROR_MSG("Failed to convert ~s to ~s: ~s",
|
||||
[Path, OutPath, eimp:format_error(Why)]),
|
||||
pass
|
||||
end
|
||||
end.
|
||||
|
||||
-spec thumb_el(binary(), binary()) -> xmlel().
|
||||
-spec thumb_el(media_info(), binary()) -> xmlel().
|
||||
|
||||
thumb_el(Path, URI) ->
|
||||
ContentType = guess_content_type(Path),
|
||||
xmpp:encode(
|
||||
case identify(Path) of
|
||||
{ok, #media_info{height = H, width = W}} ->
|
||||
#thumbnail{'media-type' = ContentType, uri = URI,
|
||||
height = H, width = W};
|
||||
pass ->
|
||||
#thumbnail{uri = URI, 'media-type' = ContentType}
|
||||
end).
|
||||
thumb_el(#media_info{type = T, height = H, width = W}, URI) ->
|
||||
MimeType = <<"image/", (atom_to_binary(T, latin1))/binary>>,
|
||||
Thumb = #thumbnail{'media-type' = MimeType, uri = URI,
|
||||
height = H, width = W},
|
||||
xmpp:encode(Thumb).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Remove user.
|
||||
|
||||
+1
-1
@@ -262,7 +262,7 @@ do_route(Host, ServerHost, Access, Packet) ->
|
||||
end;
|
||||
deny ->
|
||||
Lang = xmpp:get_lang(Packet),
|
||||
Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
|
||||
Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang),
|
||||
ejabberd_router:route_error(Packet, Err)
|
||||
end.
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ authenticate(#{stream_id := StreamID, server := Server,
|
||||
Err = xmpp:make_error(IQ, xmpp:err_jid_malformed()),
|
||||
process_auth_failure(State, U, Err, 'jid-malformed');
|
||||
false ->
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
Err = xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)),
|
||||
process_auth_failure(State, U, Err, 'forbidden')
|
||||
end.
|
||||
|
||||
+14
-2
@@ -863,8 +863,13 @@ select(_LServer, JidRequestor, JidArchive, Query, RSM,
|
||||
{Msgs, true, L}
|
||||
end;
|
||||
select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) ->
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType).
|
||||
case might_expose_jid(Query, MsgType) of
|
||||
true ->
|
||||
{[], true, 0};
|
||||
false ->
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType)
|
||||
end.
|
||||
|
||||
msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick,
|
||||
peer = Peer, id = ID},
|
||||
@@ -988,6 +993,13 @@ match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> ->
|
||||
match_rsm(_Now, _) ->
|
||||
true.
|
||||
|
||||
might_expose_jid(Query,
|
||||
{groupchat, Role, #state{config = #config{anonymous = true}}})
|
||||
when Role /= moderator ->
|
||||
proplists:is_defined(with, Query);
|
||||
might_expose_jid(_Query, _MsgType) ->
|
||||
false.
|
||||
|
||||
get_jids(undefined) ->
|
||||
[];
|
||||
get_jids(Js) ->
|
||||
|
||||
+4
-3
@@ -61,6 +61,7 @@
|
||||
count_online_rooms/1,
|
||||
register_online_user/4,
|
||||
unregister_online_user/4,
|
||||
iq_set_register_info/5,
|
||||
count_online_rooms_by_user/3,
|
||||
get_online_rooms_by_user/3,
|
||||
can_use_nick/4]).
|
||||
@@ -680,7 +681,7 @@ iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM
|
||||
|
||||
-spec get_room_disco_item({binary(), binary(), pid()},
|
||||
term()) -> {ok, disco_item()} |
|
||||
{error, timeout | notfound}.
|
||||
{error, timeout | notfound}.
|
||||
get_room_disco_item({Name, Host, Pid}, Query) ->
|
||||
RoomJID = jid:make(Name, Host),
|
||||
try p1_fsm:sync_send_all_state_event(Pid, Query, 100) of
|
||||
@@ -688,9 +689,9 @@ get_room_disco_item({Name, Host, Pid}, Query) ->
|
||||
{ok, #disco_item{jid = RoomJID, name = Desc}};
|
||||
false ->
|
||||
{error, notfound}
|
||||
catch _:{timeout, _} ->
|
||||
catch _:{timeout, {p1_fsm, _, _}} ->
|
||||
{error, timeout};
|
||||
_:{noproc, _} ->
|
||||
_:{_, {p1_fsm, _, _}} ->
|
||||
{error, notfound}
|
||||
end.
|
||||
|
||||
|
||||
+25
-33
@@ -29,7 +29,7 @@
|
||||
-behaviour(gen_mod).
|
||||
|
||||
-export([start/2, stop/1, reload/3, depends/2, muc_online_rooms/1,
|
||||
muc_register_nick/3, muc_unregister_nick/1,
|
||||
muc_register_nick/3, muc_unregister_nick/2,
|
||||
create_room_with_opts/4, create_room/3, destroy_room/2,
|
||||
create_rooms_file/1, destroy_rooms_file/1,
|
||||
rooms_unused_list/2, rooms_unused_destroy/2,
|
||||
@@ -91,16 +91,18 @@ get_commands_spec() ->
|
||||
args = [{host, binary}],
|
||||
result = {rooms, {list, {room, string}}}},
|
||||
#ejabberd_commands{name = muc_register_nick, tags = [muc],
|
||||
desc = "Register a nick in the MUC service",
|
||||
desc = "Register a nick to a User JID in the MUC service of a server",
|
||||
module = ?MODULE, function = muc_register_nick,
|
||||
args_desc = ["Nick", "User JID", "MUC service"],
|
||||
args_example = [<<"Tim">>, <<"tim@example.org">>, <<"muc.example.org">>],
|
||||
args = [{nick, binary}, {jid, binary}, {domain, binary}],
|
||||
args_desc = ["Nick", "User JID", "Server Host"],
|
||||
args_example = [<<"Tim">>, <<"tim@example.org">>, <<"example.org">>],
|
||||
args = [{nick, binary}, {jid, binary}, {serverhost, binary}],
|
||||
result = {res, rescode}},
|
||||
#ejabberd_commands{name = muc_unregister_nick, tags = [muc],
|
||||
desc = "Unregister the nick in the MUC service",
|
||||
desc = "Unregister the nick registered by that account in the MUC service",
|
||||
module = ?MODULE, function = muc_unregister_nick,
|
||||
args = [{nick, binary}],
|
||||
args_desc = ["User JID", "MUC service"],
|
||||
args_example = [<<"tim@example.org">>, <<"example.org">>],
|
||||
args = [{jid, binary}, {serverhost, binary}],
|
||||
result = {res, rescode}},
|
||||
|
||||
#ejabberd_commands{name = create_room, tags = [muc_room],
|
||||
@@ -305,31 +307,14 @@ muc_online_rooms(ServerHost) ->
|
||||
|| {Name, _, _} <- mod_muc:get_online_rooms(Host)]
|
||||
end, Hosts).
|
||||
|
||||
muc_register_nick(Nick, JIDBinary, Domain) ->
|
||||
try jid:decode(JIDBinary) of
|
||||
JID ->
|
||||
F = fun (MHost, MNick) ->
|
||||
mnesia:write(#muc_registered{us_host=MHost, nick=MNick})
|
||||
end,
|
||||
case mnesia:transaction(F, [{{JID#jid.luser, JID#jid.lserver},
|
||||
Domain}, Nick]) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, _Error} -> error
|
||||
end
|
||||
catch _:{bad_jid, _} -> throw({error, "Malformed JID"})
|
||||
end.
|
||||
muc_register_nick(Nick, FromBinary, ServerHost) ->
|
||||
Host = find_host(ServerHost),
|
||||
From = jid:decode(FromBinary),
|
||||
Lang = <<"en">>,
|
||||
mod_muc:iq_set_register_info(ServerHost, Host, From, Nick, Lang).
|
||||
|
||||
muc_unregister_nick(Nick) ->
|
||||
F2 = fun(N) ->
|
||||
[{_,Key,_}|_] = mnesia:index_read(muc_registered, N, 3),
|
||||
mnesia:delete({muc_registered, Key})
|
||||
end,
|
||||
case mnesia:transaction(F2, [Nick], 1) of
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{aborted, _Error} ->
|
||||
error
|
||||
end.
|
||||
muc_unregister_nick(FromBinary, ServerHost) ->
|
||||
muc_register_nick(<<"">>, FromBinary, ServerHost).
|
||||
|
||||
get_user_rooms(LUser, LServer) ->
|
||||
lists:flatmap(
|
||||
@@ -536,7 +521,8 @@ prepare_room_info(Room_info) ->
|
||||
%% ok | error
|
||||
%% @doc Create a room immediately with the default options.
|
||||
create_room(Name1, Host1, ServerHost) ->
|
||||
create_room_with_opts(Name1, Host1, ServerHost, []).
|
||||
create_room_with_opts(Name1, Host1, ServerHost, []),
|
||||
change_room_option(Name1, Host1, <<"persistent">>, <<"true">>).
|
||||
|
||||
create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) ->
|
||||
true = (error /= (Name = jid:nodeprep(Name1))),
|
||||
@@ -816,7 +802,13 @@ get_room_occupants(Pid) ->
|
||||
dict:to_list(S#state.users)).
|
||||
|
||||
get_room_occupants_number(Room, Host) ->
|
||||
length(get_room_occupants(Room, Host)).
|
||||
case get_room_pid(Room, Host) of
|
||||
room_not_found ->
|
||||
throw({error, room_not_found});
|
||||
Pid ->
|
||||
S = get_room_state(Pid),
|
||||
dict:size(S#state.users)
|
||||
end.
|
||||
|
||||
%%----------------------------
|
||||
%% Send Direct Invitation
|
||||
|
||||
+45
-46
@@ -974,10 +974,9 @@ roomconfig_to_string(Options, Lang, FileFormat) ->
|
||||
Os2 = lists:sort(Os1),
|
||||
Options2 = Title ++ Os2,
|
||||
lists:foldl(fun ({Opt, Val}, R) ->
|
||||
case get_roomconfig_text(Opt) of
|
||||
case get_roomconfig_text(Opt, Lang) of
|
||||
undefined -> R;
|
||||
OptT ->
|
||||
OptText = (?T(OptT)),
|
||||
OptText ->
|
||||
R2 = case Val of
|
||||
false ->
|
||||
<<"<div class=\"rcod\">",
|
||||
@@ -1025,49 +1024,49 @@ roomconfig_to_string(Options, Lang, FileFormat) ->
|
||||
end,
|
||||
<<"">>, Options2).
|
||||
|
||||
get_roomconfig_text(title) -> <<"Room title">>;
|
||||
get_roomconfig_text(persistent) ->
|
||||
<<"Make room persistent">>;
|
||||
get_roomconfig_text(public) ->
|
||||
<<"Make room public searchable">>;
|
||||
get_roomconfig_text(public_list) ->
|
||||
<<"Make participants list public">>;
|
||||
get_roomconfig_text(password_protected) ->
|
||||
<<"Make room password protected">>;
|
||||
get_roomconfig_text(password) -> <<"Password">>;
|
||||
get_roomconfig_text(anonymous) ->
|
||||
<<"This room is not anonymous">>;
|
||||
get_roomconfig_text(members_only) ->
|
||||
<<"Make room members-only">>;
|
||||
get_roomconfig_text(moderated) ->
|
||||
<<"Make room moderated">>;
|
||||
get_roomconfig_text(members_by_default) ->
|
||||
<<"Default users as participants">>;
|
||||
get_roomconfig_text(allow_change_subj) ->
|
||||
<<"Allow users to change the subject">>;
|
||||
get_roomconfig_text(allow_private_messages) ->
|
||||
<<"Allow users to send private messages">>;
|
||||
get_roomconfig_text(allow_private_messages_from_visitors) ->
|
||||
<<"Allow visitors to send private messages to">>;
|
||||
get_roomconfig_text(allow_query_users) ->
|
||||
<<"Allow users to query other users">>;
|
||||
get_roomconfig_text(allow_user_invites) ->
|
||||
<<"Allow users to send invites">>;
|
||||
get_roomconfig_text(logging) -> <<"Enable logging">>;
|
||||
get_roomconfig_text(allow_visitor_nickchange) ->
|
||||
<<"Allow visitors to change nickname">>;
|
||||
get_roomconfig_text(allow_visitor_status) ->
|
||||
<<"Allow visitors to send status text in "
|
||||
"presence updates">>;
|
||||
get_roomconfig_text(captcha_protected) ->
|
||||
<<"Make room captcha protected">>;
|
||||
get_roomconfig_text(description) ->
|
||||
<<"Room description">>;
|
||||
%% get_roomconfig_text(subject) -> "Subject";
|
||||
%% get_roomconfig_text(subject_author) -> "Subject author";
|
||||
get_roomconfig_text(max_users) ->
|
||||
<<"Maximum Number of Occupants">>;
|
||||
get_roomconfig_text(_) -> undefined.
|
||||
get_roomconfig_text(title, Lang) -> ?T(<<"Room title">>);
|
||||
get_roomconfig_text(persistent, Lang) ->
|
||||
?T(<<"Make room persistent">>);
|
||||
get_roomconfig_text(public, Lang) ->
|
||||
?T(<<"Make room public searchable">>);
|
||||
get_roomconfig_text(public_list, Lang) ->
|
||||
?T(<<"Make participants list public">>);
|
||||
get_roomconfig_text(password_protected, Lang) ->
|
||||
?T(<<"Make room password protected">>);
|
||||
get_roomconfig_text(password, Lang) -> ?T(<<"Password">>);
|
||||
get_roomconfig_text(anonymous, Lang) ->
|
||||
?T(<<"This room is not anonymous">>);
|
||||
get_roomconfig_text(members_only, Lang) ->
|
||||
?T(<<"Make room members-only">>);
|
||||
get_roomconfig_text(moderated, Lang) ->
|
||||
?T(<<"Make room moderated">>);
|
||||
get_roomconfig_text(members_by_default, Lang) ->
|
||||
?T(<<"Default users as participants">>);
|
||||
get_roomconfig_text(allow_change_subj, Lang) ->
|
||||
?T(<<"Allow users to change the subject">>);
|
||||
get_roomconfig_text(allow_private_messages, Lang) ->
|
||||
?T(<<"Allow users to send private messages">>);
|
||||
get_roomconfig_text(allow_private_messages_from_visitors, Lang) ->
|
||||
?T(<<"Allow visitors to send private messages to">>);
|
||||
get_roomconfig_text(allow_query_users, Lang) ->
|
||||
?T(<<"Allow users to query other users">>);
|
||||
get_roomconfig_text(allow_user_invites, Lang) ->
|
||||
?T(<<"Allow users to send invites">>);
|
||||
get_roomconfig_text(logging, Lang) -> ?T(<<"Enable logging">>);
|
||||
get_roomconfig_text(allow_visitor_nickchange, Lang) ->
|
||||
?T(<<"Allow visitors to change nickname">>);
|
||||
get_roomconfig_text(allow_visitor_status, Lang) ->
|
||||
?T(<<"Allow visitors to send status text in "
|
||||
"presence updates">>);
|
||||
get_roomconfig_text(captcha_protected, Lang) ->
|
||||
?T(<<"Make room CAPTCHA protected">>);
|
||||
get_roomconfig_text(description, Lang) ->
|
||||
?T(<<"Room description">>);
|
||||
%% get_roomconfig_text(subject, Lang) -> "Subject";
|
||||
%% get_roomconfig_text(subject_author, Lang) -> "Subject author";
|
||||
get_roomconfig_text(max_users, Lang) ->
|
||||
?T(<<"Maximum Number of Occupants">>);
|
||||
get_roomconfig_text(_, _) -> undefined.
|
||||
|
||||
%% Users = [{JID, Nick, Role}]
|
||||
roomoccupants_to_string(Users, _FileFormat) ->
|
||||
|
||||
+19
-6
@@ -492,7 +492,7 @@ handle_event({destroy, Reason}, _StateName,
|
||||
handle_event(destroy, StateName, StateData) ->
|
||||
?INFO_MSG("Destroyed MUC room ~s",
|
||||
[jid:encode(StateData#state.jid)]),
|
||||
handle_event({destroy, undefined}, StateName, StateData);
|
||||
handle_event({destroy, <<"">>}, StateName, StateData);
|
||||
handle_event({set_affiliations, Affiliations},
|
||||
StateName, StateData) ->
|
||||
{next_state, StateName,
|
||||
@@ -1028,8 +1028,16 @@ do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet,
|
||||
end,
|
||||
NewState = add_user_presence_un(From, NewPacket, StateData),
|
||||
case (?DICT):find(Nick, StateData#state.nicks) of
|
||||
{ok, [_, _ | _]} -> ok;
|
||||
_ -> send_new_presence(From, NewState, StateData)
|
||||
{ok, [_, _ | _]} ->
|
||||
Aff = get_affiliation(From, StateData),
|
||||
Item = #muc_item{affiliation = Aff, role = none, jid = From},
|
||||
Pres = xmpp:set_subtag(
|
||||
Packet, #muc_user{items = [Item],
|
||||
status_codes = [110]}),
|
||||
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
|
||||
From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData);
|
||||
_ ->
|
||||
send_new_presence(From, NewState, StateData)
|
||||
end,
|
||||
Reason = xmpp:get_text(NewPacket#presence.status),
|
||||
remove_online_user(From, NewState, Reason);
|
||||
@@ -1243,7 +1251,12 @@ expulse_participant(Packet, From, StateData, Reason1) ->
|
||||
#presence{type = unavailable,
|
||||
status = xmpp:mk_text(Reason2)},
|
||||
StateData),
|
||||
send_new_presence(From, NewState, StateData),
|
||||
LJID = jid:tolower(From),
|
||||
{ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users),
|
||||
case (?DICT):find(Nick, StateData#state.nicks) of
|
||||
{ok, [_, _ | _]} -> ok;
|
||||
_ -> send_new_presence(From, NewState, StateData)
|
||||
end,
|
||||
remove_online_user(From, NewState).
|
||||
|
||||
-spec set_affiliation(jid(), affiliation(), state()) -> state().
|
||||
@@ -2697,8 +2710,8 @@ find_changed_items(UJID, UAffiliation, URole,
|
||||
Nick /= <<"">> ->
|
||||
case find_jids_by_nick(Nick, StateData) of
|
||||
[] ->
|
||||
ErrText = str:format(<<"Nickname ~s does not exist in the room">>,
|
||||
[Nick]),
|
||||
ErrText = {<<"Nickname ~s does not exist in the room">>,
|
||||
[Nick]},
|
||||
throw({error, xmpp:err_not_acceptable(ErrText, Lang)});
|
||||
JIDList ->
|
||||
JIDList
|
||||
|
||||
@@ -998,8 +998,10 @@ build_service_limit_record(LimitOpts) ->
|
||||
build_limit_record(LimitOptsR, remote)}.
|
||||
|
||||
get_from_limitopts(LimitOpts, SenderT) ->
|
||||
{SenderT, Result} = lists:keyfind(SenderT, 1, LimitOpts),
|
||||
Result.
|
||||
case lists:keyfind(SenderT, 1, LimitOpts) of
|
||||
false -> [];
|
||||
{SenderT, Result} -> Result
|
||||
end.
|
||||
|
||||
build_remote_limit_record(LimitOpts, SenderT) ->
|
||||
build_limit_record(LimitOpts, SenderT).
|
||||
@@ -1120,10 +1122,10 @@ mod_opt_type(host) -> fun iolist_to_binary/1;
|
||||
mod_opt_type({limits, Type}) when (Type == local) or (Type == remote) ->
|
||||
fun(L) ->
|
||||
lists:map(
|
||||
fun ({message, infinite}) -> infinite;
|
||||
({presence, infinite}) -> infinite;
|
||||
({message, I}) when is_integer(I) -> I;
|
||||
({presence, I}) when is_integer(I) -> I
|
||||
fun ({message, infinite} = O) -> O;
|
||||
({presence, infinite} = O) -> O;
|
||||
({message, I} = O) when is_integer(I) -> O;
|
||||
({presence, I} = O) when is_integer(I) -> O
|
||||
end, L)
|
||||
end;
|
||||
mod_opt_type(_) -> [access, host, {limits, local}, {limits, remote}].
|
||||
|
||||
+15
-12
@@ -706,22 +706,25 @@ user_queue_parse_query(LUser, LServer, Query) ->
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
case lists:keysearch(<<"delete">>, 1, Query) of
|
||||
{value, _} ->
|
||||
case lists:keyfind(<<"selected">>, 1, Query) of
|
||||
{_, Seq} ->
|
||||
case catch binary_to_integer(Seq) of
|
||||
I when is_integer(I), I>=0 ->
|
||||
Mod:remove_message(LUser, LServer, I),
|
||||
ok;
|
||||
_ ->
|
||||
nothing
|
||||
end;
|
||||
false ->
|
||||
nothing
|
||||
end;
|
||||
user_queue_parse_query(LUser, LServer, Query, Mod);
|
||||
_ ->
|
||||
nothing
|
||||
end.
|
||||
|
||||
user_queue_parse_query(LUser, LServer, Query, Mod) ->
|
||||
case lists:keytake(<<"selected">>, 1, Query) of
|
||||
{value, {_, Seq}, Query2} ->
|
||||
case catch binary_to_integer(Seq) of
|
||||
I when is_integer(I), I>=0 ->
|
||||
Mod:remove_message(LUser, LServer, I);
|
||||
_ ->
|
||||
nothing
|
||||
end,
|
||||
user_queue_parse_query(LUser, LServer, Query2, Mod);
|
||||
false ->
|
||||
nothing
|
||||
end.
|
||||
|
||||
us_to_list({User, Server}) ->
|
||||
jid:encode({User, Server, <<"">>}).
|
||||
|
||||
|
||||
+32
-20
@@ -638,25 +638,29 @@ is_ptype_match(Item, PType) ->
|
||||
ljid(), none | both | from | to, [binary()]) -> boolean().
|
||||
is_type_match(none, _Value, _JID, _Subscription, _Groups) ->
|
||||
true;
|
||||
is_type_match(Type, Value, JID, Subscription, Groups) ->
|
||||
case Type of
|
||||
jid ->
|
||||
case Value of
|
||||
{<<"">>, Server, <<"">>} ->
|
||||
case JID of
|
||||
{_, Server, _} -> true;
|
||||
_ -> false
|
||||
end;
|
||||
{User, Server, <<"">>} ->
|
||||
case JID of
|
||||
{User, Server, _} -> true;
|
||||
_ -> false
|
||||
end;
|
||||
_ -> Value == JID
|
||||
end;
|
||||
subscription -> Value == Subscription;
|
||||
group -> lists:member(Value, Groups)
|
||||
end.
|
||||
is_type_match(jid, Value, JID, _Subscription, _Groups) ->
|
||||
case Value of
|
||||
{<<"">>, Server, <<"">>} ->
|
||||
case JID of
|
||||
{_, Server, _} -> true;
|
||||
_ -> false
|
||||
end;
|
||||
{User, Server, <<"">>} ->
|
||||
case JID of
|
||||
{User, Server, _} -> true;
|
||||
_ -> false
|
||||
end;
|
||||
{<<"">>, Server, Resource} ->
|
||||
case JID of
|
||||
{_, Server, Resource} -> true;
|
||||
_ -> false
|
||||
end;
|
||||
_ -> Value == JID
|
||||
end;
|
||||
is_type_match(subscription, Value, _JID, Subscription, _Groups) ->
|
||||
Value == Subscription;
|
||||
is_type_match(group, Group, _JID, _Subscription, Groups) ->
|
||||
lists:member(Group, Groups).
|
||||
|
||||
-spec remove_user(binary(), binary()) -> ok.
|
||||
remove_user(User, Server) ->
|
||||
@@ -842,4 +846,12 @@ depends(_Host, _Opts) ->
|
||||
|
||||
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
|
||||
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
|
||||
mod_opt_type(_) -> [db_type, iqdisc].
|
||||
mod_opt_type(O) when O == cache_life_time; O == cache_size ->
|
||||
fun (I) when is_integer(I), I > 0 -> I;
|
||||
(infinity) -> infinity
|
||||
end;
|
||||
mod_opt_type(O) when O == use_cache; O == cache_missed ->
|
||||
fun (B) when is_boolean(B) -> B end;
|
||||
mod_opt_type(_) ->
|
||||
[db_type, iqdisc, cache_life_time, cache_size,
|
||||
use_cache, cache_missed].
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ set_data(LUser, LServer, Data) ->
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
case Mod:set_data(LUser, LServer, Data) of
|
||||
ok ->
|
||||
delete_cache(Mod, LServer, LServer, Data);
|
||||
delete_cache(Mod, LUser, LServer, Data);
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
@@ -183,7 +183,7 @@ process_bytestreams(#iq{type = get, from = JID, to = To, lang = Lang} = IQ) ->
|
||||
StreamHost = get_streamhost(Host, ServerHost),
|
||||
xmpp:make_iq_result(IQ, #bytestreams{hosts = [StreamHost]});
|
||||
deny ->
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(<<"Denied by ACL">>, Lang))
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang))
|
||||
end;
|
||||
process_bytestreams(#iq{type = set, lang = Lang,
|
||||
sub_els = [#bytestreams{sid = SID}]} = IQ)
|
||||
@@ -232,7 +232,7 @@ process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To,
|
||||
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
|
||||
end;
|
||||
deny ->
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
|
||||
end.
|
||||
|
||||
|
||||
+209
-268
@@ -52,7 +52,7 @@
|
||||
%% exports for hooks
|
||||
-export([presence_probe/3, caps_add/3, caps_update/3,
|
||||
in_subscription/6, out_subscription/4,
|
||||
on_user_offline/3, remove_user/2,
|
||||
on_user_online/1, on_user_offline/2, remove_user/2,
|
||||
disco_local_identity/5, disco_local_features/5,
|
||||
disco_local_items/5, disco_sm_identity/5,
|
||||
disco_sm_features/5, disco_sm_items/5,
|
||||
@@ -89,11 +89,7 @@
|
||||
%% API and gen_server callbacks
|
||||
-export([start/2, stop/1, init/1,
|
||||
handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3, depends/2, export/1]).
|
||||
|
||||
-export([send_loop/1, mod_opt_type/1]).
|
||||
|
||||
-define(LOOPNAME, ejabberd_mod_pubsub_loop).
|
||||
terminate/2, code_change/3, depends/2, export/1, mod_opt_type/1]).
|
||||
|
||||
%%====================================================================
|
||||
%% API
|
||||
@@ -263,10 +259,9 @@ init([ServerHost, Opts]) ->
|
||||
end,
|
||||
{Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts),
|
||||
DefaultModule = plugin(Host, hd(Plugins)),
|
||||
BaseOptions = DefaultModule:options(),
|
||||
DefaultNodeCfg = filter_node_options(
|
||||
DefaultNodeCfg = merge_config(
|
||||
gen_mod:get_opt(default_node_config, Opts, []),
|
||||
BaseOptions),
|
||||
DefaultModule:options()),
|
||||
lists:foreach(
|
||||
fun(H) ->
|
||||
T = gen_mod:get_module_proc(H, config),
|
||||
@@ -300,7 +295,9 @@ init([ServerHost, Opts]) ->
|
||||
?MODULE, process_commands, IQDisc),
|
||||
Plugins
|
||||
end, Hosts),
|
||||
ejabberd_hooks:add(sm_remove_connection_hook, ServerHost,
|
||||
ejabberd_hooks:add(c2s_session_opened, ServerHost,
|
||||
?MODULE, on_user_online, 75),
|
||||
ejabberd_hooks:add(c2s_terminated, ServerHost,
|
||||
?MODULE, on_user_offline, 75),
|
||||
ejabberd_hooks:add(disco_local_identity, ServerHost,
|
||||
?MODULE, disco_local_identity, 75),
|
||||
@@ -337,34 +334,16 @@ init([ServerHost, Opts]) ->
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
{_, State} = init_send_loop(ServerHost, Hosts),
|
||||
{ok, State}.
|
||||
|
||||
init_send_loop(ServerHost, Hosts) ->
|
||||
NodeTree = config(ServerHost, nodetree),
|
||||
Plugins = config(ServerHost, plugins),
|
||||
LastItemCache = config(ServerHost, last_item_cache),
|
||||
MaxItemsNode = config(ServerHost, max_items_node),
|
||||
PepMapping = config(ServerHost, pep_mapping),
|
||||
PepOffline = config(ServerHost, ignore_pep_from_offline),
|
||||
Access = config(ServerHost, access),
|
||||
DBType = gen_mod:db_type(ServerHost, ?MODULE),
|
||||
State = #state{hosts = Hosts, server_host = ServerHost,
|
||||
access = Access, pep_mapping = PepMapping,
|
||||
ignore_pep_from_offline = PepOffline,
|
||||
last_item_cache = LastItemCache,
|
||||
max_items_node = MaxItemsNode, nodetree = NodeTree,
|
||||
plugins = Plugins, db_type = DBType},
|
||||
Proc = gen_mod:get_module_proc(ServerHost, ?LOOPNAME),
|
||||
Pid = case whereis(Proc) of
|
||||
undefined ->
|
||||
SendLoop = spawn(?MODULE, send_loop, [State]),
|
||||
register(Proc, SendLoop),
|
||||
SendLoop;
|
||||
Loop ->
|
||||
Loop
|
||||
end,
|
||||
{Pid, State}.
|
||||
{ok, #state{hosts = Hosts, server_host = ServerHost,
|
||||
access = Access, pep_mapping = PepMapping,
|
||||
ignore_pep_from_offline = PepOffline,
|
||||
last_item_cache = LastItemCache,
|
||||
max_items_node = MaxItemsNode, nodetree = NodeTree,
|
||||
plugins = Plugins, db_type = DBType}}.
|
||||
|
||||
depends(ServerHost, Opts) ->
|
||||
Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>),
|
||||
@@ -414,94 +393,6 @@ terminate_plugins(Host, ServerHost, Plugins, TreePlugin) ->
|
||||
TreePlugin:terminate(Host, ServerHost),
|
||||
ok.
|
||||
|
||||
get_subscribed(User, Server) ->
|
||||
Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]),
|
||||
lists:filtermap(
|
||||
fun(#roster{jid = LJID, subscription = Sub})
|
||||
when Sub == both orelse Sub == from ->
|
||||
{true, LJID};
|
||||
(_) ->
|
||||
false
|
||||
end, Items).
|
||||
|
||||
send_loop(State) ->
|
||||
receive
|
||||
{presence, JID, _Pid} ->
|
||||
ServerHost = State#state.server_host,
|
||||
Host = host(State#state.server_host),
|
||||
DBType = State#state.db_type,
|
||||
LJID = jid:tolower(JID),
|
||||
BJID = jid:remove_resource(LJID),
|
||||
lists:foreach(
|
||||
fun(PType) ->
|
||||
Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID),
|
||||
lists:foreach(
|
||||
fun({NodeRec, _, _, SubJID}) ->
|
||||
{_, Node} = NodeRec#pubsub_node.nodeid,
|
||||
Nidx = NodeRec#pubsub_node.id,
|
||||
Options = NodeRec#pubsub_node.options,
|
||||
[send_items(Host, Node, Nidx, PType, Options, SubJID, last)
|
||||
|| NodeRec#pubsub_node.type == PType]
|
||||
end,
|
||||
lists:usort(Subs))
|
||||
end,
|
||||
State#state.plugins),
|
||||
if not State#state.ignore_pep_from_offline ->
|
||||
{User, Server, Resource} = LJID,
|
||||
Contacts = get_subscribed(User, Server),
|
||||
lists:foreach(
|
||||
fun({U, S, R}) when S == ServerHost ->
|
||||
case user_resources(U, S) of
|
||||
[] -> %% offline
|
||||
PeerJID = jid:make(U, S, R),
|
||||
self() ! {presence, User, Server, [Resource], PeerJID};
|
||||
_ -> %% online
|
||||
%% this is already handled by presence probe
|
||||
ok
|
||||
end;
|
||||
(_) ->
|
||||
%% we can not do anything in any cases
|
||||
ok
|
||||
end, Contacts);
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
send_loop(State);
|
||||
{presence, User, Server, Resources, JID} ->
|
||||
spawn(fun() ->
|
||||
Host = host(State#state.server_host),
|
||||
Owner = jid:remove_resource(jid:tolower(JID)),
|
||||
lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) ->
|
||||
case match_option(Options, send_last_published_item, on_sub_and_presence) of
|
||||
true ->
|
||||
lists:foreach(fun(Resource) ->
|
||||
LJID = {User, Server, Resource},
|
||||
Subscribed = case get_option(Options, access_model) of
|
||||
open -> true;
|
||||
presence -> true;
|
||||
whitelist -> false; % subscribers are added manually
|
||||
authorize -> false; % likewise
|
||||
roster ->
|
||||
Grps = get_option(Options, roster_groups_allowed, []),
|
||||
{OU, OS, _} = Owner,
|
||||
element(2, get_roster_info(OU, OS, LJID, Grps))
|
||||
end,
|
||||
if Subscribed -> send_items(Owner, Node, Nidx, Type, Options, LJID, last);
|
||||
true -> ok
|
||||
end
|
||||
end,
|
||||
Resources);
|
||||
_ ->
|
||||
ok
|
||||
end
|
||||
end,
|
||||
tree_action(Host, get_nodes, [Owner, JID]))
|
||||
end),
|
||||
send_loop(State);
|
||||
stop ->
|
||||
ok
|
||||
end.
|
||||
|
||||
%% -------
|
||||
%% disco hooks handling functions
|
||||
%%
|
||||
@@ -660,12 +551,12 @@ disco_items(Host, Node, From) ->
|
||||
end.
|
||||
|
||||
%% -------
|
||||
%% presence hooks handling functions
|
||||
%% presence and session hooks handling functions
|
||||
%%
|
||||
|
||||
-spec caps_add(jid(), jid(), [binary()]) -> ok.
|
||||
caps_add(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features)
|
||||
when Host =/= S ->
|
||||
caps_add(#jid{lserver = S1} = From, #jid{lserver = S2} = To, _Features)
|
||||
when S1 =/= S2 ->
|
||||
%% When a remote contact goes online while the local user is offline, the
|
||||
%% remote contact won't receive last items from the local user even if
|
||||
%% ignore_pep_from_offline is set to false. To work around this issue a bit,
|
||||
@@ -675,30 +566,36 @@ caps_add(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID
|
||||
%% contact becomes available; the former is also executed when the local
|
||||
%% user goes online (because that triggers the contact to send a presence
|
||||
%% packet with CAPS).
|
||||
presence(Host, {presence, U, S, [R], JID});
|
||||
send_last_pep(To, From);
|
||||
caps_add(_From, _To, _Feature) ->
|
||||
ok.
|
||||
|
||||
-spec caps_update(jid(), jid(), [binary()]) -> ok.
|
||||
caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) ->
|
||||
presence(Host, {presence, U, S, [R], JID}).
|
||||
caps_update(From, To, _Features) ->
|
||||
send_last_pep(To, From).
|
||||
|
||||
-spec presence_probe(jid(), jid(), pid()) -> ok.
|
||||
presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) ->
|
||||
%% ignore presence_probe from my other ressources
|
||||
%% to not get duplicated last items
|
||||
ok;
|
||||
presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, Pid) ->
|
||||
presence(S, {presence, From, Pid}),
|
||||
presence(S, {presence, From#jid.luser, S, [From#jid.lresource], To});
|
||||
presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, _Pid) ->
|
||||
send_last_pep(To, From);
|
||||
presence_probe(_From, _To, _Pid) ->
|
||||
%% ignore presence_probe from remote contacts,
|
||||
%% those are handled via caps_add
|
||||
%% ignore presence_probe from remote contacts, those are handled via caps_add
|
||||
ok.
|
||||
|
||||
presence(ServerHost, Presence) ->
|
||||
gen_mod:get_module_proc(ServerHost, ?LOOPNAME) ! Presence,
|
||||
ok.
|
||||
-spec on_user_online(ejabberd_c2s:state()) -> ejabberd_c2s:state().
|
||||
on_user_online(C2SState) ->
|
||||
JID = maps:get(jid, C2SState),
|
||||
send_last_items(JID),
|
||||
C2SState.
|
||||
|
||||
-spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state().
|
||||
on_user_offline(#{jid := JID} = C2SState, _Reason) ->
|
||||
purge_offline(jid:tolower(JID)),
|
||||
C2SState;
|
||||
on_user_offline(C2SState, _Reason) ->
|
||||
C2SState.
|
||||
|
||||
%% -------
|
||||
%% subscription hooks handling functions
|
||||
@@ -707,14 +604,8 @@ presence(ServerHost, Presence) ->
|
||||
-spec out_subscription(
|
||||
binary(), binary(), jid(),
|
||||
subscribed | unsubscribed | subscribe | unsubscribe) -> boolean().
|
||||
out_subscription(User, Server, JID, subscribed) ->
|
||||
Owner = jid:make(User, Server),
|
||||
{PUser, PServer, PResource} = jid:tolower(JID),
|
||||
PResources = case PResource of
|
||||
<<>> -> user_resources(PUser, PServer);
|
||||
_ -> [PResource]
|
||||
end,
|
||||
presence(Server, {presence, PUser, PServer, PResources, Owner}),
|
||||
out_subscription(User, Server, To, subscribed) ->
|
||||
send_last_pep(jid:make(User, Server), To),
|
||||
true;
|
||||
out_subscription(_, _, _, _) ->
|
||||
true.
|
||||
@@ -883,7 +774,9 @@ terminate(_Reason,
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
ejabberd_hooks:delete(sm_remove_connection_hook, ServerHost,
|
||||
ejabberd_hooks:delete(c2s_session_opened, ServerHost,
|
||||
?MODULE, on_user_online, 75),
|
||||
ejabberd_hooks:delete(c2s_terminated, ServerHost,
|
||||
?MODULE, on_user_offline, 75),
|
||||
ejabberd_hooks:delete(disco_local_identity, ServerHost,
|
||||
?MODULE, disco_local_identity, 75),
|
||||
@@ -901,12 +794,6 @@ terminate(_Reason,
|
||||
?MODULE, remove_user, 50),
|
||||
ejabberd_hooks:delete(c2s_handle_info, ServerHost,
|
||||
?MODULE, c2s_handle_info, 50),
|
||||
case whereis(gen_mod:get_module_proc(ServerHost, ?LOOPNAME)) of
|
||||
undefined ->
|
||||
?ERROR_MSG("~s process is dead, pubsub was broken", [?LOOPNAME]);
|
||||
Pid ->
|
||||
Pid ! stop
|
||||
end,
|
||||
lists:foreach(
|
||||
fun(Host) ->
|
||||
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO),
|
||||
@@ -1797,7 +1684,7 @@ subscribe_node(Host, Node, From, JID, Configuration) ->
|
||||
Nidx = TNode#pubsub_node.id,
|
||||
Type = TNode#pubsub_node.type,
|
||||
Options = TNode#pubsub_node.options,
|
||||
send_items(Host, Node, Nidx, Type, Options, Subscriber, last),
|
||||
send_items(Host, Node, Nidx, Type, Options, Subscriber, 1),
|
||||
ServerHost = serverhost(Host),
|
||||
ejabberd_hooks:run(pubsub_subscribe_node, ServerHost,
|
||||
[ServerHost, Host, Node, Subscriber, SubId]),
|
||||
@@ -1908,8 +1795,6 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access
|
||||
broadcast -> Payload;
|
||||
PluginPayload -> PluginPayload
|
||||
end,
|
||||
ejabberd_hooks:run(pubsub_publish_item, ServerHost,
|
||||
[ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]),
|
||||
set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload),
|
||||
case get_option(Options, deliver_notifications) of
|
||||
true ->
|
||||
@@ -1918,6 +1803,8 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
ejabberd_hooks:run(pubsub_publish_item, ServerHost,
|
||||
[ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]),
|
||||
case Result of
|
||||
default -> {result, Reply};
|
||||
_ -> {result, Result}
|
||||
@@ -2069,20 +1956,14 @@ purge_node(Host, Node, Owner) ->
|
||||
%% @doc <p>Return the items of a given node.</p>
|
||||
%% <p>The number of items to return is limited by MaxItems.</p>
|
||||
%% <p>The permission are not checked in this function.</p>
|
||||
%% @todo We probably need to check that the user doing the query has the right
|
||||
%% to read the items.
|
||||
-spec get_items(host(), binary(), jid(), binary(),
|
||||
binary(), [binary()], undefined | rsm_set()) ->
|
||||
{result, pubsub()} | {error, stanza_error()}.
|
||||
get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) ->
|
||||
MaxItems = if SMaxItems == undefined ->
|
||||
case get_max_items_node(Host) of
|
||||
undefined -> ?MAXITEMS;
|
||||
Max -> Max
|
||||
end;
|
||||
true ->
|
||||
SMaxItems
|
||||
end,
|
||||
get_items(Host, Node, From, SubId, MaxItems, ItemIds, undefined)
|
||||
when MaxItems =/= undefined ->
|
||||
get_items(Host, Node, From, SubId, MaxItems, ItemIds,
|
||||
#rsm_set{max = MaxItems, before = <<>>});
|
||||
get_items(Host, Node, From, SubId, _MaxItems, ItemIds, RSM) ->
|
||||
Action =
|
||||
fun(#pubsub_node{options = Options, type = Type,
|
||||
id = Nidx, owners = O}) ->
|
||||
@@ -2101,8 +1982,14 @@ get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) ->
|
||||
Owners = node_owners_call(Host, Type, Nidx, O),
|
||||
{PS, RG} = get_presence_and_roster_permissions(
|
||||
Host, From, Owners, AccessModel, AllowedGroups),
|
||||
node_call(Host, Type, get_items,
|
||||
[Nidx, From, AccessModel, PS, RG, SubId, RSM])
|
||||
case ItemIds of
|
||||
[ItemId] ->
|
||||
node_call(Host, Type, get_item,
|
||||
[Nidx, ItemId, From, AccessModel, PS, RG, undefined]);
|
||||
_ ->
|
||||
node_call(Host, Type, get_items,
|
||||
[Nidx, From, AccessModel, PS, RG, SubId, RSM])
|
||||
end
|
||||
end
|
||||
end,
|
||||
case transaction(Host, Node, Action, sync_dirty) of
|
||||
@@ -2118,8 +2005,12 @@ get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) ->
|
||||
end,
|
||||
{result,
|
||||
#pubsub{items = #ps_items{node = Node,
|
||||
items = itemsEls(lists:sublist(SendItems, MaxItems))},
|
||||
items = itemsEls(SendItems)},
|
||||
rsm = RsmOut}};
|
||||
{result, {_, Item}} ->
|
||||
{result,
|
||||
#pubsub{items = #ps_items{node = Node,
|
||||
items = itemsEls([Item])}}};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
@@ -2153,43 +2044,23 @@ get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) ->
|
||||
{PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups),
|
||||
node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]).
|
||||
|
||||
get_last_items(Host, Type, Nidx, LJID, Count) ->
|
||||
get_last_items(Host, Type, Nidx, LJID, 1) ->
|
||||
case get_cached_item(Host, Nidx) of
|
||||
undefined ->
|
||||
case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of
|
||||
case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of
|
||||
{result, Items} -> Items;
|
||||
_ -> []
|
||||
_ -> []
|
||||
end;
|
||||
LastItem ->
|
||||
[LastItem]
|
||||
end.
|
||||
|
||||
%% @doc <p>Resend the items of a node to the user.</p>
|
||||
%% @todo use cache-last-item feature
|
||||
send_items(Host, Node, Nidx, Type, Options, LJID, last) ->
|
||||
case get_last_items(Host, Type, Nidx, LJID, 1) of
|
||||
[LastItem] ->
|
||||
Stanza = items_event_stanza(Node, Options, [LastItem]),
|
||||
dispatch_items(Host, LJID, Node, Stanza);
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
send_items(Host, Node, Nidx, Type, Options, LJID, Number) when Number > 0 ->
|
||||
Stanza = items_event_stanza(Node, Options, get_last_items(Host, Type, Nidx, Number, LJID)),
|
||||
dispatch_items(Host, LJID, Node, Stanza);
|
||||
send_items(Host, Node, _Nidx, _Type, Options, LJID, _) ->
|
||||
Stanza = items_event_stanza(Node, Options, []),
|
||||
dispatch_items(Host, LJID, Node, Stanza).
|
||||
|
||||
dispatch_items({FromU, FromS, FromR}, To, Node, Stanza) ->
|
||||
SenderResource = user_resource(FromU, FromS, FromR),
|
||||
ejabberd_sm:route(jid:make(FromU, FromS, SenderResource),
|
||||
{send_filtered, {pep_message, <<((Node))/binary, "+notify">>},
|
||||
jid:make(FromU, FromS), jid:make(To),
|
||||
Stanza});
|
||||
dispatch_items(From, To, _Node, Stanza) ->
|
||||
ejabberd_router:route(
|
||||
xmpp:set_from_to(Stanza, service_jid(From), jid:make(To))).
|
||||
get_last_items(Host, Type, Nidx, LJID, Count) when Count > 1 ->
|
||||
case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of
|
||||
{result, Items} -> Items;
|
||||
_ -> []
|
||||
end;
|
||||
get_last_items(_Host, _Type, _Nidx, _LJID, _Count) ->
|
||||
[].
|
||||
|
||||
%% @doc <p>Return the list of affiliations as an XMPP response.</p>
|
||||
-spec get_affiliations(host(), binary(), jid(), [binary()]) ->
|
||||
@@ -2536,14 +2407,15 @@ get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) ->
|
||||
{result, Subs} = node_action(Host, PType,
|
||||
get_entity_subscriptions_for_send_last,
|
||||
[Host, JID]),
|
||||
[{Node, Sub, SubId, SubJID}
|
||||
[{Node, SubId, SubJID}
|
||||
|| {Node, Sub, SubId, SubJID} <- Subs,
|
||||
Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)];
|
||||
% sql version already filter result by on_sub_and_presence
|
||||
get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) ->
|
||||
{result, Subs} = node_action(Host, PType,
|
||||
get_entity_subscriptions,
|
||||
[Host, JID]),
|
||||
[{Node, Sub, SubId, SubJID}
|
||||
[{Node, SubId, SubJID}
|
||||
|| {Node, Sub, SubId, SubJID} <- Subs,
|
||||
Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID),
|
||||
match_option(Node, send_last_published_item, on_sub_and_presence)].
|
||||
@@ -2932,8 +2804,9 @@ get_options_for_subs(Host, Nidx, Subs, true) ->
|
||||
broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
|
||||
NotificationType = get_option(NodeOptions, notification_type, headline),
|
||||
BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull
|
||||
From = service_jid(Host),
|
||||
Stanza = add_message_type(BaseStanza, NotificationType),
|
||||
Stanza = add_message_type(
|
||||
xmpp:set_from(BaseStanza, service_jid(Host)),
|
||||
NotificationType),
|
||||
%% Handles explicit subscriptions
|
||||
SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth),
|
||||
lists:foreach(fun ({LJID, _NodeName, SubIDs}) ->
|
||||
@@ -2956,7 +2829,7 @@ broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType
|
||||
end,
|
||||
lists:foreach(fun(To) ->
|
||||
ejabberd_router:route(
|
||||
xmpp:set_from_to(StanzaToSend, From, jid:make(To)))
|
||||
xmpp:set_to(StanzaToSend, jid:make(To)))
|
||||
end, LJIDs)
|
||||
end, SubIDsByJID).
|
||||
|
||||
@@ -2965,55 +2838,142 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeO
|
||||
%% Handles implicit presence subscriptions
|
||||
SenderResource = user_resource(LUser, LServer, LResource),
|
||||
NotificationType = get_option(NodeOptions, notification_type, headline),
|
||||
Stanza = add_message_type(BaseStanza, NotificationType),
|
||||
Stanza = add_message_type(
|
||||
xmpp:set_from(BaseStanza, jid:make(LUser, LServer)),
|
||||
NotificationType),
|
||||
%% set the from address on the notification to the bare JID of the account owner
|
||||
%% Also, add "replyto" if entity has presence subscription to the account owner
|
||||
%% See XEP-0163 1.1 section 4.3.1
|
||||
ejabberd_sm:route(jid:make(LUser, LServer, SenderResource),
|
||||
{pep_message, <<((Node))/binary, "+notify">>,
|
||||
jid:make(LUser, LServer),
|
||||
add_extended_headers(
|
||||
Stanza, extended_headers([Publisher]))});
|
||||
broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) ->
|
||||
broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM).
|
||||
|
||||
-spec c2s_handle_info(ejabberd_c2s:state(), term()) -> ejabberd_c2s:state().
|
||||
c2s_handle_info(#{server := Server} = C2SState,
|
||||
{pep_message, Feature, From, Packet}) ->
|
||||
LServer = jid:nameprep(Server),
|
||||
lists:foreach(
|
||||
fun({USR, Caps}) ->
|
||||
Features = mod_caps:get_features(LServer, Caps),
|
||||
case lists:member(Feature, Features) of
|
||||
true ->
|
||||
To = jid:make(USR),
|
||||
NewPacket = xmpp:set_from_to(Packet, From, To),
|
||||
ejabberd_router:route(NewPacket);
|
||||
false ->
|
||||
ok
|
||||
end
|
||||
end, mod_caps:list_features(C2SState)),
|
||||
c2s_handle_info(#{lserver := LServer} = C2SState,
|
||||
{pep_message, Feature, Packet}) ->
|
||||
[maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet)
|
||||
|| {USR, Caps} <- mod_caps:list_features(C2SState)],
|
||||
{stop, C2SState};
|
||||
c2s_handle_info(#{server := Server} = C2SState,
|
||||
{send_filtered, {pep_message, Feature}, From, To, Packet}) ->
|
||||
LServer = jid:nameprep(Server),
|
||||
case mod_caps:get_user_caps(To, C2SState) of
|
||||
{ok, Caps} ->
|
||||
Features = mod_caps:get_features(LServer, Caps),
|
||||
case lists:member(Feature, Features) of
|
||||
true ->
|
||||
NewPacket = xmpp:set_from_to(Packet, From, To),
|
||||
ejabberd_router:route(NewPacket);
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
error ->
|
||||
ok
|
||||
c2s_handle_info(#{lserver := LServer} = C2SState,
|
||||
{pep_message, Feature, Packet, USR}) ->
|
||||
case mod_caps:get_user_caps(USR, C2SState) of
|
||||
{ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet);
|
||||
error -> ok
|
||||
end,
|
||||
{stop, C2SState};
|
||||
c2s_handle_info(C2SState, _) ->
|
||||
C2SState.
|
||||
|
||||
send_items(Host, Node, Nidx, Type, Options, LJID, Number) ->
|
||||
send_items(Host, Node, Nidx, Type, Options, Host, LJID, LJID, Number).
|
||||
send_items(Host, Node, Nidx, Type, Options, Publisher, SubLJID, ToLJID, Number) ->
|
||||
case get_last_items(Host, Type, Nidx, SubLJID, Number) of
|
||||
[] ->
|
||||
ok;
|
||||
Items ->
|
||||
Stanza = items_event_stanza(Node, Options, Items),
|
||||
send_stanza(Publisher, ToLJID, Node, Stanza)
|
||||
end.
|
||||
|
||||
send_stanza({LUser, LServer, _} = Publisher, USR, Node, BaseStanza) ->
|
||||
Stanza = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)),
|
||||
USRs = case USR of
|
||||
{PUser, PServer, <<>>} ->
|
||||
[{PUser, PServer, PRessource}
|
||||
|| PRessource <- user_resources(PUser, PServer)];
|
||||
_ ->
|
||||
[USR]
|
||||
end,
|
||||
[ejabberd_sm:route(jid:make(Publisher),
|
||||
{pep_message, <<((Node))/binary, "+notify">>,
|
||||
add_extended_headers(
|
||||
Stanza, extended_headers([Publisher])),
|
||||
To}) || To <- USRs];
|
||||
send_stanza(Host, USR, _Node, Stanza) ->
|
||||
ejabberd_router:route(
|
||||
xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))).
|
||||
|
||||
maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) ->
|
||||
Features = mod_caps:get_features(LServer, Caps),
|
||||
case lists:member(Feature, Features) of
|
||||
true ->
|
||||
ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR)));
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
|
||||
send_last_items(JID) ->
|
||||
ServerHost = JID#jid.lserver,
|
||||
Host = host(ServerHost),
|
||||
DBType = config(ServerHost, db_type),
|
||||
LJID = jid:tolower(JID),
|
||||
BJID = jid:remove_resource(LJID),
|
||||
lists:foreach(
|
||||
fun(PType) ->
|
||||
Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID),
|
||||
lists:foreach(
|
||||
fun({#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx,
|
||||
options = Options}, _, SubJID})
|
||||
when Type == PType->
|
||||
send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1);
|
||||
(_) ->
|
||||
ok
|
||||
end,
|
||||
lists:usort(Subs))
|
||||
end, config(ServerHost, plugins)).
|
||||
% pep_from_offline hack can not work anymore, as sender c2s does not
|
||||
% exists when sender is offline, so we can't get match receiver caps
|
||||
% does it make sens to send PEP from an offline contact anyway ?
|
||||
% case config(ServerHost, ignore_pep_from_offline) of
|
||||
% false ->
|
||||
% Roster = ejabberd_hooks:run_fold(roster_get, ServerHost, [],
|
||||
% [{JID#jid.luser, ServerHost}]),
|
||||
% lists:foreach(
|
||||
% fun(#roster{jid = {U, S, R}, subscription = Sub})
|
||||
% when Sub == both orelse Sub == from,
|
||||
% S == ServerHost ->
|
||||
% case user_resources(U, S) of
|
||||
% [] -> send_last_pep(jid:make(U, S, R), JID);
|
||||
% _ -> ok %% this is already handled by presence probe
|
||||
% end;
|
||||
% (_) ->
|
||||
% ok %% we can not do anything in any cases
|
||||
% end, Roster);
|
||||
% true ->
|
||||
% ok
|
||||
% end.
|
||||
send_last_pep(From, To) ->
|
||||
ServerHost = From#jid.lserver,
|
||||
Host = host(ServerHost),
|
||||
Publisher = jid:tolower(From),
|
||||
Owner = jid:remove_resource(Publisher),
|
||||
lists:foreach(
|
||||
fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) ->
|
||||
case match_option(Options, send_last_published_item, on_sub_and_presence) of
|
||||
true ->
|
||||
LJID = jid:tolower(To),
|
||||
Subscribed = case get_option(Options, access_model) of
|
||||
open -> true;
|
||||
presence -> true;
|
||||
whitelist -> false; % subscribers are added manually
|
||||
authorize -> false; % likewise
|
||||
roster ->
|
||||
Grps = get_option(Options, roster_groups_allowed, []),
|
||||
{OU, OS, _} = Owner,
|
||||
element(2, get_roster_info(OU, OS, LJID, Grps))
|
||||
end,
|
||||
if Subscribed -> send_items(Owner, Node, Nidx, Type, Options, Publisher, LJID, LJID, 1);
|
||||
true -> ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end
|
||||
end,
|
||||
tree_action(Host, get_nodes, [Owner, From])).
|
||||
|
||||
subscribed_nodes_by_jid(NotifyType, SubsByDepth) ->
|
||||
NodesToDeliver = fun (Depth, Node, Subs, Acc) ->
|
||||
NodeName = case Node#pubsub_node.nodeid of
|
||||
@@ -3136,10 +3096,10 @@ get_option(Options, Var, Def) ->
|
||||
|
||||
-spec node_options(host(), binary()) -> [{atom(), any()}].
|
||||
node_options(Host, Type) ->
|
||||
case config(Host, default_node_config) of
|
||||
undefined -> node_plugin_options(Host, Type);
|
||||
[] -> node_plugin_options(Host, Type);
|
||||
Config -> Config
|
||||
DefaultOpts = node_plugin_options(Host, Type),
|
||||
case config(Host, plugins) of
|
||||
[Type|_] -> config(Host, default_node_config, DefaultOpts);
|
||||
_ -> DefaultOpts
|
||||
end.
|
||||
|
||||
-spec node_plugin_options(host(), binary()) -> [{atom(), any()}].
|
||||
@@ -3153,13 +3113,6 @@ node_plugin_options(Host, Type) ->
|
||||
Result
|
||||
end.
|
||||
|
||||
-spec filter_node_options([{atom(), any()}], [{atom(), any()}]) -> [{atom(), any()}].
|
||||
filter_node_options(Options, BaseOptions) ->
|
||||
lists:foldl(fun({Key, Val}, Acc) ->
|
||||
DefaultValue = proplists:get_value(Key, Options, Val),
|
||||
[{Key, DefaultValue}|Acc]
|
||||
end, [], BaseOptions).
|
||||
|
||||
-spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()].
|
||||
node_owners_action(Host, Type, Nidx, []) ->
|
||||
case node_action(Host, Type, get_node_affiliations, [Nidx]) of
|
||||
@@ -3186,9 +3139,6 @@ node_owners_call(_Host, _Type, _Nidx, Owners) ->
|
||||
%% @doc <p>Return the maximum number of items for a given node.</p>
|
||||
%% <p>Unlimited means that there is no limit in the number of items that can
|
||||
%% be stored.</p>
|
||||
%% @todo In practice, the current data structure means that we cannot manage
|
||||
%% millions of items on a given node. This should be addressed in a new
|
||||
%% version.
|
||||
-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer().
|
||||
max_items(Host, Options) ->
|
||||
case get_option(Options, persist_items) of
|
||||
@@ -3247,8 +3197,8 @@ set_configure(Host, Node, From, Config, Lang) ->
|
||||
case tree_call(Host,
|
||||
set_node,
|
||||
[N#pubsub_node{options = NewOpts}]) of
|
||||
{result, Nidx} -> {result, ok};
|
||||
ok -> {result, ok};
|
||||
{result, Nidx} -> {result, NewOpts};
|
||||
ok -> {result, NewOpts};
|
||||
Err -> Err
|
||||
end;
|
||||
_ ->
|
||||
@@ -3257,10 +3207,9 @@ set_configure(Host, Node, From, Config, Lang) ->
|
||||
end
|
||||
end,
|
||||
case transaction(Host, Node, Action, transaction) of
|
||||
{result, {TNode, ok}} ->
|
||||
{result, {TNode, Options}} ->
|
||||
Nidx = TNode#pubsub_node.id,
|
||||
Type = TNode#pubsub_node.type,
|
||||
Options = TNode#pubsub_node.options,
|
||||
broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang),
|
||||
{result, undefined};
|
||||
Other ->
|
||||
@@ -3268,11 +3217,11 @@ set_configure(Host, Node, From, Config, Lang) ->
|
||||
end.
|
||||
|
||||
-spec merge_config([proplists:property()], [proplists:property()]) -> [proplists:property()].
|
||||
merge_config(Config1, Config2) ->
|
||||
merge_config(CustomConfig, DefaultConfig) ->
|
||||
lists:foldl(
|
||||
fun({Opt, Val}, Acc) ->
|
||||
lists:keystore(Opt, 1, Acc, {Opt, Val})
|
||||
end, Config2, Config1).
|
||||
end, DefaultConfig, CustomConfig).
|
||||
|
||||
-spec decode_node_config(undefined | xdata(), binary(), binary()) ->
|
||||
pubsub_node_config:result() |
|
||||
@@ -3792,14 +3741,6 @@ subid_shim(SubIds) ->
|
||||
extended_headers(Jids) ->
|
||||
[#address{type = replyto, jid = Jid} || Jid <- Jids].
|
||||
|
||||
-spec on_user_offline(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok.
|
||||
on_user_offline(_, JID, _) ->
|
||||
{User, Server, Resource} = jid:tolower(JID),
|
||||
case user_resources(User, Server) of
|
||||
[] -> purge_offline({User, Server, Resource});
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
-spec purge_offline(ljid()) -> ok.
|
||||
purge_offline(LJID) ->
|
||||
Host = host(element(2, LJID)),
|
||||
@@ -3840,7 +3781,7 @@ purge_offline(LJID) ->
|
||||
end
|
||||
end, lists:usort(lists:flatten(Affs)));
|
||||
{Error, _} ->
|
||||
?DEBUG("on_user_offline ~p", [Error])
|
||||
?ERROR_MSG("can not purge offline: ~p", [Error])
|
||||
end.
|
||||
|
||||
-spec purge_offline(host(), ljid(), binary()) -> ok | {error, stanza_error()}.
|
||||
@@ -3852,13 +3793,13 @@ purge_offline(Host, LJID, Node) ->
|
||||
{result, {[], _}} ->
|
||||
ok;
|
||||
{result, {Items, _}} ->
|
||||
{User, Server, _} = LJID,
|
||||
{User, Server, Resource} = LJID,
|
||||
PublishModel = get_option(Options, publish_model),
|
||||
ForceNotify = get_option(Options, notify_retract),
|
||||
{_, NodeId} = Node#pubsub_node.nodeid,
|
||||
lists:foreach(fun
|
||||
(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, _}}})
|
||||
when (U == User) and (S == Server) ->
|
||||
(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}})
|
||||
when (U == User) and (S == Server) and (R == Resource) ->
|
||||
case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of
|
||||
{result, {_, broadcast}} ->
|
||||
broadcast_retract_items(Host, NodeId, Nidx, Type, Options, [ItemId], ForceNotify),
|
||||
|
||||
+1
-1
@@ -547,7 +547,7 @@ delete_sessions(LUser, LServer, LookupFun, Mod) ->
|
||||
drop_online_sessions(LUser, LServer, Clients) ->
|
||||
SessIDs = ejabberd_sm:get_session_sids(LUser, LServer),
|
||||
[Client || {TS, _, _, _} = Client <- Clients,
|
||||
not lists:keyfind(TS, 1, SessIDs)].
|
||||
lists:keyfind(TS, 1, SessIDs) == false].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Caching.
|
||||
|
||||
@@ -91,7 +91,7 @@ mod_opt_type(O) when O == cache_life_time; O == cache_size ->
|
||||
mod_opt_type(O) when O == use_cache; O == cache_missed ->
|
||||
fun (B) when is_boolean(B) -> B end;
|
||||
mod_opt_type(_) ->
|
||||
[resume_timeout, wake_on_start, wake_on_timeout, db_type, cache_life_time,
|
||||
[resume_timeout, wake_on_start, wake_on_timeout, cache_life_time,
|
||||
cache_size, use_cache, cache_missed, iqdisc].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
@@ -162,7 +162,7 @@ delete_session(LUser, LServer, TS) ->
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{aborted, E} ->
|
||||
?ERROR_MSG("Cannot delete push seesion of ~s@~s: ~p",
|
||||
?ERROR_MSG("Cannot delete push session of ~s@~s: ~p",
|
||||
[LUser, LServer, E]),
|
||||
error
|
||||
end.
|
||||
|
||||
+15
-6
@@ -127,7 +127,7 @@ process_iq(#iq{from = From, to = To} = IQ, Source) ->
|
||||
process_iq(#iq{type = set, lang = Lang,
|
||||
sub_els = [#register{remove = true}]} = IQ,
|
||||
_Source, _IsCaptchaEnabled, _AllowRemove = false) ->
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang));
|
||||
process_iq(#iq{type = set, lang = Lang, to = To, from = From,
|
||||
sub_els = [#register{remove = true,
|
||||
@@ -210,7 +210,14 @@ process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ,
|
||||
Instr = translate:translate(
|
||||
Lang, <<"Choose a username and password to register "
|
||||
"with this server">>),
|
||||
if IsCaptchaEnabled and not IsRegistered ->
|
||||
URL = gen_mod:get_module_opt(Server, ?MODULE, redirect_url, <<"">>),
|
||||
if (URL /= <<"">>) and not IsRegistered ->
|
||||
Txt = translate:translate(Lang, <<"To register, visit ~s">>),
|
||||
Desc = str:format(Txt, [URL]),
|
||||
xmpp:make_iq_result(
|
||||
IQ, #register{instructions = Desc,
|
||||
sub_els = [#oob_x{url = URL}]});
|
||||
IsCaptchaEnabled and not IsRegistered ->
|
||||
TopInstr = translate:translate(
|
||||
Lang, <<"You need a client that supports x:data "
|
||||
"and CAPTCHA to register">>),
|
||||
@@ -263,7 +270,7 @@ try_register_or_set_password(User, Server, Password,
|
||||
xmpp:make_error(IQ, Error)
|
||||
end;
|
||||
deny ->
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
|
||||
end;
|
||||
_ ->
|
||||
@@ -315,8 +322,8 @@ try_register(User, Server, Password, SourceRaw, Lang) ->
|
||||
case {acl:match_rule(Server, Access, JID),
|
||||
check_ip_access(SourceRaw, IPAccess)}
|
||||
of
|
||||
{deny, _} -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{_, deny} -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
|
||||
{deny, _} -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
{_, deny} -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)};
|
||||
{allow, allow} ->
|
||||
Source = may_remove_resource(SourceRaw),
|
||||
case check_timeout(Source) of
|
||||
@@ -614,9 +621,11 @@ mod_opt_type({welcome_message, subject}) ->
|
||||
fun iolist_to_binary/1;
|
||||
mod_opt_type({welcome_message, body}) ->
|
||||
fun iolist_to_binary/1;
|
||||
mod_opt_type(redirect_url) ->
|
||||
fun iolist_to_binary/1;
|
||||
mod_opt_type(_) ->
|
||||
[access, access_from, access_remove, captcha_protected, ip_access,
|
||||
iqdisc, password_strength, registration_watchers,
|
||||
iqdisc, password_strength, registration_watchers, redirect_url,
|
||||
{welcome_message, subject}, {welcome_message, body}].
|
||||
|
||||
-spec opt_type(registration_timeout) -> fun((timeout()) -> timeout());
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ process_local_iq(#iq{type = set, from = From, lang = Lang,
|
||||
Access = gen_mod:get_module_opt(Server, ?MODULE, access, all),
|
||||
case acl:match_rule(Server, Access, From) of
|
||||
deny ->
|
||||
Txt = <<"Denied by ACL">>,
|
||||
Txt = <<"Access denied by service policy">>,
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
|
||||
allow ->
|
||||
process_iq_set(IQ)
|
||||
|
||||
@@ -320,7 +320,7 @@ check_from_to(From, To) ->
|
||||
|
||||
-spec mk_error(term()) -> stanza_error().
|
||||
mk_error(forbidden) ->
|
||||
xmpp:err_forbidden(<<"Denied by ACL">>, ?MYLANG);
|
||||
xmpp:err_forbidden(<<"Access denied by service policy">>, ?MYLANG);
|
||||
mk_error(host_unknown) ->
|
||||
xmpp:err_not_allowed(<<"Host unknown">>, ?MYLANG);
|
||||
mk_error({codec_error, Why}) ->
|
||||
|
||||
@@ -71,7 +71,12 @@ start(Host, _Opts) ->
|
||||
ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, c2s_terminated, 50).
|
||||
|
||||
stop(Host) ->
|
||||
%% TODO: do something with global 'c2s_init' hook
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
ejabberd_hooks:delete(c2s_init, ?MODULE, c2s_stream_init, 50)
|
||||
end,
|
||||
ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE,
|
||||
c2s_stream_started, 50),
|
||||
ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE,
|
||||
@@ -405,8 +410,6 @@ handle_resume(#{user := User, lserver := LServer, sockmod := SockMod,
|
||||
previd = AttrId}),
|
||||
State3 = resend_unacked_stanzas(State2),
|
||||
State4 = send(State3, #sm_r{xmlns = AttrXmlns}),
|
||||
%% TODO: move this to mod_client_state
|
||||
%% csi_flush_queue(State4),
|
||||
State5 = ejabberd_hooks:run_fold(c2s_session_resumed, LServer, State4, []),
|
||||
?INFO_MSG("(~s) Resumed session for ~s",
|
||||
[SockMod:pp(Socket), jid:encode(JID)]),
|
||||
|
||||
+29
-19
@@ -38,7 +38,7 @@
|
||||
remove_user/2, export/1, import_info/0, import/5, import_start/2,
|
||||
depends/2, process_search/1, process_vcard/1, get_vcard/2,
|
||||
disco_items/5, disco_features/5, disco_identity/5,
|
||||
decode_iq_subel/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]).
|
||||
vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]).
|
||||
-export([init/1, handle_call/3, handle_cast/2,
|
||||
handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
@@ -95,6 +95,7 @@ init([Host, Opts]) ->
|
||||
?NS_VCARD, ?MODULE, process_sm_iq, IQDisc),
|
||||
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
|
||||
get_sm_features, 50),
|
||||
ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50),
|
||||
MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>),
|
||||
Search = gen_mod:get_opt(search, Opts, false),
|
||||
if Search ->
|
||||
@@ -152,6 +153,7 @@ terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) ->
|
||||
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD),
|
||||
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD),
|
||||
ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
|
||||
ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50),
|
||||
Mod = gen_mod:db_mod(Host, ?MODULE),
|
||||
Mod:stop(Host),
|
||||
lists:foreach(
|
||||
@@ -191,14 +193,6 @@ get_sm_features(Acc, _From, _To, Node, _Lang) ->
|
||||
_ -> Acc
|
||||
end.
|
||||
|
||||
-spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel().
|
||||
%% Tell gen_iq_handler not to decode vcard elements
|
||||
decode_iq_subel(El) ->
|
||||
case xmpp:get_ns(El) of
|
||||
?NS_VCARD -> xmpp:encode(El);
|
||||
_ -> xmpp:decode(El)
|
||||
end.
|
||||
|
||||
-spec process_local_iq(iq()) -> iq().
|
||||
process_local_iq(#iq{type = set, lang = Lang} = IQ) ->
|
||||
Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
|
||||
@@ -212,13 +206,15 @@ process_local_iq(#iq{type = get, lang = Lang} = IQ) ->
|
||||
bday = <<"2002-11-16">>}).
|
||||
|
||||
-spec process_sm_iq(iq()) -> iq().
|
||||
process_sm_iq(#iq{type = set, lang = Lang, from = From,
|
||||
sub_els = [SubEl]} = IQ) ->
|
||||
#jid{user = User, lserver = LServer} = From,
|
||||
process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) ->
|
||||
#jid{lserver = LServer} = From,
|
||||
case lists:member(LServer, ?MYHOSTS) of
|
||||
true ->
|
||||
set_vcard(User, LServer, SubEl),
|
||||
xmpp:make_iq_result(IQ);
|
||||
case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of
|
||||
drop -> ignore;
|
||||
#stanza_error{} = Err -> xmpp:make_error(IQ, Err);
|
||||
_ -> xmpp:make_iq_result(IQ)
|
||||
end;
|
||||
false ->
|
||||
Txt = <<"The query is only allowed from local users">>,
|
||||
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang))
|
||||
@@ -380,19 +376,33 @@ make_vcard_search(User, LUser, LServer, VCARD) ->
|
||||
orgunit = OrgUnit,
|
||||
lorgunit = LOrgUnit}.
|
||||
|
||||
-spec set_vcard(binary(), binary(), xmlel()) -> {error, badarg} | ok.
|
||||
-spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}.
|
||||
vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) ->
|
||||
#jid{user = User, lserver = LServer} = From,
|
||||
case set_vcard(User, LServer, VCard) of
|
||||
{error, badarg} ->
|
||||
%% Should not be here?
|
||||
Txt = <<"Nodeprep has failed">>,
|
||||
{stop, xmpp:err_internal_server_error(Txt, Lang)};
|
||||
ok ->
|
||||
IQ
|
||||
end;
|
||||
vcard_iq_set(Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) -> {error, badarg} | ok.
|
||||
set_vcard(User, LServer, VCARD) ->
|
||||
case jid:nodeprep(User) of
|
||||
error ->
|
||||
{error, badarg};
|
||||
LUser ->
|
||||
VCardSearch = make_vcard_search(User, LUser, LServer, VCARD),
|
||||
VCardEl = xmpp:encode(VCARD),
|
||||
VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl),
|
||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||
Mod:set_vcard(LUser, LServer, VCARD, VCardSearch),
|
||||
Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch),
|
||||
ets_cache:delete(?VCARD_CACHE, {LUser, LServer},
|
||||
cache_nodes(Mod, LServer)),
|
||||
ejabberd_hooks:run(vcard_set, LServer,
|
||||
[LUser, LServer, VCARD])
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec string2lower(binary()) -> binary().
|
||||
|
||||
+24
-23
@@ -42,6 +42,7 @@
|
||||
-include("logger.hrl").
|
||||
-include("eldap.hrl").
|
||||
-include("xmpp.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
-define(PROCNAME, ejabberd_mod_vcard_ldap).
|
||||
|
||||
@@ -324,31 +325,31 @@ default_vcard_map() ->
|
||||
{<<"PHOTO">>, <<"%s">>, [<<"jpegPhoto">>]}].
|
||||
|
||||
default_search_fields() ->
|
||||
[{<<"User">>, <<"%u">>},
|
||||
{<<"Full Name">>, <<"displayName">>},
|
||||
{<<"Given Name">>, <<"givenName">>},
|
||||
{<<"Middle Name">>, <<"initials">>},
|
||||
{<<"Family Name">>, <<"sn">>},
|
||||
{<<"Nickname">>, <<"%u">>},
|
||||
{<<"Birthday">>, <<"birthDay">>},
|
||||
{<<"Country">>, <<"c">>},
|
||||
{<<"City">>, <<"l">>},
|
||||
{<<"Email">>, <<"mail">>},
|
||||
{<<"Organization Name">>, <<"o">>},
|
||||
{<<"Organization Unit">>, <<"ou">>}].
|
||||
[{?T("User"), <<"%u">>},
|
||||
{?T("Full Name"), <<"displayName">>},
|
||||
{?T("Given Name"), <<"givenName">>},
|
||||
{?T("Middle Name"), <<"initials">>},
|
||||
{?T("Family Name"), <<"sn">>},
|
||||
{?T("Nickname"), <<"%u">>},
|
||||
{?T("Birthday"), <<"birthDay">>},
|
||||
{?T("Country"), <<"c">>},
|
||||
{?T("City"), <<"l">>},
|
||||
{?T("Email"), <<"mail">>},
|
||||
{?T("Organization Name"), <<"o">>},
|
||||
{?T("Organization Unit"), <<"ou">>}].
|
||||
|
||||
default_search_reported() ->
|
||||
[{<<"Full Name">>, <<"FN">>},
|
||||
{<<"Given Name">>, <<"FIRST">>},
|
||||
{<<"Middle Name">>, <<"MIDDLE">>},
|
||||
{<<"Family Name">>, <<"LAST">>},
|
||||
{<<"Nickname">>, <<"NICK">>},
|
||||
{<<"Birthday">>, <<"BDAY">>},
|
||||
{<<"Country">>, <<"CTRY">>},
|
||||
{<<"City">>, <<"LOCALITY">>},
|
||||
{<<"Email">>, <<"EMAIL">>},
|
||||
{<<"Organization Name">>, <<"ORGNAME">>},
|
||||
{<<"Organization Unit">>, <<"ORGUNIT">>}].
|
||||
[{?T("Full Name"), <<"FN">>},
|
||||
{?T("Given Name"), <<"FIRST">>},
|
||||
{?T("Middle Name"), <<"MIDDLE">>},
|
||||
{?T("Family Name"), <<"LAST">>},
|
||||
{?T("Nickname"), <<"NICK">>},
|
||||
{?T("Birthday"), <<"BDAY">>},
|
||||
{?T("Country"), <<"CTRY">>},
|
||||
{?T("City"), <<"LOCALITY">>},
|
||||
{?T("Email"), <<"EMAIL">>},
|
||||
{?T("Organization Name"), <<"ORGNAME">>},
|
||||
{?T("Organization Unit"), <<"ORGUNIT">>}].
|
||||
|
||||
parse_options(Host, Opts) ->
|
||||
MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>),
|
||||
|
||||
+25
-24
@@ -36,6 +36,7 @@
|
||||
-include("xmpp.hrl").
|
||||
-include("mod_vcard.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
@@ -95,32 +96,32 @@ search(LServer, Data, AllowReturnAll, MaxMatch) ->
|
||||
end.
|
||||
|
||||
search_fields(_LServer) ->
|
||||
[{<<"User">>, <<"user">>},
|
||||
{<<"Full Name">>, <<"fn">>},
|
||||
{<<"Name">>, <<"first">>},
|
||||
{<<"Middle Name">>, <<"middle">>},
|
||||
{<<"Family Name">>, <<"last">>},
|
||||
{<<"Nickname">>, <<"nick">>},
|
||||
{<<"Birthday">>, <<"bday">>},
|
||||
{<<"Country">>, <<"ctry">>},
|
||||
{<<"City">>, <<"locality">>},
|
||||
{<<"Email">>, <<"email">>},
|
||||
{<<"Organization Name">>, <<"orgname">>},
|
||||
{<<"Organization Unit">>, <<"orgunit">>}].
|
||||
[{?T("User"), <<"user">>},
|
||||
{?T("Full Name"), <<"fn">>},
|
||||
{?T("Name"), <<"first">>},
|
||||
{?T("Middle Name"), <<"middle">>},
|
||||
{?T("Family Name"), <<"last">>},
|
||||
{?T("Nickname"), <<"nick">>},
|
||||
{?T("Birthday"), <<"bday">>},
|
||||
{?T("Country"), <<"ctry">>},
|
||||
{?T("City"), <<"locality">>},
|
||||
{?T("Email"), <<"email">>},
|
||||
{?T("Organization Name"), <<"orgname">>},
|
||||
{?T("Organization Unit"), <<"orgunit">>}].
|
||||
|
||||
search_reported(_LServer) ->
|
||||
[{<<"Jabber ID">>, <<"jid">>},
|
||||
{<<"Full Name">>, <<"fn">>},
|
||||
{<<"Name">>, <<"first">>},
|
||||
{<<"Middle Name">>, <<"middle">>},
|
||||
{<<"Family Name">>, <<"last">>},
|
||||
{<<"Nickname">>, <<"nick">>},
|
||||
{<<"Birthday">>, <<"bday">>},
|
||||
{<<"Country">>, <<"ctry">>},
|
||||
{<<"City">>, <<"locality">>},
|
||||
{<<"Email">>, <<"email">>},
|
||||
{<<"Organization Name">>, <<"orgname">>},
|
||||
{<<"Organization Unit">>, <<"orgunit">>}].
|
||||
[{?T("Jabber ID"), <<"jid">>},
|
||||
{?T("Full Name"), <<"fn">>},
|
||||
{?T("Name"), <<"first">>},
|
||||
{?T("Middle Name"), <<"middle">>},
|
||||
{?T("Family Name"), <<"last">>},
|
||||
{?T("Nickname"), <<"nick">>},
|
||||
{?T("Birthday"), <<"bday">>},
|
||||
{?T("Country"), <<"ctry">>},
|
||||
{?T("City"), <<"locality">>},
|
||||
{?T("Email"), <<"email">>},
|
||||
{?T("Organization Name"), <<"orgname">>},
|
||||
{?T("Organization Unit"), <<"orgunit">>}].
|
||||
|
||||
remove_user(LUser, LServer) ->
|
||||
US = {LUser, LServer},
|
||||
|
||||
+25
-24
@@ -37,6 +37,7 @@
|
||||
-include("mod_vcard.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("ejabberd_sql_pt.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
@@ -150,32 +151,32 @@ search(LServer, Data, AllowReturnAll, MaxMatch) ->
|
||||
end.
|
||||
|
||||
search_fields(_LServer) ->
|
||||
[{<<"User">>, <<"user">>},
|
||||
{<<"Full Name">>, <<"fn">>},
|
||||
{<<"Name">>, <<"first">>},
|
||||
{<<"Middle Name">>, <<"middle">>},
|
||||
{<<"Family Name">>, <<"last">>},
|
||||
{<<"Nickname">>, <<"nick">>},
|
||||
{<<"Birthday">>, <<"bday">>},
|
||||
{<<"Country">>, <<"ctry">>},
|
||||
{<<"City">>, <<"locality">>},
|
||||
{<<"Email">>, <<"email">>},
|
||||
{<<"Organization Name">>, <<"orgname">>},
|
||||
{<<"Organization Unit">>, <<"orgunit">>}].
|
||||
[{?T("User"), <<"user">>},
|
||||
{?T("Full Name"), <<"fn">>},
|
||||
{?T("Name"), <<"first">>},
|
||||
{?T("Middle Name"), <<"middle">>},
|
||||
{?T("Family Name"), <<"last">>},
|
||||
{?T("Nickname"), <<"nick">>},
|
||||
{?T("Birthday"), <<"bday">>},
|
||||
{?T("Country"), <<"ctry">>},
|
||||
{?T("City"), <<"locality">>},
|
||||
{?T("Email"), <<"email">>},
|
||||
{?T("Organization Name"), <<"orgname">>},
|
||||
{?T("Organization Unit"), <<"orgunit">>}].
|
||||
|
||||
search_reported(_LServer) ->
|
||||
[{<<"Jabber ID">>, <<"jid">>},
|
||||
{<<"Full Name">>, <<"fn">>},
|
||||
{<<"Name">>, <<"first">>},
|
||||
{<<"Middle Name">>, <<"middle">>},
|
||||
{<<"Family Name">>, <<"last">>},
|
||||
{<<"Nickname">>, <<"nick">>},
|
||||
{<<"Birthday">>, <<"bday">>},
|
||||
{<<"Country">>, <<"ctry">>},
|
||||
{<<"City">>, <<"locality">>},
|
||||
{<<"Email">>, <<"email">>},
|
||||
{<<"Organization Name">>, <<"orgname">>},
|
||||
{<<"Organization Unit">>, <<"orgunit">>}].
|
||||
[{?T("Jabber ID"), <<"jid">>},
|
||||
{?T("Full Name"), <<"fn">>},
|
||||
{?T("Name"), <<"first">>},
|
||||
{?T("Middle Name"), <<"middle">>},
|
||||
{?T("Family Name"), <<"last">>},
|
||||
{?T("Nickname"), <<"nick">>},
|
||||
{?T("Birthday"), <<"bday">>},
|
||||
{?T("Country"), <<"ctry">>},
|
||||
{?T("City"), <<"locality">>},
|
||||
{?T("Email"), <<"email">>},
|
||||
{?T("Organization Name"), <<"orgname">>},
|
||||
{?T("Organization Unit"), <<"orgunit">>}].
|
||||
|
||||
remove_user(LUser, LServer) ->
|
||||
ejabberd_sql:sql_transaction(
|
||||
|
||||
+50
-24
@@ -30,8 +30,8 @@
|
||||
%% gen_mod callbacks
|
||||
-export([start/2, stop/1, reload/3]).
|
||||
|
||||
-export([update_presence/1, vcard_set/3, remove_user/2,
|
||||
mod_opt_type/1, depends/2]).
|
||||
-export([update_presence/1, vcard_set/1, remove_user/2,
|
||||
user_send_packet/1, mod_opt_type/1, depends/2]).
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
-include("logger.hrl").
|
||||
@@ -47,15 +47,19 @@ start(Host, Opts) ->
|
||||
init_cache(Host, Opts),
|
||||
ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE,
|
||||
update_presence, 100),
|
||||
ejabberd_hooks:add(vcard_set, Host, ?MODULE, vcard_set,
|
||||
100),
|
||||
ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
|
||||
user_send_packet, 50),
|
||||
ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_set,
|
||||
90),
|
||||
ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50).
|
||||
|
||||
stop(Host) ->
|
||||
ejabberd_hooks:delete(c2s_self_presence, Host,
|
||||
?MODULE, update_presence, 100),
|
||||
ejabberd_hooks:delete(vcard_set, Host, ?MODULE,
|
||||
vcard_set, 100),
|
||||
ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
|
||||
user_send_packet, 50),
|
||||
ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE,
|
||||
vcard_set, 90),
|
||||
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50).
|
||||
|
||||
reload(Host, NewOpts, _OldOpts) ->
|
||||
@@ -71,16 +75,33 @@ depends(_Host, _Opts) ->
|
||||
-> {presence(), ejabberd_c2s:state()}.
|
||||
update_presence({#presence{type = available} = Pres,
|
||||
#{jid := #jid{luser = LUser, lserver = LServer}} = State}) ->
|
||||
Hash = get_xupdate(LUser, LServer),
|
||||
Pres1 = xmpp:set_subtag(Pres, #vcard_xupdate{hash = Hash}),
|
||||
Pres1 = case get_xupdate(LUser, LServer) of
|
||||
undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{});
|
||||
XUpdate -> xmpp:set_subtag(Pres, XUpdate)
|
||||
end,
|
||||
{Pres1, State};
|
||||
update_presence(Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec vcard_set(binary(), binary(), xmlel()) -> ok.
|
||||
vcard_set(LUser, LServer, _VCARD) ->
|
||||
-spec user_send_packet({presence(), ejabberd_c2s:state()})
|
||||
-> {presence(), ejabberd_c2s:state()}.
|
||||
user_send_packet({#presence{type = available,
|
||||
to = #jid{luser = U, lserver = S,
|
||||
lresource = <<"">>}},
|
||||
#{jid := #jid{luser = U, lserver = S}}} = Acc) ->
|
||||
%% This is processed by update_presence/2 explicitly, we don't
|
||||
%% want to call this multiple times for performance reasons
|
||||
Acc;
|
||||
user_send_packet(Acc) ->
|
||||
update_presence(Acc).
|
||||
|
||||
-spec vcard_set(iq()) -> iq().
|
||||
vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) ->
|
||||
ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}),
|
||||
ejabberd_sm:force_update_presence({LUser, LServer}).
|
||||
ejabberd_sm:force_update_presence({LUser, LServer}),
|
||||
IQ;
|
||||
vcard_set(Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec remove_user(binary(), binary()) -> ok.
|
||||
remove_user(User, Server) ->
|
||||
@@ -91,7 +112,7 @@ remove_user(User, Server) ->
|
||||
%%====================================================================
|
||||
%% Storage
|
||||
%%====================================================================
|
||||
-spec get_xupdate(binary(), binary()) -> binary() | undefined.
|
||||
-spec get_xupdate(binary(), binary()) -> vcard_xupdate() | undefined.
|
||||
get_xupdate(LUser, LServer) ->
|
||||
Result = case use_cache(LServer) of
|
||||
true ->
|
||||
@@ -102,11 +123,12 @@ get_xupdate(LUser, LServer) ->
|
||||
db_get_xupdate(LUser, LServer)
|
||||
end,
|
||||
case Result of
|
||||
{ok, Hash} -> Hash;
|
||||
error -> undefined
|
||||
{ok, external} -> undefined;
|
||||
{ok, Hash} -> #vcard_xupdate{hash = Hash};
|
||||
error -> #vcard_xupdate{}
|
||||
end.
|
||||
|
||||
-spec db_get_xupdate(binary(), binary()) -> {ok, binary()} | error.
|
||||
-spec db_get_xupdate(binary(), binary()) -> {ok, binary() | external} | error.
|
||||
db_get_xupdate(LUser, LServer) ->
|
||||
case mod_vcard:get_vcard(LUser, LServer) of
|
||||
[VCard] ->
|
||||
@@ -147,17 +169,21 @@ use_cache(Host) ->
|
||||
Host, ?MODULE, use_cache,
|
||||
ejabberd_config:use_cache(Host)).
|
||||
|
||||
-spec compute_hash(xmlel()) -> binary().
|
||||
-spec compute_hash(xmlel()) -> binary() | external.
|
||||
compute_hash(VCard) ->
|
||||
case fxml:get_path_s(VCard,
|
||||
[{elem, <<"PHOTO">>},
|
||||
{elem, <<"BINVAL">>},
|
||||
cdata]) of
|
||||
<<>> ->
|
||||
case fxml:get_subtag(VCard, <<"PHOTO">>) of
|
||||
false ->
|
||||
<<>>;
|
||||
BinVal ->
|
||||
try str:sha(base64:decode(BinVal))
|
||||
catch _:badarg -> <<>>
|
||||
Photo ->
|
||||
try xmpp:decode(Photo, ?NS_VCARD, []) of
|
||||
#vcard_photo{binval = <<_, _/binary>> = BinVal} ->
|
||||
str:sha(BinVal);
|
||||
#vcard_photo{extval = <<_, _/binary>>} ->
|
||||
external;
|
||||
_ ->
|
||||
<<>>
|
||||
catch _:{xmpp_codec, _} ->
|
||||
<<>>
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
+90
-22
@@ -376,23 +376,26 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
|
||||
true ->
|
||||
if MaxItems > 0 ->
|
||||
Now = p1_time_compat:timestamp(),
|
||||
PubId = {Now, SubKey},
|
||||
Item = case get_item(Nidx, ItemId) of
|
||||
{result, OldItem} ->
|
||||
OldItem#pubsub_item{modification = PubId,
|
||||
payload = Payload};
|
||||
case get_item(Nidx, ItemId) of
|
||||
{result, #pubsub_item{creation = {_, GenKey}} = OldItem} ->
|
||||
set_item(OldItem#pubsub_item{
|
||||
modification = {Now, SubKey},
|
||||
payload = Payload}),
|
||||
{result, {default, broadcast, []}};
|
||||
{result, _} ->
|
||||
{error, xmpp:err_forbidden()};
|
||||
_ ->
|
||||
#pubsub_item{itemid = {ItemId, Nidx},
|
||||
nodeidx = Nidx,
|
||||
creation = {Now, GenKey},
|
||||
modification = PubId,
|
||||
payload = Payload}
|
||||
end,
|
||||
Items = [ItemId | GenState#pubsub_state.items -- [ItemId]],
|
||||
{result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items),
|
||||
set_item(Item),
|
||||
set_state(GenState#pubsub_state{items = NI}),
|
||||
{result, {default, broadcast, OI}};
|
||||
Items = [ItemId | GenState#pubsub_state.items],
|
||||
{result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items),
|
||||
set_state(GenState#pubsub_state{items = NI}),
|
||||
set_item(#pubsub_item{
|
||||
itemid = {ItemId, Nidx},
|
||||
nodeidx = Nidx,
|
||||
creation = {Now, GenKey},
|
||||
modification = {Now, SubKey},
|
||||
payload = Payload}),
|
||||
{result, {default, broadcast, OI}}
|
||||
end;
|
||||
true ->
|
||||
{result, {default, broadcast, []}}
|
||||
end
|
||||
@@ -725,9 +728,53 @@ del_state(#pubsub_state{stateid = {Key, Nidx}, items = Items}) ->
|
||||
%% mod_pubsub module.</p>
|
||||
%% <p>PubSub plugins can store the items where they wants (for example in a
|
||||
%% relational database), or they can even decide not to persist any items.</p>
|
||||
get_items(Nidx, _From, _RSM) ->
|
||||
Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx),
|
||||
{result, {lists:reverse(lists:keysort(#pubsub_item.modification, Items)), undefined}}.
|
||||
get_items(Nidx, _From, undefined) ->
|
||||
RItems = lists:keysort(#pubsub_item.creation,
|
||||
mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)),
|
||||
Count = length(RItems),
|
||||
if Count =< ?MAXITEMS ->
|
||||
{result, {RItems, undefined}};
|
||||
true ->
|
||||
ItemsPage = lists:sublist(RItems, ?MAXITEMS),
|
||||
Rsm = rsm_page(Count, 0, 0, ItemsPage),
|
||||
{result, {ItemsPage, Rsm}}
|
||||
end;
|
||||
|
||||
get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex,
|
||||
'after' = After, before = Before}) ->
|
||||
RItems = lists:keysort(#pubsub_item.creation,
|
||||
mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)),
|
||||
Count = length(RItems),
|
||||
Limit = case Max of
|
||||
undefined -> ?MAXITEMS;
|
||||
_ -> Max
|
||||
end,
|
||||
{Offset, ItemsPage} =
|
||||
case {IncIndex, Before, After} of
|
||||
{I, undefined, undefined} ->
|
||||
SubList = lists:nthtail(I, RItems),
|
||||
{I, lists:sublist(SubList, Limit)};
|
||||
{_, <<>>, undefined} ->
|
||||
%% 2.5 Requesting the Last Page in a Result Set
|
||||
SubList = lists:reverse(RItems),
|
||||
{0, lists:sublist(SubList, Limit)};
|
||||
{_, Stamp, undefined} ->
|
||||
BeforeNow = encode_stamp(Stamp),
|
||||
SubList = lists:dropwhile(
|
||||
fun(#pubsub_item{creation = {Now, _}}) ->
|
||||
Now >= BeforeNow
|
||||
end, lists:reverse(RItems)),
|
||||
{0, lists:sublist(SubList, Limit)};
|
||||
{_, undefined, Stamp} ->
|
||||
AfterNow = encode_stamp(Stamp),
|
||||
SubList = lists:dropwhile(
|
||||
fun(#pubsub_item{creation = {Now, _}}) ->
|
||||
Now =< AfterNow
|
||||
end, RItems),
|
||||
{0, lists:sublist(SubList, Limit)}
|
||||
end,
|
||||
Rsm = rsm_page(Count, IncIndex, Offset, ItemsPage),
|
||||
{result, {ItemsPage, Rsm}}.
|
||||
|
||||
get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) ->
|
||||
SubKey = jid:tolower(JID),
|
||||
@@ -765,9 +812,10 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM
|
||||
get_items(Nidx, JID, RSM)
|
||||
end.
|
||||
|
||||
get_last_items(Nidx, From, Count) when Count > 0 ->
|
||||
{result, {Items, _}} = get_items(Nidx, From, undefined),
|
||||
{result, lists:sublist(Items, Count)};
|
||||
get_last_items(Nidx, _From, Count) when Count > 0 ->
|
||||
Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx),
|
||||
LastItems = lists:reverse(lists:keysort(#pubsub_item.modification, Items)),
|
||||
{result, lists:sublist(LastItems, Count)};
|
||||
get_last_items(_Nidx, _From, _Count) ->
|
||||
{result, []}.
|
||||
|
||||
@@ -876,6 +924,26 @@ first_in_list(Pred, [H | T]) ->
|
||||
_ -> first_in_list(Pred, T)
|
||||
end.
|
||||
|
||||
rsm_page(Count, Index, Offset, Items) ->
|
||||
FirstItem = hd(Items),
|
||||
LastItem = lists:last(Items),
|
||||
First = decode_stamp(element(1, FirstItem#pubsub_item.creation)),
|
||||
Last = decode_stamp(element(1, LastItem#pubsub_item.creation)),
|
||||
#rsm_set{count = Count, index = Index,
|
||||
first = #rsm_first{index = Offset, data = First},
|
||||
last = Last}.
|
||||
|
||||
encode_stamp(Stamp) ->
|
||||
case catch xmpp_util:decode_timestamp(Stamp) of
|
||||
{MS,S,US} -> {MS,S,US};
|
||||
_ -> Stamp
|
||||
end.
|
||||
decode_stamp(Stamp) ->
|
||||
case catch xmpp_util:encode_timestamp(Stamp) of
|
||||
TimeStamp when is_binary(TimeStamp) -> TimeStamp;
|
||||
_ -> Stamp
|
||||
end.
|
||||
|
||||
transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) ->
|
||||
{pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss};
|
||||
transform({pubsub_item, {Id, Nidx}, C, M, P}) ->
|
||||
|
||||
+141
-115
@@ -243,20 +243,31 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
|
||||
if not ((PublishModel == open) or
|
||||
(PublishModel == publishers) and
|
||||
((Affiliation == owner)
|
||||
or (Affiliation == publisher)
|
||||
or (Affiliation == publish_only))
|
||||
or (Affiliation == publisher)
|
||||
or (Affiliation == publish_only))
|
||||
or (Subscribed == true)) ->
|
||||
{error, xmpp:err_forbidden()};
|
||||
true ->
|
||||
if MaxItems > 0 ->
|
||||
PubId = {p1_time_compat:timestamp(), SubKey},
|
||||
set_item(#pubsub_item{itemid = {ItemId, Nidx},
|
||||
creation = {p1_time_compat:timestamp(), GenKey},
|
||||
modification = PubId,
|
||||
payload = Payload}),
|
||||
Items = [ItemId | itemids(Nidx, GenKey) -- [ItemId]],
|
||||
{result, {_, OI}} = remove_extra_items(Nidx, MaxItems, Items),
|
||||
{result, {default, broadcast, OI}};
|
||||
Now = p1_time_compat:timestamp(),
|
||||
case get_item(Nidx, ItemId) of
|
||||
{result, #pubsub_item{creation = {_, GenKey}} = OldItem} ->
|
||||
set_item(OldItem#pubsub_item{
|
||||
modification = {Now, SubKey},
|
||||
payload = Payload}),
|
||||
{result, {default, broadcast, []}};
|
||||
{result, _} ->
|
||||
{error, xmpp:err_forbidden()};
|
||||
_ ->
|
||||
Items = [ItemId | itemids(Nidx, GenKey)],
|
||||
{result, {_NI, OI}} = remove_extra_items(Nidx, MaxItems, Items),
|
||||
set_item(#pubsub_item{
|
||||
itemid = {ItemId, Nidx},
|
||||
creation = {Now, GenKey},
|
||||
modification = {Now, SubKey},
|
||||
payload = Payload}),
|
||||
{result, {default, broadcast, OI}}
|
||||
end;
|
||||
true ->
|
||||
{result, {default, broadcast, []}}
|
||||
end
|
||||
@@ -284,9 +295,23 @@ delete_item(Nidx, Publisher, PublishModel, ItemId) ->
|
||||
if not Allowed ->
|
||||
{error, xmpp:err_forbidden()};
|
||||
true ->
|
||||
case del_item(Nidx, ItemId) of
|
||||
{updated, 1} -> {result, {default, broadcast}};
|
||||
_ -> {error, xmpp:err_item_not_found()}
|
||||
Items = itemids(Nidx, GenKey),
|
||||
case lists:member(ItemId, Items) of
|
||||
true ->
|
||||
case del_item(Nidx, ItemId) of
|
||||
{updated, 1} -> {result, {default, broadcast}};
|
||||
_ -> {error, xmpp:err_item_not_found()}
|
||||
end;
|
||||
false ->
|
||||
case Affiliation of
|
||||
owner ->
|
||||
case del_item(Nidx, ItemId) of
|
||||
{updated, 1} -> {result, {default, broadcast}};
|
||||
_ -> {error, xmpp:err_item_not_found()}
|
||||
end;
|
||||
_ ->
|
||||
{error, xmpp:err_forbidden()}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
@@ -647,95 +672,61 @@ del_state(Nidx, JID) ->
|
||||
" where jid=%(J)s and nodeid=%(Nidx)d")),
|
||||
ok.
|
||||
|
||||
get_items(Nidx, From, undefined) ->
|
||||
MaxItems = case ejabberd_sql:sql_query_t(
|
||||
?SQL("select @(val)s from pubsub_node_option "
|
||||
"where nodeid=%(Nidx)d and name='max_items'")) of
|
||||
{selected, [{Value}]} ->
|
||||
misc:expr_to_term(Value);
|
||||
_ ->
|
||||
?MAXITEMS
|
||||
end,
|
||||
get_items(Nidx, From, #rsm_set{max = MaxItems});
|
||||
get_items(Nidx, _From, undefined) ->
|
||||
SNidx = misc:i2l(Nidx),
|
||||
case ejabberd_sql:sql_query_t(
|
||||
[<<"select itemid, publisher, creation, modification, payload",
|
||||
" from pubsub_item where nodeid='", SNidx/binary, "'",
|
||||
" order by creation asc">>]) of
|
||||
{selected, _, AllItems} ->
|
||||
Count = length(AllItems),
|
||||
if Count =< ?MAXITEMS ->
|
||||
{result, {[raw_to_item(Nidx, RItem) || RItem <- AllItems], undefined}};
|
||||
true ->
|
||||
RItems = lists:sublist(AllItems, ?MAXITEMS),
|
||||
Rsm = rsm_page(Count, 0, 0, RItems),
|
||||
{result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}}
|
||||
end;
|
||||
_ ->
|
||||
{result, {[], undefined}}
|
||||
end;
|
||||
get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex,
|
||||
'after' = After, before = Before}) ->
|
||||
{Way, Order} = if After == <<>> -> {<<"is not">>, <<"desc">>};
|
||||
After /= undefined -> {<<"<">>, <<"desc">>};
|
||||
Before == <<>> -> {<<"is not">>, <<"asc">>};
|
||||
Before /= undefined -> {<<">">>, <<"asc">>};
|
||||
true -> {<<"is not">>, <<"desc">>}
|
||||
end,
|
||||
SNidx = misc:i2l(Nidx),
|
||||
I = if After /= undefined -> After;
|
||||
Before /= undefined -> Before;
|
||||
true -> undefined
|
||||
end,
|
||||
[AttrName, Id] =
|
||||
case I of
|
||||
undefined when IncIndex =/= undefined ->
|
||||
case ejabberd_sql:sql_query_t(
|
||||
[<<"select creation from pubsub_item pi "
|
||||
"where exists ( select count(*) as count1 "
|
||||
"from pubsub_item where nodeid='">>, SNidx,
|
||||
<<"' and creation > pi.creation having count1 = ">>,
|
||||
integer_to_binary(IncIndex), <<" );">>]) of
|
||||
{selected, [_], [[O]]} ->
|
||||
[<<"creation">>, <<"'", O/binary, "'">>];
|
||||
_ ->
|
||||
[<<"creation">>, <<"null">>]
|
||||
end;
|
||||
undefined ->
|
||||
[<<"creation">>, <<"null">>];
|
||||
<<>> ->
|
||||
[<<"creation">>, <<"null">>];
|
||||
I ->
|
||||
[A, B] = str:tokens(ejabberd_sql:escape(I), <<"@">>),
|
||||
[A, <<"'", B/binary, "'">>]
|
||||
end,
|
||||
Count = case ejabberd_sql:sql_query_t(
|
||||
[<<"select count(*) from pubsub_item where nodeid='">>,
|
||||
SNidx, <<"';">>]) of
|
||||
{selected, [_], [[C]]} -> binary_to_integer(C);
|
||||
Count = case catch ejabberd_sql:sql_query_t(
|
||||
?SQL("select @(count(itemid))d from pubsub_item"
|
||||
" where nodeid=%(Nidx)d")) of
|
||||
{selected, [{C}]} -> C;
|
||||
_ -> 0
|
||||
end,
|
||||
Offset = case {IncIndex, Before, After} of
|
||||
{I, undefined, undefined} when is_integer(I) -> I;
|
||||
_ -> 0
|
||||
end,
|
||||
Limit = case Max of
|
||||
undefined -> ?MAXITEMS;
|
||||
_ -> Max
|
||||
end,
|
||||
Filters = rsm_filters(misc:i2l(Nidx), Before, After),
|
||||
Query = fun(mssql, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
[<<"select top ">>, integer_to_binary(Max),
|
||||
<<" itemid, publisher, creation, modification, payload "
|
||||
"from pubsub_item where nodeid='">>, SNidx,
|
||||
<<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>,
|
||||
AttrName, <<" ">>, Order, <<";">>]);
|
||||
[<<"select top ", (integer_to_binary(Limit))/binary,
|
||||
" itemid, publisher, creation, modification, payload",
|
||||
" from pubsub_item", Filters/binary>>]);
|
||||
%OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;
|
||||
(_, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
[<<"select itemid, publisher, creation, modification, payload "
|
||||
"from pubsub_item where nodeid='">>, SNidx,
|
||||
<<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>,
|
||||
AttrName, <<" ">>, Order, <<" limit ">>,
|
||||
integer_to_binary(Max), <<" ;">>])
|
||||
[<<"select itemid, publisher, creation, modification, payload",
|
||||
" from pubsub_item", Filters/binary,
|
||||
" limit ", (integer_to_binary(Limit))/binary,
|
||||
" offset ", (integer_to_binary(Offset))/binary>>])
|
||||
end,
|
||||
case ejabberd_sql:sql_query_t(Query) of
|
||||
{selected, _, []} ->
|
||||
{result, {[], #rsm_set{count = Count}}};
|
||||
{selected, [<<"itemid">>, <<"publisher">>, <<"creation">>,
|
||||
<<"modification">>, <<"payload">>], RItems} ->
|
||||
case RItems of
|
||||
[[_, _, _, F, _]|_] ->
|
||||
Index = case catch ejabberd_sql:sql_query_t(
|
||||
[<<"select count(*) from pubsub_item "
|
||||
"where nodeid='">>, SNidx, <<"' and ">>,
|
||||
AttrName, <<" > '">>, F, <<"';">>]) of
|
||||
{selected, [_], [[In]]} -> binary_to_integer(In);
|
||||
_ -> 0
|
||||
end,
|
||||
[_, _, _, L, _] = lists:last(RItems),
|
||||
RsmOut = #rsm_set{count = Count,
|
||||
index = Index,
|
||||
first = #rsm_first{
|
||||
index = Index,
|
||||
data = <<"creation@", F/binary>>},
|
||||
last = <<"creation@", L/binary>>},
|
||||
{result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], RsmOut}};
|
||||
[] ->
|
||||
{result, {[], #rsm_set{count = Count}}}
|
||||
end;
|
||||
Rsm = rsm_page(Count, IncIndex, Offset, RItems),
|
||||
{result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}};
|
||||
_ ->
|
||||
{result, {[], undefined}}
|
||||
end.
|
||||
@@ -773,24 +764,24 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM
|
||||
get_items(Nidx, JID, RSM)
|
||||
end.
|
||||
|
||||
get_last_items(Nidx, _From, Count) ->
|
||||
Limit = misc:i2l(Count),
|
||||
get_last_items(Nidx, _From, Limit) ->
|
||||
SNidx = misc:i2l(Nidx),
|
||||
Query = fun(mssql, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
[<<"select top ">>, Limit,
|
||||
<<" itemid, publisher, creation, modification, payload "
|
||||
"from pubsub_item where nodeid='">>, SNidx,
|
||||
<<"' order by modification desc ;">>]);
|
||||
[<<"select top ", (integer_to_binary(Limit))/binary,
|
||||
" itemid, publisher, creation, modification, payload",
|
||||
" from pubsub_item where nodeid='", SNidx/binary,
|
||||
"' order by modification desc">>]);
|
||||
(_, _) ->
|
||||
ejabberd_sql:sql_query_t(
|
||||
[<<"select itemid, publisher, creation, modification, payload "
|
||||
"from pubsub_item where nodeid='">>, SNidx,
|
||||
<<"' order by modification desc limit ">>, Limit, <<";">>])
|
||||
[<<"select itemid, publisher, creation, modification, payload",
|
||||
" from pubsub_item where nodeid='", SNidx/binary,
|
||||
"' order by modification desc ",
|
||||
" limit ", (integer_to_binary(Limit))/binary>>])
|
||||
end,
|
||||
case catch ejabberd_sql:sql_query_t(Query) of
|
||||
{selected,
|
||||
[<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} ->
|
||||
{selected, [<<"itemid">>, <<"publisher">>, <<"creation">>,
|
||||
<<"modification">>, <<"payload">>], RItems} ->
|
||||
{result, [raw_to_item(Nidx, RItem) || RItem <- RItems]};
|
||||
_ ->
|
||||
{result, []}
|
||||
@@ -798,9 +789,9 @@ get_last_items(Nidx, _From, Count) ->
|
||||
|
||||
get_item(Nidx, ItemId) ->
|
||||
case catch ejabberd_sql:sql_query_t(
|
||||
?SQL("select @(itemid)s, @(publisher)s, @(creation)s,"
|
||||
" @(modification)s, @(payload)s from pubsub_item"
|
||||
" where nodeid=%(Nidx)d and itemid=%(ItemId)s"))
|
||||
?SQL("select @(itemid)s, @(publisher)s, @(creation)s,"
|
||||
" @(modification)s, @(payload)s from pubsub_item"
|
||||
" where nodeid=%(Nidx)d and itemid=%(ItemId)s"))
|
||||
of
|
||||
{selected, [RItem]} ->
|
||||
{result, raw_to_item(Nidx, RItem)};
|
||||
@@ -850,11 +841,8 @@ set_item(Item) ->
|
||||
P = encode_jid(JID),
|
||||
Payload = Item#pubsub_item.payload,
|
||||
XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>),
|
||||
S = fun ({T1, T2, T3}) ->
|
||||
str:join([misc:i2l(T1, 6), misc:i2l(T2, 6), misc:i2l(T3, 6)], <<":">>)
|
||||
end,
|
||||
SM = S(M),
|
||||
SC = S(C),
|
||||
SM = encode_now(M),
|
||||
SC = encode_now(C),
|
||||
?SQL_UPSERT_T(
|
||||
"pubsub_item",
|
||||
["!nodeid=%(Nidx)d",
|
||||
@@ -1029,15 +1017,53 @@ raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) ->
|
||||
raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML});
|
||||
raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) ->
|
||||
JID = decode_jid(SJID),
|
||||
ToTime = fun (Str) ->
|
||||
[T1, T2, T3] = str:tokens(Str, <<":">>),
|
||||
{misc:l2i(T1), misc:l2i(T2), misc:l2i(T3)}
|
||||
end,
|
||||
Payload = case fxml_stream:parse_element(XML) of
|
||||
{error, _Reason} -> [];
|
||||
El -> [El]
|
||||
end,
|
||||
#pubsub_item{itemid = {ItemId, Nidx},
|
||||
creation = {ToTime(Creation), jid:remove_resource(JID)},
|
||||
modification = {ToTime(Modification), JID},
|
||||
creation = {decode_now(Creation), jid:remove_resource(JID)},
|
||||
modification = {decode_now(Modification), JID},
|
||||
payload = Payload}.
|
||||
|
||||
rsm_filters(SNidx, undefined, undefined) ->
|
||||
<<" where nodeid='", SNidx/binary, "'",
|
||||
" order by creation asc">>;
|
||||
rsm_filters(SNidx, undefined, After) ->
|
||||
<<" where nodeid='", SNidx/binary, "'",
|
||||
" and creation>'", (encode_stamp(After))/binary, "'",
|
||||
" order by creation asc">>;
|
||||
rsm_filters(SNidx, <<>>, undefined) ->
|
||||
%% 2.5 Requesting the Last Page in a Result Set
|
||||
<<" where nodeid='", SNidx/binary, "'",
|
||||
" order by creation desc">>;
|
||||
rsm_filters(SNidx, Before, undefined) ->
|
||||
<<" where nodeid='", SNidx/binary, "'",
|
||||
" and creation<'", (encode_stamp(Before))/binary, "'",
|
||||
" order by creation desc">>.
|
||||
|
||||
rsm_page(Count, Index, Offset, Items) ->
|
||||
First = decode_stamp(lists:nth(3, hd(Items))),
|
||||
Last = decode_stamp(lists:nth(3, lists:last(Items))),
|
||||
#rsm_set{count = Count, index = Index,
|
||||
first = #rsm_first{index = Offset, data = First},
|
||||
last = Last}.
|
||||
|
||||
encode_stamp(Stamp) ->
|
||||
case catch xmpp_util:decode_timestamp(Stamp) of
|
||||
{MS,S,US} -> encode_now({MS,S,US});
|
||||
_ -> Stamp
|
||||
end.
|
||||
decode_stamp(Stamp) ->
|
||||
case catch xmpp_util:encode_timestamp(decode_now(Stamp)) of
|
||||
TimeStamp when is_binary(TimeStamp) -> TimeStamp;
|
||||
_ -> Stamp
|
||||
end.
|
||||
|
||||
encode_now({T1, T2, T3}) ->
|
||||
<<(misc:i2l(T1, 6))/binary, ":",
|
||||
(misc:i2l(T2, 6))/binary, ":",
|
||||
(misc:i2l(T3, 6))/binary>>.
|
||||
decode_now(NowStr) ->
|
||||
[MS, S, US] = binary:split(NowStr, <<":">>, [global]),
|
||||
{binary_to_integer(MS), binary_to_integer(S), binary_to_integer(US)}.
|
||||
|
||||
@@ -518,5 +518,5 @@ deserialize([{_, S}|T], #xmlel{children = Els} = El, Acc) when is_binary(S) ->
|
||||
deserialize(T, El#xmlel{children = [{xmlcdata, S}|Els]}, Acc);
|
||||
deserialize([{_, L}|T], #xmlel{children = Els} = El, Acc) when is_list(L) ->
|
||||
deserialize(T, El#xmlel{children = deserialize(L) ++ Els}, Acc);
|
||||
deserialize([], El, Acc) ->
|
||||
[El|Acc].
|
||||
deserialize([], #xmlel{children = Els} = El, Acc) ->
|
||||
[El#xmlel{children = lists:reverse(Els)}|Acc].
|
||||
|
||||
@@ -32,6 +32,20 @@
|
||||
|
||||
-define(THRESHOLD, 16#10000000000000000).
|
||||
|
||||
-ifdef(RAND_UNIFORM).
|
||||
get_string() ->
|
||||
R = rand:uniform(?THRESHOLD),
|
||||
integer_to_binary(R).
|
||||
|
||||
uniform() ->
|
||||
rand:uniform().
|
||||
|
||||
uniform(N) ->
|
||||
rand:uniform(N).
|
||||
|
||||
uniform(N, M) ->
|
||||
rand:uniform(M-N+1) + N-1.
|
||||
-else.
|
||||
get_string() ->
|
||||
R = crypto:rand_uniform(0, ?THRESHOLD),
|
||||
integer_to_binary(R).
|
||||
@@ -44,6 +58,7 @@ uniform(N) ->
|
||||
|
||||
uniform(N, M) ->
|
||||
crypto:rand_uniform(N, M+1).
|
||||
-endif.
|
||||
|
||||
-ifdef(STRONG_RAND_BYTES).
|
||||
bytes(N) ->
|
||||
|
||||
@@ -671,7 +671,7 @@ process_stream_established(#{stream_state := StateName} = State)
|
||||
when StateName == disconnected; StateName == established ->
|
||||
State;
|
||||
process_stream_established(#{mod := Mod} = State) ->
|
||||
State1 = State#{stream_authenticated := true,
|
||||
State1 = State#{stream_authenticated => true,
|
||||
stream_state => established,
|
||||
stream_timeout => infinity},
|
||||
try Mod:handle_stream_established(State1)
|
||||
|
||||
+68
-29
@@ -51,13 +51,15 @@
|
||||
-type host_port() :: {inet:hostname(), inet:port_number()}.
|
||||
-type ip_port() :: {inet:ip_address(), inet:port_number()}.
|
||||
-type network_error() :: {error, inet:posix() | inet_res:res_error()}.
|
||||
-type tls_error_reason() :: inet:posix() | atom() | binary().
|
||||
-type socket_error_reason() :: inet:posix() | atom().
|
||||
-type stop_reason() :: {idna, bad_string} |
|
||||
{dns, inet:posix() | inet_res:res_error()} |
|
||||
{stream, reset | {in | out, stream_error()}} |
|
||||
{tls, inet:posix() | atom() | binary()} |
|
||||
{tls, tls_error_reason()} |
|
||||
{pkix, binary()} |
|
||||
{auth, atom() | binary() | string()} |
|
||||
{socket, inet:posix() | atom()} |
|
||||
{socket, socket_error_reason()} |
|
||||
internal_failure.
|
||||
-export_type([state/0, stop_reason/0]).
|
||||
-callback init(list()) -> {ok, state()} | {error, term()} | ignore.
|
||||
@@ -276,19 +278,20 @@ handle_cast(connect, #{remote_server := RemoteServer,
|
||||
process_stream_end({idna, bad_string}, State);
|
||||
ASCIIName ->
|
||||
case resolve(binary_to_list(ASCIIName), State) of
|
||||
{ok, AddrPorts} ->
|
||||
case connect(AddrPorts, State) of
|
||||
{{ok, AddrPorts}, Encrypted} ->
|
||||
case connect(AddrPorts, State, Encrypted) of
|
||||
{ok, Socket, AddrPort} ->
|
||||
SocketMonitor = SockMod:monitor(Socket),
|
||||
State1 = State#{ip => AddrPort,
|
||||
socket => Socket,
|
||||
stream_encrypted => Encrypted,
|
||||
socket_monitor => SocketMonitor},
|
||||
State2 = State1#{stream_state => wait_for_stream},
|
||||
send_header(State2);
|
||||
{error, Why} ->
|
||||
process_stream_end({socket, Why}, State)
|
||||
{error, {Class, Why}} ->
|
||||
process_stream_end({Class, Why}, State)
|
||||
end;
|
||||
{error, Why} ->
|
||||
{{error, Why}, _} ->
|
||||
process_stream_end({dns, Why}, State)
|
||||
end
|
||||
end);
|
||||
@@ -578,11 +581,8 @@ process_sasl_mechanisms(Mechs, #{user := User, server := Server} = State) ->
|
||||
end.
|
||||
|
||||
-spec process_starttls(state()) -> state().
|
||||
process_starttls(#{sockmod := SockMod, socket := Socket, mod := Mod} = State) ->
|
||||
TLSOpts = try Mod:tls_options(State)
|
||||
catch _:undef -> []
|
||||
end,
|
||||
case SockMod:starttls(Socket, [connect|TLSOpts]) of
|
||||
process_starttls(#{socket := Socket} = State) ->
|
||||
case starttls(Socket, State) of
|
||||
{ok, TLSSocket} ->
|
||||
State1 = State#{socket => TLSSocket,
|
||||
stream_id => new_id(),
|
||||
@@ -770,6 +770,19 @@ close_socket(State) ->
|
||||
State#{stream_timeout => infinity,
|
||||
stream_state => disconnected}.
|
||||
|
||||
-spec starttls(term(), state()) -> {ok, term()} | {error, tls_error_reason()}.
|
||||
starttls(Socket, #{sockmod := SockMod, mod := Mod,
|
||||
xmlns := NS, remote_server := RemoteServer} = State) ->
|
||||
TLSOpts = try Mod:tls_options(State)
|
||||
catch _:undef -> []
|
||||
end,
|
||||
SNI = idna_to_ascii(RemoteServer),
|
||||
ALPN = case NS of
|
||||
?NS_SERVER -> <<"xmpp-server">>;
|
||||
?NS_CLIENT -> <<"xmpp-client">>
|
||||
end,
|
||||
SockMod:starttls(Socket, [connect, {sni, SNI}, {alpn, [ALPN]}|TLSOpts]).
|
||||
|
||||
-spec select_lang(binary(), binary()) -> binary().
|
||||
select_lang(Lang, <<"">>) -> Lang;
|
||||
select_lang(_, Lang) -> Lang.
|
||||
@@ -841,47 +854,61 @@ idna_to_ascii(Host) ->
|
||||
{error, _} -> ejabberd_idna:domain_utf8_to_ascii(Host)
|
||||
end.
|
||||
|
||||
-spec resolve(string(), state()) -> {ok, [ip_port()]} | network_error().
|
||||
-spec resolve(string(), state()) -> {{ok, [ip_port()]} | network_error(), boolean()}.
|
||||
resolve(Host, State) ->
|
||||
case srv_lookup(Host, State) of
|
||||
{error, _Reason} ->
|
||||
{{error, _Reason}, _} ->
|
||||
DefaultPort = get_default_port(State),
|
||||
a_lookup([{Host, DefaultPort}], State);
|
||||
{ok, HostPorts} ->
|
||||
a_lookup(HostPorts, State)
|
||||
{a_lookup([{Host, DefaultPort}], State), false};
|
||||
{{ok, HostPorts}, TLS} ->
|
||||
{a_lookup(HostPorts, State), TLS}
|
||||
end.
|
||||
|
||||
-spec srv_lookup(string(), state()) -> {ok, [host_port()]} | network_error().
|
||||
-spec srv_lookup(string(), state()) -> {{ok, [host_port()]} | network_error(), boolean()}.
|
||||
srv_lookup(_Host, #{xmlns := ?NS_COMPONENT}) ->
|
||||
%% Do not attempt to lookup SRV for component connections
|
||||
{error, nxdomain};
|
||||
{{error, nxdomain}, false};
|
||||
srv_lookup(Host, State) ->
|
||||
%% Only perform SRV lookups for FQDN names
|
||||
case string:chr(Host, $.) of
|
||||
0 ->
|
||||
{error, nxdomain};
|
||||
{{error, nxdomain}, false};
|
||||
_ ->
|
||||
case inet:parse_address(Host) of
|
||||
{ok, _} ->
|
||||
{error, nxdomain};
|
||||
{{error, nxdomain}, false};
|
||||
{error, _} ->
|
||||
Timeout = get_dns_timeout(State),
|
||||
Retries = get_dns_retries(State),
|
||||
srv_lookup(Host, Timeout, Retries)
|
||||
case is_starttls_available(State) of
|
||||
true ->
|
||||
case srv_lookup("_xmpps-server._tcp." ++ Host,
|
||||
Timeout, Retries) of
|
||||
{error, _} ->
|
||||
{srv_lookup("_xmpp-server._tcp." ++ Host,
|
||||
Timeout, Retries),
|
||||
false};
|
||||
{ok, Res} ->
|
||||
{{ok, Res}, true}
|
||||
end;
|
||||
false ->
|
||||
{srv_lookup("_xmpp-server._tcp." ++ Host,
|
||||
Timeout, Retries),
|
||||
false}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
-spec srv_lookup(string(), timeout(), integer()) ->
|
||||
{ok, [host_port()]} | network_error().
|
||||
srv_lookup(_Host, _Timeout, Retries) when Retries < 1 ->
|
||||
srv_lookup(_SRVName, _Timeout, Retries) when Retries < 1 ->
|
||||
{error, timeout};
|
||||
srv_lookup(Host, Timeout, Retries) ->
|
||||
SRVName = "_xmpp-server._tcp." ++ Host,
|
||||
srv_lookup(SRVName, Timeout, Retries) ->
|
||||
case inet_res:getbyname(SRVName, srv, Timeout) of
|
||||
{ok, HostEntry} ->
|
||||
host_entry_to_host_ports(HostEntry);
|
||||
{error, timeout} ->
|
||||
srv_lookup(Host, Timeout, Retries - 1);
|
||||
srv_lookup(SRVName, Timeout, Retries - 1);
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
end.
|
||||
@@ -971,10 +998,22 @@ host_entry_to_addr_ports(#hostent{h_addr_list = AddrList}, Port) ->
|
||||
_ -> {ok, AddrPorts}
|
||||
end.
|
||||
|
||||
-spec connect([ip_port()], state()) -> {ok, term(), ip_port()} | network_error().
|
||||
connect(AddrPorts, #{sockmod := SockMod} = State) ->
|
||||
-spec connect([ip_port()], state(), boolean()) -> {ok, term(), ip_port()} |
|
||||
{error, {socket, socket_error_reason()}} |
|
||||
{error, {tls, tls_error_reason()}}.
|
||||
connect(AddrPorts, #{sockmod := SockMod} = State, TLS) ->
|
||||
Timeout = get_connect_timeout(State),
|
||||
connect(AddrPorts, SockMod, Timeout, {error, nxdomain}).
|
||||
case connect(AddrPorts, SockMod, Timeout, {error, nxdomain}) of
|
||||
{ok, Socket, AddrPort} when TLS ->
|
||||
case starttls(Socket, State) of
|
||||
{ok, TLSSocket} -> {ok, TLSSocket, AddrPort};
|
||||
{error, Why} -> {error, {tls, Why}}
|
||||
end;
|
||||
{ok, _Socket, _AddrPort} = OK ->
|
||||
OK;
|
||||
{error, Why} ->
|
||||
{error, {socket, Why}}
|
||||
end.
|
||||
|
||||
-spec connect([ip_port()], module(), timeout(), network_error()) ->
|
||||
{ok, term(), ip_port()} | network_error().
|
||||
|
||||
+11
-3
@@ -258,7 +258,8 @@ master_slave_cases() ->
|
||||
{privacy_master_slave, [sequence],
|
||||
[master_slave_test(deny_bare_jid),
|
||||
master_slave_test(deny_full_jid),
|
||||
master_slave_test(deny_server_jid),
|
||||
master_slave_test(deny_server_bare_jid),
|
||||
master_slave_test(deny_server_full_jid),
|
||||
master_slave_test(deny_group),
|
||||
master_slave_test(deny_sub_both),
|
||||
master_slave_test(deny_sub_from),
|
||||
@@ -285,11 +286,18 @@ deny_full_jid_master(Config) ->
|
||||
deny_full_jid_slave(Config) ->
|
||||
deny_slave(Config).
|
||||
|
||||
deny_server_jid_master(Config) ->
|
||||
deny_server_bare_jid_master(Config) ->
|
||||
{_, Server, _} = jid:tolower(?config(peer, Config)),
|
||||
deny_master(Config, {jid, Server}).
|
||||
|
||||
deny_server_jid_slave(Config) ->
|
||||
deny_server_bare_jid_slave(Config) ->
|
||||
deny_slave(Config).
|
||||
|
||||
deny_server_full_jid_master(Config) ->
|
||||
{_, Server, Resource} = jid:tolower(?config(peer, Config)),
|
||||
deny_master(Config, {jid, jid:encode({<<"">>, Server, Resource})}).
|
||||
|
||||
deny_server_full_jid_slave(Config) ->
|
||||
deny_slave(Config).
|
||||
|
||||
deny_group_master(Config) ->
|
||||
|
||||
+2
-2
@@ -134,8 +134,8 @@ mam_slave(Config) ->
|
||||
self_presence(Config, available),
|
||||
ct:comment("Receiving message from offline storage"),
|
||||
recv_test_message(Config),
|
||||
ct:comment("Re-enabling push notifications"),
|
||||
ok = enable_push(Config),
|
||||
%% Don't re-enable push notifications, otherwise the notification would be
|
||||
%% suppressed while the slave is online.
|
||||
ct:comment("Enabling MAM"),
|
||||
ok = enable_mam(Config),
|
||||
ct:comment("Letting the master know that we're ready"),
|
||||
|
||||
Executable
+300
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env escript
|
||||
%% -*- erlang -*-
|
||||
%%! -pa ebin
|
||||
|
||||
main([Dir]) ->
|
||||
Txts =
|
||||
filelib:fold_files(
|
||||
Dir, ".+\.beam\$", false,
|
||||
fun(BeamFile, Res) ->
|
||||
Mod = mod(BeamFile),
|
||||
ErlFile = filename:join("src", Mod ++ ".erl"),
|
||||
case get_forms(BeamFile, ErlFile) of
|
||||
{ok, BeamForms, ErlForms} ->
|
||||
process_forms(BeamForms, Mod, application) ++
|
||||
process_forms(ErlForms, Mod, macro) ++ Res;
|
||||
_Err ->
|
||||
Res
|
||||
end
|
||||
end, []),
|
||||
Dict = lists:foldl(
|
||||
fun({B, Meta}, Acc) ->
|
||||
dict:update(
|
||||
binary_to_list(B),
|
||||
fun(OldMeta) ->
|
||||
lists:usort([Meta|OldMeta])
|
||||
end,
|
||||
[Meta], Acc)
|
||||
end, dict:new(), Txts),
|
||||
generate_pot(Dict).
|
||||
|
||||
process_forms(Forms, Mod, Type) ->
|
||||
Tree = erl_syntax:form_list(Forms),
|
||||
erl_syntax_lib:fold_subtrees(
|
||||
fun(Form, Acc) ->
|
||||
case erl_syntax:type(Form) of
|
||||
function ->
|
||||
case map(Form, Mod, Type) of
|
||||
[] ->
|
||||
Acc;
|
||||
Vars ->
|
||||
Vars ++ Acc
|
||||
end;
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end, [], Tree).
|
||||
|
||||
map(Tree, Mod, Type) ->
|
||||
Vars = erl_syntax_lib:fold(
|
||||
fun(Form, Acc) ->
|
||||
case erl_syntax:type(Form) of
|
||||
Type when Type == application ->
|
||||
analyze_app(Form, Mod) ++ Acc;
|
||||
Type when Type == macro ->
|
||||
analyze_macro(Form, Mod) ++ Acc;
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end, [], Tree),
|
||||
Bins = lists:flatmap(
|
||||
fun({Var, Pos}) when is_atom(Var) ->
|
||||
Res = erl_syntax_lib:fold(
|
||||
fun(Form, Acc) ->
|
||||
case process_match_expr(
|
||||
Form, Var, Mod) of
|
||||
{ok, Binary, NewPos} ->
|
||||
[{Binary, NewPos}|Acc];
|
||||
error ->
|
||||
Acc
|
||||
end
|
||||
end, [], Tree),
|
||||
case Res of
|
||||
[] ->
|
||||
log("~s:~p: unresolved variable: ~s~n",
|
||||
[Mod, Pos, Var]);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
Res;
|
||||
({Var, Pos}) when is_binary(Var) ->
|
||||
[{Var, Pos}]
|
||||
end, lists:usort(Vars)),
|
||||
[{B, {Mod, Pos}} || {B, Pos} <- Bins, B /= <<"">>].
|
||||
|
||||
process_match_expr(Form, Var, Mod) ->
|
||||
case erl_syntax:type(Form) of
|
||||
match_expr ->
|
||||
Pattern = erl_syntax:match_expr_pattern(Form),
|
||||
Body = erl_syntax:match_expr_body(Form),
|
||||
{V, Expr} =
|
||||
case {erl_syntax:type(Pattern), erl_syntax:type(Body)} of
|
||||
{variable, _} ->
|
||||
{erl_syntax:variable_name(Pattern), Body};
|
||||
{_, variable} ->
|
||||
{erl_syntax:variable_name(Body), Pattern};
|
||||
_ ->
|
||||
{'', none}
|
||||
end,
|
||||
Text = maybe_extract_tuple(Expr),
|
||||
if V == Var ->
|
||||
Pos = erl_syntax:get_pos(Text),
|
||||
try {ok, erl_syntax:concrete(Text), Pos}
|
||||
catch _:_ ->
|
||||
case catch erl_syntax_lib:analyze_application(Text) of
|
||||
{_M, {Fn, 1}} when Fn == format_error;
|
||||
Fn == io_format_error ->
|
||||
error;
|
||||
_ ->
|
||||
log("~s:~p: not a binary: ~s~n",
|
||||
[Mod, Pos, erl_prettypr:format(Text)]),
|
||||
{ok, <<>>, Pos}
|
||||
end
|
||||
end;
|
||||
true ->
|
||||
error
|
||||
end;
|
||||
_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
maybe_extract_tuple(none) ->
|
||||
none;
|
||||
maybe_extract_tuple(Form) ->
|
||||
try
|
||||
tuple = erl_syntax:type(Form),
|
||||
[Text, _] = erl_syntax:tuple_elements(Form),
|
||||
Text
|
||||
catch _:{badmatch, _} ->
|
||||
Form
|
||||
end.
|
||||
|
||||
analyze_app(Form, Mod) ->
|
||||
try
|
||||
{M, {F, A}} = erl_syntax_lib:analyze_application(Form),
|
||||
Args = erl_syntax:application_arguments(Form),
|
||||
Txt = case {M, atom_to_list(F), A, Args} of
|
||||
{xmpp, "err_" ++ _, 2, [T|_]} -> T;
|
||||
{xmpp, "serr_" ++ _, 2, [T|_]} -> T;
|
||||
{xmpp, "mk_text", 2, [T|_]} -> T;
|
||||
{translate, "translate", 2, [_,T|_]} -> T
|
||||
end,
|
||||
Pos = erl_syntax:get_pos(Txt),
|
||||
case erl_syntax:type(Txt) of
|
||||
binary ->
|
||||
try [{erl_syntax:concrete(Txt), Pos}]
|
||||
catch _:_ ->
|
||||
Pos = erl_syntax:get_pos(Txt),
|
||||
log("~s:~p: not a binary: ~s~n",
|
||||
[Mod, Pos, erl_prettypr:format(Txt)]),
|
||||
[]
|
||||
end;
|
||||
variable ->
|
||||
[{erl_syntax:variable_name(Txt), Pos}];
|
||||
application ->
|
||||
Vars = sets:to_list(erl_syntax_lib:variables(Txt)),
|
||||
case Vars of
|
||||
[Var] ->
|
||||
[{Var, Pos}];
|
||||
[_|_] ->
|
||||
log("Too many variables: ~p~n", [Vars]),
|
||||
[];
|
||||
[] ->
|
||||
[]
|
||||
end;
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
catch _:{badmatch, _} ->
|
||||
[];
|
||||
_:{case_clause, _} ->
|
||||
[]
|
||||
end.
|
||||
|
||||
analyze_macro(Form, Mod) ->
|
||||
try
|
||||
Name = erl_syntax:macro_name(Form),
|
||||
variable = erl_syntax:type(Name),
|
||||
'T' = erl_syntax:variable_name(Name),
|
||||
[Txt] = erl_syntax:macro_arguments(Form),
|
||||
string = erl_syntax:type(Txt),
|
||||
Pos = erl_syntax:get_pos(Txt),
|
||||
try [{list_to_binary(erl_syntax:string_value(Txt)), Pos}]
|
||||
catch _:_ ->
|
||||
log("~s:~p: not a binary: ~s~n",
|
||||
[Mod, Pos, erl_prettypr:format(Txt)]),
|
||||
[]
|
||||
end
|
||||
catch _:{badmatch, _} ->
|
||||
[]
|
||||
end.
|
||||
|
||||
generate_pot(Dict) ->
|
||||
io:format("~s~n~n", [pot_header()]),
|
||||
lists:foreach(
|
||||
fun({Msg, Location}) ->
|
||||
S1 = format_location(Location),
|
||||
S2 = format_msg(Msg),
|
||||
io:format("~smsgstr \"\"~n~n", [S1 ++ S2])
|
||||
end, lists:keysort(1, dict:to_list(Dict))).
|
||||
|
||||
format_location([A, B, C|T]) ->
|
||||
format_location_list([A,B,C]) ++ format_location(T);
|
||||
format_location([A, B|T]) ->
|
||||
format_location_list([A,B]) ++ format_location(T);
|
||||
format_location([A|T]) ->
|
||||
format_location_list([A]) ++ format_location(T);
|
||||
format_location([]) ->
|
||||
"".
|
||||
|
||||
format_location_list(L) ->
|
||||
"#: " ++ string:join(
|
||||
lists:map(
|
||||
fun({File, Pos}) ->
|
||||
io_lib:format("~s:~B", [File, Pos])
|
||||
end, L),
|
||||
" ") ++ io_lib:nl().
|
||||
|
||||
format_msg(Bin) ->
|
||||
io_lib:format("msgid \"~s\"~n", [escape(Bin)]).
|
||||
|
||||
escape(Bin) ->
|
||||
lists:map(
|
||||
fun($") -> "\\\"";
|
||||
(C) -> C
|
||||
end, binary_to_list(iolist_to_binary(Bin))).
|
||||
|
||||
pot_header() ->
|
||||
string:join(
|
||||
["msgid \"\"",
|
||||
"msgstr \"\"",
|
||||
"\"Project-Id-Version: 15.11.127\\n\"",
|
||||
"\"X-Language: Language Name\\n\"",
|
||||
"\"Last-Translator: Translator name and contact method\\n\"",
|
||||
"\"MIME-Version: 1.0\\n\"",
|
||||
"\"Content-Type: text/plain; charset=UTF-8\\n\"",
|
||||
"\"Content-Transfer-Encoding: 8bit\\n\""],
|
||||
io_lib:nl()).
|
||||
|
||||
mod(Path) ->
|
||||
filename:rootname(filename:basename(Path)).
|
||||
|
||||
log(Format, Args) ->
|
||||
io:format(standard_error, Format, Args).
|
||||
|
||||
get_forms(BeamFile, ErlFile) ->
|
||||
try
|
||||
{ok, BeamForms} = get_beam_forms(BeamFile),
|
||||
{ok, ErlForms} = get_erl_forms(ErlFile),
|
||||
{ok, BeamForms, ErlForms}
|
||||
catch _:{badmatch, error} ->
|
||||
error
|
||||
end.
|
||||
|
||||
get_beam_forms(File) ->
|
||||
case beam_lib:chunks(File, [abstract_code]) of
|
||||
{ok, {_, List}} ->
|
||||
case lists:keyfind(abstract_code, 1, List) of
|
||||
{abstract_code, {raw_abstract_v1, Abstr}} ->
|
||||
{ok, Abstr};
|
||||
_Err ->
|
||||
log("failed to get abstract code from ~s~n", [File]),
|
||||
error
|
||||
end;
|
||||
Err ->
|
||||
log("failed to read chunks from ~s: ~p~n", [File, Err]),
|
||||
error
|
||||
end.
|
||||
|
||||
get_erl_forms(Path) ->
|
||||
case file:open(Path, [read]) of
|
||||
{ok, Fd} ->
|
||||
parse(Path, Fd, 1, []);
|
||||
{error, Why} ->
|
||||
log("failed to read ~s: ~s~n", [Path, file:format_error(Why)]),
|
||||
error
|
||||
end.
|
||||
|
||||
parse(Path, Fd, Line, Acc) ->
|
||||
{ok, Pos} = file:position(Fd, cur),
|
||||
case epp_dodger:parse_form(Fd, Line) of
|
||||
{ok, Form, NewLine} ->
|
||||
{ok, NewPos} = file:position(Fd, cur),
|
||||
{ok, RawForm} = file:pread(Fd, Pos, NewPos - Pos),
|
||||
file:position(Fd, {bof, NewPos}),
|
||||
AnnForm = erl_syntax:set_ann(Form, RawForm),
|
||||
parse(Path, Fd, NewLine, [AnnForm|Acc]);
|
||||
{eof, _} ->
|
||||
{ok, NewPos} = file:position(Fd, cur),
|
||||
if NewPos > Pos ->
|
||||
{ok, RawForm} = file:pread(Fd, Pos, NewPos - Pos),
|
||||
Form = erl_syntax:text(""),
|
||||
AnnForm = erl_syntax:set_ann(Form, RawForm),
|
||||
{ok, lists:reverse([AnnForm|Acc])};
|
||||
true ->
|
||||
{ok, lists:reverse(Acc)}
|
||||
end;
|
||||
Err ->
|
||||
log("failed to parse ~s: ~p~n", [Path, Err]),
|
||||
error
|
||||
end.
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Frontend for ejabberd's extract-tr.sh
|
||||
|
||||
# How to create template files for a new language:
|
||||
# NEWLANG=zh
|
||||
# cp priv/msgs/ejabberd.pot priv/msgs/$NEWLANG.po
|
||||
# echo \{\"\",\"\"\}. > priv/msgs/$NEWLANG.msg
|
||||
# make translations
|
||||
|
||||
extract_lang_src2pot ()
|
||||
{
|
||||
./tools/extract-tr.sh ebin/ > priv/msgs/ejabberd.pot
|
||||
}
|
||||
|
||||
extract_lang_popot2po ()
|
||||
{
|
||||
LANG_CODE=$1
|
||||
PO_PATH=$MSGS_DIR/$LANG_CODE.po
|
||||
POT_PATH=$MSGS_DIR/$PROJECT.pot
|
||||
|
||||
msgmerge $PO_PATH $POT_PATH >$PO_PATH.translate 2>/dev/null
|
||||
mv $PO_PATH.translate $PO_PATH
|
||||
}
|
||||
|
||||
extract_lang_po2msg ()
|
||||
{
|
||||
LANG_CODE=$1
|
||||
PO_PATH=$LANG_CODE.po
|
||||
MS_PATH=$PO_PATH.ms
|
||||
MSGID_PATH=$PO_PATH.msgid
|
||||
MSGSTR_PATH=$PO_PATH.msgstr
|
||||
MSGS_PATH=$LANG_CODE.msg
|
||||
|
||||
cd $MSGS_DIR
|
||||
|
||||
# Check PO has correct ~
|
||||
# Let's convert to C format so we can use msgfmt
|
||||
PO_TEMP=$LANG_CODE.po.temp
|
||||
cat $PO_PATH | sed 's/%/perc/g' | sed 's/~/%/g' | sed 's/#:.*/#, c-format/g' >$PO_TEMP
|
||||
msgfmt $PO_TEMP --check-format
|
||||
result=$?
|
||||
rm $PO_TEMP
|
||||
if [ $result -ne 0 ] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msgattrib $PO_PATH --translated --no-fuzzy --no-obsolete --no-location --no-wrap | grep "^msg" | tail --lines=+3 >$MS_PATH
|
||||
grep "^msgid" $PO_PATH.ms | sed 's/^msgid //g' >$MSGID_PATH
|
||||
grep "^msgstr" $PO_PATH.ms | sed 's/^msgstr //g' >$MSGSTR_PATH
|
||||
echo "%% -*- coding: latin-1 -*-" >$MSGS_PATH
|
||||
paste $MSGID_PATH $MSGSTR_PATH --delimiter=, | awk '{print "{" $0 "}."}' | sort -g >>$MSGS_PATH
|
||||
|
||||
rm $MS_PATH
|
||||
rm $MSGID_PATH
|
||||
rm $MSGSTR_PATH
|
||||
}
|
||||
|
||||
extract_lang_updateall ()
|
||||
{
|
||||
echo ""
|
||||
echo "Generating POT..."
|
||||
extract_lang_src2pot
|
||||
|
||||
cd $MSGS_DIR
|
||||
echo ""
|
||||
echo -e "File Missing (fuzzy) Language Last translator"
|
||||
echo -e "---- ------- ------- -------- ---------------"
|
||||
for i in $( ls *.msg ) ; do
|
||||
LANG_CODE=${i%.msg}
|
||||
echo -n $LANG_CODE | awk '{printf "%-6s", $1 }'
|
||||
|
||||
PO=$LANG_CODE.po
|
||||
|
||||
extract_lang_popot2po $LANG_CODE
|
||||
extract_lang_po2msg $LANG_CODE
|
||||
|
||||
MISSING=`msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4+$7 }'`
|
||||
echo -n " $MISSING"
|
||||
|
||||
FUZZY=`msgfmt --statistics $PO 2>&1 | awk '{printf "%7s", $4 }'`
|
||||
echo -n " $FUZZY"
|
||||
|
||||
LANGUAGE=`grep "X-Language:" $PO | sed 's/\"X-Language: //g' | sed 's/\\\\n\"//g' | awk '{printf "%-12s", $1}'`
|
||||
echo -n " $LANGUAGE"
|
||||
|
||||
LASTAUTH=`grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\\\n\"//g'`
|
||||
echo " $LASTAUTH"
|
||||
done
|
||||
echo ""
|
||||
rm messages.mo
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
EJA_DIR=`pwd`
|
||||
PROJECT=ejabberd
|
||||
MSGS_DIR=$EJA_DIR/priv/msgs
|
||||
|
||||
extract_lang_updateall
|
||||
@@ -42,6 +42,7 @@
|
||||
{iconv, @iconv@}.
|
||||
{stun, @stun@}.
|
||||
{sip, @sip@}.
|
||||
{graphics, @graphics@}.
|
||||
|
||||
%% Version
|
||||
{vsn, "@PACKAGE_VERSION@"}.
|
||||
|
||||
Reference in New Issue
Block a user