PageRenderTime 33ms CodeModel.GetById 14ms app.highlight 14ms RepoModel.GetById 2ms app.codeStats 0ms

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