From e4d21c1941cd204a458d1ecd35dd8615f72d3628 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Sun, 17 Sep 2017 10:26:48 +0300 Subject: [PATCH] Introduce mod_avatar The purpose of the module is to cope with legacy and modern XMPP clients posting avatars. It automatically converts vCard based avatars (XEP-0153) to PEP based avatars (XEP-0084) and vice versa. Also, the module supports convertation between avatar image formats on the fly: this is controlled by `convert` option. For example, to convert all avatars into PNG format, configure the module as: mod_avatar: convert: default: png In order to convert only `webp` format to `jpeg`, set the following: mod_avatar: convert: webp: jpeg Note: the module depends on mod_vcard, mod_vcard_xupdate and mod_pubsub. Also, ejabberd should be built with --enable-graphics option. --- configure.ac | 9 + rebar.config | 6 +- src/ejabberd_app.erl | 11 +- src/mod_avatar.erl | 436 ++++++++++++++++++++++++++++++++++++++ src/mod_pubsub.erl | 4 +- src/mod_vcard.erl | 48 +++-- src/mod_vcard_xupdate.erl | 56 +++-- vars.config.in | 1 + 8 files changed, 525 insertions(+), 46 deletions(-) create mode 100644 src/mod_avatar.erl diff --git a/configure.ac b/configure.ac index edf54722c..1fdf30293 100644 --- a/configure.ac +++ b/configure.ac @@ -236,6 +236,14 @@ AC_ARG_ENABLE(sip, *) AC_MSG_ERROR(bad value ${enableval} for --enable-sip) ;; esac],[if test "x$sip" = "x"; then sip=false; fi]) +AC_ARG_ENABLE(graphics, +[AC_HELP_STRING([--enable-graphics], [enable support for graphic images manipulation (default: yes)])], +[case "${enableval}" in + yes) graphics=true ;; + no) graphics=false ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-graphics) ;; +esac],[if test "x$graphics" = "x"; then graphics=true; fi]) + AC_CONFIG_FILES([Makefile vars.config src/ejabberd.app.src]) @@ -280,6 +288,7 @@ AC_SUBST(iconv) AC_SUBST(stun) AC_SUBST(sip) AC_SUBST(debug) +AC_SUBST(graphics) AC_SUBST(tools) AC_SUBST(latest_deps) AC_SUBST(system_deps) diff --git a/rebar.config b/rebar.config index a1127d2b9..ab326390e 100644 --- a/rebar.config +++ b/rebar.config @@ -25,7 +25,7 @@ {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.15"}}}, {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.9"}}}, {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.23"}}}, - {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.1.14"}}}, + {xmpp, ".*", {git, "https://github.com/processone/xmpp", "d98be4a3159"}}, {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.10"}}}, {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}}, {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.2"}}}, @@ -44,6 +44,7 @@ {tag, "1.0.2"}}}}, {if_var_true, riak, {riakc, ".*", {git, "https://github.com/processone/riak-erlang-client.git", {tag, "2.5.3"}}}}, + {if_var_true, graphics, {eimp, ".*", {git, "https://github.com/processone/eimp.git"}}}, %% Elixir support, needed to run tests {if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir", {tag, {if_version_above, "17", "v1.4.4", "v1.1.1"}}}}}, @@ -74,6 +75,7 @@ p1_oauth2, epam, ezlib, + eimp, iconv]}}. {erl_first_files, ["src/ejabberd_config.erl", "src/gen_mod.erl", "src/mod_muc_room.erl", "src/mod_push.erl"]}. @@ -87,6 +89,7 @@ {if_var_true, debug, debug_info}, {if_var_true, sip, {d, 'SIP'}}, {if_var_true, stun, {d, 'STUN'}}, + {if_var_true, graphics, {d, 'GRAPHICS'}}, {if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}}, {if_var_match, db_type, mssql, {d, 'mssql'}}, {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, @@ -154,6 +157,7 @@ {"fast_xml", [{if_var_true, full_xml, "--enable-full-xml"}]}, {if_var_true, pam, {"epam", []}}, {if_var_true, zlib, {"ezlib", []}}, + {if_var_true, graphics, {"eimp", []}}, {if_var_true, iconv, {"iconv", []}}]}. {port_env, [{"CFLAGS", "-g -O2 -Wall"}]}. diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 64edf508c..56f225220 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -146,7 +146,8 @@ start_apps() -> ejabberd:start_app(fast_yaml), ejabberd:start_app(fast_tls), ejabberd:start_app(xmpp), - ejabberd:start_app(cache_tab). + ejabberd:start_app(cache_tab), + start_eimp(). setup_if_elixir_conf_used() -> case ejabberd_config:is_using_elixir_config() of @@ -170,3 +171,11 @@ start_elixir_application() -> _ -> ok end. + +-ifdef(GRAPHICS). +start_eimp() -> + ejabberd:start_app(eimp). +-else. +start_eimp() -> + ok. +-endif. diff --git a/src/mod_avatar.erl b/src/mod_avatar.erl new file mode 100644 index 000000000..f7b520d2e --- /dev/null +++ b/src/mod_avatar.erl @@ -0,0 +1,436 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% Created : 13 Sep 2017 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2017 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., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- +-module(mod_avatar). +-behaviour(gen_mod). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1]). +%% Hooks +-export([pubsub_publish_item/6, vcard_iq_convert/1, vcard_iq_publish/1]). + +-include("xmpp.hrl"). +-include("logger.hrl"). +-include("pubsub.hrl"). + +-type convert_rules() :: {default | eimp:img_type(), eimp:img_type()}. + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, _Opts) -> + case have_eimp() of + true -> + ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, + pubsub_publish_item, 50), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, + vcard_iq_convert, 30), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, + vcard_iq_publish, 100); + false -> + ?CRITICAL_MSG("ejabberd is built without " + "graphics support: reconfigure it with " + "--enable-graphics or disable '~s'", + [?MODULE]), + {error, graphics_not_compiled} + end. + +stop(Host) -> + ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE, + pubsub_publish_item, 50), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_convert, 30), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_publish, 100). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + [{mod_vcard, hard}, {mod_vcard_xupdate, hard}, {mod_pubsub, hard}]. + +%%%=================================================================== +%%% Hooks +%%%=================================================================== +pubsub_publish_item(LServer, ?NS_AVATAR_METADATA, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer} = Host, + ItemId, [Payload|_]) -> + try xmpp:decode(Payload) of + #avatar_meta{info = []} -> + delete_vcard_avatar(From); + #avatar_meta{info = Info} -> + Rules = get_converting_rules(LServer), + case get_meta_info(Info, Rules) of + #avatar_info{type = MimeType, id = ID, url = <<"">>} = I -> + case get_avatar_data(Host, ID) of + {ok, Data} -> + Meta = #avatar_meta{info = [I]}, + Photo = #vcard_photo{type = MimeType, + binval = Data}, + set_vcard_avatar(From, Photo, + #{avatar_meta => {ID, Meta}}); + {error, _} -> + ok + end; + #avatar_info{type = MimeType, url = URL} -> + Photo = #vcard_photo{type = MimeType, + extval = URL}, + set_vcard_avatar(From, Photo, #{}) + end; + _ -> + ?WARNING_MSG("invalid avatar metadata of ~s@~s published " + "with item id ~s", + [LUser, LServer, ItemId]) + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("failed to decode avatar metadata of ~s@~s: ~s", + [LUser, LServer, xmpp:format_error(Why)]) + end; +pubsub_publish_item(_, _, _, _, _, _) -> + ok. + +-spec vcard_iq_convert(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_convert(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case convert_avatar(LUser, LServer, VCard) of + {ok, MimeType, Data} -> + VCard1 = VCard#vcard_temp{ + photo = #vcard_photo{type = MimeType, + binval = Data}}, + IQ#iq{sub_els = [VCard1]}; + pass -> + IQ; + {error, Reason} -> + stop_with_error(Lang, Reason) + end; +vcard_iq_convert(Acc) -> + Acc. + +-spec vcard_iq_publish(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_publish(#iq{sub_els = [#vcard_temp{photo = undefined}]} = IQ) -> + publish_avatar(IQ, #avatar_meta{}, <<>>, <<>>, <<>>); +vcard_iq_publish(#iq{sub_els = [#vcard_temp{ + photo = #vcard_photo{ + type = MimeType, + binval = Data}}]} = IQ) + when is_binary(Data), Data /= <<>> -> + SHA1 = str:sha(Data), + M = get_avatar_meta(IQ), + case M of + {ok, SHA1, _} -> + IQ; + {ok, _ItemID, #avatar_meta{info = Info} = Meta} -> + case lists:keyfind(SHA1, #avatar_info.id, Info) of + #avatar_info{} -> + IQ; + false -> + Info1 = lists:filter( + fun(#avatar_info{url = URL}) -> URL /= <<"">> end, + Info), + Meta1 = Meta#avatar_meta{info = Info1}, + publish_avatar(IQ, Meta1, MimeType, Data, SHA1) + end; + {error, _} -> + publish_avatar(IQ, #avatar_meta{}, MimeType, Data, SHA1) + end; +vcard_iq_publish(Acc) -> + Acc. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_meta_info([avatar_info()], convert_rules()) -> avatar_info(). +get_meta_info(Info, Rules) -> + case lists:foldl( + fun(_, #avatar_info{} = Acc) -> + Acc; + (#avatar_info{url = URL}, Acc) when URL /= <<"">> -> + Acc; + (#avatar_info{} = I, _) when Rules == [] -> + I; + (#avatar_info{type = MimeType} = I, Acc) -> + T = decode_mime_type(MimeType), + case lists:keymember(T, 2, Rules) of + true -> + I; + false -> + case convert_to_type(T, Rules) of + undefined -> + Acc; + _ -> + [I|Acc] + end + end + end, [], Info) of + #avatar_info{} = I -> I; + [] -> hd(Info); + Is -> hd(lists:reverse(Is)) + end. + +-spec get_avatar_data(jid(), binary()) -> {ok, binary()} | + {error, + notfound | invalid_data | internal_error}. +get_avatar_data(JID, ItemID) -> + {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:get_item(LBJID, ?NS_AVATAR_DATA, ItemID) of + #pubsub_item{payload = [Payload|_]} -> + try xmpp:decode(Payload) of + #avatar_data{data = Data} -> + {ok, Data}; + _ -> + ?WARNING_MSG("invalid avatar data detected " + "for ~s@~s with item id ~s", + [LUser, LServer, ItemID]), + {error, invalid_data} + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("failed to decode avatar data for " + "~s@~s with item id ~s: ~s", + [LUser, LServer, ItemID, + xmpp:format_error(Why)]), + {error, invalid_data} + end; + {error, #stanza_error{reason = 'item-not-found'}} -> + {error, notfound}; + {error, Reason} -> + ?WARNING_MSG("failed to get item for ~s@~s at node ~s " + "with item id ~s: ~p", + [LUser, LServer, ?NS_AVATAR_METADATA, ItemID, Reason]), + {error, internal_error} + end. + +-spec get_avatar_meta(iq()) -> {ok, binary(), avatar_meta()} | + {error, + notfound | invalid_metadata | internal_error}. +get_avatar_meta(#iq{meta = #{avatar_meta := {ItemID, Meta}}}) -> + {ok, ItemID, Meta}; +get_avatar_meta(#iq{from = JID}) -> + {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:get_items(LBJID, ?NS_AVATAR_METADATA) of + [#pubsub_item{itemid = {ItemID, _}, payload = [Payload|_]}|_] -> + try xmpp:decode(Payload) of + #avatar_meta{} = Meta -> + {ok, ItemID, Meta}; + _ -> + ?WARNING_MSG("invalid metadata payload detected " + "for ~s@~s with item id ~s", + [LUser, LServer, ItemID]), + {error, invalid_metadata} + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("failed to decode metadata for " + "~s@~s with item id ~s: ~s", + [LUser, LServer, ItemID, + xmpp:format_error(Why)]), + {error, invalid_metadata} + end; + {error, #stanza_error{reason = 'item-not-found'}} -> + {error, notfound}; + {error, Reason} -> + ?WARNING_MSG("failed to get items for ~s@~s at node ~s: ~p", + [LUser, LServer, ?NS_AVATAR_METADATA, Reason]), + {error, internal_error} + end. + +-spec publish_avatar(iq(), avatar_meta(), binary(), binary(), binary()) -> + iq() | {stop, stanza_error()}. +publish_avatar(#iq{from = JID} = IQ, Meta, <<>>, <<>>, <<>>) -> + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_METADATA, + JID, <<>>, [xmpp:encode(Meta)]) of + {result, _} -> + IQ; + {error, StanzaErr} -> + {stop, StanzaErr} + end; +publish_avatar(#iq{from = JID} = IQ, Meta, MimeType, Data, ItemID) -> + #avatar_meta{info = Info} = Meta, + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + Payload = xmpp:encode(#avatar_data{data = Data}), + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_DATA, + JID, ItemID, [Payload]) of + {result, _} -> + I = #avatar_info{id = ItemID, + type = MimeType, + bytes = size(Data)}, + Meta1 = Meta#avatar_meta{info = [I|Info]}, + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_METADATA, + JID, ItemID, [xmpp:encode(Meta1)]) of + {result, _} -> + IQ; + {error, StanzaErr} -> + ?ERROR_MSG("Failed to publish avatar metadata for ~s: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr} + end; + {error, StanzaErr} -> + ?ERROR_MSG("Failed to publish avatar data for ~s: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr} + end. + +-spec convert_avatar(binary(), binary(), vcard_temp()) -> + {ok, binary(), binary()} | + {error, eimp:error_reason() | base64_error} | + pass. +convert_avatar(LUser, LServer, VCard) -> + case get_converting_rules(LServer) of + [] -> + pass; + Rules -> + case VCard#vcard_temp.photo of + #vcard_photo{binval = Data} when is_binary(Data) -> + convert_avatar(LUser, LServer, Data, Rules); + _ -> + pass + end + end. + +-spec convert_avatar(binary(), binary(), binary(), convert_rules()) -> + {ok, eimp:img_type(), binary()} | + {error, eimp:error_reason()} | + pass. +convert_avatar(LUser, LServer, Data, Rules) -> + Type = get_type(Data), + NewType = convert_to_type(Type, Rules), + if NewType == undefined orelse Type == NewType -> + pass; + true -> + ?DEBUG("Converting avatar of ~s@~s: ~s -> ~s", + [LUser, LServer, Type, NewType]), + case eimp:convert(Data, NewType) of + {ok, NewData} -> + {ok, encode_mime_type(NewType), NewData}; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to convert avatar of " + "~s@~s (~s -> ~s): ~s", + [LUser, LServer, Type, NewType, + eimp:format_error(Reason)]), + Err + end + end. + +-spec set_vcard_avatar(jid(), vcard_photo() | undefined, map()) -> ok. +set_vcard_avatar(JID, VCardPhoto, Meta) -> + case get_vcard(JID) of + {ok, #vcard_temp{photo = VCardPhoto}} -> + ok; + {ok, VCard} -> + VCard1 = VCard#vcard_temp{photo = VCardPhoto}, + IQ = #iq{from = JID, to = JID, id = randoms:get_string(), + type = set, sub_els = [VCard1], meta = Meta}, + LServer = JID#jid.lserver, + ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []), + ok; + {error, _} -> + ok + end. + +-spec delete_vcard_avatar(jid()) -> ok. +delete_vcard_avatar(JID) -> + set_vcard_avatar(JID, undefined, #{}). + +-spec get_vcard(jid()) -> {ok, vcard_temp()} | {error, invalid_vcard}. +get_vcard(#jid{luser = LUser, lserver = LServer}) -> + VCardEl = case mod_vcard:get_vcard(LUser, LServer) of + [El] -> El; + _ -> #vcard_temp{} + end, + try xmpp:decode(VCardEl, ?NS_VCARD, []) of + #vcard_temp{} = VCard -> + {ok, VCard}; + _ -> + ?ERROR_MSG("invalid vCard of ~s@~s in the database", + [LUser, LServer]), + {error, invalid_vcard} + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("failed to decode vCard of ~s@~s: ~s", + [LUser, LServer, xmpp:format_error(Why)]), + {error, invalid_vcard} + end. + +-spec stop_with_error(binary(), eimp:error_reason()) -> + {stop, stanza_error()}. +stop_with_error(Lang, Reason) -> + Txt = eimp:format_error(Reason), + {stop, xmpp:err_internal_server_error(Txt, Lang)}. + +-spec get_converting_rules(binary()) -> convert_rules(). +get_converting_rules(LServer) -> + gen_mod:get_module_opt(LServer, ?MODULE, convert, []). + +-spec get_type(binary()) -> eimp:img_type() | unknown. +get_type(Data) -> + eimp:get_type(Data). + +-spec convert_to_type(eimp:img_type() | unknown, convert_rules()) -> + eimp:img_type() | undefined. +convert_to_type(unknown, _Rules) -> + undefined; +convert_to_type(Type, Rules) -> + case proplists:get_value(Type, Rules) of + undefined -> + proplists:get_value(default, Rules); + T -> + T + end. + +-spec decode_mime_type(binary()) -> eimp:img_type() | unknown. +decode_mime_type(MimeType) -> + case str:to_lower(MimeType) of + <<"image/jpeg">> -> jpeg; + <<"image/png">> -> png; + <<"image/webp">> -> webp; + _ -> unknown + end. + +-spec encode_mime_type(eimp:img_type()) -> binary(). +encode_mime_type(Type) -> + <<"image/", (atom_to_binary(Type, latin1))/binary>>. + +-ifdef(GRAPHICS). +have_eimp() -> true. +-else. +have_eimp() -> false. +-endif. + +mod_opt_type({convert, png}) -> + fun(jpeg) -> jpeg; + (webp) -> webp + end; +mod_opt_type({convert, webp}) -> + fun(jpeg) -> jpeg; + (png) -> png + end; +mod_opt_type({convert, jpeg}) -> + fun(png) -> png; + (webp) -> webp + end; +mod_opt_type({convert, default}) -> + fun(png) -> png; + (webp) -> webp; + (jpeg) -> jpeg + end; +mod_opt_type(_) -> + [{convert, default}, + {convert, webp}, + {convert, png}, + {convert, jpeg}]. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index a67ae5bfc..9ed5e65b6 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -1796,8 +1796,6 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access broadcast -> Payload; PluginPayload -> PluginPayload end, - ejabberd_hooks:run(pubsub_publish_item, ServerHost, - [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload), case get_option(Options, deliver_notifications) of true -> @@ -1806,6 +1804,8 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access false -> ok end, + ejabberd_hooks:run(pubsub_publish_item, ServerHost, + [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), case Result of default -> {result, Reply}; _ -> {result, Result} diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 67d01a085..378b9430f 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -38,7 +38,7 @@ remove_user/2, export/1, import_info/0, import/5, import_start/2, depends/2, process_search/1, process_vcard/1, get_vcard/2, disco_items/5, disco_features/5, disco_identity/5, - decode_iq_subel/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). + vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -95,6 +95,7 @@ init([Host, Opts]) -> ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>), Search = gen_mod:get_opt(search, Opts, false), if Search -> @@ -152,6 +153,7 @@ terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), Mod = gen_mod:db_mod(Host, ?MODULE), Mod:stop(Host), lists:foreach( @@ -191,14 +193,6 @@ get_sm_features(Acc, _From, _To, Node, _Lang) -> _ -> Acc end. --spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel(). -%% Tell gen_iq_handler not to decode vcard elements -decode_iq_subel(El) -> - case xmpp:get_ns(El) of - ?NS_VCARD -> xmpp:encode(El); - _ -> xmpp:decode(El) - end. - -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = <<"Value 'set' of 'type' attribute is not allowed">>, @@ -212,13 +206,15 @@ process_local_iq(#iq{type = get, lang = Lang} = IQ) -> bday = <<"2002-11-16">>}). -spec process_sm_iq(iq()) -> iq(). -process_sm_iq(#iq{type = set, lang = Lang, from = From, - sub_els = [SubEl]} = IQ) -> - #jid{user = User, lserver = LServer} = From, +process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) -> + #jid{lserver = LServer} = From, case lists:member(LServer, ?MYHOSTS) of true -> - set_vcard(User, LServer, SubEl), - xmpp:make_iq_result(IQ); + case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of + drop -> ignore; + #stanza_error{} = Err -> xmpp:make_error(IQ, Err); + _ -> xmpp:make_iq_result(IQ) + end; false -> Txt = <<"The query is only allowed from local users">>, xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) @@ -380,19 +376,33 @@ make_vcard_search(User, LUser, LServer, VCARD) -> orgunit = OrgUnit, lorgunit = LOrgUnit}. --spec set_vcard(binary(), binary(), xmlel()) -> {error, badarg} | ok. +-spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> + #jid{user = User, lserver = LServer} = From, + case set_vcard(User, LServer, VCard) of + {error, badarg} -> + %% Should not be here? + Txt = <<"Nodeprep has failed">>, + {stop, xmpp:err_internal_server_error(Txt, Lang)}; + ok -> + IQ + end; +vcard_iq_set(Acc) -> + Acc. + +-spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) -> {error, badarg} | ok. set_vcard(User, LServer, VCARD) -> case jid:nodeprep(User) of error -> {error, badarg}; LUser -> - VCardSearch = make_vcard_search(User, LUser, LServer, VCARD), + VCardEl = xmpp:encode(VCARD), + VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl), Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:set_vcard(LUser, LServer, VCARD, VCardSearch), + Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch), ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)), - ejabberd_hooks:run(vcard_set, LServer, - [LUser, LServer, VCARD]) + ok end. -spec string2lower(binary()) -> binary(). diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index c9819913b..d34cffa83 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -30,7 +30,7 @@ %% gen_mod callbacks -export([start/2, stop/1, reload/3]). --export([update_presence/1, vcard_set/3, remove_user/2, +-export([update_presence/1, vcard_set/1, remove_user/2, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). @@ -47,15 +47,15 @@ start(Host, Opts) -> init_cache(Host, Opts), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, update_presence, 100), - ejabberd_hooks:add(vcard_set, Host, ?MODULE, vcard_set, - 100), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_set, + 90), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). stop(Host) -> ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, update_presence, 100), - ejabberd_hooks:delete(vcard_set, Host, ?MODULE, - vcard_set, 100), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, + vcard_set, 90), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). reload(Host, NewOpts, _OldOpts) -> @@ -71,16 +71,21 @@ depends(_Host, _Opts) -> -> {presence(), ejabberd_c2s:state()}. update_presence({#presence{type = available} = Pres, #{jid := #jid{luser = LUser, lserver = LServer}} = State}) -> - Hash = get_xupdate(LUser, LServer), - Pres1 = xmpp:set_subtag(Pres, #vcard_xupdate{hash = Hash}), + Pres1 = case get_xupdate(LUser, LServer) of + undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{}); + XUpdate -> xmpp:set_subtag(Pres, XUpdate) + end, {Pres1, State}; update_presence(Acc) -> Acc. --spec vcard_set(binary(), binary(), xmlel()) -> ok. -vcard_set(LUser, LServer, _VCARD) -> +-spec vcard_set(iq()) -> iq(). +vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}), - ejabberd_sm:force_update_presence({LUser, LServer}). + ejabberd_sm:force_update_presence({LUser, LServer}), + IQ; +vcard_set(Acc) -> + Acc. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> @@ -91,7 +96,7 @@ remove_user(User, Server) -> %%==================================================================== %% Storage %%==================================================================== --spec get_xupdate(binary(), binary()) -> binary() | undefined. +-spec get_xupdate(binary(), binary()) -> vcard_xupdate() | undefined. get_xupdate(LUser, LServer) -> Result = case use_cache(LServer) of true -> @@ -102,11 +107,12 @@ get_xupdate(LUser, LServer) -> db_get_xupdate(LUser, LServer) end, case Result of - {ok, Hash} -> Hash; - error -> undefined + {ok, external} -> undefined; + {ok, Hash} -> #vcard_xupdate{hash = Hash}; + error -> #vcard_xupdate{} end. --spec db_get_xupdate(binary(), binary()) -> {ok, binary()} | error. +-spec db_get_xupdate(binary(), binary()) -> {ok, binary() | external} | error. db_get_xupdate(LUser, LServer) -> case mod_vcard:get_vcard(LUser, LServer) of [VCard] -> @@ -147,17 +153,21 @@ use_cache(Host) -> Host, ?MODULE, use_cache, ejabberd_config:use_cache(Host)). --spec compute_hash(xmlel()) -> binary(). +-spec compute_hash(xmlel()) -> binary() | external. compute_hash(VCard) -> - case fxml:get_path_s(VCard, - [{elem, <<"PHOTO">>}, - {elem, <<"BINVAL">>}, - cdata]) of - <<>> -> + case fxml:get_subtag(VCard, <<"PHOTO">>) of + false -> <<>>; - BinVal -> - try str:sha(base64:decode(BinVal)) - catch _:badarg -> <<>> + Photo -> + try xmpp:decode(Photo, ?NS_VCARD, []) of + #vcard_photo{binval = <<_, _/binary>> = BinVal} -> + str:sha(BinVal); + #vcard_photo{extval = <<_, _/binary>>} -> + external; + _ -> + <<>> + catch _:{xmpp_codec, _} -> + <<>> end end. diff --git a/vars.config.in b/vars.config.in index 469711182..47fc5dd44 100644 --- a/vars.config.in +++ b/vars.config.in @@ -42,6 +42,7 @@ {iconv, @iconv@}. {stun, @stun@}. {sip, @sip@}. +{graphics, @graphics@}. %% Version {vsn, "@PACKAGE_VERSION@"}.