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