From b43444f674e577a21bac889b732c4752530036e5 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Mon, 2 Apr 2012 14:49:13 +1000 Subject: [PATCH] ODBC support for mod_announce --- doc/guide.tex | 4 + src/ejd2odbc.erl | 29 ++ src/mod_announce_odbc.erl | 907 ++++++++++++++++++++++++++++++++++++++ src/odbc/mysql.sql | 6 + src/odbc/pg.sql | 6 + 5 files changed, 952 insertions(+) create mode 100644 src/mod_announce_odbc.erl diff --git a/doc/guide.tex b/doc/guide.tex index 4c701e4a0..4ae4ba033 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -66,6 +66,7 @@ \newcommand{\module}[1]{\texttt{#1}} \newcommand{\modadhoc}{\module{mod\_adhoc}} \newcommand{\modannounce}{\module{mod\_announce}} +\newcommand{\modannounceodbc}{\module{mod\_announce\_odbc}} \newcommand{\modblocking}{\module{mod\_blocking}} \newcommand{\modcaps}{\module{mod\_caps}} \newcommand{\modconfigure}{\module{mod\_configure}} @@ -2579,6 +2580,8 @@ The following table lists all modules included in \ejabberd{}. \hline \hline \modadhoc{} & Ad-Hoc Commands (\xepref{0050}) & \\ \hline \ahrefloc{modannounce}{\modannounce{}} & Manage announcements & recommends \modadhoc{} \\ + \hline \ahrefloc{modannounce}{\modannounceodbc{}} & Manage announcements & recommends \modadhoc{} \\ + & & supported DB (*) \\ \hline \modblocking{} & Simple Communications Blocking (\xepref{0191}) & \modprivacy{} \\ \hline \modcaps{} & Entity Capabilities (\xepref{0115}) & \\ \hline \modconfigure{} & Server configuration using Ad-Hoc & \modadhoc{} \\ @@ -2654,6 +2657,7 @@ database for the following data: \item User rules for blocking communications: Use \term{mod\_privacy\_odbc} instead of \term{mod\_privacy}. \item Pub-Sub nodes, items and subscriptions: Use \term{mod\_pubsub\_odbc} instead of \term{mod\_pubsub}. \item Multi-user chats: Use \term{mod\_muc\_odbc} instead of \term{mod\_muc}. +\item Manage announcements: Use \term{mod\_announce\_odbc} instead of \term{mod\_announce}. \end{itemize} You can find more diff --git a/src/ejd2odbc.erl b/src/ejd2odbc.erl index a1f3a1eed..4f09cd7b2 100644 --- a/src/ejd2odbc.erl +++ b/src/ejd2odbc.erl @@ -36,6 +36,8 @@ export_vcard_search/2, export_private_storage/2, export_privacy/2, + export_motd/2, + export_motd_users/2, export_muc_room/2, export_muc_registered/2]). @@ -64,6 +66,8 @@ -record(private_storage, {usns, xml}). -record(muc_room, {name_host, opts}). -record(muc_registered, {us_host, nick}). +-record(motd, {server, packet}). +-record(motd_users, {us, dummy = []}). -define(MAX_RECORDS_PER_TRANSACTION, 1000). @@ -355,6 +359,31 @@ export_privacy(Server, Output) -> [] end). +export_motd(Server, Output) -> + export_common( + Server, motd, Output, + fun(Host, #motd{server = LServer, packet = El}) + when LServer == Host -> + ["delete from motd where username='';" + "insert into motd(username, xml) values ('', '", + ejabberd_odbc:escape(xml:element_to_binary(El)), "');"]; + (_Host, _R) -> + [] + end). + +export_motd_users(Server, Output) -> + export_common( + Server, motd_users, Output, + fun(Host, #motd_users{us = {LUser, LServer}}) + when LServer == Host, LUser /= "" -> + Username = ejabberd_odbc:escape(LUser), + ["delete from motd where username='", Username, "';" + "insert into motd(username, xml) values ('", + Username, "', '');"]; + (_Host, _R) -> + [] + end). + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- diff --git a/src/mod_announce_odbc.erl b/src/mod_announce_odbc.erl new file mode 100644 index 000000000..7e1e9dca0 --- /dev/null +++ b/src/mod_announce_odbc.erl @@ -0,0 +1,907 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_announce_odbc.erl +%%% Author : Alexey Shchepin +%%% Purpose : Manage announce messages +%%% Created : 11 Aug 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +%%% Implements a small subset of XEP-0133: Service Administration +%%% Version 1.1 (2005-08-19) + +-module(mod_announce_odbc). +-author('alexey@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, + init/0, + stop/1, + announce/3, + send_motd/1, + disco_identity/5, + disco_features/5, + disco_items/5, + send_announcement_to_all/3, + announce_commands/4, + announce_items/4]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("adhoc.hrl"). + +-define(PROCNAME, ejabberd_announce). + +-define(NS_ADMINL(Sub), ["http:","jabber.org","protocol","admin", Sub]). +tokenize(Node) -> string:tokens(Node, "/#"). + +start(Host, _Opts) -> + ejabberd_hooks:add(local_send_to_resource_hook, Host, + ?MODULE, announce, 50), + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 50), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), + ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 50), + ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, announce_items, 50), + ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, announce_commands, 50), + ejabberd_hooks:add(user_available_hook, Host, + ?MODULE, send_motd, 50), + register(gen_mod:get_module_proc(Host, ?PROCNAME), + proc_lib:spawn(?MODULE, init, [])). + +init() -> + loop(). + +loop() -> + receive + {announce_all, From, To, Packet} -> + announce_all(From, To, Packet), + loop(); + {announce_all_hosts_all, From, To, Packet} -> + announce_all_hosts_all(From, To, Packet), + loop(); + {announce_online, From, To, Packet} -> + announce_online(From, To, Packet), + loop(); + {announce_all_hosts_online, From, To, Packet} -> + announce_all_hosts_online(From, To, Packet), + loop(); + {announce_motd, From, To, Packet} -> + announce_motd(From, To, Packet), + loop(); + {announce_all_hosts_motd, From, To, Packet} -> + announce_all_hosts_motd(From, To, Packet), + loop(); + {announce_motd_update, From, To, Packet} -> + announce_motd_update(From, To, Packet), + loop(); + {announce_all_hosts_motd_update, From, To, Packet} -> + announce_all_hosts_motd_update(From, To, Packet), + loop(); + {announce_motd_delete, From, To, Packet} -> + announce_motd_delete(From, To, Packet), + loop(); + {announce_all_hosts_motd_delete, From, To, Packet} -> + announce_all_hosts_motd_delete(From, To, Packet), + loop(); + _ -> + loop() + end. + +stop(Host) -> + ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, announce_commands, 50), + ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, announce_items, 50), + ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 50), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), + ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 50), + ejabberd_hooks:delete(local_send_to_resource_hook, Host, + ?MODULE, announce, 50), + ejabberd_hooks:delete(user_available_hook, Host, + ?MODULE, send_motd, 50), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + exit(whereis(Proc), stop), + {wait, Proc}. + +%% Announcing via messages to a custom resource +announce(From, To, Packet) -> + case To of + #jid{luser = "", lresource = Res} -> + {xmlelement, Name, _Attrs, _Els} = Packet, + Proc = gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME), + case {Res, Name} of + {"announce/all", "message"} -> + Proc ! {announce_all, From, To, Packet}, + stop; + {"announce/all-hosts/all", "message"} -> + Proc ! {announce_all_hosts_all, From, To, Packet}, + stop; + {"announce/online", "message"} -> + Proc ! {announce_online, From, To, Packet}, + stop; + {"announce/all-hosts/online", "message"} -> + Proc ! {announce_all_hosts_online, From, To, Packet}, + stop; + {"announce/motd", "message"} -> + Proc ! {announce_motd, From, To, Packet}, + stop; + {"announce/all-hosts/motd", "message"} -> + Proc ! {announce_all_hosts_motd, From, To, Packet}, + stop; + {"announce/motd/update", "message"} -> + Proc ! {announce_motd_update, From, To, Packet}, + stop; + {"announce/all-hosts/motd/update", "message"} -> + Proc ! {announce_all_hosts_motd_update, From, To, Packet}, + stop; + {"announce/motd/delete", "message"} -> + Proc ! {announce_motd_delete, From, To, Packet}, + stop; + {"announce/all-hosts/motd/delete", "message"} -> + Proc ! {announce_all_hosts_motd_delete, From, To, Packet}, + stop; + _ -> + ok + end; + _ -> + ok + end. + +%%------------------------------------------------------------------------- +%% Announcing via ad-hoc commands +-define(INFO_COMMAND(Lang, Node), + [{xmlelement, "identity", + [{"category", "automation"}, + {"type", "command-node"}, + {"name", get_title(Lang, Node)}], []}]). + +disco_identity(Acc, _From, _To, Node, Lang) -> + LNode = tokenize(Node), + case LNode of + ?NS_ADMINL("announce") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("announce-allhosts") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("announce-all") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("announce-all-allhosts") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("set-motd") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("set-motd-allhosts") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("edit-motd") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("edit-motd-allhosts") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("delete-motd") -> + ?INFO_COMMAND(Lang, Node); + ?NS_ADMINL("delete-motd-allhosts") -> + ?INFO_COMMAND(Lang, Node); + _ -> + Acc + end. + +%%------------------------------------------------------------------------- + +-define(INFO_RESULT(Allow, Feats), + case Allow of + deny -> + {error, ?ERR_FORBIDDEN}; + allow -> + {result, Feats} + end). + +disco_features(Acc, From, #jid{lserver = LServer} = _To, + "announce", _Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + Access1 = gen_mod:get_module_opt(LServer, ?MODULE, access, none), + Access2 = gen_mod:get_module_opt(global, ?MODULE, access, none), + case {acl:match_rule(LServer, Access1, From), + acl:match_rule(global, Access2, From)} of + {deny, deny} -> + {error, ?ERR_FORBIDDEN}; + _ -> + {result, []} + end + end; + +disco_features(Acc, From, #jid{lserver = LServer} = _To, + Node, _Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + Access = gen_mod:get_module_opt(LServer, ?MODULE, access, none), + Allow = acl:match_rule(LServer, Access, From), + AccessGlobal = gen_mod:get_module_opt(global, ?MODULE, access, none), + AllowGlobal = acl:match_rule(global, AccessGlobal, From), + case Node of + ?NS_ADMIN ++ "#announce" -> + ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#announce-all" -> + ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#set-motd" -> + ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#edit-motd" -> + ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#delete-motd" -> + ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#announce-allhosts" -> + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#announce-all-allhosts" -> + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#set-motd-allhosts" -> + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#edit-motd-allhosts" -> + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?NS_ADMIN ++ "#delete-motd-allhosts" -> + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + _ -> + Acc + end + end. + +%%------------------------------------------------------------------------- + +-define(NODE_TO_ITEM(Lang, Server, Node), + {xmlelement, "item", + [{"jid", Server}, + {"node", Node}, + {"name", get_title(Lang, Node)}], + []}). + +-define(ITEMS_RESULT(Allow, Items), + case Allow of + deny -> + {error, ?ERR_FORBIDDEN}; + allow -> + {result, Items} + end). + +disco_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, + "", Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + Access1 = gen_mod:get_module_opt(LServer, ?MODULE, access, none), + Access2 = gen_mod:get_module_opt(global, ?MODULE, access, none), + case {acl:match_rule(LServer, Access1, From), + acl:match_rule(global, Access2, From)} of + {deny, deny} -> + Acc; + _ -> + Items = case Acc of + {result, I} -> I; + _ -> [] + end, + Nodes = [?NODE_TO_ITEM(Lang, Server, "announce")], + {result, Items ++ Nodes} + end + end; + +disco_items(Acc, From, #jid{lserver = LServer} = To, "announce", Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + announce_items(Acc, From, To, Lang) + end; + +disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> + case gen_mod:is_loaded(LServer, mod_adhoc) of + false -> + Acc; + _ -> + Access = gen_mod:get_module_opt(LServer, ?MODULE, access, none), + Allow = acl:match_rule(LServer, Access, From), + AccessGlobal = gen_mod:get_module_opt(global, ?MODULE, access, none), + AllowGlobal = acl:match_rule(global, AccessGlobal, From), + case Node of + ?NS_ADMIN ++ "#announce" -> + ?ITEMS_RESULT(Allow, []); + ?NS_ADMIN ++ "#announce-all" -> + ?ITEMS_RESULT(Allow, []); + ?NS_ADMIN ++ "#set-motd" -> + ?ITEMS_RESULT(Allow, []); + ?NS_ADMIN ++ "#edit-motd" -> + ?ITEMS_RESULT(Allow, []); + ?NS_ADMIN ++ "#delete-motd" -> + ?ITEMS_RESULT(Allow, []); + ?NS_ADMIN ++ "#announce-allhosts" -> + ?ITEMS_RESULT(AllowGlobal, []); + ?NS_ADMIN ++ "#announce-all-allhosts" -> + ?ITEMS_RESULT(AllowGlobal, []); + ?NS_ADMIN ++ "#set-motd-allhosts" -> + ?ITEMS_RESULT(AllowGlobal, []); + ?NS_ADMIN ++ "#edit-motd-allhosts" -> + ?ITEMS_RESULT(AllowGlobal, []); + ?NS_ADMIN ++ "#delete-motd-allhosts" -> + ?ITEMS_RESULT(AllowGlobal, []); + _ -> + Acc + end + end. + +%%------------------------------------------------------------------------- + +announce_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, Lang) -> + Access1 = gen_mod:get_module_opt(LServer, ?MODULE, access, none), + Nodes1 = case acl:match_rule(LServer, Access1, From) of + allow -> + [?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#announce"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#announce-all"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#set-motd"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#edit-motd"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#delete-motd")]; + deny -> + [] + end, + Access2 = gen_mod:get_module_opt(global, ?MODULE, access, none), + Nodes2 = case acl:match_rule(global, Access2, From) of + allow -> + [?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#announce-allhosts"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#announce-all-allhosts"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#set-motd-allhosts"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#edit-motd-allhosts"), + ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN ++ "#delete-motd-allhosts")]; + deny -> + [] + end, + case {Nodes1, Nodes2} of + {[], []} -> + Acc; + _ -> + Items = case Acc of + {result, I} -> I; + _ -> [] + end, + {result, Items ++ Nodes1 ++ Nodes2} + end. + +%%------------------------------------------------------------------------- + +commands_result(Allow, From, To, Request) -> + case Allow of + deny -> + {error, ?ERR_FORBIDDEN}; + allow -> + announce_commands(From, To, Request) + end. + + +announce_commands(Acc, From, #jid{lserver = LServer} = To, + #adhoc_request{ node = Node} = Request) -> + LNode = tokenize(Node), + F = fun() -> + Access = gen_mod:get_module_opt(global, ?MODULE, access, none), + Allow = acl:match_rule(global, Access, From), + commands_result(Allow, From, To, Request) + end, + R = case LNode of + ?NS_ADMINL("announce-allhosts") -> F(); + ?NS_ADMINL("announce-all-allhosts") -> F(); + ?NS_ADMINL("set-motd-allhosts") -> F(); + ?NS_ADMINL("edit-motd-allhosts") -> F(); + ?NS_ADMINL("delete-motd-allhosts") -> F(); + _ -> + Access = gen_mod:get_module_opt(LServer, ?MODULE, access, none), + Allow = acl:match_rule(LServer, Access, From), + case LNode of + ?NS_ADMINL("announce") -> + commands_result(Allow, From, To, Request); + ?NS_ADMINL("announce-all") -> + commands_result(Allow, From, To, Request); + ?NS_ADMINL("set-motd") -> + commands_result(Allow, From, To, Request); + ?NS_ADMINL("edit-motd") -> + commands_result(Allow, From, To, Request); + ?NS_ADMINL("delete-motd") -> + commands_result(Allow, From, To, Request); + _ -> + unknown + end + end, + case R of + unknown -> Acc; + _ -> {stop, R} + end. + +%%------------------------------------------------------------------------- + +announce_commands(From, To, + #adhoc_request{lang = Lang, + node = Node, + action = Action, + xdata = XData} = Request) -> + %% If the "action" attribute is not present, it is + %% understood as "execute". If there was no + %% element in the first response (which there isn't in our + %% case), "execute" and "complete" are equivalent. + ActionIsExecute = lists:member(Action, + ["", "execute", "complete"]), + if Action == "cancel" -> + %% User cancels request + adhoc:produce_response(Request, + #adhoc_response{status = canceled}); + XData == false, ActionIsExecute -> + %% User requests form + Elements = generate_adhoc_form(Lang, Node, To#jid.lserver), + adhoc:produce_response( + Request, + #adhoc_response{status = executing, + elements = [Elements]}); + XData /= false, ActionIsExecute -> + %% User returns form. + case jlib:parse_xdata_submit(XData) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + Fields -> + handle_adhoc_form(From, To, Request, Fields) + end; + true -> + {error, ?ERR_BAD_REQUEST} + end. + +-define(VVALUE(Val), + {xmlelement, "value", [], [{xmlcdata, Val}]}). +-define(TVFIELD(Type, Var, Val), + {xmlelement, "field", [{"type", Type}, + {"var", Var}], + vvaluel(Val)}). +-define(HFIELD(), ?TVFIELD("hidden", "FORM_TYPE", ?NS_ADMIN)). + +vvaluel(Val) -> + case Val of + "" -> []; + _ -> [?VVALUE(Val)] + end. + +generate_adhoc_form(Lang, Node, ServerHost) -> + LNode = tokenize(Node), + {OldSubject, OldBody} = if (LNode == ?NS_ADMINL("edit-motd")) + or (LNode == ?NS_ADMINL("edit-motd-allhosts")) -> + get_stored_motd(ServerHost); + true -> + {[], []} + end, + {xmlelement, "x", + [{"xmlns", ?NS_XDATA}, + {"type", "form"}], + [?HFIELD(), + {xmlelement, "title", [], [{xmlcdata, get_title(Lang, Node)}]}] + ++ + if (LNode == ?NS_ADMINL("delete-motd")) + or (LNode == ?NS_ADMINL("delete-motd-allhosts")) -> + [{xmlelement, "field", + [{"var", "confirm"}, + {"type", "boolean"}, + {"label", translate:translate(Lang, "Really delete message of the day?")}], + [{xmlelement, "value", + [], + [{xmlcdata, "true"}]}]}]; + true -> + [{xmlelement, "field", + [{"var", "subject"}, + {"type", "text-single"}, + {"label", translate:translate(Lang, "Subject")}], + vvaluel(OldSubject)}, + {xmlelement, "field", + [{"var", "body"}, + {"type", "text-multi"}, + {"label", translate:translate(Lang, "Message body")}], + vvaluel(OldBody)}] + end}. + +join_lines([]) -> + []; +join_lines(Lines) -> + join_lines(Lines, []). +join_lines([Line|Lines], Acc) -> + join_lines(Lines, ["\n",Line|Acc]); +join_lines([], Acc) -> + %% Remove last newline + lists:flatten(lists:reverse(tl(Acc))). + +handle_adhoc_form(From, #jid{lserver = LServer} = To, + #adhoc_request{lang = Lang, + node = Node, + sessionid = SessionID}, + Fields) -> + Confirm = case lists:keysearch("confirm", 1, Fields) of + {value, {"confirm", ["true"]}} -> + true; + {value, {"confirm", ["1"]}} -> + true; + _ -> + false + end, + Subject = case lists:keysearch("subject", 1, Fields) of + {value, {"subject", SubjectLines}} -> + %% There really shouldn't be more than one + %% subject line, but can we stop them? + join_lines(SubjectLines); + _ -> + [] + end, + Body = case lists:keysearch("body", 1, Fields) of + {value, {"body", BodyLines}} -> + join_lines(BodyLines); + _ -> + [] + end, + Response = #adhoc_response{lang = Lang, + node = Node, + sessionid = SessionID, + status = completed}, + Packet = {xmlelement, "message", [{"type", "normal"}], + if Subject /= [] -> + [{xmlelement, "subject", [], + [{xmlcdata, Subject}]}]; + true -> + [] + end ++ + if Body /= [] -> + [{xmlelement, "body", [], + [{xmlcdata, Body}]}]; + true -> + [] + end}, + + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), + case {Node, Body} of + {?NS_ADMIN ++ "#delete-motd", _} -> + if Confirm -> + Proc ! {announce_motd_delete, From, To, Packet}, + adhoc:produce_response(Response); + true -> + adhoc:produce_response(Response) + end; + {?NS_ADMIN ++ "#delete-motd-allhosts", _} -> + if Confirm -> + Proc ! {announce_all_hosts_motd_delete, From, To, Packet}, + adhoc:produce_response(Response); + true -> + adhoc:produce_response(Response) + end; + {_, []} -> + %% An announce message with no body is definitely an operator error. + %% Throw an error and give him/her a chance to send message again. + {error, ?ERRT_NOT_ACCEPTABLE( + Lang, + "No body provided for announce message")}; + %% Now send the packet to ?PROCNAME. + %% We don't use direct announce_* functions because it + %% leads to large delay in response and queries processing + {?NS_ADMIN ++ "#announce", _} -> + Proc ! {announce_online, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#announce-allhosts", _} -> + Proc ! {announce_all_hosts_online, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#announce-all", _} -> + Proc ! {announce_all, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#announce-all-allhosts", _} -> + Proc ! {announce_all_hosts_all, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#set-motd", _} -> + Proc ! {announce_motd, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#set-motd-allhosts", _} -> + Proc ! {announce_all_hosts_motd, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#edit-motd", _} -> + Proc ! {announce_motd_update, From, To, Packet}, + adhoc:produce_response(Response); + {?NS_ADMIN ++ "#edit-motd-allhosts", _} -> + Proc ! {announce_all_hosts_motd_update, From, To, Packet}, + adhoc:produce_response(Response); + _ -> + %% This can't happen, as we haven't registered any other + %% command nodes. + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +get_title(Lang, "announce") -> + translate:translate(Lang, "Announcements"); +get_title(Lang, ?NS_ADMIN ++ "#announce-all") -> + translate:translate(Lang, "Send announcement to all users"); +get_title(Lang, ?NS_ADMIN ++ "#announce-all-allhosts") -> + translate:translate(Lang, "Send announcement to all users on all hosts"); +get_title(Lang, ?NS_ADMIN ++ "#announce") -> + translate:translate(Lang, "Send announcement to all online users"); +get_title(Lang, ?NS_ADMIN ++ "#announce-allhosts") -> + translate:translate(Lang, "Send announcement to all online users on all hosts"); +get_title(Lang, ?NS_ADMIN ++ "#set-motd") -> + translate:translate(Lang, "Set message of the day and send to online users"); +get_title(Lang, ?NS_ADMIN ++ "#set-motd-allhosts") -> + translate:translate(Lang, "Set message of the day on all hosts and send to online users"); +get_title(Lang, ?NS_ADMIN ++ "#edit-motd") -> + translate:translate(Lang, "Update message of the day (don't send)"); +get_title(Lang, ?NS_ADMIN ++ "#edit-motd-allhosts") -> + translate:translate(Lang, "Update message of the day on all hosts (don't send)"); +get_title(Lang, ?NS_ADMIN ++ "#delete-motd") -> + translate:translate(Lang, "Delete message of the day"); +get_title(Lang, ?NS_ADMIN ++ "#delete-motd-allhosts") -> + translate:translate(Lang, "Delete message of the day on all hosts"). + +%%------------------------------------------------------------------------- + +announce_all(From, To, Packet) -> + Host = To#jid.lserver, + Access = gen_mod:get_module_opt(Host, ?MODULE, access, none), + case acl:match_rule(Host, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + Local = jlib:make_jid("", To#jid.server, ""), + lists:foreach( + fun({User, Server}) -> + Dest = jlib:make_jid(User, Server, ""), + ejabberd_router:route(Local, Dest, Packet) + end, ejabberd_auth:get_vh_registered_users(Host)) + end. + +announce_all_hosts_all(From, To, Packet) -> + Access = gen_mod:get_module_opt(global, ?MODULE, access, none), + case acl:match_rule(global, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + Local = jlib:make_jid("", To#jid.server, ""), + lists:foreach( + fun({User, Server}) -> + Dest = jlib:make_jid(User, Server, ""), + ejabberd_router:route(Local, Dest, Packet) + end, ejabberd_auth:dirty_get_registered_users()) + end. + +announce_online(From, To, Packet) -> + Host = To#jid.lserver, + Access = gen_mod:get_module_opt(Host, ?MODULE, access, none), + case acl:match_rule(Host, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + announce_online1(ejabberd_sm:get_vh_session_list(Host), + To#jid.server, + Packet) + end. + +announce_all_hosts_online(From, To, Packet) -> + Access = gen_mod:get_module_opt(global, ?MODULE, access, none), + case acl:match_rule(global, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + announce_online1(ejabberd_sm:dirty_get_sessions_list(), + To#jid.server, + Packet) + end. + +announce_online1(Sessions, Server, Packet) -> + Local = jlib:make_jid("", Server, ""), + lists:foreach( + fun({U, S, R}) -> + Dest = jlib:make_jid(U, S, R), + ejabberd_router:route(Local, Dest, Packet) + end, Sessions). + +announce_motd(From, To, Packet) -> + Host = To#jid.lserver, + Access = gen_mod:get_module_opt(Host, ?MODULE, access, none), + case acl:match_rule(Host, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + announce_motd(Host, Packet) + end. + +announce_all_hosts_motd(From, To, Packet) -> + Access = gen_mod:get_module_opt(global, ?MODULE, access, none), + case acl:match_rule(global, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + Hosts = ?MYHOSTS, + [announce_motd(Host, Packet) || Host <- Hosts] + end. + +announce_motd(Host, Packet) -> + announce_motd_update(Host, Packet), + Sessions = ejabberd_sm:get_vh_session_list(Host), + announce_online1(Sessions, Host, Packet), + F = fun() -> + lists:foreach( + fun({U, _S, _R}) -> + Username = ejabberd_odbc:escape(U), + update_t("motd", + ["username", "xml"], + [Username, ""], + ["username='", Username, "'"]) + end, Sessions) + end, + LServer = jlib:nameprep(Host), + ejabberd_odbc:sql_transaction(LServer, F). + +announce_motd_update(From, To, Packet) -> + Host = To#jid.lserver, + Access = gen_mod:get_module_opt(Host, ?MODULE, access, none), + case acl:match_rule(Host, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + announce_motd_update(Host, Packet) + end. + +announce_all_hosts_motd_update(From, To, Packet) -> + Access = gen_mod:get_module_opt(global, ?MODULE, access, none), + case acl:match_rule(global, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + Hosts = ?MYHOSTS, + [announce_motd_update(Host, Packet) || Host <- Hosts] + end. + +announce_motd_update(LServer, Packet) -> + announce_motd_delete(LServer), + XML = ejabberd_odbc:escape(xml:element_to_binary(Packet)), + F = fun() -> + update_t("motd", + ["username", "xml"], + ["", XML], + ["username=''"]) + end, + ejabberd_odbc:sql_transaction(LServer, F). + +announce_motd_delete(From, To, Packet) -> + Host = To#jid.lserver, + Access = gen_mod:get_module_opt(Host, ?MODULE, access, none), + case acl:match_rule(Host, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + announce_motd_delete(Host) + end. + +announce_all_hosts_motd_delete(From, To, Packet) -> + Access = gen_mod:get_module_opt(global, ?MODULE, access, none), + case acl:match_rule(global, Access, From) of + deny -> + Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + ejabberd_router:route(To, From, Err); + allow -> + Hosts = ?MYHOSTS, + [announce_motd_delete(Host) || Host <- Hosts] + end. + +announce_motd_delete(LServer) -> + F = fun() -> + ejabberd_odbc:sql_query_t(["delete from motd;"]) + end, + ejabberd_odbc:sql_transaction(LServer, F). + +send_motd(#jid{luser = LUser, lserver = LServer} = JID) when LUser /= "" -> + case catch ejabberd_odbc:sql_query( + LServer, ["select xml from motd where username='';"]) of + {selected, ["xml"], [{XML}]} -> + case xml_stream:parse_element(XML) of + {error, _} -> + ok; + Packet -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + LServer, + ["select username from motd " + "where username='", Username, "';"]) of + {selected, ["username"], []} -> + Local = jlib:make_jid("", LServer, ""), + ejabberd_router:route(Local, JID, Packet), + F = fun() -> + update_t( + ["motd"], + ["username", "xml"], + [Username, ""], + ["username='", Username, "'"]) + end, + ejabberd_odbc:sql_transaction(LServer, F); + _ -> + ok + end + end; + _ -> + ok + end; +send_motd(_) -> + ok. + +get_stored_motd(LServer) -> + case catch ejabberd_odbc:sql_query( + LServer, ["select xml from motd where username='';"]) of + {selected, ["xml"], [{XML}]} -> + case xml_stream:parse_element(XML) of + {error, _} -> + {"", ""}; + Packet -> + {xml:get_subtag_cdata(Packet, "subject"), + xml:get_subtag_cdata(Packet, "body")} + end; + _ -> + {"", ""} + end. + +%% This function is similar to others, but doesn't perform any ACL verification +send_announcement_to_all(Host, SubjectS, BodyS) -> + SubjectEls = if SubjectS /= [] -> + [{xmlelement, "subject", [], [{xmlcdata, SubjectS}]}]; + true -> + [] + end, + BodyEls = if BodyS /= [] -> + [{xmlelement, "body", [], [{xmlcdata, BodyS}]}]; + true -> + [] + end, + Packet = {xmlelement, "message", [{"type", "normal"}], SubjectEls ++ BodyEls}, + Sessions = ejabberd_sm:dirty_get_sessions_list(), + Local = jlib:make_jid("", Host, ""), + lists:foreach( + fun({U, S, R}) -> + Dest = jlib:make_jid(U, S, R), + ejabberd_router:route(Local, Dest, Packet) + end, Sessions). + +%% Almost a copy of string:join/2. +%% We use this version because string:join/2 is relatively +%% new function (introduced in R12B-0). +join([], _Sep) -> + []; +join([H|T], Sep) -> + [H, [[Sep, X] || X <- T]]. + +%% Safe atomic update. +update_t(Table, Fields, Vals, Where) -> + UPairs = lists:zipwith(fun(A, B) -> A ++ "='" ++ B ++ "'" end, + Fields, Vals), + case ejabberd_odbc:sql_query_t( + ["update ", Table, " set ", + join(UPairs, ", "), + " where ", Where, ";"]) of + {updated, 1} -> + ok; + _ -> + ejabberd_odbc:sql_query_t( + ["insert into ", Table, "(", join(Fields, ", "), + ") values ('", join(Vals, "', '"), "');"]) + end. diff --git a/src/odbc/mysql.sql b/src/odbc/mysql.sql index d67ca6c88..104b44481 100644 --- a/src/odbc/mysql.sql +++ b/src/odbc/mysql.sql @@ -238,3 +238,9 @@ CREATE TABLE muc_registered ( CREATE INDEX i_muc_registered_nick USING BTREE ON muc_registered(nick(75)); CREATE UNIQUE INDEX i_muc_registered_jid_host USING BTREE ON muc_registered(jid(75), host(75)); + +CREATE TABLE motd ( + username varchar(250) PRIMARY KEY, + xml text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) CHARACTER SET utf8; diff --git a/src/odbc/pg.sql b/src/odbc/pg.sql index aae472d6b..f28cf8af2 100644 --- a/src/odbc/pg.sql +++ b/src/odbc/pg.sql @@ -238,3 +238,9 @@ CREATE TABLE muc_registered ( CREATE INDEX i_muc_registered_nick ON muc_registered USING btree (nick); CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered USING btree (jid, host); + +CREATE TABLE motd ( + username text PRIMARY KEY, + xml text, + created_at TIMESTAMP NOT NULL DEFAULT now() +);