mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-04 15:36:57 +01:00
Merge branch '2.1.x' into 2.2.x
Conflicts: src/ejabberd_captcha.erl src/expat_erl.c src/mod_muc/mod_muc_room.erl
This commit is contained in:
commit
c98ddeb59f
@ -1031,7 +1031,7 @@ However, the c2s and s2s connections to the domain \term{example.com} use the fi
|
||||
\item Port 5269 listens for s2s connections with STARTTLS. The socket is set for IPv6 instead of IPv4.
|
||||
\item Port 3478 listens for STUN requests over UDP.
|
||||
\item Port 5280 listens for HTTP requests, and serves the HTTP Poll service.
|
||||
\item Port 5281 listens for HTTP requests, and serves the Web Admin using HTTPS as explained in
|
||||
\item Port 5281 listens for HTTP requests, using HTTPS to serve HTTP-Bind (BOSH) and the Web Admin as explained in
|
||||
section~\ref{webadmin}. The socket only listens connections to the IP address 127.0.0.1.
|
||||
\end{itemize}
|
||||
\begin{verbatim}
|
||||
@ -1060,6 +1060,7 @@ However, the c2s and s2s connections to the domain \term{example.com} use the fi
|
||||
]},
|
||||
{{5281, "127.0.0.1"}, ejabberd_http, [
|
||||
web_admin,
|
||||
http_bind,
|
||||
tls, {certfile, "/etc/ejabberd/server.pem"},
|
||||
]}
|
||||
]
|
||||
@ -1637,13 +1638,13 @@ The configurable options are:
|
||||
Full path to a script that generates the image.
|
||||
The default value is an empty string: \term{""}
|
||||
\titem{\{captcha\_host, ProtocolHostPort\}}
|
||||
Host part of the URL sent to the user,
|
||||
and the port number where ejabberd listens for CAPTCHA requests.
|
||||
The URL sent to the user is formed by: \term{http://Host:Port/captcha/}
|
||||
The default value is: the first hostname configured, and port 5280.
|
||||
If the port number you specify does not match exactly an ejabberd listener
|
||||
ProtocolHostPort is a string with the host, and optionally the Protocol and Port number.
|
||||
It must identify where ejabberd listens for CAPTCHA requests.
|
||||
The URL sent to the user is formed by: \term{Protocol://Host:Port/captcha/}
|
||||
The default value is: protocol \term{http}, the first hostname configured, and port \term{80}.
|
||||
If you specify a port number that does not match exactly an ejabberd listener
|
||||
(because you are using a reverse proxy or other port-forwarding tool),
|
||||
then specify also the transfer protocol, as seen in the example below.
|
||||
then you must specify the transfer protocol, as seen in the example below.
|
||||
\end{description}
|
||||
|
||||
Additionally, an \term{ejabberd\_http} listener must be enabled with the \term{captcha} option.
|
||||
@ -1656,6 +1657,7 @@ Example configuration:
|
||||
{captcha_cmd, "/lib/ejabberd/priv/bin/captcha.sh"}.
|
||||
{captcha_host, "example.org:5280"}.
|
||||
%% {captcha_host, "https://example.org:443"}.
|
||||
%% {captcha_host, "http://example.com"}.
|
||||
|
||||
{listen,
|
||||
[
|
||||
@ -3828,10 +3830,11 @@ enables end users to use a \XMPP{} client to:
|
||||
|
||||
Options:
|
||||
\begin{description}
|
||||
\titem{\{access, AccessName\}} \ind{options!access}This option can be configured to specify
|
||||
rules to restrict registration. If a rule returns `deny' on the requested
|
||||
user name, registration for that user name is denied. (there are no
|
||||
restrictions by default).
|
||||
\titem{\{access, AccessName\}} \ind{options!access}
|
||||
Specify rules to restrict what usernames can be registered and unregistered.
|
||||
If a rule returns `deny' on the requested username,
|
||||
registration and unregistration of that user name is denied.
|
||||
There are no restrictions by default.
|
||||
\titem{\{access\_from, AccessName\}} \ind{options!access\_from}By default, \ejabberd{}
|
||||
doesn't allow to register new accounts from s2s or existing c2s sessions. You can
|
||||
change it by defining access rule in this option. Use with care: allowing registration
|
||||
|
@ -479,6 +479,10 @@
|
||||
%%
|
||||
%%{captcha_host, "example.org:5280"}.
|
||||
|
||||
%%
|
||||
%% Limit CAPTCHA calls per minute for JID/IP to avoid DoS.
|
||||
%%
|
||||
%%{captcha_limit, 5}.
|
||||
|
||||
%%%. =======
|
||||
%%%' MODULES
|
||||
|
@ -35,9 +35,9 @@
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
-export([create_captcha/5, build_captcha_html/2, check_captcha/2,
|
||||
-export([create_captcha/6, build_captcha_html/2, check_captcha/2,
|
||||
process_reply/1, process/2, is_feature_available/0,
|
||||
create_captcha_x/4, create_captcha_x/5]).
|
||||
create_captcha_x/5, create_captcha_x/6]).
|
||||
|
||||
-include("jlib.hrl").
|
||||
-include("ejabberd.hrl").
|
||||
@ -50,8 +50,9 @@
|
||||
-define(CAPTCHA_TEXT(Lang), translate:translate(Lang, "Enter the text you see")).
|
||||
-define(CAPTCHA_LIFETIME, 120000). % two minutes
|
||||
-define(RPC_TIMEOUT, 5000).
|
||||
-define(LIMIT_PERIOD, 60*1000*1000). % one minute
|
||||
|
||||
-record(state, {}).
|
||||
-record(state, {limits = treap:empty()}).
|
||||
-record(captcha, {id, pid, key, tref, args}).
|
||||
|
||||
%%====================================================================
|
||||
@ -64,10 +65,10 @@
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
create_captcha(SID, From, To, Lang, Args)
|
||||
create_captcha(SID, From, To, Lang, Limiter, Args)
|
||||
when is_list(Lang), is_list(SID),
|
||||
is_record(From, jid), is_record(To, jid) ->
|
||||
case create_image() of
|
||||
case create_image(Limiter) of
|
||||
{ok, Type, Key, Image} ->
|
||||
Id = randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id(),
|
||||
B64Image = jlib:encode_base64(binary_to_list(Image)),
|
||||
@ -99,15 +100,15 @@ create_captcha(SID, From, To, Lang, Args)
|
||||
ets:insert(captcha, #captcha{id=Id, pid=self(), key=Key,
|
||||
tref=Tref, args=Args}),
|
||||
{ok, Id, [Body, OOB, Captcha, Data]};
|
||||
_Err ->
|
||||
error
|
||||
Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
create_captcha_x(SID, To, Lang, HeadEls) ->
|
||||
create_captcha_x(SID, To, Lang, HeadEls, []).
|
||||
create_captcha_x(SID, To, Lang, Limiter, HeadEls) ->
|
||||
create_captcha_x(SID, To, Lang, Limiter, HeadEls, []).
|
||||
|
||||
create_captcha_x(SID, To, Lang, HeadEls, TailEls) ->
|
||||
case create_image() of
|
||||
create_captcha_x(SID, To, Lang, Limiter, HeadEls, TailEls) ->
|
||||
case create_image(Limiter) of
|
||||
{ok, Type, Key, Image} ->
|
||||
Id = randoms:get_string() ++ "-" ++ ejabberd_cluster:node_id(),
|
||||
B64Image = jlib:encode_base64(binary_to_list(Image)),
|
||||
@ -144,8 +145,8 @@ create_captcha_x(SID, To, Lang, HeadEls, TailEls) ->
|
||||
Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}),
|
||||
ets:insert(captcha, #captcha{id=Id, key=Key, tref=Tref}),
|
||||
{ok, [Captcha, Data]};
|
||||
_ ->
|
||||
error
|
||||
Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
%% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found
|
||||
@ -242,16 +243,19 @@ process(_Handlers, #request{method='GET', lang=Lang, path=[_, Id]}) ->
|
||||
ejabberd_web:error(not_found)
|
||||
end;
|
||||
|
||||
process(_Handlers, #request{method='GET', path=[_, Id, "image"]}) ->
|
||||
process(_Handlers, #request{method='GET', path=[_, Id, "image"], ip = IP}) ->
|
||||
{Addr, _Port} = IP,
|
||||
case lookup_captcha(Id) of
|
||||
{ok, #captcha{key=Key}} ->
|
||||
case create_image(Key) of
|
||||
case create_image(Addr, Key) of
|
||||
{ok, Type, _, Img} ->
|
||||
{200,
|
||||
[{"Content-Type", Type},
|
||||
{"Cache-Control", "no-cache"},
|
||||
{"Last-Modified", httpd_util:rfc1123_date()}],
|
||||
Img};
|
||||
{error, limit} ->
|
||||
ejabberd_web:error(not_allowed);
|
||||
_ ->
|
||||
ejabberd_web:error(not_found)
|
||||
end;
|
||||
@ -288,6 +292,20 @@ init([]) ->
|
||||
check_captcha_setup(),
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call({is_limited, Limiter, RateLimit}, _From, State) ->
|
||||
NowPriority = now_priority(),
|
||||
CleanPriority = NowPriority + ?LIMIT_PERIOD,
|
||||
Limits = clean_treap(State#state.limits, CleanPriority),
|
||||
case treap:lookup(Limiter, Limits) of
|
||||
{ok, _, Rate} when Rate >= RateLimit ->
|
||||
{reply, true, State#state{limits = Limits}};
|
||||
{ok, Priority, Rate} ->
|
||||
NewLimits = treap:insert(Limiter, Priority, Rate+1, Limits),
|
||||
{reply, false, State#state{limits = NewLimits}};
|
||||
_ ->
|
||||
NewLimits = treap:insert(Limiter, NowPriority, 1, Limits),
|
||||
{reply, false, State#state{limits = NewLimits}}
|
||||
end;
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, bad_request, State}.
|
||||
|
||||
@ -329,11 +347,22 @@ code_change(_OldVsn, State, _Extra) ->
|
||||
%% Reason = atom()
|
||||
%%--------------------------------------------------------------------
|
||||
create_image() ->
|
||||
create_image(undefined).
|
||||
|
||||
create_image(Limiter) ->
|
||||
%% Six numbers from 1 to 9.
|
||||
Key = string:substr(randoms:get_string(), 1, 6),
|
||||
create_image(Key).
|
||||
create_image(Limiter, Key).
|
||||
|
||||
create_image(Key) ->
|
||||
create_image(Limiter, Key) ->
|
||||
case is_limited(Limiter) of
|
||||
true ->
|
||||
{error, limit};
|
||||
false ->
|
||||
do_create_image(Key)
|
||||
end.
|
||||
|
||||
do_create_image(Key) ->
|
||||
FileName = get_prog_name(),
|
||||
Cmd = lists:flatten(io_lib:format("~s ~s", [FileName, Key])),
|
||||
case cmd(Cmd) of
|
||||
@ -363,22 +392,26 @@ get_prog_name() ->
|
||||
case ejabberd_config:get_local_option(captcha_cmd) of
|
||||
FileName when is_list(FileName) ->
|
||||
FileName;
|
||||
_ ->
|
||||
Value when (Value == undefined) or (Value == "") ->
|
||||
?DEBUG("The option captcha_cmd is not configured, but some "
|
||||
"module wants to use the CAPTCHA feature.", []),
|
||||
throw({error, option_not_configured_captcha_cmd})
|
||||
false
|
||||
end.
|
||||
|
||||
get_url(Str) ->
|
||||
CaptchaHost = ejabberd_config:get_local_option(captcha_host),
|
||||
case string:tokens(CaptchaHost, ":") of
|
||||
[TransferProt, Host, PortString] ->
|
||||
TransferProt ++ ":" ++ Host ++ ":" ++ PortString ++ "/captcha/" ++ Str;
|
||||
[Host] ->
|
||||
"http://" ++ Host ++ "/captcha/" ++ Str;
|
||||
["http"++_ = TransferProt, Host] ->
|
||||
TransferProt ++ ":" ++ Host ++ "/captcha/" ++ Str;
|
||||
[Host, PortString] ->
|
||||
TransferProt = atom_to_list(get_transfer_protocol(PortString)),
|
||||
TransferProt ++ "://" ++ Host ++ ":" ++ PortString ++ "/captcha/" ++ Str;
|
||||
[TransferProt, Host, PortString] ->
|
||||
TransferProt ++ ":" ++ Host ++ ":" ++ PortString ++ "/captcha/" ++ Str;
|
||||
_ ->
|
||||
"http://" ++ ?MYNAME ++ ":5280/captcha/" ++ Str
|
||||
"http://" ++ ?MYNAME ++ "/captcha/" ++ Str
|
||||
end.
|
||||
|
||||
get_transfer_protocol(PortString) ->
|
||||
@ -416,6 +449,25 @@ get_captcha_transfer_protocol([{{_Port, _Ip, tcp}, ejabberd_http, Opts}
|
||||
get_captcha_transfer_protocol([_ | Listeners]) ->
|
||||
get_captcha_transfer_protocol(Listeners).
|
||||
|
||||
is_limited(undefined) ->
|
||||
false;
|
||||
is_limited(Limiter) ->
|
||||
case ejabberd_config:get_local_option(captcha_limit) of
|
||||
Int when is_integer(Int), Int > 0 ->
|
||||
case catch gen_server:call(?MODULE, {is_limited, Limiter, Int},
|
||||
5000) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
false;
|
||||
Err ->
|
||||
?ERROR_MSG("Call failed: ~p", [Err]),
|
||||
false
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Function: cmd(Cmd) -> Data | {error, Reason}
|
||||
%% Cmd = string()
|
||||
@ -464,28 +516,22 @@ return(Port, TRef, Result) ->
|
||||
catch port_close(Port),
|
||||
Result.
|
||||
|
||||
is_feature_enabled() ->
|
||||
try get_prog_name() of
|
||||
Prog when is_list(Prog) -> true
|
||||
catch
|
||||
_:_ -> false
|
||||
end.
|
||||
|
||||
is_feature_available() ->
|
||||
case is_feature_enabled() of
|
||||
false -> false;
|
||||
true ->
|
||||
case create_image() of
|
||||
{ok, _, _, _} -> true;
|
||||
_Error -> false
|
||||
end
|
||||
case get_prog_name() of
|
||||
Prog when is_list(Prog) -> true;
|
||||
false -> false
|
||||
end.
|
||||
|
||||
check_captcha_setup() ->
|
||||
case is_feature_enabled() andalso not is_feature_available() of
|
||||
AbleToGenerateCaptcha = case create_image() of
|
||||
{ok, _, _, _} -> true;
|
||||
_Error -> false
|
||||
end,
|
||||
case is_feature_available() andalso not AbleToGenerateCaptcha of
|
||||
true ->
|
||||
?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, "
|
||||
"but it can't generate images.", []);
|
||||
"but it can't generate images.", []),
|
||||
throw({error, captcha_cmd_enabled_but_fails});
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
@ -537,3 +583,21 @@ do_check_captcha(Id, ProvidedKey) ->
|
||||
_ ->
|
||||
captcha_not_found
|
||||
end.
|
||||
|
||||
clean_treap(Treap, CleanPriority) ->
|
||||
case treap:is_empty(Treap) of
|
||||
true ->
|
||||
Treap;
|
||||
false ->
|
||||
{_Key, Priority, _Value} = treap:get_root(Treap),
|
||||
if
|
||||
Priority > CleanPriority ->
|
||||
clean_treap(treap:delete_root(Treap), CleanPriority);
|
||||
true ->
|
||||
Treap
|
||||
end
|
||||
end.
|
||||
|
||||
now_priority() ->
|
||||
{MSec, Sec, USec} = now(),
|
||||
-((MSec*1000000 + Sec)*1000000 + USec).
|
||||
|
@ -431,6 +431,8 @@ process_term(Term, State) ->
|
||||
add_option(captcha_cmd, Cmd, State);
|
||||
{captcha_host, Host} ->
|
||||
add_option(captcha_host, Host, State);
|
||||
{captcha_limit, Limit} ->
|
||||
add_option(captcha_limit, Limit, State);
|
||||
{ejabberdctl_access_commands, ACs} ->
|
||||
add_option(ejabberdctl_access_commands, ACs, State);
|
||||
{loglevel, Loglevel} ->
|
||||
|
@ -35,6 +35,7 @@
|
||||
#define PARSE_FINAL_COMMAND 1
|
||||
|
||||
ei_x_buff event_buf;
|
||||
ei_x_buff xmlns_buf;
|
||||
|
||||
typedef struct {
|
||||
ErlDrvPort port;
|
||||
@ -43,6 +44,32 @@ typedef struct {
|
||||
|
||||
static XML_Memory_Handling_Suite ms = {driver_alloc, driver_realloc, driver_free};
|
||||
|
||||
void encode_name(const XML_Char *name)
|
||||
{
|
||||
char *name_start;
|
||||
char *prefix_start;
|
||||
char *buf;
|
||||
int name_len, prefix_len, buf_len;
|
||||
|
||||
if ((name_start = strchr(name, '\n'))) {
|
||||
if ((prefix_start = strchr(name_start+1, '\n'))) {
|
||||
name_len = prefix_start - name_start;
|
||||
prefix_len = strlen(prefix_start+1);
|
||||
buf_len = prefix_len + name_len;
|
||||
buf = driver_alloc(buf_len);
|
||||
memcpy(buf, prefix_start+1, prefix_len);
|
||||
memcpy(buf+prefix_len, name_start, name_len);
|
||||
buf[prefix_len] = ':';
|
||||
ei_x_encode_string_len(&event_buf, buf, buf_len);
|
||||
driver_free(buf);
|
||||
} else {
|
||||
ei_x_encode_string(&event_buf, name_start+1);
|
||||
};
|
||||
} else {
|
||||
ei_x_encode_string(&event_buf, name);
|
||||
}
|
||||
}
|
||||
|
||||
void *erlXML_StartElementHandler(expat_data *d,
|
||||
const XML_Char *name,
|
||||
const XML_Char **atts)
|
||||
@ -53,7 +80,10 @@ void *erlXML_StartElementHandler(expat_data *d,
|
||||
ei_x_encode_tuple_header(&event_buf, 2);
|
||||
ei_x_encode_long(&event_buf, XML_START);
|
||||
ei_x_encode_tuple_header(&event_buf, 2);
|
||||
ei_x_encode_string(&event_buf, name);
|
||||
encode_name(name);
|
||||
ei_x_append(&event_buf, &xmlns_buf);
|
||||
ei_x_free(&xmlns_buf);
|
||||
ei_x_new(&xmlns_buf);
|
||||
|
||||
for (i = 0; atts[i]; i += 2) {}
|
||||
|
||||
@ -64,7 +94,7 @@ void *erlXML_StartElementHandler(expat_data *d,
|
||||
for (i = 0; atts[i]; i += 2)
|
||||
{
|
||||
ei_x_encode_tuple_header(&event_buf, 2);
|
||||
ei_x_encode_string(&event_buf, atts[i]);
|
||||
encode_name(atts[i]);
|
||||
ei_x_encode_string(&event_buf, atts[i+1]);
|
||||
}
|
||||
}
|
||||
@ -80,7 +110,7 @@ void *erlXML_EndElementHandler(expat_data *d,
|
||||
ei_x_encode_list_header(&event_buf, 1);
|
||||
ei_x_encode_tuple_header(&event_buf, 2);
|
||||
ei_x_encode_long(&event_buf, XML_END);
|
||||
ei_x_encode_string(&event_buf, name);
|
||||
encode_name(name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@ -95,12 +125,35 @@ void *erlXML_CharacterDataHandler(expat_data *d,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *erlXML_StartNamespaceDeclHandler(expat_data *d,
|
||||
const XML_Char *prefix,
|
||||
const XML_Char *uri)
|
||||
{
|
||||
int prefix_len;
|
||||
char *buf;
|
||||
|
||||
ei_x_encode_list_header(&xmlns_buf, 1);
|
||||
ei_x_encode_tuple_header(&xmlns_buf, 2);
|
||||
if (prefix) {
|
||||
prefix_len = strlen(prefix);
|
||||
buf = driver_alloc(7 + prefix_len);
|
||||
strcpy(buf, "xmlns:");
|
||||
strcpy(buf+6, prefix);
|
||||
ei_x_encode_string(&xmlns_buf, buf);
|
||||
driver_free(buf);
|
||||
} else {
|
||||
ei_x_encode_string(&xmlns_buf, "xmlns");
|
||||
};
|
||||
ei_x_encode_string(&xmlns_buf, uri);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ErlDrvData expat_erl_start(ErlDrvPort port, char *buff)
|
||||
{
|
||||
expat_data* d = (expat_data*)driver_alloc(sizeof(expat_data));
|
||||
d->port = port;
|
||||
d->parser = XML_ParserCreate_MM("UTF-8", &ms, NULL);
|
||||
d->parser = XML_ParserCreate_MM("UTF-8", &ms, "\n");
|
||||
XML_SetUserData(d->parser, d);
|
||||
|
||||
set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);
|
||||
@ -112,6 +165,9 @@ static ErlDrvData expat_erl_start(ErlDrvPort port, char *buff)
|
||||
XML_SetCharacterDataHandler(
|
||||
d->parser, (XML_CharacterDataHandler)erlXML_CharacterDataHandler);
|
||||
|
||||
XML_SetStartNamespaceDeclHandler(
|
||||
d->parser, (XML_StartNamespaceDeclHandler) erlXML_StartNamespaceDeclHandler);
|
||||
XML_SetReturnNSTriplet(d->parser, 1);
|
||||
|
||||
return (ErlDrvData)d;
|
||||
}
|
||||
@ -138,6 +194,7 @@ static int expat_erl_control(ErlDrvData drv_data,
|
||||
case PARSE_COMMAND:
|
||||
case PARSE_FINAL_COMMAND:
|
||||
ei_x_new_with_version(&event_buf);
|
||||
ei_x_new(&xmlns_buf);
|
||||
#ifdef ENABLE_FLASH_HACK
|
||||
/* Flash hack - Flash clients send a null byte after the stanza. Remove that... */
|
||||
{
|
||||
@ -190,6 +247,7 @@ static int expat_erl_control(ErlDrvData drv_data,
|
||||
memcpy(b->orig_bytes, event_buf.buff, size);
|
||||
|
||||
ei_x_free(&event_buf);
|
||||
ei_x_free(&xmlns_buf);
|
||||
|
||||
*rbuf = (char *)b;
|
||||
return size;
|
||||
|
@ -45,11 +45,18 @@
|
||||
start(Host, ExtPrg) ->
|
||||
lists:foreach(
|
||||
fun(This) ->
|
||||
spawn(?MODULE, init, [get_process_name(Host, This), ExtPrg])
|
||||
start_instance(get_process_name(Host, This), ExtPrg)
|
||||
end,
|
||||
lists:seq(0, get_instances(Host)-1)
|
||||
).
|
||||
|
||||
start_instance(ProcessName, ExtPrg) ->
|
||||
spawn(?MODULE, init, [ProcessName, ExtPrg]).
|
||||
|
||||
restart_instance(ProcessName, ExtPrg) ->
|
||||
unregister(ProcessName),
|
||||
start_instance(ProcessName, ExtPrg).
|
||||
|
||||
init(ProcessName, ExtPrg) ->
|
||||
register(ProcessName, self()),
|
||||
process_flag(trap_exit,true),
|
||||
@ -125,8 +132,7 @@ loop(Port, Timeout, ProcessName, ExtPrg) ->
|
||||
Timeout ->
|
||||
?ERROR_MSG("extauth call '~p' didn't receive response", [Msg]),
|
||||
Caller ! {eauth, false},
|
||||
unregister(ProcessName),
|
||||
Pid = spawn(?MODULE, init, [ProcessName, ExtPrg]),
|
||||
Pid = restart_instance(ProcessName, ExtPrg),
|
||||
flush_buffer_and_forward_messages(Pid),
|
||||
exit(port_terminated)
|
||||
end;
|
||||
@ -137,7 +143,9 @@ loop(Port, Timeout, ProcessName, ExtPrg) ->
|
||||
exit(normal)
|
||||
end;
|
||||
{'EXIT', Port, Reason} ->
|
||||
?CRITICAL_MSG("~p ~n", [Reason]),
|
||||
?CRITICAL_MSG("extauth script has exitted abruptly with reason '~p'", [Reason]),
|
||||
Pid = restart_instance(ProcessName, ExtPrg),
|
||||
flush_buffer_and_forward_messages(Pid),
|
||||
exit(port_terminated)
|
||||
end.
|
||||
|
||||
|
@ -1666,15 +1666,25 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
|
||||
SID = xml:get_attr_s("id", Attrs),
|
||||
RoomJID = StateData#state.jid,
|
||||
To = jlib:jid_replace_resource(RoomJID, Nick),
|
||||
Limiter = {From#jid.luser, From#jid.lserver},
|
||||
case ejabberd_captcha:create_captcha(
|
||||
SID, RoomJID, To, Lang, From) of
|
||||
SID, RoomJID, To, Lang, Limiter, From) of
|
||||
{ok, ID, CaptchaEls} ->
|
||||
MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls},
|
||||
Robots = ?DICT:store(From,
|
||||
{Nick, Packet}, StateData#state.robots),
|
||||
ejabberd_router:route(RoomJID, From, MsgPkt),
|
||||
StateData#state{robots = Robots};
|
||||
error ->
|
||||
{error, limit} ->
|
||||
ErrText = "Too many CAPTCHA requests",
|
||||
Err = jlib:make_error_reply(
|
||||
Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
|
||||
ejabberd_router:route( % TODO: s/Nick/""/
|
||||
jlib:jid_replace_resource(
|
||||
StateData#state.jid, Nick),
|
||||
From, Err),
|
||||
StateData;
|
||||
_ ->
|
||||
ErrText = "Unable to generate a captcha",
|
||||
Err = jlib:make_error_reply(
|
||||
Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)),
|
||||
@ -1726,7 +1736,24 @@ check_captcha(Affiliation, From, StateData) ->
|
||||
{ok, passed} ->
|
||||
true;
|
||||
_ ->
|
||||
WList = (StateData#state.config)#config.captcha_whitelist,
|
||||
#jid{luser = U, lserver = S, lresource = R} = From,
|
||||
case ?SETS:is_element({U, S, R}, WList) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
case ?SETS:is_element({U, S, ""}, WList) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
case ?SETS:is_element({"", S, ""}, WList) of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
captcha_required
|
||||
end
|
||||
end
|
||||
end
|
||||
end;
|
||||
_ ->
|
||||
true
|
||||
@ -2526,6 +2553,11 @@ can_change_ra(_FAffiliation, _FRole,
|
||||
%% A room owner tries to add as persistent owner a
|
||||
%% participant that is already owner because he is MUC admin
|
||||
true;
|
||||
can_change_ra(_FAffiliation, _FRole,
|
||||
_TAffiliation, _TRole,
|
||||
_RoleorAffiliation, _Value, owner) ->
|
||||
%% Nobody can decrease MUC admin's role/affiliation
|
||||
false;
|
||||
can_change_ra(_FAffiliation, _FRole,
|
||||
TAffiliation, _TRole,
|
||||
affiliation, Value, _ServiceAf)
|
||||
@ -2915,6 +2947,13 @@ is_password_settings_correct(XEl, StateData) ->
|
||||
-define(PRIVATEXFIELD(Label, Var, Val),
|
||||
?XFIELD("text-private", Label, Var, Val)).
|
||||
|
||||
-define(JIDMULTIXFIELD(Label, Var, JIDList),
|
||||
{xmlelement, "field", [{"type", "jid-multi"},
|
||||
{"label", translate:translate(Lang, Label)},
|
||||
{"var", Var}],
|
||||
[{xmlelement, "value", [], [{xmlcdata, jlib:jid_to_string(JID)}]}
|
||||
|| JID <- JIDList]}).
|
||||
|
||||
get_default_room_maxusers(RoomState) ->
|
||||
DefRoomOpts = gen_mod:get_module_opt(RoomState#state.server_host, mod_muc, default_room_options, []),
|
||||
RoomState2 = set_opts(DefRoomOpts, RoomState),
|
||||
@ -3035,6 +3074,9 @@ get_config(Lang, StateData, From) ->
|
||||
Config#config.captcha_protected)];
|
||||
false -> []
|
||||
end ++
|
||||
[?JIDMULTIXFIELD("Exclude Jabber IDs from CAPTCHA challenge",
|
||||
"muc#roomconfig_captcha_whitelist",
|
||||
?SETS:to_list(Config#config.captcha_whitelist))] ++
|
||||
case mod_muc_log:check_access_log(
|
||||
StateData#state.server_host, From) of
|
||||
allow ->
|
||||
@ -3104,6 +3146,18 @@ set_config(XEl, StateData) ->
|
||||
-define(SET_STRING_XOPT(Opt, Val),
|
||||
set_xoption(Opts, Config#config{Opt = Val})).
|
||||
|
||||
-define(SET_JIDMULTI_XOPT(Opt, Vals),
|
||||
begin
|
||||
Set = lists:foldl(
|
||||
fun({U, S, R}, Set1) ->
|
||||
?SETS:add_element({U, S, R}, Set1);
|
||||
(#jid{luser = U, lserver = S, lresource = R}, Set1) ->
|
||||
?SETS:add_element({U, S, R}, Set1);
|
||||
(_, Set1) ->
|
||||
Set1
|
||||
end, ?SETS:empty(), Vals),
|
||||
set_xoption(Opts, Config#config{Opt = Set})
|
||||
end).
|
||||
|
||||
set_xoption([], Config) ->
|
||||
Config;
|
||||
@ -3161,6 +3215,9 @@ set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) ->
|
||||
end;
|
||||
set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) ->
|
||||
?SET_BOOL_XOPT(logging, Val);
|
||||
set_xoption([{"muc#roomconfig_captcha_whitelist", Vals} | Opts], Config) ->
|
||||
JIDs = [jlib:string_to_jid(Val) || Val <- Vals],
|
||||
?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs);
|
||||
set_xoption([{"FORM_TYPE", _} | Opts], Config) ->
|
||||
%% Ignore our FORM_TYPE
|
||||
set_xoption(Opts, Config);
|
||||
@ -3230,6 +3287,7 @@ set_opts([{Opt, Val} | Opts], StateData) ->
|
||||
password -> StateData#state{config = (StateData#state.config)#config{password = Val}};
|
||||
anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}};
|
||||
logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}};
|
||||
captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist = ?SETS:from_list(Val)}};
|
||||
max_users ->
|
||||
ServiceMaxUsers = get_service_max_users(StateData),
|
||||
MaxUsers = if
|
||||
@ -3274,6 +3332,8 @@ make_opts(StateData) ->
|
||||
?MAKE_CONFIG_OPT(anonymous),
|
||||
?MAKE_CONFIG_OPT(logging),
|
||||
?MAKE_CONFIG_OPT(max_users),
|
||||
{captcha_whitelist,
|
||||
?SETS:to_list((StateData#state.config)#config.captcha_whitelist)},
|
||||
{affiliations, ?DICT:to_list(StateData#state.affiliations)},
|
||||
{subject, StateData#state.subject},
|
||||
{subject_author, StateData#state.subject_author}
|
||||
|
@ -45,7 +45,8 @@
|
||||
password = "",
|
||||
anonymous = true,
|
||||
max_users = ?MAX_USERS_DEFAULT,
|
||||
logging = false
|
||||
logging = false,
|
||||
captcha_whitelist = ?SETS:empty()
|
||||
}).
|
||||
|
||||
-record(user, {jid,
|
||||
|
@ -751,9 +751,9 @@ item_to_raw(#listitem{type = Type,
|
||||
none ->
|
||||
{"n", ""};
|
||||
jid ->
|
||||
{"j", jlib:jid_to_string(Value)};
|
||||
{"j", ejabberd_odbc:escape(jlib:jid_to_string(Value))};
|
||||
group ->
|
||||
{"g", Value};
|
||||
{"g", ejabberd_odbc:escape(Value)};
|
||||
subscription ->
|
||||
case Value of
|
||||
none ->
|
||||
|
@ -106,8 +106,10 @@ process_iq(From, To,
|
||||
PTag = xml:get_subtag(SubEl, "password"),
|
||||
RTag = xml:get_subtag(SubEl, "remove"),
|
||||
Server = To#jid.lserver,
|
||||
Access = gen_mod:get_module_opt(Server, ?MODULE, access, all),
|
||||
AllowRemove = (allow == acl:match_rule(Server, Access, From)),
|
||||
if
|
||||
(UTag /= false) and (RTag /= false) ->
|
||||
(UTag /= false) and (RTag /= false) and AllowRemove ->
|
||||
User = xml:get_tag_cdata(UTag),
|
||||
case From of
|
||||
#jid{user = User, lserver = Server} ->
|
||||
@ -148,7 +150,7 @@ process_iq(From, To,
|
||||
sub_el = [SubEl, ?ERR_BAD_REQUEST]}
|
||||
end
|
||||
end;
|
||||
(UTag == false) and (RTag /= false) ->
|
||||
(UTag == false) and (RTag /= false) and AllowRemove ->
|
||||
case From of
|
||||
#jid{user = User,
|
||||
lserver = Server,
|
||||
@ -234,13 +236,18 @@ process_iq(From, To,
|
||||
{"var", "password"}],
|
||||
[{xmlelement, "required", [], []}]},
|
||||
case ejabberd_captcha:create_captcha_x(
|
||||
ID, To, Lang, [InstrEl, UField, PField]) of
|
||||
ID, To, Lang, Source, [InstrEl, UField, PField]) of
|
||||
{ok, CaptchaEls} ->
|
||||
IQ#iq{type = result,
|
||||
sub_el = [{xmlelement, "query",
|
||||
[{"xmlns", "jabber:iq:register"}],
|
||||
[TopInstrEl | CaptchaEls]}]};
|
||||
error ->
|
||||
{error, limit} ->
|
||||
ErrText = "Too many CAPTCHA requests",
|
||||
IQ#iq{type = error,
|
||||
sub_el = [SubEl, ?ERRT_RESOURCE_CONSTRAINT(
|
||||
Lang, ErrText)]};
|
||||
_Err ->
|
||||
ErrText = "Unable to generate a CAPTCHA",
|
||||
IQ#iq{type = error,
|
||||
sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(
|
||||
|
@ -161,7 +161,7 @@ get_versioning_feature(Acc, Host) ->
|
||||
Feature = {xmlelement,
|
||||
"ver",
|
||||
[{"xmlns", ?NS_ROSTER_VER}],
|
||||
[{xmlelement, "optional", [], []}]},
|
||||
[]},
|
||||
[Feature | Acc];
|
||||
false -> []
|
||||
end.
|
||||
@ -411,10 +411,17 @@ push_item(User, Server, From, Item) ->
|
||||
|
||||
% TODO: don't push to those who didn't load roster
|
||||
push_item(User, Server, Resource, From, Item) ->
|
||||
push_item(User, Server, Resource, From, Item, not_found).
|
||||
|
||||
push_item(User, Server, Resource, From, Item, RosterVersion) ->
|
||||
ExtraAttrs = case RosterVersion of
|
||||
not_found -> [];
|
||||
_ -> [{"ver", RosterVersion}]
|
||||
end,
|
||||
ResIQ = #iq{type = set, xmlns = ?NS_ROSTER,
|
||||
id = "push" ++ randoms:get_string(),
|
||||
sub_el = [{xmlelement, "query",
|
||||
[{"xmlns", ?NS_ROSTER}],
|
||||
[{"xmlns", ?NS_ROSTER}|ExtraAttrs],
|
||||
[item_to_xml(Item)]}]},
|
||||
ejabberd_router:route(
|
||||
From,
|
||||
@ -425,21 +432,9 @@ push_item(User, Server, Resource, From, Item) ->
|
||||
%% TODO: don't push to those who didn't load roster
|
||||
push_item_version(Server, User, From, Item, RosterVersion) ->
|
||||
lists:foreach(fun(Resource) ->
|
||||
push_item_version(User, Server, Resource, From, Item, RosterVersion)
|
||||
push_item(User, Server, Resource, From, Item, RosterVersion)
|
||||
end, ejabberd_sm:get_user_resources(User, Server)).
|
||||
|
||||
push_item_version(User, Server, Resource, From, Item, RosterVersion) ->
|
||||
IQPush = #iq{type = 'set', xmlns = ?NS_ROSTER,
|
||||
id = "push" ++ randoms:get_string(),
|
||||
sub_el = [{xmlelement, "query",
|
||||
[{"xmlns", ?NS_ROSTER},
|
||||
{"ver", RosterVersion}],
|
||||
[item_to_xml(Item)]}]},
|
||||
ejabberd_router:route(
|
||||
From,
|
||||
jlib:make_jid(User, Server, Resource),
|
||||
jlib:iq_to_xml(IQPush)).
|
||||
|
||||
get_subscription_lists(_, User, Server) ->
|
||||
LUser = jlib:nodeprep(User),
|
||||
LServer = jlib:nameprep(Server),
|
||||
|
@ -597,6 +597,40 @@ BEGIN
|
||||
END
|
||||
GO
|
||||
|
||||
/******************************************************************/
|
||||
/****** Object: StoredProcedure [dbo].[set_roster_version] **/
|
||||
/** Update users roster_version **/
|
||||
/******************************************************************/
|
||||
CREATE PROCEDURE [dbo].[set_roster_version]
|
||||
@Username varchar(200),
|
||||
@Version varchar(50)
|
||||
AS
|
||||
BEGIN
|
||||
IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username)
|
||||
BEGIN
|
||||
UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT INTO roster_version (username, version) VALUES (@Username, @Version);
|
||||
END
|
||||
END
|
||||
GO
|
||||
|
||||
/******************************************************************/
|
||||
/****** Object: StoredProcedure [dbo].[get_roster_version] **/
|
||||
/** Retrive the user roster_version **/
|
||||
/******************************************************************/
|
||||
CREATE PROCEDURE [dbo].[get_roster_version]
|
||||
@Username varchar(200)
|
||||
AS
|
||||
BEGIN
|
||||
SELECT roster_version.version as version
|
||||
FROM roster_version WITH (NOLOCK)
|
||||
WHERE username=@Username;
|
||||
END
|
||||
GO
|
||||
|
||||
/***************************************************************/
|
||||
/****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/
|
||||
/** Delete messages older that 3 days from spool **/
|
||||
|
@ -918,6 +918,40 @@ BEGIN
|
||||
END
|
||||
GO
|
||||
|
||||
/******************************************************************/
|
||||
/****** Object: StoredProcedure [dbo].[set_roster_version] **/
|
||||
/** Update users roster_version **/
|
||||
/******************************************************************/
|
||||
CREATE PROCEDURE [dbo].[set_roster_version]
|
||||
@Username varchar(200),
|
||||
@Version varchar(8000)
|
||||
AS
|
||||
BEGIN
|
||||
IF EXISTS (SELECT username FROM roster_version WITH (NOLOCK) WHERE username=@Username)
|
||||
BEGIN
|
||||
UPDATE roster_version SET username=@Username, version=@Version WHERE username=@Username;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT INTO roster_version (username, version) VALUES (@Username, @Version);
|
||||
END
|
||||
END
|
||||
GO
|
||||
|
||||
/******************************************************************/
|
||||
/****** Object: StoredProcedure [dbo].[get_roster_version] **/
|
||||
/** Retrive the user roster_version **/
|
||||
/******************************************************************/
|
||||
CREATE PROCEDURE [dbo].[get_roster_version]
|
||||
@Username varchar(200)
|
||||
AS
|
||||
BEGIN
|
||||
SELECT roster_version.version as version
|
||||
FROM roster_version WITH (NOLOCK)
|
||||
WHERE username=@Username;
|
||||
END
|
||||
GO
|
||||
|
||||
/***************************************************************/
|
||||
/****** Object: StoredProcedure [dbo].[clean_spool_msg] ******/
|
||||
/** Delete messages older that 3 days from spool **/
|
||||
|
@ -90,6 +90,8 @@
|
||||
-define(generic, true).
|
||||
-endif.
|
||||
|
||||
-include("ejabberd.hrl").
|
||||
|
||||
%% Almost a copy of string:join/2.
|
||||
%% We use this version because string:join/2 is relatively
|
||||
%% new function (introduced in R12B-0).
|
||||
@ -882,8 +884,14 @@ count_records_where(LServer, Table, WhereClause) ->
|
||||
["select count(*) from ", Table, " with (nolock) ", WhereClause]).
|
||||
|
||||
get_roster_version(LServer, LUser) ->
|
||||
ejabberd_odbc:sql_query(LServer,
|
||||
["select version from dbo.roster_version with (nolock) where username = '", LUser, "'"]).
|
||||
set_roster_version(LUser, Version) ->
|
||||
update_t("dbo.roster_version", ["username", "version"], [LUser, Version], ["username = '", LUser, "'"]).
|
||||
ejabberd_odbc:sql_query(
|
||||
LServer,
|
||||
["EXECUTE dbo.get_roster_version '", LUser, "'"]).
|
||||
|
||||
set_roster_version(Username, Version) ->
|
||||
%% This function doesn't know the vhost, so we hope it's the first one defined:
|
||||
LServer = ?MYNAME,
|
||||
ejabberd_odbc:sql_query(
|
||||
LServer,
|
||||
["EXECUTE dbo.set_roster_version '", Username, "', '", Version, "'"]).
|
||||
-endif.
|
||||
|
@ -636,7 +636,13 @@ make_xhtml_output(State, Status, Headers, XHTML) ->
|
||||
end, HeadersOut),
|
||||
SL = [Version, integer_to_list(Status), " ",
|
||||
code_to_phrase(Status), "\r\n"],
|
||||
[SL, H, "\r\n", Data].
|
||||
|
||||
Data2 = case State#state.request_method of
|
||||
'HEAD' -> "";
|
||||
_ -> Data
|
||||
end,
|
||||
|
||||
[SL, H, "\r\n", Data2].
|
||||
|
||||
make_text_output(State, Status, Headers, Text) when is_list(Text) ->
|
||||
make_text_output(State, Status, Headers, list_to_binary(Text));
|
||||
@ -673,7 +679,13 @@ make_text_output(State, Status, Headers, Data) when is_binary(Data) ->
|
||||
end, HeadersOut),
|
||||
SL = [Version, integer_to_list(Status), " ",
|
||||
code_to_phrase(Status), "\r\n"],
|
||||
[SL, H, "\r\n", Data].
|
||||
|
||||
Data2 = case State#state.request_method of
|
||||
'HEAD' -> "";
|
||||
_ -> Data
|
||||
end,
|
||||
|
||||
[SL, H, "\r\n", Data2].
|
||||
|
||||
|
||||
parse_lang(Langs) ->
|
||||
|
@ -64,11 +64,15 @@ get_acl_rule(["additions.js"],_) -> {"localhost", [all]};
|
||||
get_acl_rule(["vhosts"],_) -> {"localhost", [all]};
|
||||
|
||||
%% The pages of a vhost are only accesible if the user is admin of that vhost:
|
||||
get_acl_rule(["server", VHost | _RPath], 'GET') -> {VHost, [configure, webadmin_view]};
|
||||
get_acl_rule(["server", VHost | _RPath], Method)
|
||||
when Method=:='GET' orelse Method=:='HEAD' ->
|
||||
{VHost, [configure, webadmin_view]};
|
||||
get_acl_rule(["server", VHost | _RPath], 'POST') -> {VHost, [configure]};
|
||||
|
||||
%% Default rule: only global admins can access any other random page
|
||||
get_acl_rule(_RPath, 'GET') -> {global, [configure, webadmin_view]};
|
||||
get_acl_rule(_RPath, Method)
|
||||
when Method=:='GET' orelse Method=:='HEAD' ->
|
||||
{global, [configure, webadmin_view]};
|
||||
get_acl_rule(_RPath, 'POST') -> {global, [configure]}.
|
||||
|
||||
is_acl_match(Host, Rules, Jid) ->
|
||||
|
@ -86,8 +86,9 @@ process([], #request{method = 'GET', lang = Lang}) ->
|
||||
process(["register.css"], #request{method = 'GET'}) ->
|
||||
serve_css();
|
||||
|
||||
process(["new"], #request{method = 'GET', lang = Lang, host = Host}) ->
|
||||
form_new_get(Host, Lang);
|
||||
process(["new"], #request{method = 'GET', lang = Lang, host = Host, ip = IP}) ->
|
||||
{Addr, _Port} = IP,
|
||||
form_new_get(Host, Lang, Addr);
|
||||
|
||||
process(["delete"], #request{method = 'GET', lang = Lang, host = Host}) ->
|
||||
form_del_get(Host, Lang);
|
||||
@ -185,8 +186,8 @@ index_page(Lang) ->
|
||||
%%% Formulary new account GET
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
form_new_get(Host, Lang) ->
|
||||
CaptchaEls = build_captcha_li_list(Lang),
|
||||
form_new_get(Host, Lang, IP) ->
|
||||
CaptchaEls = build_captcha_li_list(Lang, IP),
|
||||
HeadEls = [
|
||||
?XCT("title", "Register a Jabber account"),
|
||||
?XA("link",
|
||||
@ -336,27 +337,31 @@ form_new_post(Username, Host, Password, {Id, Key}) ->
|
||||
%%% Formulary Captcha support for new GET/POST
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
build_captcha_li_list(Lang) ->
|
||||
build_captcha_li_list(Lang, IP) ->
|
||||
case ejabberd_captcha:is_feature_available() of
|
||||
true -> build_captcha_li_list2(Lang);
|
||||
true -> build_captcha_li_list2(Lang, IP);
|
||||
false -> []
|
||||
end.
|
||||
|
||||
build_captcha_li_list2(Lang) ->
|
||||
Id = randoms:get_string(),
|
||||
build_captcha_li_list2(Lang, IP) ->
|
||||
SID = "",
|
||||
From = #jid{user = "", server = "test", resource = ""},
|
||||
To = #jid{user = "", server = "test", resource = ""},
|
||||
Args = [],
|
||||
ejabberd_captcha:create_captcha(Id, SID, From, To, Lang, Args),
|
||||
{_, {CImg,CText,CId,CKey}} = ejabberd_captcha:build_captcha_html(Id, Lang),
|
||||
case ejabberd_captcha:create_captcha(SID, From, To, Lang, IP, Args) of
|
||||
{ok, Id, _} ->
|
||||
{_, {CImg,CText,CId,CKey}} =
|
||||
ejabberd_captcha:build_captcha_html(Id, Lang),
|
||||
[?XE("li", [CText,
|
||||
?C(" "),
|
||||
CId,
|
||||
CKey,
|
||||
?BR,
|
||||
CImg]
|
||||
)].
|
||||
)];
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
%%%----------------------------------------------------------------------
|
||||
%%% Formulary change password GET
|
||||
|
Loading…
Reference in New Issue
Block a user