/modules/mod_facebook/resources/resource_facebook_redirect.erl

https://code.google.com/p/zotonic/ · Erlang · 209 lines · 140 code · 31 blank · 38 comment · 1 complexity · 98100f9e107b0d7f6ac5aa48ecb21257 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2010 Marc Worrell
  3. %% Date: 2010-05-11
  4. %% @doc Handle the OAuth redirect of the Facebook logon handshake.
  5. %% See http://developers.facebook.com/docs/authentication/
  6. %% @todo Update a user record when we receive a new e-mail address.
  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(resource_facebook_redirect).
  21. -author("Marc Worrell <marc@worrell.nl>").
  22. -export([init/1, service_available/2, charsets_provided/2, content_types_provided/2]).
  23. -export([resource_exists/2, previously_existed/2, moved_temporarily/2]).
  24. -include_lib("webmachine_resource.hrl").
  25. -include_lib("include/zotonic.hrl").
  26. init(DispatchArgs) -> {ok, DispatchArgs}.
  27. service_available(ReqData, DispatchArgs) when is_list(DispatchArgs) ->
  28. Context = z_context:new(ReqData, ?MODULE),
  29. Context1 = z_context:set(DispatchArgs, Context),
  30. Context2 = z_context:ensure_all(Context1),
  31. ?WM_REPLY(true, Context2).
  32. charsets_provided(ReqData, Context) ->
  33. {[{"utf-8", fun(X) -> X end}], ReqData, Context}.
  34. content_types_provided(ReqData, Context) ->
  35. {[{"text/html", provide_content}], ReqData, Context}.
  36. resource_exists(ReqData, Context) ->
  37. {false, ReqData, Context}.
  38. previously_existed(ReqData, Context) ->
  39. {true, ReqData, Context}.
  40. moved_temporarily(ReqData, Context) ->
  41. Context1 = ?WM_REQ(ReqData, Context),
  42. Code = z_context:get_q("code", Context1),
  43. case fetch_access_token(Code, Context1) of
  44. {ok, AccessToken, Expires} ->
  45. z_context:set_session(facebook_logon, true, Context1),
  46. z_context:set_session(facebook_access_token, AccessToken, Context1),
  47. z_context:set_session(facebook_access_token_expires, Expires, Context1),
  48. case fetch_user_data(AccessToken) of
  49. {ok, UserProps} ->
  50. logon_fb_user(UserProps, z_context:get_q("p", Context1), Context1);
  51. {error, Reason} ->
  52. redirect_error(Reason, Context1)
  53. end;
  54. {error, Reason} ->
  55. redirect_error(Reason, Context1)
  56. end.
  57. redirect_error(Reason, Context) ->
  58. ?DEBUG({?MODULE, Reason}),
  59. z_context:set_session(facebook_logon, false, Context),
  60. z_context:set_session(facebook_access_token, undefined, Context),
  61. z_context:set_session(facebook_access_token_expires, undefined, Context),
  62. Location = z_context:abs_url(z_dispatcher:url_for(logon, Context), Context),
  63. ?WM_REPLY({true, Location}, Context).
  64. % Exchange the code for an access token
  65. fetch_access_token(Code, Context) ->
  66. {AppId, AppSecret, _Scope} = mod_facebook:get_config(Context),
  67. Page = z_context:get_q("p", Context, "/"),
  68. RedirectUrl = lists:flatten(
  69. z_context:abs_url(
  70. lists:flatten(z_dispatcher:url_for(facebook_redirect, [{p,Page}], Context)),
  71. Context)),
  72. FacebookUrl = "https://graph.facebook.com/oauth/access_token?client_id="
  73. ++ z_utils:url_encode(AppId)
  74. ++ "&redirect_uri=" ++ z_utils:url_encode(RedirectUrl)
  75. ++ "&client_secret=" ++ z_utils:url_encode(AppSecret)
  76. ++ "&code=" ++ z_utils:url_encode(Code),
  77. case http:request(FacebookUrl) of
  78. {ok, {{_, 200, _}, _Headers, Payload}} ->
  79. Qs = mochiweb_util:parse_qs(Payload),
  80. {ok, proplists:get_value("access_token", Qs), z_convert:to_integer(proplists:get_value("expires", Qs))};
  81. Other ->
  82. {error, {http_error, FacebookUrl, Other}}
  83. end.
  84. % Given the access token, fetch data about the user
  85. fetch_user_data(AccessToken) ->
  86. FacebookUrl = "https://graph.facebook.com/me?access_token=" ++ z_utils:url_encode(AccessToken),
  87. case http:request(FacebookUrl) of
  88. {ok, {{_, 200, _}, _Headers, Payload}} ->
  89. {struct, Props} = mochijson:decode(Payload),
  90. {ok, [ {list_to_atom(K), V} || {K,V} <- Props ]};
  91. Other ->
  92. {error, {http_error, FacebookUrl, Other}}
  93. end.
  94. %% @doc Check if the user exists, if not then hand over control to the auth_signup resource.
  95. logon_fb_user(FacebookProps, LocationAfterSignup, Context) ->
  96. Props = [
  97. {title, unicode:characters_to_binary(proplists:get_value(name, FacebookProps))},
  98. {name_first, unicode:characters_to_binary(proplists:get_value(first_name, FacebookProps))},
  99. {name_surname, unicode:characters_to_binary(proplists:get_value(last_name, FacebookProps))},
  100. {website, unicode:characters_to_binary(proplists:get_value(link, FacebookProps))},
  101. {email, unicode:characters_to_binary(proplists:get_value(email, FacebookProps))}
  102. ],
  103. UID = unicode:characters_to_binary(proplists:get_value(id, FacebookProps)),
  104. case m_identity:lookup_by_type_and_key("facebook", UID, Context) of
  105. undefined ->
  106. % Register the Facebook identities as verified
  107. SignupProps = [
  108. {identity, {username_pw, {generate_username(Props, Context), z_ids:id(6)}, true, true}},
  109. {identity, {facebook, UID, true, true}},
  110. {identity, {email, proplists:get_value(email, FacebookProps), true, true}},
  111. {ready_page, LocationAfterSignup}
  112. ],
  113. case z_notifier:first({signup_url, Props, SignupProps}, Context) of
  114. {ok, Location} ->
  115. use_see_other(Location, Context);
  116. %?WM_REPLY({true, Location}, Context);
  117. undefined ->
  118. throw({error, {?MODULE, "No result from signup_url notification handler"}})
  119. end;
  120. Row ->
  121. UserId = proplists:get_value(rsc_id, Row),
  122. {Location,Context1} = case z_auth:logon(UserId, Context) of
  123. {ok, ContextUser} ->
  124. update_user(UserId, Props, ContextUser),
  125. case has_location(LocationAfterSignup) of
  126. false -> {m_rsc:p(UserId, page_url, ContextUser), ContextUser};
  127. true -> {LocationAfterSignup, ContextUser}
  128. end;
  129. {error, _Reason} ->
  130. {z_dispatcher:url_for(logon, [{error_uid,UserId}], Context), Context}
  131. end,
  132. LocationAbs = lists:flatten(z_context:abs_url(Location, Context1)),
  133. use_see_other(LocationAbs, Context1)
  134. %?WM_REPLY({true, LocationAbs}, Context1)
  135. end.
  136. has_location(undefined) -> false;
  137. has_location([]) -> false;
  138. has_location(<<>>) -> false;
  139. has_location("/") -> false;
  140. has_location(_) -> true.
  141. %% HACK ALERT!
  142. %% We use a 303 See Other here as there is a serious bug in Safari 4.0.5
  143. %% When we use a 307 then the orginal login post at Facebook will be posted
  144. %% to our redirect location. Including the Facebook username and password....
  145. use_see_other(Location, Context) ->
  146. ContextLoc = z_context:set_resp_header("Location", Location, Context),
  147. ?WM_REPLY({halt, 303}, ContextLoc).
  148. generate_username(Props, Context) ->
  149. case proplists:get_value(title, Props) of
  150. [] ->
  151. First = proplists:get_value(name_first, Props),
  152. Last = proplists:get_value(name_surname, Props),
  153. generate_username1(z_string:nospaces(z_string:to_lower(First) ++ "." ++ z_string:to_lower(Last)), Context);
  154. Title ->
  155. generate_username1(z_string:nospaces(z_string:to_lower(Title)), Context)
  156. end.
  157. generate_username1(Name, Context) ->
  158. case m_identity:lookup_by_username(Name, Context) of
  159. undefined -> Name;
  160. _ -> generate_username2(Name, Context)
  161. end.
  162. generate_username2(Name, Context) ->
  163. N = integer_to_list(z_ids:number() rem 1000),
  164. case m_identity:lookup_by_username(Name++N, Context) of
  165. undefined -> Name;
  166. _ -> generate_username2(Name, Context)
  167. end.
  168. % [{"id","100001090298809"},
  169. % {"name","Marc Worrell"},
  170. % {"first_name","Marc"},
  171. % {"last_name","Worrell"},
  172. % {"link","http://www.facebook.com/profile.php?id=100001090298809"},
  173. % {"gender","man"},
  174. % {"email","marc@worrell.nl"},
  175. % {"timezone",2},
  176. % {"updated_time","2010-05-09T11:27:09+0000"}]
  177. update_user(_UserId, _Props, _Context) ->
  178. ok.