PageRenderTime 53ms CodeModel.GetById 15ms app.highlight 33ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/mod_signup/mod_signup.erl

https://code.google.com/p/zotonic/
Erlang | 244 lines | 170 code | 36 blank | 38 comment | 4 complexity | 362c2a77d61582619448b1fbab137b8b MD5 | raw file
  1%% @author Marc Worrell <marc@worrell.nl>
  2%% @copyright 2010 Marc Worrell
  3%% Date: 2010-05-12
  4%% @doc Let new members register themselves.
  5%% @todo Check person props before sign up
  6%% @todo Add verification and verification e-mails (check for _Verified, add to m_identity)
  7
  8%% Copyright 2010 Marc Worrell
  9%%
 10%% Licensed under the Apache License, Version 2.0 (the "License");
 11%% you may not use this file except in compliance with the License.
 12%% You may obtain a copy of the License at
 13%% 
 14%%     http://www.apache.org/licenses/LICENSE-2.0
 15%% 
 16%% Unless required by applicable law or agreed to in writing, software
 17%% distributed under the License is distributed on an "AS IS" BASIS,
 18%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 19%% See the License for the specific language governing permissions and
 20%% limitations under the License.
 21
 22-module(mod_signup).
 23-author("Marc Worrell <marc@worrell.nl>").
 24
 25-mod_title("Sign up users").
 26-mod_description("Implements public sign up to register as member of this site.").
 27-mod_prio(500).
 28
 29-export([
 30    datamodel/0,
 31
 32    observe_signup_url/2,
 33    observe_identity_verification/2,
 34    observe_logon_ready_page/2
 35]).
 36-export([signup/4, request_verification/2]).
 37
 38-include("zotonic.hrl").
 39
 40
 41%% @doc Check if a module wants to redirect to the signup form.  Returns either {ok, Location} or undefined.
 42observe_signup_url({signup_url, Props, SignupProps}, Context) ->
 43    CheckId = z_ids:id(),
 44    z_session:set(signup_xs, {CheckId, Props, SignupProps}, Context),
 45    {ok, lists:flatten(z_dispatcher:url_for(signup, [{xs, CheckId}], Context))}.
 46
 47observe_identity_verification({identity_verification, UserId, Ident}, Context) ->
 48    case proplists:get_value(type, Ident) of
 49        <<"email">> -> send_verify_email(UserId, Ident, Context);
 50        _ -> false
 51    end;
 52observe_identity_verification({identity_verification, UserId}, Context) ->
 53    request_verification(UserId, Context).
 54
 55
 56%% @doc Return the url to redirect to when the user logged on, defaults to the user's personal page.
 57observe_logon_ready_page({logon_ready_page, []}, Context) ->
 58    case z_auth:is_auth(Context) of
 59        true -> m_rsc:p(z_acl:user(Context), page_url, Context);
 60        false -> []
 61    end;
 62observe_logon_ready_page({logon_ready_page, Url}, _Context) ->
 63    Url.
 64
 65
 66%% @doc Sign up a new user.
 67%% @spec signup(proplist(), proplist(), RequestConfirm, Context) -> {ok, UserId} | {error, Reason}
 68signup(Props, SignupProps, RequestConfirm, Context) ->
 69    ContextSudo = z_acl:sudo(Context),
 70    case check_signup(Props, SignupProps, ContextSudo) of
 71        {ok, Props1, SignupProps1} ->
 72            z_db:transaction(fun(Ctx) -> do_signup(Props1, SignupProps1, RequestConfirm, Ctx) end, ContextSudo);
 73        {error, _} = Error ->
 74            Error
 75    end.
 76
 77
 78%% @doc Sent verification requests to non verified identities
 79request_verification(UserId, Context) ->
 80    Unverified = [ R || R <- m_identity:get_rsc(UserId, Context), proplists:get_value(is_verified, R) == false ],
 81    request_verification(UserId, Unverified, false, Context).
 82    
 83    request_verification(_, [], false, _Context) ->
 84        {error, no_verifiable_identities};
 85    request_verification(_, [], true, _Context) ->
 86        ok;
 87    request_verification(UserId, [Ident|Rest], Requested, Context) ->
 88        case z_notifier:first({identity_verification, UserId, Ident}, Context) of
 89            ok -> request_verification(UserId, Rest, true, Context);
 90            _ -> request_verification(UserId, Rest, Requested, Context)
 91        end.
 92
 93%%====================================================================
 94%% support functions
 95%%====================================================================
 96
 97%% @doc Preflight checks on a signup
 98%% This function is called with a 'sudo' context.
 99check_signup(Props, SignupProps, Context) ->
