%%%------------------------------------------------------------------- %%% File : mod_register_web.erl %%% Author : Badlop %%% Purpose : Web page to register account and related tasks %%% Created : 4 May 2008 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2013 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 %%% %%%---------------------------------------------------------------------- %%% IDEAS: %%% %%% * Implement those options, already present in mod_register: %%% + access %%% + captcha_protected %%% + password_strength %%% + welcome_message %%% + registration_timeout %%% %%% * Improve this module to allow each virtual host to have different %%% options. See http://support.process-one.net/browse/EJAB-561 %%% %%% * Check that all the text is translatable. %%% %%% * Add option to use a custom CSS file, or custom CSS lines. %%% %%% * Don't hardcode the "register" path in URL. %%% %%% * Allow private email during register, and store in custom table. %%% * Optionally require private email to register. %%% * Optionally require email confirmation to register. %%% * Allow to set a private email address anytime. %%% * Allow to recover password using private email to confirm (mod_passrecover) %%% * Optionally require invitation %%% * Optionally register request is forwarded to admin, no account created. -module(mod_register_web). -author('badlop@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, process/2]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). %%%---------------------------------------------------------------------- %%% gen_mod callbacks %%%---------------------------------------------------------------------- start(_Host, _Opts) -> %% case gen_mod:get_opt(docroot, Opts, fun(A) -> A end, undefined) of ok. stop(_Host) -> ok. %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- process([], #request{method = 'GET', lang = Lang}) -> index_page(Lang); process([<<"register.css">>], #request{method = 'GET'}) -> serve_css(); process([<<"new">>], #request{method = 'GET', lang = Lang, host = Host, ip = IP}) -> {Addr, _Port} = IP, form_new_get(Host, Lang, Addr); process([<<"delete">>], #request{method = 'GET', lang = Lang, host = Host}) -> form_del_get(Host, Lang); process([<<"change_password">>], #request{method = 'GET', lang = Lang, host = Host}) -> form_changepass_get(Host, Lang); process([<<"new">>], #request{method = 'POST', q = Q, ip = {Ip, _Port}, lang = Lang, host = Host}) -> case form_new_post(Q, Host) of {success, ok, {Username, Host, _Password}} -> Jid = jlib:make_jid(Username, Host, <<"">>), mod_register:send_registration_notifications(?MODULE, Jid, Ip), Text = (?T(<<"Your Jabber account was successfully " "created.">>)), {200, [], Text}; Error -> ErrorText = list_to_binary([?T(<<"There was an error creating the account: ">>), ?T(get_error_text(Error))]), {404, [], ErrorText} end; process([<<"delete">>], #request{method = 'POST', q = Q, lang = Lang, host = Host}) -> case form_del_post(Q, Host) of {atomic, ok} -> Text = (?T(<<"Your Jabber account was successfully " "deleted.">>)), {200, [], Text}; Error -> ErrorText = list_to_binary([?T(<<"There was an error deleting the account: ">>), ?T(get_error_text(Error))]), {404, [], ErrorText} end; %% TODO: Currently only the first vhost is usable. The web request record %% should include the host where the POST was sent. process([<<"change_password">>], #request{method = 'POST', q = Q, lang = Lang, host = Host}) -> case form_changepass_post(Q, Host) of {atomic, ok} -> Text = (?T(<<"The password of your Jabber account " "was successfully changed.">>)), {200, [], Text}; Error -> ErrorText = list_to_binary([?T(<<"There was an error changing the password: ">>), ?T(get_error_text(Error))]), {404, [], ErrorText} end. %%%---------------------------------------------------------------------- %%% CSS %%%---------------------------------------------------------------------- serve_css() -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), cache_control_public()], css()}. last_modified() -> {<<"Last-Modified">>, <<"Mon, 25 Feb 2008 13:23:30 GMT">>}. cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. css() -> <<"html,body {\nbackground: white;\nmargin: " "0;\npadding: 0;\nheight: 100%;\n}">>. %%%---------------------------------------------------------------------- %%% Index page %%%---------------------------------------------------------------------- index_page(Lang) -> HeadEls = [?XCT(<<"title">>, <<"Jabber Account Registration">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], <<"Jabber Account Registration">>), ?XE(<<"ul">>, [?XE(<<"li">>, [?ACT(<<"new">>, <<"Register a Jabber account">>)]), ?XE(<<"li">>, [?ACT(<<"change_password">>, <<"Change Password">>)]), ?XE(<<"li">>, [?ACT(<<"delete">>, <<"Unregister a Jabber account">>)])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %%%---------------------------------------------------------------------- %%% Formulary new account GET %%%---------------------------------------------------------------------- form_new_get(Host, Lang, IP) -> CaptchaEls = build_captcha_li_list(Lang, IP), HeadEls = [?XCT(<<"title">>, <<"Register a Jabber account">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], <<"Register a Jabber account">>), ?XCT(<<"p">>, <<"This page allows to create a Jabber " "account in this Jabber server. Your " "JID (Jabber IDentifier) will be of the " "form: username@server. Please read carefully " "the instructions to fill correctly the " "fields.">>), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, ([?XE(<<"li">>, [?CT(<<"Username:">>), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, <<"This is case insensitive: macbeth is " "the same that MacBeth and Macbeth.">>), ?XC(<<"li">>, <<(?T(<<"Characters not allowed:">>))/binary, " \" & ' / : < > @ ">>)])]), ?XE(<<"li">>, [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), ?XE(<<"li">>, [?CT(<<"Password:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, <<"Don't tell your password to anybody, " "not even the administrators of the Jabber " "server.">>), ?XCT(<<"li">>, <<"You can later change your password using " "a Jabber client.">>), ?XCT(<<"li">>, <<"Some Jabber clients can store your password " "in your computer. Use that feature only " "if you trust your computer is safe.">>), ?XCT(<<"li">>, <<"Memorize your password, or write it " "in a paper placed in a safe place. In " "Jabber there isn't an automated way " "to recover your password if you forget " "it.">>)])]), ?XE(<<"li">>, [?CT(<<"Password Verification:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)])] ++ CaptchaEls ++ [?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"register">>, <<"Register">>)])]))])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %% Copied from mod_register.erl %% Function copied from ejabberd_logger_h.erl and customized %%%---------------------------------------------------------------------- %%% Formulary new POST %%%---------------------------------------------------------------------- form_new_post(Q, Host) -> case catch get_register_parameters(Q) of [Username, Password, Password, Id, Key] -> form_new_post(Username, Host, Password, {Id, Key}); [_Username, _Password, _Password2, false, false] -> {error, passwords_not_identical}; [_Username, _Password, _Password2, Id, Key] -> ejabberd_captcha:check_captcha(Id, Key), {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. get_register_parameters(Q) -> lists:map(fun (Key) -> case lists:keysearch(Key, 1, Q) of {value, {_Key, Value}} -> Value; false -> false end end, [<<"username">>, <<"password">>, <<"password2">>, <<"id">>, <<"key">>]). form_new_post(Username, Host, Password, {false, false}) -> register_account(Username, Host, Password); form_new_post(Username, Host, Password, {Id, Key}) -> case ejabberd_captcha:check_captcha(Id, Key) of captcha_valid -> register_account(Username, Host, Password); captcha_non_valid -> {error, captcha_non_valid}; captcha_not_found -> {error, captcha_non_valid} end. %%%---------------------------------------------------------------------- %%% Formulary Captcha support for new GET/POST %%%---------------------------------------------------------------------- build_captcha_li_list(Lang, IP) -> case ejabberd_captcha:is_feature_available() of true -> build_captcha_li_list2(Lang, IP); false -> [] end. build_captcha_li_list2(Lang, IP) -> SID = <<"">>, From = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, To = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, Args = [], case ejabberd_captcha:create_captcha(SID, From, To, Lang, IP, Args) of {ok, Id, _} -> {_, {CImg, CText, CId, CKey}} = ejabberd_captcha:build_captcha_html(Id, Lang), [?XE(<<"li">>, [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; _ -> [] end. %%%---------------------------------------------------------------------- %%% Formulary change password GET %%%---------------------------------------------------------------------- form_changepass_get(Host, Lang) -> HeadEls = [?XCT(<<"title">>, <<"Change Password">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], <<"Change Password">>), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, [?CT(<<"Username:">>), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), ?XE(<<"li">>, [?CT(<<"Old Password:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"passwordold">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(<<"New Password:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(<<"Password Verification:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"changepass">>, <<"Change Password">>)])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %%%---------------------------------------------------------------------- %%% Formulary change password POST %%%---------------------------------------------------------------------- form_changepass_post(Q, Host) -> case catch get_changepass_parameters(Q) of [Username, PasswordOld, Password, Password] -> try_change_password(Username, Host, PasswordOld, Password); [_Username, _PasswordOld, _Password, _Password2] -> {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. get_changepass_parameters(Q) -> %% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | %% {error, account_doesnt_exist} | %% {error, password_not_changed} | %% {error, password_incorrect} lists:map(fun (Key) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, [<<"username">>, <<"passwordold">>, <<"password">>, <<"password2">>]). try_change_password(Username, Host, PasswordOld, Password) -> try change_password(Username, Host, PasswordOld, Password) of {atomic, ok} -> {atomic, ok} catch error:{badmatch, Error} -> {error, Error} end. change_password(Username, Host, PasswordOld, Password) -> account_exists = check_account_exists(Username, Host), password_correct = check_password(Username, Host, PasswordOld), ok = ejabberd_auth:set_password(Username, Host, Password), case check_password(Username, Host, Password) of password_correct -> {atomic, ok}; password_incorrect -> {error, password_not_changed} end. check_account_exists(Username, Host) -> case ejabberd_auth:is_user_exists(Username, Host) of true -> account_exists; false -> account_doesnt_exist end. check_password(Username, Host, Password) -> case ejabberd_auth:check_password(Username, Host, Password) of true -> password_correct; false -> password_incorrect end. %%%---------------------------------------------------------------------- %%% Formulary delete account GET %%%---------------------------------------------------------------------- form_del_get(Host, Lang) -> HeadEls = [?XCT(<<"title">>, <<"Unregister a Jabber account">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], <<"Unregister a Jabber account">>), ?XCT(<<"p">>, <<"This page allows to unregister a Jabber " "account in this Jabber server.">>), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, [?CT(<<"Username:">>), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), ?XE(<<"li">>, [?CT(<<"Password:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"unregister">>, <<"Unregister">>)])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} | %% {success, exists, {Username, Host, Password}} | %% {error, not_allowed} | %% {error, invalid_jid} register_account(Username, Host, Password) -> case jlib:make_jid(Username, Host, <<"">>) of error -> {error, invalid_jid}; _ -> register_account2(Username, Host, Password) end. register_account2(Username, Host, Password) -> case ejabberd_auth:try_register(Username, Host, Password) of {atomic, Res} -> {success, Res, {Username, Host, Password}}; Other -> Other end. %%%---------------------------------------------------------------------- %%% Formulary delete POST %%%---------------------------------------------------------------------- form_del_post(Q, Host) -> case catch get_unregister_parameters(Q) of [Username, Password] -> try_unregister_account(Username, Host, Password); _ -> {error, wrong_parameters} end. get_unregister_parameters(Q) -> %% @spec(Username, Host, Password) -> {atomic, ok} | %% {error, account_doesnt_exist} | %% {error, account_exists} | %% {error, password_incorrect} lists:map(fun (Key) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, [<<"username">>, <<"password">>]). try_unregister_account(Username, Host, Password) -> try unregister_account(Username, Host, Password) of {atomic, ok} -> {atomic, ok} catch error:{badmatch, Error} -> {error, Error} end. unregister_account(Username, Host, Password) -> account_exists = check_account_exists(Username, Host), password_correct = check_password(Username, Host, Password), ok = ejabberd_auth:remove_user(Username, Host, Password), account_doesnt_exist = check_account_exists(Username, Host), {atomic, ok}. %%%---------------------------------------------------------------------- %%% Error texts %%%---------------------------------------------------------------------- get_error_text({error, captcha_non_valid}) -> <<"The captcha you entered is wrong">>; get_error_text({success, exists, _}) -> get_error_text({atomic, exists}); get_error_text({atomic, exists}) -> <<"The account already exists">>; get_error_text({error, password_incorrect}) -> <<"Incorrect password">>; get_error_text({error, invalid_jid}) -> <<"The username is not valid">>; get_error_text({error, not_allowed}) -> <<"Not allowed">>; get_error_text({error, account_doesnt_exist}) -> <<"Account doesn't exist">>; get_error_text({error, account_exists}) -> <<"The account was not deleted">>; get_error_text({error, password_not_changed}) -> <<"The password was not changed">>; get_error_text({error, passwords_not_identical}) -> <<"The passwords are different">>; get_error_text({error, wrong_parameters}) -> <<"Wrong parameters in the web formulary">>.