diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index f77c35a9d..e61b9e987 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -310,12 +310,17 @@ wait_for_stream({xmlstreamstart, #xmlel{ns = NS} = Opening}, StateData) -> _ -> case StateData#state.resource of undefined -> + RosterVersioningFeature = + case roster_versioning:is_enabled(ServerB) of + true -> [roster_versioning:stream_feature()]; + false -> [] + end, send_element( StateData, exmpp_stream:features([ exmpp_server_binding:feature(), exmpp_server_session:feature() - ])), + | RosterVersioningFeature])), fsm_next_state(wait_for_bind, StateData#state{ server = ServerB, diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 564291077..95c9e9247 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -42,7 +42,8 @@ get_jid_info/4, item_to_xml/1, webadmin_page/3, - webadmin_user/4]). + webadmin_user/4, + roster_versioning_enabled/1]). -include_lib("exmpp/include/exmpp.hrl"). @@ -74,8 +75,11 @@ start(Host, Opts) when is_list(Host) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), mnesia:create_table(roster,[{disc_copies, [node()]}, {attributes, record_info(fields, roster)}]), + mnesia:create_table(roster_version, [{disc_copies, [node()]}, + {attributes, record_info(fields, roster_version)}]), update_table(), mnesia:add_table_index(roster, us), + mnesia:add_table_index(roster_version, us), ejabberd_hooks:add(roster_get, HostB, ?MODULE, get_user_roster, 50), ejabberd_hooks:add(roster_in_subscription, HostB, @@ -127,6 +131,16 @@ stop(Host) when is_list(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, HostB, ?NS_ROSTER). +%% @spec (Host) -> true | false +%% @type Host = binary() +roster_versioning_enabled(Host) -> + gen_mod:get_module_opt(binary_to_list(Host), ?MODULE, versioning, false). + +%% @spec (Host) -> true | false +%% @type Host = binary() +roster_version_on_db(Host) -> + gen_mod:get_module_opt(binary_to_list(Host), ?MODULE, store_current_id, false). + %% @spec (From, To, IQ_Rec) -> IQ_Result %% From = exmpp_jid:jid() %% To = exmpp_jid:jid() @@ -156,23 +170,90 @@ process_local_iq(From, To, #iq{type = set} = IQ_Rec) when ?IS_JID(From), ?IS_JID(To), ?IS_IQ_RECORD(IQ_Rec) -> process_iq_set(From, To, IQ_Rec). +roster_hash(Items) -> + sha:sha(term_to_binary( + lists:sort( + [R#roster{groups = lists:sort(Grs)} || + R = #roster{groups = Grs} <- Items]))). + +roster_version(LServer ,LUser) -> + US = {LUser, LServer}, + case roster_version_on_db(LServer) of + true -> + case mnesia:dirty_read(roster_version, US) of + [#roster_version{version =V}] -> V; + [] -> not_found + end; + false -> + roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, [], [US])) + end. + %% @spec (From, To, IQ_Rec) -> IQ_Result %% From = exmpp_jid:jid() %% To = exmpp_jid:jid() %% IQ_Rec = exmpp_iq:iq() %% IQ_Result = exmpp_iq:iq() - +%% Load roster from DB only if neccesary +%% It is neccesary if +%% - roster versioning is disabled in server OR +%% - roster versioning is not used by the client OR +%% - roster versioning is used by server and client BUT the server isn't storing version IDs on db OR +%% - the roster version from client don't match current version process_iq_get(From, To, IQ_Rec) -> - US = {exmpp_jid:prep_node(From), exmpp_jid:prep_domain(From)}, - case catch ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US]) of - Items when is_list(Items) -> - XItems = lists:map(fun item_to_xml/1, Items), - Result = #xmlel{ns = ?NS_ROSTER, name = 'query', - children = XItems}, - exmpp_iq:result(IQ_Rec, Result); - _ -> - exmpp_iq:error(IQ_Rec, 'internal-server-error') - end. + US = {_, LServer} = {exmpp_jid:prep_node(From), exmpp_jid:prep_domain(From)}, + try + {ItemsToSend, VersionToSend} = + case {exmpp_xml:get_attribute_as_list(exmpp_iq:get_request(IQ_Rec), ver, not_found), + roster_versioning_enabled(LServer), + roster_version_on_db(LServer)} of + {not_found, _ , _} -> + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), false}; + {_, false, _} -> + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), false}; + + {RequestedVersion, true, true} -> + %% Retrieve version from DB. Only load entire roster + %% when neccesary. + case mnesia:dirty_read(roster_version, US) of + [#roster_version{version = RequestedVersion}] -> + {false, false}; + [#roster_version{version = NewVersion}] -> + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), NewVersion}; + [] -> + RosterVersion = sha:sha(term_to_binary(now())), + mnesia:dirty_write(#roster_version{us = US, version = RosterVersion}), + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), RosterVersion} + end; + {RequestedVersion, true, false} -> + RosterItems = ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [] , [US]), + case roster_hash(RosterItems) of + RequestedVersion -> + {false, false}; + New -> + {lists:map(fun item_to_xml/1, RosterItems), New} + end + + end, + case {ItemsToSend, VersionToSend} of + {false, false} -> + exmpp_iq:result(IQ_Rec); + {Items, false} -> + exmpp_iq:result(IQ_Rec, exmpp_xml:element(?NS_ROSTER, 'query', [] , Items)); + {Items, Version} -> + exmpp_iq:result(IQ_Rec, exmpp_xml:element(?NS_ROSTER, 'query', [?XMLATTR('ver', Version)], Items)) + end + catch + _:_ -> + exmpp_iq:error(IQ_Rec, 'internal-server-error') + end. + + + + %% @spec (Acc, US) -> New_Acc %% Acc = [rosteritem()] @@ -279,6 +360,10 @@ process_item_set(From, To, #xmlel{} = El) -> %% subscription information from there: Item3 = ejabberd_hooks:run_fold(roster_process_item, exmpp_jid:prep_domain(From), Item2, [exmpp_jid:prep_domain(From)]), + case roster_version_on_db(LServer) of + true -> mnesia:write(#roster_version{us = {LUser, LServer}, version = sha:sha(term_to_binary(now()))}); + false -> ok + end, {Item, Item3} end, case mnesia:transaction(F) of @@ -389,9 +474,15 @@ push_item(User, Server, From, Item) [{item, Item#roster.jid, Item#roster.subscription}]}), - lists:foreach(fun(Resource) -> - push_item(User, Server, Resource, From, Item) - end, ejabberd_sm:get_user_resources(User, Server)). + + case roster_versioning_enabled(Server) of + true -> + roster_versioning:push_item(Server, User, From, Item, roster_version(Server, User)); + false -> + lists:foreach(fun(Resource) -> + push_item(User, Server, Resource, From, Item) + end, ejabberd_sm:get_user_resources(User, Server)) + end. %% @spec (User, Server, Resource, From, Item) -> term() %% User = binary() @@ -548,6 +639,10 @@ process_subscription(Direction, User, Server, JID1, Type, Reason) ask = Pending, askmessage = AskBinary}, mnesia:write(NewItem), + case roster_version_on_db(Server) of + true -> mnesia:write(#roster_version{us = {User, Server}, version = sha:sha(term_to_binary(now()))}); + false -> ok + end, {{push, NewItem}, AutoReply} end end, diff --git a/src/mod_roster.hrl b/src/mod_roster.hrl index 4278a57b1..242f1d05a 100644 --- a/src/mod_roster.hrl +++ b/src/mod_roster.hrl @@ -29,3 +29,5 @@ askmessage = <<>>, xs = []}). +-record(roster_version, {us, + version}). diff --git a/src/mod_roster_odbc.erl b/src/mod_roster_odbc.erl index d79604526..0564e78c8 100644 --- a/src/mod_roster_odbc.erl +++ b/src/mod_roster_odbc.erl @@ -41,7 +41,8 @@ remove_user/2, get_jid_info/4, webadmin_page/3, - webadmin_user/4]). + webadmin_user/4, + roster_versioning_enabled/1]). -include_lib("exmpp/include/exmpp.hrl"). @@ -102,6 +103,12 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, HostB, ?NS_ROSTER). +roster_versioning_enabled(Host) -> + gen_mod:get_module_opt(binary_to_list(Host), ?MODULE, versioning, false). + +roster_version_on_db(Host) -> + gen_mod:get_module_opt(binary_to_list(Host), ?MODULE, store_current_id, false). + process_iq(From, To, IQ_Rec) -> LServer = exmpp_jid:prep_domain_as_list(From), case lists:member(LServer, ?MYHOSTS) of @@ -118,17 +125,84 @@ process_local_iq(From, To, #iq{type = set} = IQ_Rec) -> +roster_hash(Items) -> + sha:sha(term_to_binary( + lists:sort( + [R#roster{groups = lists:sort(Grs)} || + R = #roster{groups = Grs} <- Items]))). + +roster_version(LServer ,LUser) -> + US = {LUser, LServer}, + case roster_version_on_db(LServer) of + true -> + case odbc_queries:get_roster_version(ejabberd_odbc:escape(LServer), ejabberd_odbc:escape(LUser)) of + {selected, ["version"], [{Version}]} -> Version; + {selected, ["version"], []} -> not_found + end; + false -> + roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, [], [US])) + end. + +%% Load roster from DB only if neccesary. +%% It is neccesary if +%% - roster versioning is disabled in server OR +%% - roster versioning is not used by the client OR +%% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR +%% - the roster version from client don't match current version. process_iq_get(From, To, IQ_Rec) -> - US = {exmpp_jid:prep_node(From), exmpp_jid:prep_domain(From)}, - case catch ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US]) of - Items when is_list(Items) -> - XItems = lists:map(fun item_to_xml/1, Items), - Result = #xmlel{ns = ?NS_ROSTER, name = 'query', - children = XItems}, - exmpp_iq:result(IQ_Rec, Result); - _ -> - exmpp_iq:error(IQ_Rec, 'internal-server-error') - end. + US = {LUser, LServer} = {exmpp_jid:prep_node(From), exmpp_jid:prep_domain(From)}, + try + {ItemsToSend, VersionToSend} = + case {exmpp_xml:get_attribute_as_list(exmpp_iq:get_request(IQ_Rec), ver, not_found), + roster_versioning_enabled(LServer), + roster_version_on_db(LServer)} of + {not_found, _ , _} -> + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), false}; + {_, false, _} -> + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), false}; + + {RequestedVersion, true, true} -> + %% Retrieve version from DB. Only load entire roster + %% when neccesary. + case odbc_queries:get_roster_version(ejabberd_odbc:escape(LServer), ejabberd_odbc:escape(LUser)) of + {selected, ["version"], [{RequestedVersion}]} -> + {false, false}; + {selected, ["version"], [{NewVersion}]} -> + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), NewVersion}; + {selected, ["version"], []} -> + RosterVersion = sha:sha(term_to_binary(now())), + {atomic, {updated,1}} = odbc_queries:sql_transaction(binary_to_list(LServer), fun() -> + odbc_queries:set_roster_version(ejabberd_odbc:escape(LUser), RosterVersion) + end), + {lists:map(fun item_to_xml/1, + ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [], [US])), RosterVersion} + end; + + {RequestedVersion, true, false} -> + RosterItems = ejabberd_hooks:run_fold(roster_get, exmpp_jid:prep_domain(To), [] , [US]), + case roster_hash(RosterItems) of + RequestedVersion -> + {false, false}; + New -> + {lists:map(fun item_to_xml/1, RosterItems), New} + end + + end, + case {ItemsToSend, VersionToSend} of + {false, false} -> + exmpp_iq:result(IQ_Rec); + {Items, false} -> + exmpp_iq:result(IQ_Rec, exmpp_xml:element(?NS_ROSTER, 'query', [] , Items)); + {Items, Version} -> + exmpp_iq:result(IQ_Rec, exmpp_xml:element(?NS_ROSTER, 'query', [?XMLATTR('ver', Version)], Items)) + end + catch + _:_ -> + exmpp_iq:error(IQ_Rec, 'internal-server-error') + end. get_user_roster(Acc, {LUser, LServer}) -> Items = get_roster(LUser, LServer), @@ -263,6 +337,11 @@ process_item_set(From, To, #xmlel{} = El) -> %% subscription information from there: Item3 = ejabberd_hooks:run_fold(roster_process_item, exmpp_jid:prep_domain(From), Item2, [exmpp_jid:prep_domain(From)]), + case roster_version_on_db(Server) of + true -> odbc_queries:set_roster_version(Username, sha:sha(term_to_binary(now()))); + false -> ok + end, + {Item, Item3} end, case odbc_queries:sql_transaction(LServer, F) of @@ -352,9 +431,15 @@ push_item(User, Server, From, Item) when is_binary(User), is_binary(Server) -> [{item, Item#roster.jid, Item#roster.subscription}]}), - lists:foreach(fun(Resource) -> - push_item(User, Server, Resource, From, Item) - end, ejabberd_sm:get_user_resources(User, Server)). + case roster_versioning_enabled(Server) of + true -> + roster_versioning:push_item(Server, User, From, Item, roster_version(Server, User)); + false -> + lists:foreach(fun(Resource) -> + push_item(User, Server, Resource, From, Item) + end, ejabberd_sm:get_user_resources(User, Server)) + end. + % TODO: don't push to those who not load roster push_item(User, Server, Resource, From, Item) -> @@ -492,6 +577,10 @@ process_subscription(Direction, User, Server, JID1, Type, Reason) askmessage = AskBinary}, ItemVals = record_to_string(NewItem), odbc_queries:roster_subscribe(LServer, Username, SJID, ItemVals), + case roster_version_on_db(Server) of + true -> odbc_queries:set_roster_version(Username, sha:sha(term_to_binary(now()))); + false -> ok + end, {{push, NewItem}, AutoReply} end end, diff --git a/src/odbc/mssql2000.sql b/src/odbc/mssql2000.sql index 1bb93d783..176ff8300 100644 --- a/src/odbc/mssql2000.sql +++ b/src/odbc/mssql2000.sql @@ -209,6 +209,14 @@ CREATE TABLE [dbo].[privacy_list_data] ( ) ON [PRIMARY] GO +/* Not tested on mssql */ +CREATE TABLE [dbo].[roster_version] ( + [username] [varchar] (250) NOT NULL , + [version] [varchar] (64) NOT NULL +) ON [PRIMARY] +GO + + /* Constraints to add: - id in privacy_list is a SERIAL autogenerated number - id in privacy_list_data must exist in the table privacy_list */ @@ -244,6 +252,13 @@ ALTER TABLE [dbo].[users] WITH NOCHECK ADD ) WITH FILLFACTOR = 90 ON [PRIMARY] GO +ALTER TABLE [dbo].[roster_version] WITH NOCHECK ADD + CONSTRAINT [PK_roster_version] PRIMARY KEY CLUSTERED + ( + [username] + ) WITH FILLFACTOR = 90 ON [PRIMARY] +GO + ALTER TABLE [dbo].[vcard] WITH NOCHECK ADD CONSTRAINT [PK_vcard] PRIMARY KEY CLUSTERED ( diff --git a/src/odbc/mysql.sql b/src/odbc/mysql.sql index e975b9231..dfbf69437 100644 --- a/src/odbc/mysql.sql +++ b/src/odbc/mysql.sql @@ -148,6 +148,12 @@ CREATE TABLE private_storage ( CREATE INDEX i_private_storage_username USING BTREE ON private_storage(username); CREATE UNIQUE INDEX i_private_storage_username_namespace USING BTREE ON private_storage(username(75), namespace(75)); +-- Not tested in mysql +CREATE TABLE roster_version ( + username varchar(250) PRIMARY KEY, + version text NOT NULL +) CHARACTER SET utf8; + -- To update from 1.x: -- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; -- UPDATE rosterusers SET askmessage = ''; diff --git a/src/odbc/odbc_queries.erl b/src/odbc/odbc_queries.erl index ef1f59218..eeeebdddc 100644 --- a/src/odbc/odbc_queries.erl +++ b/src/odbc/odbc_queries.erl @@ -78,7 +78,9 @@ set_vcard/26, get_vcard/2, escape/1, - count_records_where/3]). + count_records_where/3, + get_roster_version/2, + set_roster_version/2]). %% We have only two compile time options for db queries: %-define(generic, true). @@ -555,6 +557,12 @@ count_records_where(LServer, Table, WhereClause) -> ejabberd_odbc:sql_query( LServer, ["select count(*) from ", Table, " ", WhereClause, ";"]). + +get_roster_version(LServer, LUser) -> + ejabberd_odbc:sql_query(LServer, + ["select version from roster_version where username = '", LUser, "'"]). +set_roster_version(LUser, Version) -> + update_t("roster_version", ["username", "version"], [LUser, Version], ["username = '", LUser, "'"]). -endif. %% ----------------- @@ -791,4 +799,10 @@ count_records_where(LServer, Table, WhereClause) -> ejabberd_odbc:sql_query( LServer, ["select count(*) from ", Table, " ", WhereClause, " with (nolock)"]). + +get_roster_version(LServer, LUser) -> + ejabberd_odbc:sql_query(LServer, + ["select version from dbo.roster_version where username = '", LUser, "'"]). +set_roster_version(LUser, Version) -> + update_t("dbo.roster_version", ["username", "version"], [LUser, Version], ["username = '", LUser, "'"]). -endif. diff --git a/src/odbc/pg.sql b/src/odbc/pg.sql index 3cf3a0949..60df5a195 100644 --- a/src/odbc/pg.sql +++ b/src/odbc/pg.sql @@ -145,7 +145,10 @@ CREATE TABLE private_storage ( CREATE INDEX i_private_storage_username ON private_storage USING btree (username); CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage USING btree (username, namespace); - +CREATE TABLE roster_version ( + username text PRIMARY KEY, + version text NOT NULL +); -- To update from 0.9.8: -- CREATE SEQUENCE spool_seq_seq; -- ALTER TABLE spool ADD COLUMN seq integer; diff --git a/src/roster_versioning.erl b/src/roster_versioning.erl new file mode 100644 index 000000000..1aa7e4e3b --- /dev/null +++ b/src/roster_versioning.erl @@ -0,0 +1,75 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_roster.erl +%%% Author : Pablo Polvorin +%%% Purpose : Common utility functions for XEP-0237 (Roster Versioning) +%%% Created : 19 Jul 2009 by Pablo Polvorin +%%% +%%% +%%% ejabberd, Copyright (C) 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 +%%% 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 +%%% +%%% +%%% @doc The roster versioning follows an all-or-nothing strategy: +%%% - If the version supplied by the client is the lastest, return an empty response +%%% - If not, return the entire new roster (with updated version string). +%%% Roster version is a hash digest of the entire roster. +%%% No additional data is stored in DB. +%%%---------------------------------------------------------------------- +-module(roster_versioning). +-author('pablo.polvorin@process-one.net'). + +%%API +-export([is_enabled/1, + stream_feature/0, + push_item/5]). + + +-include("mod_roster.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). + +-define(NS_ROSTER_VER, "urn:xmpp:features:rosterver"). + +%%@doc is roster versioning enabled? +is_enabled(Host) -> + case gen_mod:is_loaded(binary_to_list(Host), mod_roster) of + true -> mod_roster:roster_versioning_enabled(Host); + false -> mod_roster_odbc:roster_versioning_enabled(Host) + end. + +stream_feature() -> + exmpp_xml:element(?NS_ROSTER_VER, 'ver', [], [exmpp_xml:element(?NS_ROSTER_VER, 'optional')]). + + + + +%% @doc Roster push, calculate and include the version attribute. +%% TODO: don't push to those who didn't load roster +push_item(Server, User, From, Item, RosterVersion) -> + lists:foreach(fun(Resource) -> + push_item(User, Server, Resource, From, Item, RosterVersion) + end, ejabberd_sm:get_user_resources(User, Server)). + +push_item(User, Server, Resource, From, Item, RosterVersion) -> + Request = #xmlel{ns = ?NS_ROSTER, name = 'query', attrs = [?XMLATTR('ver', RosterVersion)], + children = [mod_roster:item_to_xml(Item)]}, + ResIQ = exmpp_iq:set(?NS_JABBER_CLIENT, Request, + "push" ++ randoms:get_string()), + ejabberd_router:route( + From, + exmpp_jid:make(User, Server, Resource), + ResIQ). +