100    case z_notifier:foldl(signup_check, {ok, Props, SignupProps}, Context) of
101        {ok, Props1, SignupProps1} ->
102            case check_identity(SignupProps1, Context) of
103                ok -> 
104                    case check_props(Props1, Context) of
105                        ok -> {ok, Props1, SignupProps1};
106                        {error, _} = Error -> Error
107                    end;
108                {error, _} = Error ->
109                    Error
110            end;
111        {error, _ContextOrReason} = Error ->
112            Error
113    end.
114
115
116%% @doc Preflight check if the props are ok.
117%% @todo Add some checks on name, title etc.
118check_props(_Props, _Context) ->
119    ok.
120
121%% @doc Preflight check on identities, prevent double identity keys.
122check_identity([], _Context) ->
123    ok;
124check_identity([{identity, {username_pw, {Username, _Password}, true, _Verified}}|Idents], Context) ->
125    case username_exists(Username, Context) of
126        false -> check_identity(Idents, Context);
127        true -> {error, {identity_in_use, username}}
128    end;
129check_identity([{identity, {Type, Key, true, _Verified}}|Idents], Context) ->
130    case identity_exists(Type, Key, Context) of
131        false -> check_identity(Idents, Context);
132        true -> {error, {identity_in_use, Type}}
133    end;
134check_identity([_|Idents], Context) ->
135    check_identity(Idents, Context).
136
137
138%% @doc Insert a new user, return the user id on success. Assume all args are ok as we did
139%% a preflight check and users sign up slowly (ie. no race condition)
140%% This function is called with a 'sudo' context within a transaction. We throw errors to
141%% force a rollback of the transaction.
142do_signup(Props, SignupProps, RequestConfirm, Context) ->
143    IsVerified = not RequestConfirm orelse has_verified_identity(SignupProps),
144    case m_rsc:insert(props_to_rsc(Props, IsVerified, Context), Context) of
145        {ok, Id} ->
146            [ insert_identity(Id, Ident, Context) || {K,Ident} <- SignupProps, K == identity ],
147            z_notifier:map({signup_done, Id, IsVerified, Props, SignupProps}, Context),
148            case IsVerified of
149                true -> z_notifier:map({signup_confirm, Id}, Context);
150                false -> nop
151            end,    
152            {ok, Id};
153        {error, Reason} ->
154            throw({error, Reason})
155    end.
156
157    
158    has_verified_identity([]) -> false;
159    has_verified_identity([{identity, {Type, _, _, true}}|_Is]) when Type /= username_pw -> true;
160    has_verified_identity([_|Is]) -> has_verified_identity(Is).
161
162
163insert_identity(Id, {username_pw, {Username, Password}, true, true}, Context) ->
164    case m_identity:set_username_pw(Id, Username, Password, Context) of
165        ok -> ok;
166        Error -> throw(Error)
167    end;
168insert_identity(Id, {Type, Key, true, Verified}, Context) when is_binary(Key); is_list(Key) ->
169    m_identity:insert_unique(Id, Type, Key, [{is_verified, Verified}], Context);
170insert_identity(Id, {Type, Key, false, Verified}, Context) when is_binary(Key); is_list(Key) ->
171    m_identity:insert(Id, Type, Key, [{is_verified, Verified}], Context).
172
173
174props_to_rsc(Props, IsVerified, Context) ->
175    Category = z_convert:to_atom(m_config:get_value(mod_signup, member_category, person, Context)),
176    VisibleFor = z_convert:to_integer(m_config:get_value(mod_signup, member_visible_for, 0, Context)),
177    Props1 = [
178        {is_published, IsVerified},
179        {visible_for, VisibleFor},
180        {category, Category},
181        {is_verified_account, IsVerified},
182        {creator_id, self},
183        {pref_language, z_context:language(Context)}
184        | Props
185    ],
186    case proplists:is_defined(title, Props1) of
187        true -> Props1;
188        false ->
189            [ {title, z_convert:to_list(proplists:get_value(name_first, Props1))
190                        ++ " "
191                        ++ z_convert:to_list(proplists:get_value(name_surname, Props1))}
192                | Props1 ]
193    end.
194
195
196%% @doc Check if a username exists
197username_exists(Username, Context) ->
198    case m_identity:lookup_by_username(Username, Context) of
199        undefined -> false;
200        _Props -> true
201    end.
202
203%% @doc Check if the identity exists
204identity_exists(Type, Key, Context) ->
205    case m_identity:lookup_by_type_and_key(Type, Key, Context) of
206        undefined -> false;
207        _Props -> true
208    end.
209
210
211send_verify_email(UserId, Ident, Context) ->
212    Email = proplists:get_value(key, Ident),
213    {ok, Key} = m_identity:set_verify_key(proplists:get_value(id, Ident), Context),
214    Vars = [
215        {recipient_id, UserId},
216        {user_id, UserId},
217        {email, Email},
218        {verify_key, Key}
219    ],
220    z_email:send_render(Email, "email_verify.tpl", Vars, z_acl:sudo(Context)),
221    ok.
222
223
224datamodel() ->
225[
226    {resources, [
227		{signup_tos, text, [
228                        {is_published, true},
229						{visible_for, 0},
230						{page_path, "/terms"},
231						{title, "Terms of Service"},
232						{summary, "These Terms of Service (“Terms”) govern your access to and use of the services and COMPANY’s web sites (the “Services”), and any information, text, graphics, or other materials uploaded, downloaded or appearing on the Services (collectively referred to as “Content”). Your access to and use of the Services is conditioned on your acceptance of and compliance with these Terms. By accessing or using the Services you agree to be bound by these Terms."},
233					    {body, "<h2>INSERT YOUR TERMS OF SERVICE HERE</h2>"}
234					]},
235		{signup_privacy, text, [
236		                {is_published, true},
237                		{visible_for, 0},
238                		{page_path, "/privacy"},
239                		{title, "Privacy Policy"},
240                		{summary, "This Privacy Policy describes COMPANY’s policies and procedures on the collection, use and disclosure of your information. COMPANY receives your information through our various web sites, SMS, APIs, services and third-parties (“Services”). When using any of our Services you consent to the collection, transfer, manipulation, storage, disclosure and other uses of your information as described in this Privacy Policy. Irrespective of which country that you reside in or create information from, your information may be used by COMPANY in any country where COMPANY operates."},
241                		{body, "<h2>INSERT YOUR PRIVACY POLICY HERE</h2>"}
242		            ]}
243	]}
244].