]> granicus.if.org Git - ejabberd/commitdiff
ODBC support for mod_shared_roster
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Wed, 4 Apr 2012 10:37:43 +0000 (20:37 +1000)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Wed, 4 Apr 2012 10:37:43 +0000 (20:37 +1000)
doc/guide.tex
src/acl.erl
src/ejd2odbc.erl
src/mod_shared_roster.erl
src/mod_shared_roster_odbc.erl [new file with mode: 0644]
src/odbc/mysql.sql
src/odbc/pg.sql

index 7198bdde7aabb4db7da206ebca8c6e71804ed26b..06814b5f18d0a65e36d46ba58cb74fcd660923be 100644 (file)
@@ -98,6 +98,7 @@
 \newcommand{\modrosterodbc}{\module{mod\_roster\_odbc}}
 \newcommand{\modservicelog}{\module{mod\_service\_log}}
 \newcommand{\modsharedroster}{\module{mod\_shared\_roster}}
+\newcommand{\modsharedrosterodbc}{\module{mod\_shared\_roster\_odbc}}
 \newcommand{\modsharedrosterldap}{\module{mod\_shared\_roster\_ldap}}
 \newcommand{\modsic}{\module{mod\_sic}}
 \newcommand{\modstats}{\module{mod\_stats}}
@@ -2616,6 +2617,8 @@ The following table lists all modules included in \ejabberd{}.
     \hline \ahrefloc{modservicelog}{\modservicelog{}} & Copy user messages to logger service &  \\
     \hline \ahrefloc{modsharedroster}{\modsharedroster{}} & Shared roster management & \modroster{} or \\
     & & \modrosterodbc\\
+    \hline \ahrefloc{modsharedroster}{\modsharedrosterodbc{}} & Shared roster management & supported DB (*) and\\
+    & & \modroster{} or \modrosterodbc\\
     \hline \ahrefloc{modsharedrosterldap}{\modsharedrosterldap{}} & LDAP Shared roster management & \modroster{} or \\
     & & \modrosterodbc\\
     \hline \ahrefloc{modsic}{\modsic{}} & Server IP Check (\xepref{0279}) &  \\
@@ -2656,6 +2659,7 @@ database for the following data:
 \item Offline messages: Use \term{mod\_offline\_odbc} instead of
   \term{mod\_offline}.
 \item Rosters: Use \term{mod\_roster\_odbc} instead of \term{mod\_roster}.
+\item Shared Rosters: Use \term{mod\_shared\_roster\_odbc} instead of \term{mod\_shared\_roster}.
 \item Users' VCARD: Use \term{mod\_vcard\_odbc} instead of \term{mod\_vcard}.
 \item vCard-Based Avatars: Use \term{mod\_vcard\_xupdate\_odbc} instead of \term{mod\_vcard\_xupdate}.
 \item Private XML storage: Use \term{mod\_private\_odbc} instead of \term{mod\_private}.
@@ -4062,7 +4066,7 @@ Options:
   This option does not affect the client in any way.
   This option is only useful if Roster Versioning is enabled.
   This option is disabled by default.
-  Important: if you use \modsharedroster{} or \modsharedrosterldap{},
+  Important: if you use \modsharedroster{}, \modsharedrosterodbc{} or \modsharedrosterldap{},
   you must disable this option.
 \end{description}
 
index 8355778639ecef2e7ee9024852858610c753aa6d..dd7c441de46da8d1b41e3ebe28896a2aefbc4d6b 100644 (file)
@@ -181,9 +181,11 @@ match_acl(ACL, JID, Host) ->
                                        lists:member(Server, ?MYHOSTS)))
                                          andalso is_regexp_match(User, UR);
                                  {shared_group, G} ->
-                                     mod_shared_roster:is_user_in_group({User, Server}, G, Host);
+                                      Mod = loaded_shared_roster_module(Host),
+                                     Mod:is_user_in_group({User, Server}, G, Host);
                                  {shared_group, G, H} ->
-                                     mod_shared_roster:is_user_in_group({User, Server}, G, H);
+                                      Mod = loaded_shared_roster_module(H),
+                                     Mod:is_user_in_group({User, Server}, G, H);
                                  {user_regexp, UR, S} ->
                                      (S == Server) andalso
                                          is_regexp_match(User, UR);
@@ -238,4 +240,10 @@ is_regexp_match(String, RegExp) ->
 is_glob_match(String, Glob) ->
     is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)).
 
-
+loaded_shared_roster_module(Host) ->
+    case {gen_mod:is_loaded(Host, mod_shared_roster_odbc),
+          gen_mod:is_loaded(Host, mod_shared_roster_ldap)} of
+        {true, _} -> mod_shared_roster_odbc;
+        {_, true} -> mod_shared_roster_ldap;
+        _ -> mod_shared_roster
+    end.
index 42956fd77fe5c4741f120e8652cdddcf1c18dc2b..206b403cde2fc7eb9883952d6f3edf0772a25fc2 100644 (file)
@@ -40,6 +40,8 @@
          export_motd/2,
          export_motd_users/2,
          export_irc_custom/2,
+         export_sr_group/2,
+         export_sr_user/2,
          export_muc_room/2,
          export_muc_registered/2]).
 
@@ -70,6 +72,8 @@
 -record(irc_custom, {us_host, data}).
 -record(muc_room, {name_host, opts}).
 -record(muc_registered, {us_host, nick}).
+-record(sr_group, {group_host, opts}).
+-record(sr_user, {us, group_host}).
 -record(motd, {server, packet}).
 -record(motd_users, {us, dummy = []}).
 
@@ -397,6 +401,38 @@ export_privacy(Server, Output) ->
               []
       end).
 
