Compare commits
21 Commits
v3.0.0-alpha-1
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 23bf45e881 | |||
| 1511a5dc82 | |||
| 041d947e83 | |||
| 2007b3d496 | |||
| 09e847b97d | |||
| b9f911554c | |||
| 24323e8b3b | |||
| 1a5eb4f73d | |||
| 3930715712 | |||
| 64bc97d471 | |||
| 4aec8dda48 | |||
| 9192c75e1a | |||
| 512d701ef8 | |||
| ef3dd090c7 | |||
| 3d5fe6a337 | |||
| 0906a68c27 | |||
| 798853abb6 | |||
| 5e3fdbeeed | |||
| 04545f2668 | |||
| f383511e0d | |||
| 2f65188edb |
+2
-1
@@ -1620,7 +1620,8 @@ create accounts, change password or edit vCard that is stored in LDAP.</P><P> <A
|
||||
LDAP servers. This option is required.
|
||||
</DD><DT CLASS="dt-description"><B><TT>{ldap_encrypt, none|tls}</TT></B></DT><DD CLASS="dd-description"> Type of connection encryption to the LDAP server.
|
||||
Allowed values are: <TT>none</TT>, <TT>tls</TT>.
|
||||
Note that STARTTLS is not supported.
|
||||
The value <TT>tls</TT> enables encryption by using LDAP over SSL.
|
||||
Note that STARTTLS encryption is not supported.
|
||||
The default value is: <TT>none</TT>.
|
||||
</DD><DT CLASS="dt-description"><B><TT>{ldap_port, Number}</TT></B></DT><DD CLASS="dd-description"> Port to connect to your LDAP server.
|
||||
The default port is 389 if encryption is disabled; and 636 if encryption is enabled.
|
||||
|
||||
+2
-1
@@ -2163,7 +2163,8 @@ Parameters:
|
||||
LDAP servers. This option is required.
|
||||
\titem{\{ldap\_encrypt, none|tls\}} \ind{options!ldap\_encrypt}Type of connection encryption to the LDAP server.
|
||||
Allowed values are: \term{none}, \term{tls}.
|
||||
Note that STARTTLS is not supported.
|
||||
The value \term{tls} enables encryption by using LDAP over SSL.
|
||||
Note that STARTTLS encryption is not supported.
|
||||
The default value is: \term{none}.
|
||||
\titem{\{ldap\_port, Number\}} \ind{options!ldap\_port}Port to connect to your LDAP server.
|
||||
The default port is~389 if encryption is disabled; and 636 if encryption is enabled.
|
||||
|
||||
@@ -135,6 +135,8 @@
|
||||
- Implicit item deletion is not notified when deleting node
|
||||
- Make PubSub x-data configuration form handles list value
|
||||
- Make default node name convention XEP-compatible, document usage of hierarchy
|
||||
- Node names are used verbatim, without separating by slash, unless a
|
||||
node plugin uses its own separator
|
||||
- Send authorization update event (XEP-0060, 8.6)
|
||||
- Support of collection node subscription options
|
||||
- Support ODBC storage. Experimental, needs more testing.
|
||||
@@ -207,14 +209,24 @@
|
||||
|
||||
Upgrading From ejabberd 2.0.x:
|
||||
|
||||
- The database schemas didn't change since ejabberd 1.1.4.
|
||||
Anyway, it is recommended to backup the Mnesia spool directory and
|
||||
- The database schemas have three changes since ejabberd 2.0.x.
|
||||
Check the database creation SQL files and update your database.
|
||||
1) New table roster_version to support roster versioning.
|
||||
2) Six new tables for optional pubsub ODBC storage.
|
||||
3) Some tables in the MySQL database have a new created_at column.
|
||||
|
||||
- As usual, it is recommended to backup the Mnesia spool directory and
|
||||
your SQL database (if used) before upgrading ejabberd.
|
||||
|
||||
- The plugin of mod_pubsub "default" is renamed to "flat". You need
|
||||
to edit the ejabberd configuration file and replace those names.
|
||||
- Between ejabberd 2.0.0 and 2.0.5, mod_pubsub used "default" as the
|
||||
default node plugin. But in 2.1.0 this is renamed to "hometree".
|
||||
You have to edit your ejabberd config file and replace those names.
|
||||
If you used ejabberd 2.0.5 or older, the database will be updated
|
||||
automatically. But if you were using ejabberd from SVN, you must
|
||||
manually run ejabberdctl with the command: rename_default_nodeplugin.
|
||||
Running this command on already updated database does nothing.
|
||||
|
||||
- The listener options 'ip' and inet6' are not documented anymore
|
||||
- The listener options 'ip' and 'inet6' are not documented anymore
|
||||
but they are supported and you can continue using them.
|
||||
There is a new syntax to define IP address and IP version.
|
||||
As usual, check the ejabberd Guide for more information.
|
||||
|
||||
@@ -129,7 +129,7 @@ init_udp(PortIP, Module, Opts, SockOpts, Port, IPS) ->
|
||||
init_tcp(PortIP, Module, Opts, SockOpts, Port, IPS) ->
|
||||
SockOpts2 = case erlang:system_info(otp_release) >= "R13B" of
|
||||
true -> [{send_timeout_close, true} | SockOpts];
|
||||
false -> []
|
||||
false -> SockOpts
|
||||
end,
|
||||
Res = gen_tcp:listen(Port, [binary,
|
||||
{packet, 0},
|
||||
|
||||
@@ -272,7 +272,6 @@ open_socket2(Type, Addr, Port) ->
|
||||
case (catch ejabberd_socket:connect(Addr, Port,
|
||||
[binary, {packet, 0},
|
||||
{send_timeout, ?TCP_SEND_TIMEOUT},
|
||||
{send_timeout_close, true},
|
||||
{active, false}, Type | SockOpts],
|
||||
Timeout)) of
|
||||
{ok, _Socket} = R -> R;
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
%%% 02111-1307 USA
|
||||
%%%
|
||||
%%% 2009, improvements from Process-One to support correct PEP handling
|
||||
%%% 2009, improvements from ProcessOne to support correct PEP handling
|
||||
%%% through s2s, use less memory, and speedup global caps handling
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -191,8 +191,8 @@ init([ServerHost, Opts]) ->
|
||||
ets:insert(gen_mod:get_module_proc(Host, config), {max_items_node, MaxItemsNode}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {nodetree, NodeTree}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, Plugins}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, LastItemCache}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, LastItemCache}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, MaxItemsNode}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {pep_mapping, PepMapping}),
|
||||
ejabberd_hooks:add(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75),
|
||||
ejabberd_hooks:add(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75),
|
||||
@@ -1030,6 +1030,18 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) ->
|
||||
end
|
||||
end.
|
||||
|
||||
command_disco_info(_Host, <<?NS_COMMANDS>>, _From) ->
|
||||
IdentityEl = {xmlelement, "identity", [{"category", "automation"},
|
||||
{"type", "command-list"}],
|
||||
[]},
|
||||
{result, [IdentityEl]};
|
||||
command_disco_info(_Host, <<?NS_PUBSUB_GET_PENDING>>, _From) ->
|
||||
IdentityEl = {xmlelement, "identity", [{"category", "automation"},
|
||||
{"type", "command-node"}],
|
||||
[]},
|
||||
FeaturesEl = {xmlelement, "feature", [{"var", ?NS_COMMANDS}], []},
|
||||
{result, [IdentityEl, FeaturesEl]}.
|
||||
|
||||
node_disco_info(Host, Node, From) ->
|
||||
node_disco_info(Host, Node, From, true, true).
|
||||
node_disco_identity(Host, Node, From) ->
|
||||
@@ -1092,21 +1104,38 @@ iq_disco_info(Host, SNode, From, Lang) ->
|
||||
{xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_COMMANDS}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++
|
||||
lists:map(fun(Feature) ->
|
||||
{xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++Feature}], []}
|
||||
end, features(Host, Node))};
|
||||
<<?NS_COMMANDS>> ->
|
||||
command_disco_info(Host, Node, From);
|
||||
<<?NS_PUBSUB_GET_PENDING>> ->
|
||||
command_disco_info(Host, Node, From);
|
||||
_ ->
|
||||
node_disco_info(Host, Node, From)
|
||||
end.
|
||||
|
||||
iq_disco_items(Host, [], From) ->
|
||||
{result, lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) ->
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, tree_action(Host, get_subnodes, [Host, <<>>, From]))};
|
||||
case tree_action(Host, get_subnodes, [Host, <<>>, From]) of
|
||||
Nodes when is_list(Nodes) ->
|
||||
{result, lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) ->
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, Nodes)};
|
||||
Other ->
|
||||
Other
|
||||
end;
|
||||
iq_disco_items(Host, ?NS_COMMANDS, _From) ->
|
||||
%% TODO: support localization of this string
|
||||
CommandItems = [{xmlelement, "item", [{"jid", Host}, {"node", ?NS_PUBSUB_GET_PENDING}, {"name", "Get Pending"}], []}],
|
||||
{result, CommandItems};
|
||||
iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From) ->
|
||||
CommandItems = [],
|
||||
{result, CommandItems};
|
||||
iq_disco_items(Host, Item, From) ->
|
||||
case string:tokens(Item, "!") of
|
||||
[_SNode, _ItemID] ->
|
||||
@@ -1122,7 +1151,9 @@ iq_disco_items(Host, Item, From) ->
|
||||
end,
|
||||
Nodes = lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}}) ->
|
||||
{xmlelement, "item", [{"jid", Host}|nodeAttr(SubNode)], []}
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, tree_call(Host, get_subnodes, [Host, Node, From])),
|
||||
Items = lists:map(
|
||||
fun(#pubsub_item{itemid = {RN, _}}) ->
|
||||
@@ -1174,7 +1205,7 @@ iq_get_vcard(Lang) ->
|
||||
[{xmlcdata,
|
||||
translate:translate(Lang,
|
||||
"ejabberd Publish-Subscribe module") ++
|
||||
"\nCopyright (c) 2004-2009 Process-One"}]}].
|
||||
"\nCopyright (c) 2004-2009 ProcessOne"}]}].
|
||||
|
||||
iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) ->
|
||||
iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)).
|
||||
@@ -1366,6 +1397,13 @@ adhoc_request(Host, _ServerHost, Owner,
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
adhoc_request(_Host, _ServerHost, _Owner, #adhoc_request{action = "cancel"},
|
||||
_Access, _Plugins) ->
|
||||
#adhoc_response{status = canceled};
|
||||
adhoc_request(Host, ServerHost, Owner, #adhoc_request{action = []} = R,
|
||||
Access, Plugins) ->
|
||||
adhoc_request(Host, ServerHost, Owner, R#adhoc_request{action = "execute"},
|
||||
Access, Plugins);
|
||||
adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) ->
|
||||
?DEBUG("Couldn't process ad hoc command:~n~p", [Other]),
|
||||
{error, ?ERR_ITEM_NOT_FOUND}.
|
||||
@@ -1417,17 +1455,12 @@ send_pending_auth_events(Host, Node, Owner) ->
|
||||
?DEBUG("Sending pending auth events for ~s on ~s:~s",
|
||||
[jlib:jid_to_string(Owner), Host, node_to_string(Node)]),
|
||||
Action =
|
||||
fun (#pubsub_node{id = NodeID, type = Type} = N) ->
|
||||
fun (#pubsub_node{id = NodeID, type = Type}) ->
|
||||
case lists:member("get-pending", features(Type)) of
|
||||
true ->
|
||||
case node_call(Type, get_affiliation, [NodeID, Owner]) of
|
||||
{result, owner} ->
|
||||
{result, Subscriptions} = node_call(Type, get_node_subscriptions, [NodeID]),
|
||||
lists:foreach(fun({J, pending, _SubID}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
({J, pending}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
(_) -> ok
|
||||
end, Subscriptions),
|
||||
{result, ok};
|
||||
node_call(Type, get_node_subscriptions, [NodeID]);
|
||||
_ ->
|
||||
{error, ?ERR_FORBIDDEN}
|
||||
end;
|
||||
@@ -1436,7 +1469,11 @@ send_pending_auth_events(Host, Node, Owner) ->
|
||||
end
|
||||
end,
|
||||
case transaction(Host, Node, Action, sync_dirty) of
|
||||
{result, _} ->
|
||||
{result, {N, Subscriptions}} ->
|
||||
lists:foreach(fun({J, pending, _SubID}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
({J, pending}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
(_) -> ok
|
||||
end, Subscriptions),
|
||||
#adhoc_response{};
|
||||
Err ->
|
||||
Err
|
||||
@@ -1768,7 +1805,7 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
|
||||
%%<li>The node is the root collection node, which cannot be deleted.</li>
|
||||
%%<li>The specified node does not exist.</li>
|
||||
%%</ul>
|
||||
delete_node(_Host, [], _Owner) ->
|
||||
delete_node(_Host, <<>>, _Owner) ->
|
||||
%% Node is the root
|
||||
{error, ?ERR_NOT_ALLOWED};
|
||||
delete_node(Host, Node, Owner) ->
|
||||
@@ -3583,7 +3620,7 @@ features(Type) ->
|
||||
{'EXIT', {undef, _}} -> [];
|
||||
Result -> Result
|
||||
end.
|
||||
features(Host, []) ->
|
||||
features(Host, <<>>) ->
|
||||
lists:usort(lists:foldl(fun(Plugin, Acc) ->
|
||||
Acc ++ features(Plugin)
|
||||
end, [], plugins(Host)));
|
||||
|
||||
@@ -191,8 +191,8 @@ init([ServerHost, Opts]) ->
|
||||
ets:insert(gen_mod:get_module_proc(Host, config), {max_items_node, MaxItemsNode}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {nodetree, NodeTree}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, Plugins}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, LastItemCache}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, LastItemCache}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, MaxItemsNode}),
|
||||
ets:insert(gen_mod:get_module_proc(ServerHost, config), {pep_mapping, PepMapping}),
|
||||
ejabberd_hooks:add(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75),
|
||||
ejabberd_hooks:add(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75),
|
||||
@@ -834,6 +834,18 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) ->
|
||||
end
|
||||
end.
|
||||
|
||||
command_disco_info(_Host, <<?NS_COMMANDS>>, _From) ->
|
||||
IdentityEl = {xmlelement, "identity", [{"category", "automation"},
|
||||
{"type", "command-list"}],
|
||||
[]},
|
||||
{result, [IdentityEl]};
|
||||
command_disco_info(_Host, <<?NS_PUBSUB_GET_PENDING>>, _From) ->
|
||||
IdentityEl = {xmlelement, "identity", [{"category", "automation"},
|
||||
{"type", "command-node"}],
|
||||
[]},
|
||||
FeaturesEl = {xmlelement, "feature", [{"var", ?NS_COMMANDS}], []},
|
||||
{result, [IdentityEl, FeaturesEl]}.
|
||||
|
||||
node_disco_info(Host, Node, From) ->
|
||||
node_disco_info(Host, Node, From, true, true).
|
||||
node_disco_identity(Host, Node, From) ->
|
||||
@@ -897,22 +909,39 @@ iq_disco_info(Host, SNode, From, Lang) ->
|
||||
{xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_COMMANDS}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++
|
||||
lists:map(fun
|
||||
("rsm")-> {xmlelement, "feature", [{"var", ?NS_RSM}], []};
|
||||
(T) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []}
|
||||
end, features(Host, Node))};
|
||||
<<?NS_COMMANDS>> ->
|
||||
command_disco_info(Host, Node, From);
|
||||
<<?NS_PUBSUB_GET_PENDING>> ->
|
||||
command_disco_info(Host, Node, From);
|
||||
_ ->
|
||||
node_disco_info(Host, Node, From)
|
||||
end.
|
||||
|
||||
iq_disco_items(Host, [], From, _RSM) ->
|
||||
{result, lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) ->
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, tree_action(Host, get_subnodes, [Host, <<>>, From]))};
|
||||
case tree_action(Host, get_subnodes, [Host, <<>>, From]) of
|
||||
Nodes when is_list(Nodes) ->
|
||||
{result, lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) ->
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, Nodes)};
|
||||
Other ->
|
||||
Other
|
||||
end;
|
||||
iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) ->
|
||||
%% TODO: support localization of this string
|
||||
CommandItems = [{xmlelement, "item", [{"jid", Host}, {"node", ?NS_PUBSUB_GET_PENDING}, {"name", "Get Pending"}], []}],
|
||||
{result, CommandItems};
|
||||
iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) ->
|
||||
CommandItems = [],
|
||||
{result, CommandItems};
|
||||
iq_disco_items(Host, Item, From, RSM) ->
|
||||
case string:tokens(Item, "!") of
|
||||
[_SNode, _ItemID] ->
|
||||
@@ -928,7 +957,9 @@ iq_disco_items(Host, Item, From, RSM) ->
|
||||
end,
|
||||
Nodes = lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}}) ->
|
||||
{xmlelement, "item", [{"jid", Host}|nodeAttr(SubNode)], []}
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, tree_call(Host, get_subnodes, [Host, Node, From])),
|
||||
Items = lists:map(
|
||||
fun(#pubsub_item{itemid = {RN, _}}) ->
|
||||
@@ -980,7 +1011,7 @@ iq_get_vcard(Lang) ->
|
||||
[{xmlcdata,
|
||||
translate:translate(Lang,
|
||||
"ejabberd Publish-Subscribe module") ++
|
||||
"\nCopyright (c) 2004-2009 Process-One"}]}].
|
||||
"\nCopyright (c) 2004-2009 ProcessOne"}]}].
|
||||
|
||||
iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) ->
|
||||
iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)).
|
||||
@@ -1175,6 +1206,13 @@ adhoc_request(Host, _ServerHost, Owner,
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
adhoc_request(_Host, _ServerHost, _Owner, #adhoc_request{action = "cancel"},
|
||||
_Access, _Plugins) ->
|
||||
#adhoc_response{status = canceled};
|
||||
adhoc_request(Host, ServerHost, Owner, #adhoc_request{action = []} = R,
|
||||
Access, Plugins) ->
|
||||
adhoc_request(Host, ServerHost, Owner, R#adhoc_request{action = "execute"},
|
||||
Access, Plugins);
|
||||
adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) ->
|
||||
?DEBUG("Couldn't process ad hoc command:~n~p", [Other]),
|
||||
{error, ?ERR_ITEM_NOT_FOUND}.
|
||||
@@ -1227,17 +1265,12 @@ send_pending_auth_events(Host, Node, Owner) ->
|
||||
?DEBUG("Sending pending auth events for ~s on ~s:~s",
|
||||
[jlib:jid_to_string(Owner), Host, node_to_string(Node)]),
|
||||
Action =
|
||||
fun (#pubsub_node{id = NodeID, type = Type} = N) ->
|
||||
fun (#pubsub_node{id = NodeID, type = Type}) ->
|
||||
case lists:member("get-pending", features(Type)) of
|
||||
true ->
|
||||
case node_call(Type, get_affiliation, [NodeID, Owner]) of
|
||||
{result, owner} ->
|
||||
{result, Subscriptions} = node_call(Type, get_node_subscriptions, [NodeID]),
|
||||
lists:foreach(fun({J, pending, _SubID}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
({J, pending}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
(_) -> ok
|
||||
end, Subscriptions),
|
||||
{result, ok};
|
||||
node_call(Type, get_node_subscriptions, [NodeID]);
|
||||
_ ->
|
||||
{error, ?ERR_FORBIDDEN}
|
||||
end;
|
||||
@@ -1246,7 +1279,11 @@ send_pending_auth_events(Host, Node, Owner) ->
|
||||
end
|
||||
end,
|
||||
case transaction(Host, Node, Action, sync_dirty) of
|
||||
{result, _} ->
|
||||
{result, {N, Subscriptions}} ->
|
||||
lists:foreach(fun({J, pending, _SubID}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
({J, pending}) -> send_authorization_request(N, jlib:make_jid(J));
|
||||
(_) -> ok
|
||||
end, Subscriptions),
|
||||
#adhoc_response{};
|
||||
Err ->
|
||||
Err
|
||||
@@ -1578,7 +1615,7 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) ->
|
||||
%%<li>The node is the root collection node, which cannot be deleted.</li>
|
||||
%%<li>The specified node does not exist.</li>
|
||||
%%</ul>
|
||||
delete_node(_Host, [], _Owner) ->
|
||||
delete_node(_Host, <<>>, _Owner) ->
|
||||
%% Node is the root
|
||||
{error, ?ERR_NOT_ALLOWED};
|
||||
delete_node(Host, Node, Owner) ->
|
||||
@@ -3416,7 +3453,7 @@ features(Type) ->
|
||||
{'EXIT', {undef, _}} -> [];
|
||||
Result -> Result
|
||||
end.
|
||||
features(Host, []) ->
|
||||
features(Host, <<>>) ->
|
||||
lists:usort(lists:foldl(fun(Plugin, Acc) ->
|
||||
Acc ++ features(Plugin)
|
||||
end, [], plugins(Host)));
|
||||
|
||||
@@ -154,17 +154,21 @@ get_parentnodes_tree(Host, NodeID, _From) ->
|
||||
get_subnodes(Host, NodeID, _From) ->
|
||||
get_subnodes(Host, NodeID).
|
||||
|
||||
get_subnodes(Host, <<>>) ->
|
||||
get_subnodes_helper(Host, <<>>);
|
||||
get_subnodes(Host, NodeID) ->
|
||||
case find_node(Host, NodeID) of
|
||||
false -> {error, ?ERR_ITEM_NOT_FOUND};
|
||||
_ ->
|
||||
Q = qlc:q([Node || #pubsub_node{nodeid = {NHost, _},
|
||||
parents = Parents} = Node <- mnesia:table(pubsub_node),
|
||||
Host == NHost,
|
||||
lists:member(NodeID, Parents)]),
|
||||
qlc:e(Q)
|
||||
_ -> get_subnodes_helper(Host, NodeID)
|
||||
end.
|
||||
|
||||
get_subnodes_helper(Host, NodeID) ->
|
||||
Q = qlc:q([Node || #pubsub_node{nodeid = {NHost, _},
|
||||
parents = Parents} = Node <- mnesia:table(pubsub_node),
|
||||
Host == NHost,
|
||||
lists:member(NodeID, Parents)]),
|
||||
qlc:e(Q).
|
||||
|
||||
get_subnodes_tree(Host, NodeID, From) ->
|
||||
Pred = fun (NID, #pubsub_node{parents = Parents}) ->
|
||||
lists:member(NID, Parents)
|
||||
@@ -224,6 +228,8 @@ validate_parentage(_Key, _Owners, []) ->
|
||||
true;
|
||||
validate_parentage(Key, Owners, [[] | T]) ->
|
||||
validate_parentage(Key, Owners, T);
|
||||
validate_parentage(Key, Owners, [<<>> | T]) ->
|
||||
validate_parentage(Key, Owners, T);
|
||||
validate_parentage(Key, Owners, [ParentID | T]) ->
|
||||
case find_node(Key, ParentID) of
|
||||
false -> {error, ?ERR_ITEM_NOT_FOUND};
|
||||
|
||||
@@ -197,7 +197,7 @@ get_subnodes_tree(Host, Node) ->
|
||||
end, [], pubsub_node)
|
||||
end.
|
||||
|
||||
%% @spec (Host, Node, Type, Owner, Options) -> ok | {error, Reason}
|
||||
%% @spec (Host, Node, Type, Owner, Options, Parents) -> ok | {error, Reason}
|
||||
%% Host = mod_pubsub:host() | mod_pubsub:jid()
|
||||
%% Node = mod_pubsub:pubsubNode()
|
||||
%% NodeType = mod_pubsub:nodeType()
|
||||
|
||||
@@ -203,7 +203,7 @@ get_subnodes_tree(Host, Node) ->
|
||||
[]
|
||||
end.
|
||||
|
||||
%% @spec (Host, Node, Type, Owner, Options) -> ok | {error, Reason}
|
||||
%% @spec (Host, Node, Type, Owner, Options, Parents) -> ok | {error, Reason}
|
||||
%% Host = mod_pubsub:host() | mod_pubsub:jid()
|
||||
%% Node = mod_pubsub:pubsubNode()
|
||||
%% NodeType = mod_pubsub:nodeType()
|
||||
|
||||
@@ -148,7 +148,7 @@ get_subnodes_tree(Host, Node, _From) ->
|
||||
get_subnodes_tree(_Host, _Node) ->
|
||||
[].
|
||||
|
||||
%% @spec (Host, Node, Type, Owner, Options) -> ok
|
||||
%% @spec (Host, Node, Type, Owner, Options, Parents) -> ok
|
||||
%% Host = mod_pubsub:host()
|
||||
%% Node = mod_pubsub:pubsubNode()
|
||||
%% Type = mod_pubsub:nodeType()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--- mod_pubsub.erl 2009-10-20 16:33:47.000000000 +0200
|
||||
+++ mod_pubsub_odbc.erl 2009-10-20 16:33:26.000000000 +0200
|
||||
--- mod_pubsub.erl 2009-11-06 12:01:44.000000000 +0100
|
||||
+++ mod_pubsub_odbc.erl 2009-11-06 12:01:53.000000000 +0100
|
||||
@@ -42,7 +42,7 @@
|
||||
%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see
|
||||
%%% XEP-0060 section 12.18.
|
||||
@@ -307,7 +307,7 @@
|
||||
{result, IQRes} ->
|
||||
jlib:iq_to_xml(
|
||||
IQ#iq{type = result,
|
||||
@@ -1048,7 +852,7 @@
|
||||
@@ -1060,7 +864,7 @@
|
||||
[] ->
|
||||
["leaf"]; %% No sub-nodes: it's a leaf node
|
||||
_ ->
|
||||
@@ -316,7 +316,7 @@
|
||||
{result, []} -> ["collection"];
|
||||
{result, _} -> ["leaf", "collection"];
|
||||
_ -> []
|
||||
@@ -1064,8 +868,9 @@
|
||||
@@ -1076,8 +880,9 @@
|
||||
[];
|
||||
true ->
|
||||
[{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []} |
|
||||
@@ -328,9 +328,9 @@
|
||||
end, features(Type))]
|
||||
end,
|
||||
%% TODO: add meta-data info (spec section 5.4)
|
||||
@@ -1093,21 +898,22 @@
|
||||
{xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []},
|
||||
@@ -1106,8 +911,9 @@
|
||||
{xmlelement, "feature", [{"var", ?NS_PUBSUB}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_COMMANDS}], []},
|
||||
{xmlelement, "feature", [{"var", ?NS_VCARD}], []}] ++
|
||||
- lists:map(fun(Feature) ->
|
||||
- {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++Feature}], []}
|
||||
@@ -338,24 +338,36 @@
|
||||
+ ("rsm")-> {xmlelement, "feature", [{"var", ?NS_RSM}], []};
|
||||
+ (T) -> {xmlelement, "feature", [{"var", ?NS_PUBSUB++"#"++T}], []}
|
||||
end, features(Host, Node))};
|
||||
_ ->
|
||||
<<?NS_COMMANDS>> ->
|
||||
command_disco_info(Host, Node, From);
|
||||
@@ -1117,7 +923,7 @@
|
||||
node_disco_info(Host, Node, From)
|
||||
end.
|
||||
|
||||
-iq_disco_items(Host, [], From) ->
|
||||
+iq_disco_items(Host, [], From, _RSM) ->
|
||||
{result, lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}, type = Type}) ->
|
||||
{result, Path} = node_call(Type, node_to_path, [SubNode]),
|
||||
[Name|_] = lists:reverse(Path),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}|nodeAttr(SubNode)], []}
|
||||
end, tree_action(Host, get_subnodes, [Host, <<>>, From]))};
|
||||
case tree_action(Host, get_subnodes, [Host, <<>>, From]) of
|
||||
Nodes when is_list(Nodes) ->
|
||||
{result, lists:map(
|
||||
@@ -1129,14 +935,14 @@
|
||||
Other ->
|
||||
Other
|
||||
end;
|
||||
-iq_disco_items(Host, ?NS_COMMANDS, _From) ->
|
||||
+iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) ->
|
||||
%% TODO: support localization of this string
|
||||
CommandItems = [{xmlelement, "item", [{"jid", Host}, {"node", ?NS_PUBSUB_GET_PENDING}, {"name", "Get Pending"}], []}],
|
||||
{result, CommandItems};
|
||||
-iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From) ->
|
||||
+iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) ->
|
||||
CommandItems = [],
|
||||
{result, CommandItems};
|
||||
-iq_disco_items(Host, Item, From) ->
|
||||
+iq_disco_items(Host, Item, From, RSM) ->
|
||||
case string:tokens(Item, "!") of
|
||||
[_SNode, _ItemID] ->
|
||||
{result, []};
|
||||
@@ -1115,10 +921,10 @@
|
||||
@@ -1144,10 +950,10 @@
|
||||
Node = string_to_node(SNode),
|
||||
Action =
|
||||
fun(#pubsub_node{type = Type, id = NodeId}) ->
|
||||
@@ -369,7 +381,7 @@
|
||||
end,
|
||||
Nodes = lists:map(
|
||||
fun(#pubsub_node{nodeid = {_, SubNode}}) ->
|
||||
@@ -1129,7 +935,7 @@
|
||||
@@ -1160,7 +966,7 @@
|
||||
{result, Name} = node_call(Type, get_item_name, [Host, Node, RN]),
|
||||
{xmlelement, "item", [{"jid", Host}, {"name", Name}], []}
|
||||
end, NodeItems),
|
||||
@@ -378,7 +390,7 @@
|
||||
end,
|
||||
case transaction(Host, Node, Action, sync_dirty) of
|
||||
{result, {_, Result}} -> {result, Result};
|
||||
@@ -1258,7 +1064,8 @@
|
||||
@@ -1289,7 +1095,8 @@
|
||||
(_, Acc) ->
|
||||
Acc
|
||||
end, [], xml:remove_cdata(Els)),
|
||||
@@ -388,7 +400,7 @@
|
||||
{get, "subscriptions"} ->
|
||||
get_subscriptions(Host, Node, From, Plugins);
|
||||
{get, "affiliations"} ->
|
||||
@@ -1281,7 +1088,9 @@
|
||||
@@ -1312,7 +1119,9 @@
|
||||
|
||||
iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) ->
|
||||
{xmlelement, _, _, SubEls} = SubEl,
|
||||
@@ -399,7 +411,7 @@
|
||||
case Action of
|
||||
[{xmlelement, Name, Attrs, Els}] ->
|
||||
Node = string_to_node(xml:get_attr_s("node", Attrs)),
|
||||
@@ -1404,7 +1213,8 @@
|
||||
@@ -1442,7 +1251,8 @@
|
||||
_ -> []
|
||||
end
|
||||
end,
|
||||
@@ -409,7 +421,7 @@
|
||||
sync_dirty) of
|
||||
{result, Res} -> Res;
|
||||
Err -> Err
|
||||
@@ -1444,7 +1254,7 @@
|
||||
@@ -1481,7 +1291,7 @@
|
||||
|
||||
%%% authorization handling
|
||||
|
||||
@@ -418,7 +430,7 @@
|
||||
Lang = "en", %% TODO fix
|
||||
Stanza = {xmlelement, "message",
|
||||
[],
|
||||
@@ -1473,7 +1283,7 @@
|
||||
@@ -1510,7 +1320,7 @@
|
||||
[{xmlelement, "value", [], [{xmlcdata, "false"}]}]}]}]},
|
||||
lists:foreach(fun(Owner) ->
|
||||
ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza}
|
||||
@@ -427,7 +439,7 @@
|
||||
|
||||
find_authorization_response(Packet) ->
|
||||
{xmlelement, _Name, _Attrs, Els} = Packet,
|
||||
@@ -1537,8 +1347,8 @@
|
||||
@@ -1574,8 +1384,8 @@
|
||||
"true" -> true;
|
||||
_ -> false
|
||||
end,
|
||||
@@ -438,7 +450,7 @@
|
||||
{result, Subscriptions} = node_call(Type, get_subscriptions, [NodeId, Subscriber]),
|
||||
if
|
||||
not IsApprover ->
|
||||
@@ -1729,7 +1539,7 @@
|
||||
@@ -1766,7 +1576,7 @@
|
||||
Reply = [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}],
|
||||
[{xmlelement, "create", nodeAttr(Node),
|
||||
[]}]}],
|
||||
@@ -447,7 +459,7 @@
|
||||
{result, {Result, broadcast}} ->
|
||||
%%Lang = "en", %% TODO: fix
|
||||
%%OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)),
|
||||
@@ -1837,7 +1647,7 @@
|
||||
@@ -1874,7 +1684,7 @@
|
||||
%%<li>The node does not exist.</li>
|
||||
%%</ul>
|
||||
subscribe_node(Host, Node, From, JID, Configuration) ->
|
||||
@@ -456,7 +468,7 @@
|
||||
{result, GoodSubOpts} -> GoodSubOpts;
|
||||
_ -> invalid
|
||||
end,
|
||||
@@ -1845,7 +1655,7 @@
|
||||
@@ -1882,7 +1692,7 @@
|
||||
error -> {"", "", ""};
|
||||
J -> jlib:jid_tolower(J)
|
||||
end,
|
||||
@@ -465,7 +477,7 @@
|
||||
Features = features(Type),
|
||||
SubscribeFeature = lists:member("subscribe", Features),
|
||||
OptionsFeature = lists:member("subscription-options", Features),
|
||||
@@ -1864,9 +1674,13 @@
|
||||
@@ -1901,9 +1711,13 @@
|
||||
{"", "", ""} ->
|
||||
{false, false};
|
||||
_ ->
|
||||
@@ -482,7 +494,7 @@
|
||||
end
|
||||
end,
|
||||
if
|
||||
@@ -2197,7 +2011,7 @@
|
||||
@@ -2234,7 +2048,7 @@
|
||||
%% <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.
|
||||
@@ -491,7 +503,7 @@
|
||||
MaxItems =
|
||||
if
|
||||
SMaxItems == "" -> get_max_items_node(Host);
|
||||
@@ -2236,11 +2050,11 @@
|
||||
@@ -2273,11 +2087,11 @@
|
||||
node_call(Type, get_items,
|
||||
[NodeId, From,
|
||||
AccessModel, PresenceSubscription, RosterGroup,
|
||||
@@ -505,7 +517,7 @@
|
||||
SendItems = case ItemIDs of
|
||||
[] ->
|
||||
Items;
|
||||
@@ -2253,7 +2067,8 @@
|
||||
@@ -2290,7 +2104,8 @@
|
||||
%% number of items sent to MaxItems:
|
||||
{result, [{xmlelement, "pubsub", [{"xmlns", ?NS_PUBSUB}],
|
||||
[{xmlelement, "items", nodeAttr(Node),
|
||||
@@ -515,7 +527,7 @@
|
||||
Error ->
|
||||
Error
|
||||
end
|
||||
@@ -2285,16 +2100,27 @@
|
||||
@@ -2322,16 +2137,27 @@
|
||||
%% @doc <p>Resend the items of a node to the user.</p>
|
||||
%% @todo use cache-last-item feature
|
||||
send_items(Host, Node, NodeId, Type, LJID, last) ->
|
||||
@@ -549,7 +561,7 @@
|
||||
send_items(Host, Node, NodeId, Type, LJID, Number) ->
|
||||
ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of
|
||||
{result, []} ->
|
||||
@@ -2420,29 +2246,12 @@
|
||||
@@ -2457,29 +2283,12 @@
|
||||
error ->
|
||||
{error, ?ERR_BAD_REQUEST};
|
||||
_ ->
|
||||
@@ -582,7 +594,7 @@
|
||||
end, Entities),
|
||||
{result, []};
|
||||
_ ->
|
||||
@@ -2495,11 +2304,11 @@
|
||||
@@ -2532,11 +2341,11 @@
|
||||
end.
|
||||
|
||||
read_sub(Subscriber, Node, NodeID, SubID, Lang) ->
|
||||
@@ -596,7 +608,7 @@
|
||||
OptionsEl = {xmlelement, "options", [{"jid", jlib:jid_to_string(Subscriber)},
|
||||
{"subid", SubID}|nodeAttr(Node)],
|
||||
[XdataEl]},
|
||||
@@ -2525,7 +2334,7 @@
|
||||
@@ -2562,7 +2371,7 @@
|
||||
end.
|
||||
|
||||
set_options_helper(Configuration, JID, NodeID, SubID, Type) ->
|
||||
@@ -605,7 +617,7 @@
|
||||
{result, GoodSubOpts} -> GoodSubOpts;
|
||||
_ -> invalid
|
||||
end,
|
||||
@@ -2554,7 +2363,7 @@
|
||||
@@ -2591,7 +2400,7 @@
|
||||
write_sub(_Subscriber, _NodeID, _SubID, invalid) ->
|
||||
{error, extended_error(?ERR_BAD_REQUEST, "invalid-options")};
|
||||
write_sub(Subscriber, NodeID, SubID, Options) ->
|
||||
@@ -614,7 +626,7 @@
|
||||
{error, notfound} ->
|
||||
{error, extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")};
|
||||
{result, _} ->
|
||||
@@ -2722,8 +2531,8 @@
|
||||
@@ -2759,8 +2568,8 @@
|
||||
{"subscription", subscription_to_string(Sub)} | nodeAttr(Node)], []}]}]},
|
||||
ejabberd_router ! {route, service_jid(Host), jlib:make_jid(JID), Stanza}
|
||||
end,
|
||||
@@ -625,7 +637,7 @@
|
||||
true ->
|
||||
Result = lists:foldl(fun({JID, Subscription, SubId}, Acc) ->
|
||||
|
||||
@@ -3007,7 +2816,7 @@
|
||||
@@ -3044,7 +2853,7 @@
|
||||
{Depth, [{N, get_node_subs(N)} || N <- Nodes]}
|
||||
end, tree_call(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)]))}
|
||||
end,
|
||||
@@ -634,7 +646,7 @@
|
||||
{result, CollSubs} -> CollSubs;
|
||||
_ -> []
|
||||
end.
|
||||
@@ -3021,9 +2830,9 @@
|
||||
@@ -3058,9 +2867,9 @@
|
||||
|
||||
get_options_for_subs(NodeID, Subs) ->
|
||||
lists:foldl(fun({JID, subscribed, SubID}, Acc) ->
|
||||
@@ -646,7 +658,7 @@
|
||||
_ -> Acc
|
||||
end;
|
||||
(_, Acc) ->
|
||||
@@ -3221,6 +3030,30 @@
|
||||
@@ -3258,6 +3067,30 @@
|
||||
Result
|
||||
end.
|
||||
|
||||
@@ -677,7 +689,7 @@
|
||||
%% @spec (Host, Options) -> MaxItems
|
||||
%% Host = host()
|
||||
%% Options = [Option]
|
||||
@@ -3607,7 +3440,13 @@
|
||||
@@ -3644,7 +3477,13 @@
|
||||
tree_action(Host, Function, Args) ->
|
||||
?DEBUG("tree_action ~p ~p ~p",[Host,Function,Args]),
|
||||
Fun = fun() -> tree_call(Host, Function, Args) end,
|
||||
@@ -692,7 +704,7 @@
|
||||
|
||||
%% @doc <p>node plugin call.</p>
|
||||
node_call(Type, Function, Args) ->
|
||||
@@ -3627,13 +3466,13 @@
|
||||
@@ -3664,13 +3503,13 @@
|
||||
|
||||
node_action(Host, Type, Function, Args) ->
|
||||
?DEBUG("node_action ~p ~p ~p ~p",[Host,Type,Function,Args]),
|
||||
@@ -708,7 +720,7 @@
|
||||
case tree_call(Host, get_node, [Host, Node]) of
|
||||
N when is_record(N, pubsub_node) ->
|
||||
case Action(N) of
|
||||
@@ -3646,8 +3485,14 @@
|
||||
@@ -3683,8 +3522,14 @@
|
||||
end
|
||||
end, Trans).
|
||||
|
||||
@@ -725,7 +737,7 @@
|
||||
{result, Result} -> {result, Result};
|
||||
{error, Error} -> {error, Error};
|
||||
{atomic, {result, Result}} -> {result, Result};
|
||||
@@ -3655,6 +3500,15 @@
|
||||
@@ -3692,6 +3537,15 @@
|
||||
{aborted, Reason} ->
|
||||
?ERROR_MSG("transaction return internal error: ~p~n", [{aborted, Reason}]),
|
||||
{error, ?ERR_INTERNAL_SERVER_ERROR};
|
||||
@@ -741,7 +753,7 @@
|
||||
{'EXIT', Reason} ->
|
||||
?ERROR_MSG("transaction return internal error: ~p~n", [{'EXIT', Reason}]),
|
||||
{error, ?ERR_INTERNAL_SERVER_ERROR};
|
||||
@@ -3663,6 +3517,17 @@
|
||||
@@ -3700,6 +3554,17 @@
|
||||
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
||||
end.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* ejabberd, Copyright (C) 2002-2009 Process-one
|
||||
* ejabberd, Copyright (C) 2002-2009 ProcessOne
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
|
||||
@@ -369,7 +369,7 @@ static int stringprep_erl_control(ErlDrvData drv_data,
|
||||
}
|
||||
last_ral = info & D1Mask;
|
||||
have_ral = have_ral || last_ral;
|
||||
have_l = info & D2Mask;
|
||||
have_l |= info & D2Mask;
|
||||
ADD_UCHAR(ruc);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user