diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index a278e3ae371..1d0ca444279 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -44,7 +44,7 @@ allow_visitor_nickchange = true :: boolean(), public = true :: boolean(), public_list = true :: boolean(), - persistent = false :: boolean(), + persistent = false :: boolean() | {destroying, boolean()}, moderated = true :: boolean(), captcha_protected = false :: boolean(), members_by_default = true :: boolean(), diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index c37d4c18778..3f2b65b3ee4 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -293,6 +293,8 @@ try_register(User, Server, Password) -> false -> case ejabberd_router:is_my_host(LServer) of true -> + case ejabberd_hooks:run_fold(check_register_user, LServer, true, [User, Server, Password]) of + true -> case lists:foldl( fun(_, ok) -> ok; @@ -307,6 +309,9 @@ try_register(User, Server, Password) -> {error, _} = Err -> Err end; + false -> + {error, not_allowed} + end; false -> {error, not_allowed} end diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 9dce1ff7000..35441b7004e 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -1726,7 +1726,7 @@ make_command_raw_value(Name, Request, BaseArguments) -> raw_value | raw_and_value} | {input_name_append, [binary()]} | - {force_execution, boolean()} | + {force_execution, boolean() | undefined} | {table_options, {PageSize :: integer(), RemainingPath :: [binary()]}} | {result_named, boolean()} | {result_links, @@ -1737,7 +1737,7 @@ make_command_raw_value(Name, Request, BaseArguments) -> {style, normal | danger}. make_command2(Name, Request, BaseArguments, Options) -> Only = proplists:get_value(only, Options, all), - ForceExecution = proplists:get_value(force_execution, Options, false), + ForceExecution = proplists:get_value(force_execution, Options, undefined), InputNameAppend = proplists:get_value(input_name_append, Options, []), Resultnamed = proplists:get_value(result_named, Options, false), ResultLinks = proplists:get_value(result_links, Options, []), @@ -1791,6 +1791,8 @@ make_command2(Name, case {ForceExecution, ResultFormatApi} of {true, _} -> auto; + {false, _} -> + manual; {_, {_, rescode}} -> manual; {_, {_, restuple}} -> diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 73b7a738639..da2f70b78ff 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -540,7 +540,7 @@ normal_state({route, <<"">>, case NewStateData of stop -> Conf = StateData#state.config, - {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; _ when NewStateData#state.just_created -> close_room_if_temporary_and_empty(NewStateData); _ -> @@ -736,7 +736,7 @@ handle_event({destroy, Reason}, _StateName, [jid:encode(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), Conf = StateData#state.config, - {stop, shutdown, StateData#state{config = Conf#config{persistent = false}}}; + {stop, shutdown, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; handle_event(destroy, StateName, StateData) -> ?INFO_MSG("Destroyed MUC room ~ts", [jid:encode(StateData#state.jid)]), @@ -856,7 +856,7 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName, from = From, sub_els = [#muc_unsubscribe{}]}, case process_iq_mucsub(From, IQ, StateData) of {result, _, stop} -> - {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; + {stop, normal, StateData#state{config = Conf#config{persistent = {destroying, Conf#config.persistent}}}}; {result, _, NewState} -> {reply, ok, StateName, NewState}; {ignore, NewState} -> @@ -1015,8 +1015,10 @@ terminate(Reason, _StateName, _ -> add_to_log(room_existence, stopped, StateData), case (StateData#state.config)#config.persistent of - false -> - ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host]); + false -> + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, false]); + {destroying, Persistent} -> + ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host, Persistent]); _ -> ok end diff --git a/src/mod_tombstones.erl b/src/mod_tombstones.erl new file mode 100644 index 00000000000..09f47384163 --- /dev/null +++ b/src/mod_tombstones.erl @@ -0,0 +1,383 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_tombstones.erl +%%% Author : Badlop +%%% Purpose : Keep graveyard of accounts and rooms tombstones +%%% Created : 17 Oct 2024 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2024 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. +%%% +%%%---------------------------------------------------------------------- + +%% @format-begin + +%%---------------------------------------------------------------------- +%% definitions +%%---------------------------------------------------------------------- + +-module(mod_tombstones). + +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). +%% user management +-export([remove_user/2]). +-export([check_register_user/4]). +%% room management +-export([room_destroyed/4]). +-export([check_create_room/4]). +%% commands +-export([get_commands_spec/0, has_tombstone_command/2, delete_tombstone_command/2, + delete_expired_tombstones_command/0, list_tombstones_command/2]). +%% webadmin +-export([webadmin_menu/3, webadmin_page/3]). + +-import(ejabberd_web_admin, + [make_command/4, make_command_raw_value/3, make_table/2, make_table/4]). + +-include_lib("xmpp/include/xmpp.hrl"). + +-include("ejabberd_commands.hrl"). +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). +-include("logger.hrl"). +-include("translate.hrl"). + +-define(GRAVEYARD, mod_tombstones_graveyard). + +-record(?GRAVEYARD, + {key :: {binary(), binary()} | '_', value :: time() | infinity | '_' | '$1'}). + +%%---------------------------------------------------------------------- +%% gen_mod API +%%---------------------------------------------------------------------- + +start(_Host, _Opts) -> + prepare_graveyard(), + ejabberd_commands:register_commands(?MODULE, get_commands_spec()), + {ok, + [{hook, remove_user, remove_user, 50}, + {hook, check_register_user, check_register_user, 50}, + {hook, room_destroyed, room_destroyed, 50}, + {hook, check_create_room, check_create_room, 50}, + {hook, webadmin_menu_host, webadmin_menu, 70}, + {hook, webadmin_page_host, webadmin_page, 50}]}. + +stop(Host) -> + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end. + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +mod_opt_type(room_tombstone_expiry) -> + econf:timeout(second, infinity); +mod_opt_type(user_tombstone_expiry) -> + econf:timeout(second, infinity). + +mod_options(_Host) -> + [{room_tombstone_expiry, infinity}, {user_tombstone_expiry, infinity}]. + +mod_doc() -> + #{desc => + [?T("Keep tombstones for accounts and rooms that were removed. " + "Prevent registration of that account, and creation of that room.")], + note => "added in 24.xx", + opts => + [{room_tombstone_expiry, + #{value => ?T("time | infinity"), + desc => + ?T("How long to keep MUC room tombstones, for example '30 days'. " + "The default value is 'infinity', keep tombstones forever.")}}, + {user_tombstone_expiry, + #{value => ?T("time | infinity"), + desc => + ?T("How long to keep users tombstones, for example '365 days'. " + "The default value is 'infinity', keep tombstones forever.")}}]}. + +%%---------------------------------------------------------------------- +%% Tombstones +%%---------------------------------------------------------------------- + +prepare_graveyard() -> + ejabberd_mnesia:create(?MODULE, + ?GRAVEYARD, + [{disc_copies, [node()]}, + {attributes, record_info(fields, ?GRAVEYARD)}]). + +create_tombstone(Key) -> + Timestamp = get_timestamp(), + mnesia:dirty_write({?GRAVEYARD, Key, Timestamp}). + +has_tombstone(Key) -> + case mnesia:dirty_read(?GRAVEYARD, Key) of + [] -> + false; + [{_, Key, Timestamp}] -> + has_tombstone(Key, Timestamp) + end. + +has_tombstone({_User, Host} = Key, Timestamp) -> + case is_expired(Host, Timestamp) of + true -> + delete_tombstone(Key), + false; + false -> + true + end. + +delete_tombstone(Key) -> + mnesia:dirty_delete(?GRAVEYARD, Key). + +delete_expired_tombstones() -> + F = fun() -> + mnesia:write_lock_table(?GRAVEYARD), + mnesia:foldl(fun(#?GRAVEYARD{key = {U, H} = Key, value = TS}, Acc) -> + case is_expired(H, TS) of + true -> + delete_tombstone(Key), + [jid:encode( + jid:make(U, H)) + | Acc]; + false -> + Acc + end + end, + [], + ?GRAVEYARD) + end, + {atomic, DeletedTombstones} = mnesia:transaction(F), + DeletedTombstones. + +list_tombstones(Host, <<"rooms">>) -> + case mod_muc_admin:find_hosts(Host) of + [] -> + []; + [MucHost] -> + mnesia:dirty_select(?GRAVEYARD, + [{{?GRAVEYARD, {'$1', MucHost}, '$2'}, + [], + [['$1', MucHost, '$2']]}]) + end; +list_tombstones(Host, <<"users">>) -> + mnesia:dirty_select(?GRAVEYARD, + [{{?GRAVEYARD, {'$1', Host}, '$2'}, [], [['$1', Host, '$2']]}]); +list_tombstones(Host, <<"all">>) -> + list_tombstones(Host, <<"rooms">>) ++ list_tombstones(Host, <<"users">>). + +%%---------------------------------------------------------------------- +%% Time Management +%%---------------------------------------------------------------------- + +get_timestamp() -> + erlang:system_time(second). + +is_expired(Host1, Timestamp) -> + Host = get_server_host(Host1), + ExpiryOption = + case Host == Host1 of + true -> + user_tombstone_expiry; + false -> + room_tombstone_expiry + end, + %% Expiry = mod_tombstones_opt:user_tombstone_expiry(Host), + Expiry = gen_mod:get_module_opt(Host, ?MODULE, ExpiryOption), + is_expired(Host, Timestamp, Expiry). + +is_expired(_Host, _Timestamp, infinity) -> + false; +is_expired(_Host, Timestamp, Expiry) -> + Timestamp + Expiry div 1000 < get_timestamp(). + +get_server_host(Host) -> + case lists:member(Host, ejabberd_option:hosts()) of + true -> + Host; + false -> + mod_muc_admin:get_room_serverhost(Host) + end. + +timestamp_to_string(TS) -> + xmpp_util:encode_timestamp({TS div 1000000, TS rem 1000000, 0}). + +%%---------------------------------------------------------------------- +%% User API +%%---------------------------------------------------------------------- + +-spec remove_user(binary(), binary()) -> ok. +remove_user(User, Server) -> + case has_tombstone({User, Server}) of + false -> + ok = create_tombstone({User, Server}), + ok; + true -> + ok + end. + +-spec check_register_user(boolean(), binary(), binary(), binary()) -> boolean(). +check_register_user(Acc, User, Server, _Password) -> + Acc and not has_tombstone({User, Server}). + +%%---------------------------------------------------------------------- +%% MUC Room API +%%---------------------------------------------------------------------- + +-spec room_destroyed(binary(), binary(), binary(), boolean()) -> ok. +room_destroyed(_LServer, Room, Host, true) -> + case has_tombstone({Room, Host}) of + false -> + ok = create_tombstone({Room, Host}), + ok; + true -> + ok + end; +room_destroyed(_LServer, _Room, _Host, _Persistent) -> + ok. + +-spec check_create_room(boolean(), binary(), binary(), binary()) -> boolean(). +check_create_room(Acc, _ServerHost, RoomID, Host) -> + Acc and not has_tombstone({RoomID, Host}). + +%%---------------------------------------------------------------------- +%% Commands +%%---------------------------------------------------------------------- + +get_commands_spec() -> + [#ejabberd_commands{name = has_tombstone, + tags = [tombstone], + desc = "Check if there's an active tombstone", + module = ?MODULE, + function = has_tombstone_command, + policy = user, + args = [], + result = {res, rescode}}, + #ejabberd_commands{name = delete_tombstone, + tags = [tombstone], + desc = "Delete this tombstone", + module = ?MODULE, + function = delete_tombstone_command, + policy = user, + args = [], + result = {res, rescode}}, + #ejabberd_commands{name = delete_expired_tombstones, + tags = [tombstone], + desc = "Delete expired tombstones", + module = ?MODULE, + function = delete_expired_tombstones_command, + args = [], + result = {tombstones, {list, {jid, string}}}}, + #ejabberd_commands{name = list_tombstones, + tags = [tombstone], + desc = "List tombstones in the graveyard", + module = ?MODULE, + function = list_tombstones_command, + args = [{host, binary}, {type, binary}], + args_example = [<<"myserver.com">>, <<"all">>], + args_desc = + ["Server name", "Type of tombstones to list: all | rooms | users"], + result = + {tombstones, + {list, {tombstone, {tuple, [{jid, string}, {timestamp, string}]}}}}}]. + +has_tombstone_command(Username, Host) -> + has_tombstone({Username, Host}). + +delete_tombstone_command(Username, Host) -> + delete_tombstone({Username, Host}). + +delete_expired_tombstones_command() -> + delete_expired_tombstones(). + +list_tombstones_command(Host, Type) -> + [{jid:encode( + jid:make(Username, H)), + timestamp_to_string(Timestamp)} + || [Username, H, Timestamp] <- list_tombstones(Host, Type)]. + +%%---------------------------------------------------------------------- +%% WebAdmin: Host +%%---------------------------------------------------------------------- + +webadmin_menu(Acc, _Host, Lang) -> + [{<<"tombstones">>, translate:translate(Lang, ?T("Tombstones"))} | Acc]. + +webadmin_page(_, + Host, + #request{us = _US, + path = [<<"tombstones">>, <<"users">> | _RPath], + lang = Lang} = + R) -> + PageTitle = translate:translate(Lang, ?T("Tombstones: Users")), + Head = ?H1GL(PageTitle, <<"modules/#mod_tombstones">>, <<"mod_tombstones">>), + Set = [make_command(delete_tombstone, R, [{<<"host">>, Host}], [{style, danger}])], + Get = [make_command(list_tombstones, + R, + [{<<"host">>, Host}, {<<"type">>, <<"users">>}], + [])], + {stop, Head ++ Get ++ Set}; +webadmin_page(_, + Host, + #request{us = _US, + path = [<<"tombstones">>, <<"rooms">> | _RPath], + lang = Lang} = + R) -> + PageTitle = translate:translate(Lang, ?T("Tombstones: Rooms")), + Head = ?H1GL(PageTitle, <<"modules/#mod_tombstones">>, <<"mod_tombstones">>), + Set = case mod_muc_admin:find_hosts(Host) of + [] -> + []; + [MucHost] -> + [make_command(delete_tombstone, R, [{<<"host">>, MucHost}], [{style, danger}])] + end, + Get = [make_command(list_tombstones, + R, + [{<<"host">>, Host}, {<<"type">>, <<"rooms">>}], + [])], + {stop, Head ++ Get ++ Set}; +webadmin_page(_, + Host, + #request{us = _US, + path = [<<"tombstones">> | _RPath], + lang = Lang} = + R) -> + PageTitle = translate:translate(Lang, ?T("Tombstones")), + Head = ?H1GL(PageTitle, <<"modules/#mod_tombstones">>, <<"mod_tombstones">>), + Types = [{<<"users">>, <<"Users">>}, {<<"rooms">>, <<"Rooms">>}], + Links = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- Types])], + Set = [make_command(delete_expired_tombstones, + R, + [], + [{style, danger}, {force_execution, false}])], + Get = [make_command(list_tombstones, + R, + [{<<"host">>, Host}, {<<"type">>, <<"all">>}], + [])], + {stop, Head ++ Links ++ Get ++ Set}; +webadmin_page(Acc, _, _) -> + Acc. +%% @format-end