+export_sr_group(Server, Output) ->
+    export_common(
+      Server, sr_group, Output,
+      fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts})
+            when LServer == Host ->
+              SGroup = ejabberd_odbc:escape(Group),
+              SOpts = mod_shared_roster_odbc:encode_opts(Opts),
+              ["delete from sr_group where name='", Group, "';"
+               "insert into sr_group(name, opts) values ('",
+               SGroup, "', '", SOpts, "');"];
+         (_Host, _R) ->
+              []
+      end).
+
+export_sr_user(Server, Output) ->
+    export_common(
+      Server, sr_user, Output,
+      fun(Host, #sr_user{us = {U, S}, group_host = {Group, LServer}})
+            when LServer == Host ->
+              SGroup = ejabberd_odbc:escape(Group),
+              SJID = ejabberd_odbc:escape(
+                       jlib:jid_to_string(
+                         jlib:jid_tolower(
+                           jlib:make_jid(U, S, "")))),
+              ["delete from sr_user where jid='", SJID,
+               "'and grp='", Group, "';"
+               "insert into sr_user(jid, grp) values ('",
+               SJID, "', '", SGroup, "');"];
+         (_Host, _R) ->
+              []
+      end).
+
 export_motd(Server, Output) ->
     export_common(
       Server, motd, Output,
index 68ea451aaf9b7558282295e68962a4e3076e55f5..a8dd977eb71907b11738575e66c860a5acc83bcc 100644 (file)
@@ -186,7 +186,7 @@ get_vcard_module(Server) ->
 get_rosteritem_name([], _, _) ->
     "";
 get_rosteritem_name([ModVcard], U, S) ->
-    From = jlib:make_jid("", S, mod_shared_roster),
+    From = jlib:make_jid("", S, ?MODULE),
     To = jlib:make_jid(U, S, ""),
     IQ = {iq,"",get,"vcard-temp","",
          {xmlelement,"vCard",[{"xmlns","vcard-temp"}],[]}},
@@ -619,14 +619,14 @@ add_user_to_group(Host, US, Group) ->
     {LUser, LServer} = US,
     case ejabberd_regexp:run(LUser, "^@.+@$") of
        match ->
-           GroupOpts = mod_shared_roster:get_group_opts(Host, Group),
+           GroupOpts = ?MODULE:get_group_opts(Host, Group),
            MoreGroupOpts =
                case LUser of
                    "@all@" -> [{all_users, true}];
                    "@online@" -> [{online_users, true}];
                    _ -> []
                end,
-            mod_shared_roster:set_group_opts(
+            ?MODULE:set_group_opts(
              Host, Group,
              GroupOpts ++ MoreGroupOpts);
        nomatch ->
@@ -652,7 +652,7 @@ remove_user_from_group(Host, US, Group) ->
     {LUser, LServer} = US,
     case ejabberd_regexp:run(LUser, "^@.+@$") of
        match ->
-           GroupOpts = mod_shared_roster:get_group_opts(Host, Group),
+           GroupOpts = ?MODULE:get_group_opts(Host, Group),
            NewGroupOpts =
                case LUser of
                    "@all@" ->
@@ -660,7 +660,7 @@ remove_user_from_group(Host, US, Group) ->
                    "@online@" ->
                        lists:filter(fun(X) -> X/={online_users,true} end, GroupOpts)
                end,
-           mod_shared_roster:set_group_opts(Host, Group, NewGroupOpts);
+           ?MODULE:set_group_opts(Host, Group, NewGroupOpts);
        nomatch ->
            R = #sr_user{us = US, group_host = GroupHost},
            F = fun() ->
@@ -876,7 +876,7 @@ webadmin_page(Acc, _, _) -> Acc.
 
 list_shared_roster_groups(Host, Query, Lang) ->
     Res = list_sr_groups_parse_query(Host, Query),
-    SRGroups = mod_shared_roster:list_groups(Host),
+    SRGroups = ?MODULE:list_groups(Host),
     FGroups =
        ?XAE("table", [],
             [?XE("tbody",
@@ -925,19 +925,19 @@ list_sr_groups_parse_query(Host, Query) ->
 list_sr_groups_parse_addnew(Host, Query) ->
     case lists:keysearch("namenew", 1, Query) of
        {value, {_, Group}} when Group /= "" ->
-           mod_shared_roster:create_group(Host, Group),
+           ?MODULE:create_group(Host, Group),
            ok;
        _ ->
            error
     end.
 
 list_sr_groups_parse_delete(Host, Query) ->
-    SRGroups = mod_shared_roster:list_groups(Host),
+    SRGroups = ?MODULE:list_groups(Host),
     lists:foreach(
       fun(Group) ->
              case lists:member({"selected", Group}, Query) of
                  true ->
-                     mod_shared_roster:delete_group(Host, Group);
+                     ?MODULE:delete_group(Host, Group);
                  _ ->
                      ok
              end
@@ -947,14 +947,14 @@ list_sr_groups_parse_delete(Host, Query) ->
 
 shared_roster_group(Host, Group, Query, Lang) ->
     Res = shared_roster_group_parse_query(Host, Group, Query),
-    GroupOpts = mod_shared_roster:get_group_opts(Host, Group),
+    GroupOpts = ?MODULE:get_group_opts(Host, Group),
     Name = get_opt(GroupOpts, name, ""),
     Description = get_opt(GroupOpts, description, ""),
     AllUsers = get_opt(GroupOpts, all_users, false),
     OnlineUsers = get_opt(GroupOpts, online_users, false),
     %%Disabled = false,
     DisplayedGroups = get_opt(GroupOpts, displayed_groups, []),
-    Members = mod_shared_roster:get_group_explicit_users(Host, Group),
+    Members = ?MODULE:get_group_explicit_users(Host, Group),
     FMembers =
        if
            AllUsers ->
@@ -1042,7 +1042,7 @@ shared_roster_group_parse_query(Host, Group, Query) ->
                    true -> [{displayed_groups, DispGroups}]
                end,
 
-           OldMembers = mod_shared_roster:get_group_explicit_users(
+           OldMembers = ?MODULE:get_group_explicit_users(
                           Host, Group),
            SJIDs = string:tokens(SMembers, ", \r\n"),
            NewMembers =
@@ -1074,7 +1074,7 @@ shared_roster_group_parse_query(Host, Group, Query) ->
                    false -> []
                end,
 
-           mod_shared_roster:set_group_opts(
+           ?MODULE:set_group_opts(
              Host, Group,
              NameOpt ++ DispGroupsOpt ++ DescriptionOpt ++ AllUsersOpt ++ OnlineUsersOpt),
 
@@ -1085,12 +1085,12 @@ shared_roster_group_parse_query(Host, Group, Query) ->
                    RemovedMembers = OldMembers -- NewMembers,
                    lists:foreach(
                      fun(US) ->
-                             mod_shared_roster:remove_user_from_group(
+                             ?MODULE:remove_user_from_group(
                                Host, US, Group)
                      end, RemovedMembers),
                    lists:foreach(
                      fun(US) ->
-                             mod_shared_roster:add_user_to_group(
+                             ?MODULE:add_user_to_group(
                                Host, US, Group)
                      end, AddedMembers),
                    ok
diff --git a/src/mod_shared_roster_odbc.erl b/src/mod_shared_roster_odbc.erl
new file mode 100644 (file)
index 0000000..8bccc98
--- /dev/null
@@ -0,0 +1,1197 @@
+%%%----------------------------------------------------------------------
+%%% File    : mod_shared_roster_odbc.erl
+%%% Author  : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : Shared roster management
+%%% Created :  5 Mar 2005 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% 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
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_shared_roster_odbc).
+-author('alexey@process-one.net').
+
+-behaviour(gen_mod).
+
+-export([start/2, stop/1,
+         item_to_xml/1,
+        webadmin_menu/3, webadmin_page/3,
+        get_user_roster/2,
+        get_subscription_lists/3,
+        get_jid_info/4,
+        process_item/2,
+        in_subscription/6,
+        out_subscription/4,
+        user_available/1,
+        unset_presence/4,
+        register_user/2,
+        remove_user/2,
+        list_groups/1,
+        create_group/2,
+        create_group/3,
+        delete_group/2,
+        get_group_opts/2,
+        set_group_opts/3,
+        get_group_users/2,
+        get_group_explicit_users/2,
+        is_user_in_group/3,
+        add_user_to_group/3,
+         encode_opts/1,
+        remove_user_from_group/3]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("mod_roster.hrl").
+-include("web/ejabberd_http.hrl").
+-include("web/ejabberd_web_admin.hrl").
+
+start(Host, _Opts) ->
+    ejabberd_hooks:add(webadmin_menu_host, Host,
+                      ?MODULE, webadmin_menu, 70),
+    ejabberd_hooks:add(webadmin_page_host, Host,
+                      ?MODULE, webadmin_page, 50),
+    ejabberd_hooks:add(roster_get, Host,
+                      ?MODULE, get_user_roster, 70),
+    ejabberd_hooks:add(roster_in_subscription, Host,
+                      ?MODULE, in_subscription, 30),
+    ejabberd_hooks:add(roster_out_subscription, Host,
+                      ?MODULE, out_subscription, 30),
+    ejabberd_hooks:add(roster_get_subscription_lists, Host,
+                      ?MODULE, get_subscription_lists, 70),
+    ejabberd_hooks:add(roster_get_jid_info, Host,
+                      ?MODULE, get_jid_info, 70),
+    ejabberd_hooks:add(roster_process_item, Host,
+                      ?MODULE, process_item, 50),
+    ejabberd_hooks:add(user_available_hook, Host,
+                      ?MODULE, user_available, 50),
+    ejabberd_hooks:add(unset_presence_hook, Host,
+                      ?MODULE, unset_presence, 50),
+    ejabberd_hooks:add(register_user, Host,
+                      ?MODULE, register_user, 50),
+    ejabberd_hooks:add(anonymous_purge_hook, Host,
+                      ?MODULE, remove_user, 50),
+    ejabberd_hooks:add(remove_user, Host,
+                      ?MODULE, remove_user, 50).
+%%ejabberd_hooks:add(remove_user, Host,
+%%            ?MODULE, remove_user, 50),
+
+stop(Host) ->
+    ejabberd_hooks:delete(webadmin_menu_host, Host,
+                         ?MODULE, webadmin_menu, 70),
+    ejabberd_hooks:delete(webadmin_page_host, Host,
+                         ?MODULE, webadmin_page, 50),
+    ejabberd_hooks:delete(roster_get, Host,
+                         ?MODULE, get_user_roster, 70),
+    ejabberd_hooks:delete(roster_in_subscription, Host,
+                         ?MODULE, in_subscription, 30),
+    ejabberd_hooks:delete(roster_out_subscription, Host,
+                         ?MODULE, out_subscription, 30),
+    ejabberd_hooks:delete(roster_get_subscription_lists, Host,
+                         ?MODULE, get_subscription_lists, 70),
+    ejabberd_hooks:delete(roster_get_jid_info, Host,
+                         ?MODULE, get_jid_info, 70),
+    ejabberd_hooks:delete(roster_process_item, Host,
+                         ?MODULE, process_item, 50),
+    ejabberd_hooks:delete(user_available_hook, Host,
+                         ?MODULE, user_available, 50),
+    ejabberd_hooks:delete(unset_presence_hook, Host,
+                         ?MODULE, unset_presence, 50),
+    ejabberd_hooks:delete(register_user, Host,
+                         ?MODULE, register_user, 50),
+    ejabberd_hooks:delete(anonymous_purge_hook, Host,
+                         ?MODULE, remove_user, 50),
+    ejabberd_hooks:delete(remove_user, Host,
+                         ?MODULE, remove_user, 50).
+%%ejabberd_hooks:delete(remove_user, Host,
+%%               ?MODULE, remove_user, 50),
+
+
+get_user_roster(Items, US) ->
+    {U, S} = US,
+    DisplayedGroups = get_user_displayed_groups(US),
+    %% Get shared roster users in all groups and remove self: 
+    SRUsers = 
+       lists:foldl(
+         fun(Group, Acc1) ->
+                 GroupName = get_group_name(S, Group),
+                 lists:foldl(
+                   fun(User, Acc2) ->
+                           if User == US -> Acc2;
+                              true -> dict:append(User, 
+                                                  GroupName,
+                                                  Acc2)
+                           end
+                   end, Acc1, get_group_users(S, Group))
+         end, dict:new(), DisplayedGroups),
+
+    %% If partially subscribed users are also in shared roster, show them as
+    %% totally subscribed:
+    {NewItems1, SRUsersRest} =
+       lists:mapfoldl(
+         fun(Item, SRUsers1) ->
+                 {_, _, {U1, S1, _}} = Item#roster.usj,
+                 US1 = {U1, S1},
+                 case dict:find(US1, SRUsers1) of
+                     {ok, _GroupNames} ->
+                         {Item#roster{subscription = both, ask = none},
+                          dict:erase(US1, SRUsers1)};
+                     error ->
+                         {Item, SRUsers1}
+                 end
+         end, SRUsers, Items),
+
+    %% Export items in roster format:
+    ModVcard = get_vcard_module(S),
+    SRItems = [#roster{usj = {U, S, {U1, S1, ""}},
+                      us = US,
+                      jid = {U1, S1, ""},
+                      name = get_rosteritem_name(ModVcard, U1, S1),
+                      subscription = both,
+                      ask = none,
+                      groups = GroupNames} ||
+                 {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)],
+    SRItems ++ NewItems1.
+
+get_vcard_module(Server) ->
+    Modules = gen_mod:loaded_modules(Server),
+    [M || M <- Modules,
+         (M == mod_vcard) or (M == mod_vcard_odbc) or (M == mod_vcard_ldap)].
+
+get_rosteritem_name([], _, _) ->
+    "";
+get_rosteritem_name([ModVcard], U, S) ->
+    From = jlib:make_jid("", S, ?MODULE),
+    To = jlib:make_jid(U, S, ""),
+    IQ = {iq,"",get,"vcard-temp","",
+         {xmlelement,"vCard",[{"xmlns","vcard-temp"}],[]}},
+    IQ_Vcard = ModVcard:process_sm_iq(From, To, IQ),
+    try get_rosteritem_name_vcard(IQ_Vcard#iq.sub_el)
+    catch E1:E2 ->
+           ?ERROR_MSG("Error ~p found when trying to get the vCard of ~s@~s "
+                      "in ~p:~n ~p", [E1, U, S, ModVcard, E2]),
+           ""
+    end.
+
+get_rosteritem_name_vcard([]) ->
+    "";
+get_rosteritem_name_vcard([Vcard]) ->
+    case xml:get_path_s(Vcard, [{elem, "NICKNAME"}, cdata]) of
+       "" -> xml:get_path_s(Vcard, [{elem, "FN"}, cdata]);
+       Nickname -> Nickname
+    end.
+
+%% This function rewrites the roster entries when moving or renaming
+%% them in the user contact list.
+process_item(RosterItem, Host) ->
+    USFrom = {UserFrom, ServerFrom} = RosterItem#roster.us,
+    {UserTo, ServerTo, ResourceTo} = RosterItem#roster.jid,
+    NameTo = RosterItem#roster.name,
+    USTo = {UserTo, ServerTo},
+    DisplayedGroups = get_user_displayed_groups(USFrom),
+    CommonGroups = lists:filter(fun(Group) ->
+                                       is_user_in_group(USTo, Group, Host)
+                               end, DisplayedGroups),
+    case CommonGroups of
+       [] -> RosterItem;
+       %% Roster item cannot be removed: We simply reset the original groups:
+       _ when RosterItem#roster.subscription == remove ->
+           GroupNames = lists:map(fun(Group) ->
+                                          get_group_name(Host, Group)
+                                  end, CommonGroups),
+           RosterItem#roster{subscription = both, ask = none,
+                             groups=[GroupNames]};
+       %% Both users have at least a common shared group,
+       %% So each user can see the other
+       _ ->
+           %% Check if the list of groups of the new roster item
+           %% include at least a new one
+           case lists:subtract(RosterItem#roster.groups, CommonGroups) of
+                %% If it doesn't, then remove this user from any
+                %% existing roster groups.
+               [] ->
+                    %% Remove pending subscription by setting it
+                    %% unsubscribed.
+                    Mod = get_roster_mod(ServerFrom),
+
+                    %% Remove pending out subscription
+                    Mod:out_subscription(UserTo, ServerTo,
+                                         jlib:make_jid(UserFrom, ServerFrom, ""),
+                                         unsubscribe),
+
+                    %% Remove pending in subscription
+                    Mod:in_subscription(aaaa, UserFrom, ServerFrom,
+                                        jlib:make_jid(UserTo, ServerTo, ""),
+                                        unsubscribe, ""),
+
+                    %% But we're still subscribed, so respond as such.
+                   RosterItem#roster{subscription = both, ask = none};
+               %% If so, it means the user wants to add that contact
+               %% to his personal roster
+               PersonalGroups ->
+                   %% Store roster items in From and To rosters
+                   set_new_rosteritems(UserFrom, ServerFrom,
+                                       UserTo, ServerTo, ResourceTo, NameTo,
+                                       PersonalGroups)
+           end
+    end.
+
+build_roster_record(User1, Server1, User2, Server2, Name2, Groups) ->
+    USR2 = {User2, Server2, ""},
+    #roster{usj = {User1, Server1, USR2},
+           us = {User1, Server1},
+           jid = USR2,
+           name = Name2,
+           subscription = both,
+           ask = none,
+           groups = Groups
+          }.
+
+set_new_rosteritems(UserFrom, ServerFrom,
+                   UserTo, ServerTo, ResourceTo, NameTo, GroupsFrom) ->
+    Mod = get_roster_mod(ServerFrom),
+
+    RIFrom = build_roster_record(UserFrom, ServerFrom,
+                                UserTo, ServerTo, NameTo, GroupsFrom),
+    set_item(UserFrom, ServerFrom, ResourceTo, RIFrom),
+    JIDTo = jlib:make_jid(UserTo, ServerTo, ""),
+
+    JIDFrom = jlib:make_jid(UserFrom, ServerFrom, ""),
+    RITo = build_roster_record(UserTo, ServerTo,
+                              UserFrom, ServerFrom, UserFrom,[]),
+    set_item(UserTo, ServerTo, "", RITo),
+
+    %% From requests
+    Mod:out_subscription(UserFrom, ServerFrom, JIDTo, subscribe),
+    Mod:in_subscription(aaa, UserTo, ServerTo, JIDFrom, subscribe, ""),
+
+    %% To accepts
+    Mod:out_subscription(UserTo, ServerTo, JIDFrom, subscribed),
+    Mod:in_subscription(aaa, UserFrom, ServerFrom, JIDTo, subscribed, ""),
+
+    %% To requests
+    Mod:out_subscription(UserTo, ServerTo, JIDFrom, subscribe),
+    Mod:in_subscription(aaa, UserFrom, ServerFrom, JIDTo, subscribe, ""),
+
+    %% From accepts
+    Mod:out_subscription(UserFrom, ServerFrom, JIDTo, subscribed),
+    Mod:in_subscription(aaa, UserTo, ServerTo, JIDFrom, subscribed, ""),
+
+    RIFrom.
+
+set_item(User, Server, Resource, Item) ->
+    ResIQ = #iq{type = set, xmlns = ?NS_ROSTER,
+               id = "push" ++ randoms:get_string(),
+               sub_el = [{xmlelement, "query",
+                          [{"xmlns", ?NS_ROSTER}],
+                          [mod_roster:item_to_xml(Item)]}]},
+    ejabberd_router:route(
+      jlib:make_jid(User, Server, Resource),
+      jlib:make_jid("", Server, ""),
+      jlib:iq_to_xml(ResIQ)).
+
+
+get_subscription_lists({F, T}, User, Server) ->
+    LUser = jlib:nodeprep(User),
+    LServer = jlib:nameprep(Server),
+    US = {LUser, LServer},
+    DisplayedGroups = get_user_displayed_groups(US),
+    SRUsers =
+       lists:usort(
+         lists:flatmap(
+           fun(Group) ->
+                   get_group_users(LServer, Group)
+           end, DisplayedGroups)),
+    SRJIDs = [{U1, S1, ""} || {U1, S1} <- SRUsers],
+    {lists:usort(SRJIDs ++ F), lists:usort(SRJIDs ++ T)}.
+
+get_jid_info({Subscription, Groups}, User, Server, JID) ->
+    LUser = jlib:nodeprep(User),
+    LServer = jlib:nameprep(Server),
+    US = {LUser, LServer},
+    {U1, S1, _} = jlib:jid_tolower(JID),
+    US1 = {U1, S1},
+    DisplayedGroups = get_user_displayed_groups(US),
+    SRUsers = 
+       lists:foldl(
+         fun(Group, Acc1) ->
+                 lists:foldl(
+                   fun(User1, Acc2) ->
+                           dict:append(
+                             User1, get_group_name(LServer, Group), Acc2)
+                   end, Acc1, get_group_users(LServer, Group))
+         end, dict:new(), DisplayedGroups),
+    case dict:find(US1, SRUsers) of
+       {ok, GroupNames} ->
+           NewGroups = if
+                           Groups == [] -> GroupNames;
+                           true -> Groups
+                       end,
+           {both, NewGroups};
+       error ->
+           {Subscription, Groups}
+    end.
+
+in_subscription(Acc, User, Server, JID, Type, _Reason) ->
+    process_subscription(in, User, Server, JID, Type, Acc).
+
+out_subscription(UserFrom, ServerFrom, JIDTo, unsubscribed) ->
+    Mod = get_roster_mod(ServerFrom),
+
+    %% Remove pending out subscription
+    #jid{luser = UserTo, lserver = ServerTo} = JIDTo,
+    JIDFrom = jlib:make_jid(UserFrom, UserTo, ""),
+    Mod:out_subscription(UserTo, ServerTo, JIDFrom, unsubscribe),
+
+    %% Remove pending in subscription
+    Mod:in_subscription(aaaa, UserFrom, ServerFrom, JIDTo, unsubscribe, ""),
+
+    process_subscription(out, UserFrom, ServerFrom, JIDTo, unsubscribed, false);
+out_subscription(User, Server, JID, Type) ->
+    process_subscription(out, User, Server, JID, Type, false).
+
+process_subscription(Direction, User, Server, JID, _Type, Acc) ->
+    LUser = jlib:nodeprep(User),
+    LServer = jlib:nameprep(Server),
+    US = {LUser, LServer},
+    {U1, S1, _} = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
+    US1 = {U1, S1},
+    DisplayedGroups = get_user_displayed_groups(US),
+    SRUsers =
+       lists:usort(
+         lists:flatmap(
+           fun(Group) ->
+                   get_group_users(LServer, Group)
+           end, DisplayedGroups)),
+    case lists:member(US1, SRUsers) of
+       true ->
+           case Direction of
+               in ->
+                   {stop, false};
+               out ->
+                   stop
+           end;
+       false ->
+           Acc
+    end.
+
+list_groups(Host) ->
+    case ejabberd_odbc:sql_query(
+           Host, ["select name from sr_group;"]) of
+        {selected, ["name"], Rs} ->
+            [G || {G} <- Rs];
+        _ ->
+            []
+    end.
+
+groups_with_opts(Host) ->
+    case ejabberd_odbc:sql_query(
+           Host, ["select name, opts from sr_group;"]) of
+        {selected, ["name", "opts"], Rs} ->
+            [{G, decode_opts(Opts)} || {G, Opts} <- Rs];
+        _ ->
+            []
+    end.
+
+create_group(Host, Group) ->
+    create_group(Host, Group, []).
+
+create_group(Host, Group, Opts) ->
+    SGroup = ejabberd_odbc:escape(Group),
+    SOpts = encode_opts(Opts),
+    F = fun() ->
+                update_t("sr_group",
+                         ["name", "opts"],
+                         [SGroup, SOpts],
+                         ["name='", SGroup, "'"])
+        end,
+    ejabberd_odbc:sql_transaction(Host, F).
+
+delete_group(Host, Group) ->
+    SGroup = ejabberd_odbc:escape(Group),
+    F = fun() ->
+                ejabberd_odbc:sql_query_t(
+                  ["delete from sr_group where name='", SGroup, "';"]),
+                ejabberd_odbc:sql_query_t(
+                  ["delete from sr_user where grp='", SGroup, "';"])
+        end,
+    ejabberd_odbc:sql_transaction(Host, F).
+
+get_group_opts(Host, Group) ->
+    SGroup = ejabberd_odbc:escape(Group),
+    case catch ejabberd_odbc:sql_query(
+                 Host, ["select opts from sr_group "
+                        "where name='", SGroup, "';"]) of
+        {selected, ["opts"], [{SOpts}]} ->
+            decode_opts(SOpts);
+        _ ->
+            error
+    end.
+
+set_group_opts(Host, Group, Opts) ->
+    SGroup = ejabberd_odbc:escape(Group),
+    SOpts = encode_opts(Opts),
+    F = fun() ->
+                update_t("sr_group",
+                         ["name", "opts"],
+                         [SGroup, SOpts],
+                         ["name='", SGroup, "'"])
+        end,
+    ejabberd_odbc:sql_transaction(Host, F).
+
+get_user_groups(US) ->
+    Host = element(2, US),
+    SJID = make_jid_s(US),
+    case catch ejabberd_odbc:sql_query(
+                 Host, ["select grp from sr_user "
+                        "where jid='", SJID, "';"]) of
+        {selected, ["grp"], Rs} ->
+            [G || {G} <- Rs];
+        _ ->
+            []
+    end ++ get_special_users_groups(Host).
+
+is_group_enabled(Host1, Group1) ->
+    {Host, Group} = split_grouphost(Host1, Group1),
+    SGroup = ejabberd_odbc:escape(Group),
+    case catch ejabberd_odbc:sql_query(
+                 Host, ["select opts from sr_group "
+                        "where name='", SGroup, "';"]) of
+        {selected, ["opts"], [{SOpts}]} ->
+            Opts = decode_opts(SOpts),
+            not lists:member(disabled, Opts);
+        _ ->
+            false
+    end.
+
+%% @spec (Host::string(), Group::string(), Opt::atom(), Default) -> OptValue | Default
+get_group_opt(Host, Group, Opt, Default) ->
+    SGroup = ejabberd_odbc:escape(Group),
+    case catch ejabberd_odbc:sql_query(
+                 Host, ["select opts from sr_group "
+                        "where name='", SGroup, "';"]) of
+        {selected, ["opts"], [{SOpts}]} ->
+            Opts = decode_opts(SOpts),
+           case lists:keysearch(Opt, 1, Opts) of
+               {value, {_, Val}} ->
+                   Val;
+               false ->
+                   Default
+           end;
+       _ ->
+           Default
+    end.
+
+get_online_users(Host) ->
+    lists:usort([{U, S} || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host)]).
+
+get_group_users(Host1, Group1) ->
+    {Host, Group} = split_grouphost(Host1, Group1),
+    case get_group_opt(Host, Group, all_users, false) of
+       true ->
+           ejabberd_auth:get_vh_registered_users(Host);
+       false ->
+           []
+    end ++
+    case get_group_opt(Host, Group, online_users, false) of
+       true ->
+           get_online_users(Host);
+       false ->
+           []
+    end ++
+    get_group_explicit_users(Host, Group).
+
+get_group_users(Host, Group, GroupOpts) ->
+    case proplists:get_value(all_users, GroupOpts, false) of
+       true ->
+           ejabberd_auth:get_vh_registered_users(Host);
+       false ->
+           []
+    end ++
+    case proplists:get_value(online_users, GroupOpts, false) of
+       true ->
+           get_online_users(Host);
+       false ->
+           []
+    end ++
+    get_group_explicit_users(Host, Group).
+
+%% @spec (Host::string(), Group::string()) -> [{User::string(), Server::string()}]
+get_group_explicit_users(Host, Group) ->
+    SGroup = ejabberd_odbc:escape(Group),
+    case catch ejabberd_odbc:sql_query(
+                 Host, ["select jid from sr_user "
+                        "where grp='", SGroup, "';"]) of
+        {selected, ["jid"], Rs} ->
+            lists:map(
+              fun({JID}) ->
+                      {U, S, _} = jlib:jid_tolower(
+                                    jlib:string_to_jid(JID)),
+                      {U, S}
+              end, Rs);
+        _ ->
+            []
+    end.
+
+get_group_name(Host1, Group1) ->
+    {Host, Group} = split_grouphost(Host1, Group1),
+    get_group_opt(Host, Group, name, Group).
+
+%% Get list of names of groups that have @all@/@online@/etc in the memberlist
+get_special_users_groups(Host) ->
+    lists:filter(
+      fun(Group) ->
+             get_group_opt(Host, Group, all_users, false)
+                 orelse get_group_opt(Host, Group, online_users, false)
+      end,
+      list_groups(Host)).
+
+%% Get list of names of groups that have @online@ in the memberlist
+get_special_users_groups_online(Host) ->
+    lists:filter(
+      fun(Group) ->
+             get_group_opt(Host, Group, online_users, false)
+      end,
+      list_groups(Host)).
+
+%% Given two lists of groupnames and their options,
+%% return the list of displayed groups to the second list
+displayed_groups(GroupsOpts, SelectedGroupsOpts) ->
+    DisplayedGroups =
+       lists:usort(
+         lists:flatmap(
+           fun({_Group, Opts}) ->
+                   [G || G <- proplists:get_value(displayed_groups, Opts, []),
+                         not lists:member(disabled, Opts)]
+           end, SelectedGroupsOpts)),
+    [G || G <- DisplayedGroups,
+         not lists:member(disabled, proplists:get_value(G, GroupsOpts, []))].
+
+%% Given a list of group names with options,
+%% for those that have @all@ in memberlist,
+%% get the list of groups displayed
+get_special_displayed_groups(GroupsOpts) ->
+    Groups = lists:filter(
+              fun({_Group, Opts}) ->
+                      proplists:get_value(all_users, Opts, false)
+              end, GroupsOpts),
+    displayed_groups(GroupsOpts, Groups).
+
+%% Given a username and server, and a list of group names with options,
+%% for the list of groups of that server that user is member
+%% get the list of groups displayed
+get_user_displayed_groups(LUser, LServer, GroupsOpts) ->
+    SJID = make_jid_s(LUser, LServer),
+    Groups = case catch ejabberd_odbc:sql_query(
+                          LServer, ["select grp from sr_user "
+                                    "where jid='", SJID, "';"]) of
+                 {selected, ["grp"], Rs} ->
+                     [{Group, proplists:get_value(Group, GroupsOpts, [])} ||
+                         {Group} <- Rs];
+                 _ ->
+                     []
+             end,
+    displayed_groups(GroupsOpts, Groups).
+
+%% @doc Get the list of groups that are displayed to this user
+get_user_displayed_groups(US) ->
+    Host = element(2, US),
+    DisplayedGroups1 =
+       lists:usort(
+         lists:flatmap(
+           fun(Group) ->
+                   case is_group_enabled(Host, Group) of
+                       true ->
+                           get_group_opt(Host, Group, displayed_groups, []);
+                       false ->
+                           []
+                   end
+           end, get_user_groups(US))),
+    [Group || Group <- DisplayedGroups1, is_group_enabled(Host, Group)].
+
+is_user_in_group(US, Group, Host) ->
+    SJID = make_jid_s(US),
+    SGroup = ejabberd_odbc:escape(Group),
+    case catch ejabberd_odbc:sql_query(
+                 Host, ["select * from sr_user where "
+                        "jid='", SJID, "' and grp='", SGroup, "';"]) of
+        {selected, _, []} ->
+            lists:member(US, get_group_users(Host, Group));
+        _ ->
+            true
+    end.
+
+%% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok}
+add_user_to_group(Host, US, Group) ->
+    {LUser, LServer} = US,
+    case ejabberd_regexp:run(LUser, "^@.+@$") of
+       match ->
+           GroupOpts = ?MODULE:get_group_opts(Host, Group),
+           MoreGroupOpts =
+               case LUser of
+                   "@all@" -> [{all_users, true}];
+                   "@online@" -> [{online_users, true}];
+                   _ -> []
+               end,
+            ?MODULE:set_group_opts(
+             Host, Group,
+             GroupOpts ++ MoreGroupOpts);
+       nomatch ->
+           %% Push this new user to members of groups where this group is displayed
+           push_user_to_displayed(LUser, LServer, Group, Host, both),
+           %% Push members of groups that are displayed to this group
+           push_displayed_to_user(LUser, LServer, Group, Host, both),
+            SJID = make_jid_s(US),
+            SGroup = ejabberd_odbc:escape(Group),
+            F = fun() ->
+                        update_t("sr_user",
+                                 ["jid", "grp"],
+                                 [SJID, SGroup],
+                                 ["jid='", SJID, "' and grp='", SGroup, "'"])
+                end,
+            ejabberd_odbc:sql_transaction(Host, F)
+    end.
+
+push_displayed_to_user(LUser, LServer, Group, Host, Subscription) ->
+    GroupsOpts = groups_with_opts(LServer),
+    GroupOpts = proplists:get_value(Group, GroupsOpts, []),
+    DisplayedGroups = proplists:get_value(displayed_groups, GroupOpts, []),
+    [push_members_to_user(LUser, LServer, DGroup, Host, Subscription) || DGroup <- DisplayedGroups].
+
+remove_user_from_group(Host, US, Group) ->
+    {LUser, LServer} = US,
+    case ejabberd_regexp:run(LUser, "^@.+@$") of
+       match ->
+           GroupOpts = ?MODULE:get_group_opts(Host, Group),
+           NewGroupOpts =
+               case LUser of
+                   "@all@" ->
+                       lists:filter(fun(X) -> X/={all_users,true} end, GroupOpts);
+                   "@online@" ->
+                       lists:filter(fun(X) -> X/={online_users,true} end, GroupOpts)
+               end,
+           ?MODULE:set_group_opts(Host, Group, NewGroupOpts);
+       nomatch ->
+            SJID = make_jid_s(US),
+            SGroup = ejabberd_odbc:escape(Group),
+            F = fun() ->
+                        ejabberd_odbc:sql_query_t(
+                          ["delete from sr_user where jid='",
+                           SJID, "' and grp='", SGroup, "';"]),
+                        ok
+                end,
+            Result = ejabberd_odbc:sql_transaction(Host, F),
+           %% Push removal of the old user to members of groups where the group that this user was members was displayed
+           push_user_to_displayed(LUser, LServer, Group, Host, remove),
+           %% Push removal of members of groups that where displayed to the group which this user has left
+           push_displayed_to_user(LUser, LServer, Group, Host, remove),
+           Result
+    end.
+
+
+push_members_to_user(LUser, LServer, Group, Host, Subscription) ->
+    GroupsOpts = groups_with_opts(LServer),
+    GroupOpts = proplists:get_value(Group, GroupsOpts, []),
+    GroupName = proplists:get_value(name, GroupOpts, Group),
+    Members = get_group_users(Host, Group),
+    lists:foreach(
+      fun({U, S}) ->
+             push_roster_item(LUser, LServer, U, S, GroupName, Subscription)
+      end, Members).
+
+register_user(User, Server) ->
+    %% Get list of groups where this user is member
+    Groups = get_user_groups({User, Server}),
+    %% Push this user to members of groups where is displayed a group which this user is member
+    [push_user_to_displayed(User, Server, Group, Server, both) || Group <- Groups].
+
+remove_user(User, Server) ->
+    push_user_to_members(User, Server, remove).
+
+push_user_to_members(User, Server, Subscription) ->
+    LUser = jlib:nodeprep(User),
+    LServer = jlib:nameprep(Server),
+    GroupsOpts = groups_with_opts(LServer),
+    SpecialGroups = get_special_displayed_groups(GroupsOpts),
+    UserGroups = get_user_displayed_groups(LUser, LServer, GroupsOpts),
+    lists:foreach(
+      fun(Group) ->
+             remove_user_from_group(LServer, {LUser, LServer}, Group),
+             GroupOpts = proplists:get_value(Group, GroupsOpts, []),
+             GroupName = proplists:get_value(name, GroupOpts, Group),
+             lists:foreach(
+               fun({U, S}) ->
+                       push_roster_item(U, S, LUser, LServer, GroupName, Subscription)
+               end, get_group_users(LServer, Group, GroupOpts))
+      end, lists:usort(SpecialGroups++UserGroups)).
+
+push_user_to_displayed(LUser, LServer, Group, Host, Subscription) ->
+    GroupsOpts = groups_with_opts(Host),
+    GroupOpts = proplists:get_value(Group, GroupsOpts, []),
+    GroupName = proplists:get_value(name, GroupOpts, Group),
+    DisplayedToGroupsOpts = displayed_to_groups(Group, Host),
+    [push_user_to_group(LUser, LServer, GroupD, Host, GroupName, Subscription) || {GroupD, _Opts} <- DisplayedToGroupsOpts].
+
+push_user_to_group(LUser, LServer, Group, Host, GroupName, Subscription) ->
+    lists:foreach(
+      fun({U, S})  when (U == LUser) and (S == LServer) -> ok;
+         ({U, S}) ->
+             push_roster_item(U, S, LUser, LServer, GroupName, Subscription)
+      end, get_group_users(Host, Group)).
+
+%% Get list of groups to which this group is displayed
+displayed_to_groups(GroupName, LServer) ->
+    GroupsOpts = groups_with_opts(LServer),
+    lists:filter(
+      fun({_Group, Opts}) ->
+             lists:member(GroupName, proplists:get_value(displayed_groups, Opts, []))
+      end, GroupsOpts).
+
+push_item(User, Server, From, Item) ->
+    %% It was
+    %%  ejabberd_sm:route(jlib:make_jid("", "", ""),
+    %%                    jlib:make_jid(User, Server, "")
+    %% why?
+    ejabberd_sm:route(From, jlib:make_jid(User, Server, ""),
+                     {xmlelement, "broadcast", [],
+                      [{item,
+                        Item#roster.jid,
+                        Item#roster.subscription}]}),
+    Stanza = jlib:iq_to_xml(
+              #iq{type = set, xmlns = ?NS_ROSTER,
+                  id = "push" ++ randoms:get_string(),
+                  sub_el = [{xmlelement, "query",
+                             [{"xmlns", ?NS_ROSTER}],
+                             [item_to_xml(Item)]}]}),
+    lists:foreach(
+      fun(Resource) ->
+             JID = jlib:make_jid(User, Server, Resource),
+             ejabberd_router:route(JID, JID, Stanza)
+      end, ejabberd_sm:get_user_resources(User, Server)).
+
+push_roster_item(User, Server, ContactU, ContactS, GroupName, Subscription) ->
+    Item = #roster{usj = {User, Server, {ContactU, ContactS, ""}},
+                  us = {User, Server},
+                  jid = {ContactU, ContactS, ""},
+                  name = "",
+                  subscription = Subscription,
+                  ask = none,
+                  groups = [GroupName]},
+    push_item(User, Server, jlib:make_jid("", Server, ""), Item).
+
+item_to_xml(Item) ->
+    Attrs1 = [{"jid", jlib:jid_to_string(Item#roster.jid)}],
+    Attrs2 = case Item#roster.name of
+                "" ->
+                    Attrs1;
+                Name ->
+                    [{"name", Name} | Attrs1]
+            end,
+    Attrs3 = case Item#roster.subscription of
+                none ->
+                    [{"subscription", "none"} | Attrs2];
+                from ->
+                    [{"subscription", "from"} | Attrs2];
+                to ->
+                    [{"subscription", "to"} | Attrs2];
+                both ->
+                    [{"subscription", "both"} | Attrs2];
+                remove ->
+                    [{"subscription", "remove"} | Attrs2]
+            end,
+    Attrs4 = case ask_to_pending(Item#roster.ask) of
+                out ->
+                    [{"ask", "subscribe"} | Attrs3];
+                both ->
+                    [{"ask", "subscribe"} | Attrs3];
+                _ ->
+                    Attrs3
+            end,
+    SubEls1 = lists:map(fun(G) ->
+                               {xmlelement, "group", [], [{xmlcdata, G}]}
+                       end, Item#roster.groups),
+    SubEls = SubEls1 ++ Item#roster.xs,
+    {xmlelement, "item", Attrs4, SubEls}.
+
+ask_to_pending(subscribe) -> out;
+ask_to_pending(unsubscribe) -> none;
+ask_to_pending(Ask) -> Ask.
+
+user_available(New) ->
+    LUser = New#jid.luser,
+    LServer = New#jid.lserver,
+    Resources = ejabberd_sm:get_user_resources(LUser, LServer),
+    ?DEBUG("user_available for ~p @ ~p (~p resources)",
+          [LUser, LServer, length(Resources)]),
+    case length(Resources) of
+       %% first session for this user
+       1 ->
+           %% This is a simplification - we ignore he 'display'
+           %% property - @online@ is always reflective.
+           OnlineGroups = get_special_users_groups_online(LServer),
+           lists:foreach(
+             fun(OG) ->
+                     ?DEBUG("user_available: pushing  ~p @ ~p grp ~p",
+                            [LUser, LServer, OG ]),
+                     push_user_to_displayed(LUser, LServer, OG, LServer, both)
+             end, OnlineGroups);
+       _ ->
+           ok
+    end.
+
+unset_presence(LUser, LServer, Resource, Status) ->
+    Resources = ejabberd_sm:get_user_resources(LUser, LServer),
+    ?DEBUG("unset_presence for ~p @ ~p / ~p -> ~p (~p resources)",
+          [LUser, LServer, Resource, Status, length(Resources)]),
+    %% if user has no resources left...
+    case length(Resources) of
+       0 ->
+           %% This is a simplification - we ignore he 'display'
+           %% property - @online@ is always reflective.
+           OnlineGroups = get_special_users_groups_online(LServer),
+           %% for each of these groups...
+           lists:foreach(
+             fun(OG) ->
+                     %% Push removal of the old user to members of groups
+                     %% where the group that this uwas members was displayed
+                     push_user_to_displayed(LUser, LServer, OG, LServer, remove),
+                     %% Push removal of members of groups that where
+                     %% displayed to the group which thiuser has left
+                     push_displayed_to_user(LUser, LServer, OG, LServer,remove)
+             end, OnlineGroups);
+       _ ->
+           ok
+    end.
+
+%%---------------------
+%% Web Admin
+%%---------------------
+
+webadmin_menu(Acc, _Host, Lang) ->
+    [{"shared-roster", ?T("Shared Roster Groups")} | Acc].
+
+webadmin_page(_, Host,
+             #request{us = _US,
+                      path = ["shared-roster"],
+                      q = Query,
+                      lang = Lang} = _Request) ->
+    Res = list_shared_roster_groups(Host, Query, Lang),
+    {stop, Res};
+
+webadmin_page(_, Host,
+             #request{us = _US,
+                      path = ["shared-roster", Group],
+                      q = Query,
+                      lang = Lang} = _Request) ->
+    Res = shared_roster_group(Host, Group, Query, Lang),
+    {stop, Res};
+
+webadmin_page(Acc, _, _) -> Acc.
+
+list_shared_roster_groups(Host, Query, Lang) ->
+    Res = list_sr_groups_parse_query(Host, Query),
+    SRGroups = ?MODULE:list_groups(Host),
+    FGroups =
+       ?XAE("table", [],
+            [?XE("tbody",
+                 lists:map(
+                   fun(Group) ->
+                           ?XE("tr",
+                               [?XE("td", [?INPUT("checkbox", "selected",
+                                                  Group)]),
+                                ?XE("td", [?AC(Group ++ "/", Group)])
+                               ]
+                              )
+                   end, lists:sort(SRGroups)) ++
+                 [?XE("tr",
+                      [?X("td"),
+                       ?XE("td", [?INPUT("text", "namenew", "")]),
+                       ?XE("td", [?INPUTT("submit", "addnew", "Add New")])
+                      ]
+                     )]
+                )]),
+    ?H1GL(?T("Shared Roster Groups"), "modsharedroster", "mod_shared_roster") ++
+       case Res of
+           ok -> [?XREST("Submitted")];
+           error -> [?XREST("Bad format")];
+           nothing -> []
+       end ++
+       [?XAE("form", [{"action", ""}, {"method", "post"}],
+             [FGroups,
+              ?BR,
+              ?INPUTT("submit", "delete", "Delete Selected")
+             ])
+       ].
+
+list_sr_groups_parse_query(Host, Query) ->
+    case lists:keysearch("addnew", 1, Query) of
+       {value, _} ->
+           list_sr_groups_parse_addnew(Host, Query);
+       _ ->
+           case lists:keysearch("delete", 1, Query) of
+               {value, _} ->
+                   list_sr_groups_parse_delete(Host, Query);
+               _ ->
+                   nothing
+           end
+    end.
+
+list_sr_groups_parse_addnew(Host, Query) ->
+    case lists:keysearch("namenew", 1, Query) of
+       {value, {_, Group}} when Group /= "" ->
+           ?MODULE:create_group(Host, Group),
+           ok;
+       _ ->
+           error
+    end.
+
+list_sr_groups_parse_delete(Host, Query) ->
+    SRGroups = ?MODULE:list_groups(Host),
+    lists:foreach(
+      fun(Group) ->
+             case lists:member({"selected", Group}, Query) of
+                 true ->
+                     ?MODULE:delete_group(Host, Group);
+                 _ ->
+                     ok
+             end
+      end, SRGroups),
+    ok.
+
+
+shared_roster_group(Host, Group, Query, Lang) ->
+    Res = shared_roster_group_parse_query(Host, Group, Query),
+    GroupOpts = ?MODULE:get_group_opts(Host, Group),
+    Name = get_opt(GroupOpts, name, ""),
+    Description = get_opt(GroupOpts, description, ""),
+    AllUsers = get_opt(GroupOpts, all_users, false),
+    OnlineUsers = get_opt(GroupOpts, online_users, false),
+    %%Disabled = false,
+    DisplayedGroups = get_opt(GroupOpts, displayed_groups, []),
+    Members = ?MODULE:get_group_explicit_users(Host, Group),
+    FMembers =
+       if
+           AllUsers ->
+               "@all@\n";
+           true ->
+               []
+       end ++
+       if
+           OnlineUsers ->
+               "@online@\n";
+           true ->
+               []
+       end ++
+       [[us_to_list(Member), $\n] || Member <- Members],
+    FDisplayedGroups = [[DG, $\n] || DG <- DisplayedGroups],
+    DescNL = length(ejabberd_regexp:split(Description, "\n")),
+    FGroup =
+       ?XAE("table", [{"class", "withtextareas"}],
+            [?XE("tbody",
+                 [?XE("tr",
+                      [?XCT("td", "Name:"),
+                       ?XE("td", [?INPUT("text", "name", Name)])
+                      ]
+                     ),
+                  ?XE("tr",
+                      [?XCT("td", "Description:"),
+                       ?XE("td", [
+                                  ?TEXTAREA("description", integer_to_list(lists:max([3, DescNL])), "20", Description)
+                                 ]
+                          )
+                      ]
+                     ),
+                  ?XE("tr",
+                      [?XCT("td", "Members:"),
+                       ?XE("td", [
+                                  ?TEXTAREA("members", integer_to_list(lists:max([3, length(FMembers)])), "20", FMembers)
+                                 ]
+                          )
+                      ]
+                     ),
+                  ?XE("tr",
+                      [?XCT("td", "Displayed Groups:"),
+                       ?XE("td", [
+                                  ?TEXTAREA("dispgroups", integer_to_list(lists:max([3, length(FDisplayedGroups)])), "20", FDisplayedGroups)
+                                 ]
+                          )
+                      ]
+                     )]
+                )]),
+    ?H1GL(?T("Shared Roster Groups"), "modsharedroster", "mod_shared_roster") ++
+       [?XC("h2", ?T("Group ") ++ Group)] ++
+       case Res of
+           ok -> [?XREST("Submitted")];
+           error -> [?XREST("Bad format")];
+           nothing -> []
+       end ++
+       [?XAE("form", [{"action", ""}, {"method", "post"}],
+             [FGroup,
+              ?BR,
+              ?INPUTT("submit", "submit", "Submit")
+             ])
+       ].
+
+shared_roster_group_parse_query(Host, Group, Query) ->
+    case lists:keysearch("submit", 1, Query) of
+       {value, _} ->
+           {value, {_, Name}} = lists:keysearch("name", 1, Query),
+           {value, {_, Description}} = lists:keysearch("description", 1, Query),
+           {value, {_, SMembers}} = lists:keysearch("members", 1, Query),
+           {value, {_, SDispGroups}} = lists:keysearch("dispgroups", 1, Query),
+           NameOpt =
+               if
+                   Name == "" -> [];
+                   true -> [{name, Name}]
+               end,
+           DescriptionOpt =
+               if
+                   Description == "" -> [];
+                   true -> [{description, Description}]
+               end,
+           DispGroups = string:tokens(SDispGroups, "\r\n"),
+           DispGroupsOpt =
+               if
+                   DispGroups == [] -> [];
+                   true -> [{displayed_groups, DispGroups}]
+               end,
+
+           OldMembers = ?MODULE:get_group_explicit_users(
+                          Host, Group),
+           SJIDs = string:tokens(SMembers, ", \r\n"),
+           NewMembers =
+               lists:foldl(
+                 fun(_SJID, error) -> error;
+                    (SJID, USs) ->
+                         case SJID of
+                             "@all@" ->
+                                 USs;
+                             "@online@" ->
+                                 USs;
+                             _ ->
+                                 case jlib:string_to_jid(SJID) of
+                                     JID when is_record(JID, jid) ->
+                                         [{JID#jid.luser, JID#jid.lserver} | USs];
+                                     error ->
+                                         error
+                                 end
+                         end
+                 end, [], SJIDs),
+           AllUsersOpt =
+               case lists:member("@all@", SJIDs) of
+                   true -> [{all_users, true}];
+                   false -> []
+               end,
+           OnlineUsersOpt =
+               case lists:member("@online@", SJIDs) of
+                   true -> [{online_users, true}];
+                   false -> []
+               end,
+
+           ?MODULE:set_group_opts(
+             Host, Group,
+             NameOpt ++ DispGroupsOpt ++ DescriptionOpt ++ AllUsersOpt ++ OnlineUsersOpt),
+
+           if
+               NewMembers == error -> error;
+               true ->
+                   AddedMembers = NewMembers -- OldMembers,
+                   RemovedMembers = OldMembers -- NewMembers,
+                   lists:foreach(
+                     fun(US) ->
+                             ?MODULE:remove_user_from_group(
+                               Host, US, Group)
+                     end, RemovedMembers),
+                   lists:foreach(
+                     fun(US) ->
+                             ?MODULE:add_user_to_group(
+                               Host, US, Group)
+                     end, AddedMembers),
+                   ok
+           end;
+       _ ->
+           nothing
+    end.
+
+%% Get the roster module for Server.
+get_roster_mod(Server) ->
+    case lists:member(mod_roster_odbc,
+                      gen_mod:loaded_modules(Server)) of
+        true -> mod_roster_odbc;
+        false -> mod_roster
+    end.
+
+get_opt(Opts, Opt, Default) ->
+    case lists:keysearch(Opt, 1, Opts) of
+       {value, {_, Val}} ->
+           Val;
+       false ->
+           Default
+    end.
+
+us_to_list({User, Server}) ->
+    jlib:jid_to_string({User, Server, ""}).
+
+split_grouphost(Host, Group) ->
+    case string:tokens(Group, "@") of
+       [GroupName, HostName] ->
+           {HostName, GroupName};
+       [_] ->
+           {Host, Group}
+    end.
+
+make_jid_s(U, S) ->
+    ejabberd_odbc:escape(
+      jlib:jid_to_string(
+        jlib:jid_tolower(
+          jlib:make_jid(U, S, "")))).
+
+make_jid_s({U, S}) ->
+    make_jid_s(U, S).
+
+encode_opts(Opts) ->
+    ejabberd_odbc:escape(erl_prettypr:format(erl_syntax:abstract(Opts))).
+
+decode_opts(Str) ->
+    {ok, Tokens, _} = erl_scan:string(Str ++ "."),
+    {ok, Opts} = erl_parse:parse_term(Tokens),
+    Opts.
+
+%% 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.
index e57b135ed7a84ea30a70bb73f9a0715aab484fe8..c2611b0d26d7ecf7827a3738592cee6a56ac09d0 100644 (file)
@@ -59,6 +59,21 @@ CREATE TABLE rostergroups (
 
 CREATE INDEX pk_rosterg_user_jid ON rostergroups(username(75), jid(75));
 
+CREATE TABLE sr_group (
+    name varchar(250) NOT NULL,
+    opts text NOT NULL,
+    created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
+) CHARACTER SET utf8;
+
+CREATE TABLE sr_user (
+    jid varchar(250) NOT NULL,
+    grp varchar(250) NOT NULL,
+    created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
+) CHARACTER SET utf8;
+
+CREATE UNIQUE INDEX i_sr_user_jid_group ON sr_user(jid(75), grp(75));
+CREATE INDEX i_sr_user_jid ON sr_user(jid);
+CREATE INDEX i_sr_user_grp ON sr_user(grp);
 
 CREATE TABLE spool (
     username varchar(250) NOT NULL,
index 1abc84abf69b658e7e96e43d6905a8d3d7ed54d9..a7d88d3def7c1633df161fe767ee117cfd9565dc 100644 (file)
@@ -57,6 +57,21 @@ CREATE TABLE rostergroups (
 
 CREATE INDEX pk_rosterg_user_jid ON rostergroups USING btree (username, jid);
 
+CREATE TABLE sr_group (
+    name text NOT NULL,
+    opts text NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT now()
+);
+
+CREATE TABLE sr_user (
+    jid text NOT NULL,
+    grp text NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT now()
+);
+
+CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user USING btree (jid, grp);
+CREATE INDEX i_sr_user_jid ON sr_user USING btree (jid);
+CREATE INDEX i_sr_user_grp ON sr_user USING btree (grp);
 
 CREATE TABLE spool (
     username text NOT NULL,