<commit_before>%% @author <NAME> <<EMAIL>>
%% @copyright 2010 <NAME>
%% @date 2010-05-11
%% @doc Handle the OAuth redirect of the Facebook logon handshake.
%% @see http://developers.facebook.com/docs/authentication/
%% @todo Update a user record when we receive a new e-mail address.

%% Copyright 2010 <NAME>
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% 
%%     http://www.apache.org/licenses/LICENSE-2.0
%% 
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(resource_facebook_redirect).
-author("<NAME> <<EMAIL>>").

-export([init/1, service_available/2, charsets_provided/2, content_types_provided/2]).
-export([resource_exists/2, previously_existed/2, moved_temporarily/2]).

-include_lib("webmachine_resource.hrl").
-include_lib("include/zotonic.hrl").

init(DispatchArgs) -> {ok, DispatchArgs}.

service_available(ReqData, DispatchArgs) when is_list(DispatchArgs) ->
    Context  = z_context:new(ReqData, ?MODULE),
    Context1 = z_context:set(DispatchArgs, Context),
    Context2 = z_context:ensure_all(Context1),
    ?WM_REPLY(true, Context2).

charsets_provided(ReqData, Context) ->
    {[{"utf-8", fun(X) -> X end}], ReqData, Context}.

content_types_provided(ReqData, Context) ->
    {[{"text/html", provide_content}], ReqData, Context}.

resource_exists(ReqData, Context) ->
    {false, ReqData, Context}.

previously_existed(ReqData, Context) ->
    {true, ReqData, Context}.

moved_temporarily(ReqData, Context) ->
    Context1 = ?WM_REQ(ReqData, Context),
    Code = z_context:get_q("code", Context1),
    case fetch_access_token(Code, Context1) of
        {ok, AccessToken, Expires} ->
            z_context:set_session(facebook_logon, true, Context1),
            z_context:set_session(facebook_access_token, AccessToken, Context1),
            z_context:set_session(facebook_access_token_expires, Expires, Context1),

            case fetch_user_data(AccessToken) of
                {ok, UserProps} ->
                    logon_fb_user(UserProps, z_context:get_q("page", Context1), Context1);
                {error, Reason} ->
                    redirect_error(Reason, Context1)
            end;
        {error, Reason} ->
            redirect_error(Reason, Context1)
    end.


    redirect_error(Reason, Context) ->
        ?DEBUG({?MODULE, Reason}),
        z_context:set_session(facebook_logon, false, Context),
        z_context:set_session(facebook_access_token, undefined, Context),
        z_context:set_session(facebook_access_token_expires, undefined, Context),
        Location = z_context:abs_url(z_dispatcher:url_for(logon, Context), Context),
        ?WM_REPLY({true, Location}, Context).


% Exchange the code for an access token
fetch_access_token(Code, Context) ->
    {AppId, AppSecret} = mod_facebook:get_appid_secret(Context),
    Page = z_context:get_q("p", Context, "/"),
    RedirectUrl = lists:flatten(
                        z_context:abs_url(
                            lists:flatten(z_dispatcher:url_for(facebook_redirect, [{p,Page}], Context)), 
                            Context)),

    FacebookUrl = "https://graph.facebook.com/oauth/access_token?client_id="
                ++ z_utils:url_encode(AppId)
                ++ "&redirect_uri=" ++ z_utils:url_encode(RedirectUrl)
                ++ "&client_secret=" ++ z_utils:url_encode(AppSecret)
                ++ "&code=" ++ z_utils:url_encode(Code),
    case http:request(FacebookUrl) of
        {ok, {{_, 200, _}, _Headers, Payload}} -> 
            Qs = mochiweb_util:parse_qs(Payload),
            {ok, proplists:get_value("access_token", Qs), z_convert:to_integer(proplists:get_value("expires", Qs))};
        Other ->
            {error, {http_error, FacebookUrl, Other}}
    end.

% Given the access token, fetch data about the user
fetch_user_data(AccessToken) ->
    FacebookUrl = "https://graph.facebook.com/me?access_token=" ++ z_utils:url_encode(AccessToken),
    case http:request(FacebookUrl) of
        {ok, {{_, 200, _}, _Headers, Payload}} ->
            {struct, Props} = mochijson:decode(Payload),
            {ok, [ {list_to_atom(K), V} || {K,V} <- Props ]};
        Other ->
            {error, {http_error, FacebookUrl, Other}}
    end.


%% @doc Check if the user exists, if not then hand over control to the auth_signup resource.
logon_fb_user(FacebookProps, LocationAfterSignup, Context) ->
    Props = [
        {title, proplists:get_value(name, FacebookProps)},
        {name_first, proplists:get_value(first_name, FacebookProps)},
        {name_surname, proplists:get_value(last_name, FacebookProps)},
        {website, proplists:get_value(link, FacebookProps)},
        {email, proplists:get_value(email, FacebookProps)}
    ],
    UID = proplists:get_value(id, FacebookProps),
    case m_identity:lookup_by_type_and_key("facebook", UID, Context) of
        undefined ->
            % Register the Facebook identities as verified
            SignupProps = [
                {identity, {username, {generate_username(Props, Context), z_ids:id(6)}, true, true}},
                {identity, {facebook, UID, true, true}},
                {identity, {email, proplists:get_value(email, FacebookProps), true, true}},
                {ready_page, LocationAfterSignup}
            ],
            case z_notifier:first({signup_url, Props, SignupProps}, Context) of
                {ok, Location} ->
                    ?WM_REPLY({true, Location}, Context);
                undefined ->
                    throw({error, {?MODULE, "No result from signup_url notification handler"}})
            end;
        Row ->
            UserId = proplists:get_value(rsc_id, Row),
            update_user(UserId, Props, Context),
            {ok, UserId}
    end.


generate_username(Props, Context) ->
    case proplists:get_value(title, Props) of
        [] ->
            First = proplists:get_value(name_first, Props),
            Last = proplists:get_value(name_surname, Props),
            generate_username1(z_string:nospaces(z_string:to_lower(First) ++ "." ++ z_string:to_lower(Last)), Context);
        Title ->
            generate_username1(z_string:nospaces(z_string:to_lower(Title)), Context)
    end.
    
    generate_username1(Name, Context) ->
        case m_identity:lookup_by_username(Name, Context) of
            undefined -> Name;
            _ -> generate_username2(Name, Context)
        end.
        
    generate_username2(Name, Context) ->
        N = integer_to_list(z_ids:number() rem 1000),
        case m_identity:lookup_by_username(Name++N, Context) of<commit_msg>Fix for when a facebook user is already known as an user.<commit_after>%% @author <NAME> <<EMAIL>>
%% @copyright 2010 <NAME>
%% @date 2010-05-11
%% @doc Handle the OAuth redirect of the Facebook logon handshake.
%% @see http://developers.facebook.com/docs/authentication/
%% @todo Update a user record when we receive a new e-mail address.

%% Copyright 2010 <NAME>
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% 
%%     http://www.apache.org/licenses/LICENSE-2.0
%% 
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(resource_facebook_redirect).
-author("<NAME> <<EMAIL>>").

-export([init/1, service_available/2, charsets_provided/2, content_types_provided/2]).
-export([resource_exists/2, previously_existed/2, moved_temporarily/2]).

-include_lib("webmachine_resource.hrl").
-include_lib("include/zotonic.hrl").

init(DispatchArgs) -> {ok, DispatchArgs}.

service_available(ReqData, DispatchArgs) when is_list(DispatchArgs) ->
    Context  = z_context:new(ReqData, ?MODULE),
    Context1 = z_context:set(DispatchArgs, Context),
    Context2 = z_context:ensure_all(Context1),
    ?WM_REPLY(true, Context2).

charsets_provided(ReqData, Context) ->
    {[{"utf-8", fun(X) -> X end}], ReqData, Context}.

content_types_provided(ReqData, Context) ->
    {[{"text/html", provide_content}], ReqData, Context}.

resource_exists(ReqData, Context) ->
    {false, ReqData, Context}.

previously_existed(ReqData, Context) ->
    {true, ReqData, Context}.

moved_temporarily(ReqData, Context) ->
    Context1 = ?WM_REQ(ReqData, Context),
    Code = z_context:get_q("code", Context1),
    case fetch_access_token(Code, Context1) of
        {ok, AccessToken, Expires} ->
            z_context:set_session(facebook_logon, true, Context1),
            z_context:set_session(facebook_access_token, AccessToken, Context1),
            z_context:set_session(facebook_access_token_expires, Expires, Context1),

            case fetch_user_data(AccessToken) of
                {ok, UserProps} ->
                    logon_fb_user(UserProps, z_context:get_q("page", Context1), Context1);
                {error, Reason} ->
                    redirect_error(Reason, Context1)
            end;
        {error, Reason} ->
            redirect_error(Reason, Context1)
    end.


    redirect_error(Reason, Context) ->
        ?DEBUG({?MODULE, Reason}),
        z_context:set_session(facebook_logon, false, Context),
        z_context:set_session(facebook_access_token, undefined, Context),
        z_context:set_session(facebook_access_token_expires, undefined, Context),
        Location = z_context:abs_url(z_dispatcher:url_for(logon, Context), Context),
        ?WM_REPLY({true, Location}, Context).


% Exchange the code for an access token
fetch_access_token(Code, Context) ->
    {AppId, AppSecret} = mod_facebook:get_appid_secret(Context),
    Page = z_context:get_q("p", Context, "/"),
    RedirectUrl = lists:flatten(
                        z_context:abs_url(
                            lists:flatten(z_dispatcher:url_for(facebook_redirect, [{p,Page}], Context)), 
                            Context)),

    FacebookUrl = "https://graph.facebook.com/oauth/access_token?client_id="
                ++ z_utils:url_encode(AppId)
                ++ "&redirect_uri=" ++ z_utils:url_encode(RedirectUrl)
                ++ "&client_secret=" ++ z_utils:url_encode(AppSecret)
                ++ "&code=" ++ z_utils:url_encode(Code),
    case http:request(FacebookUrl) of
        {ok, {{_, 200, _}, _Headers, Payload}} -> 
            Qs = mochiweb_util:parse_qs(Payload),
            {ok, proplists:get_value("access_token", Qs), z_convert:to_integer(proplists:get_value("expires", Qs))};
        Other ->
            {error, {http_error, FacebookUrl, Other}}
    end.

% Given the access token, fetch data about the user
fetch_user_data(AccessToken) ->
    FacebookUrl = "https://graph.facebook.com/me?access_token=" ++ z_utils:url_encode(AccessToken),
    case http:request(FacebookUrl) of
        {ok, {{_, 200, _}, _Headers, Payload}} ->
            {struct, Props} = mochijson:decode(Payload),
            {ok, [ {list_to_atom(K), V} || {K,V} <- Props ]};
        Other ->
            {error, {http_error, FacebookUrl, Other}}
    end.


%% @doc Check if the user exists, if not then hand over control to the auth_signup resource.
logon_fb_user(FacebookProps, LocationAfterSignup, Context) ->
    Props = [
        {title, proplists:get_value(name, FacebookProps)},
        {name_first, proplists:get_value(first_name, FacebookProps)},
        {name_surname, proplists:get_value(last_name, FacebookProps)},
        {website, proplists:get_value(link, FacebookProps)},
        {email, proplists:get_value(email, FacebookProps)}
    ],
    UID = proplists:get_value(id, FacebookProps),
    case m_identity:lookup_by_type_and_key("facebook", UID, Context) of
        undefined ->
            % Register the Facebook identities as verified
            SignupProps = [
                {identity, {username, {generate_username(Props, Context), z_ids:id(6)}, true, true}},
                {identity, {facebook, UID, true, true}},
                {identity, {email, proplists:get_value(email, FacebookProps), true, true}},
                {ready_page, LocationAfterSignup}
            ],
            case z_notifier:first({signup_url, Props, SignupProps}, Context) of
                {ok, Location} ->
                    ?WM_REPLY({true, Location}, Context);
                undefined ->
                    throw({error, {?MODULE, "No result from signup_url notification handler"}})
            end;
        Row ->
            UserId = proplists:get_value(rsc_id, Row),
            ContextUser = z_auth:logon(UserId, Context),
            update_user(UserId, Props, ContextUser),
            Location = case has_location(LocationAfterSignup) of
                            false -> m_rsc:p(UserId, page_url, ContextUser);
                            true -> LocationAfterSignup
                        end,
            LocationAbs = z_context:abs_url(Location, ContextUser),
            ?WM_REPLY({true, LocationAbs}, ContextUser)
    end.
    
    has_location(undefined) -> false;
    has_location([]) -> false;
    has_location(<<>>) -> false;
    has_location("/") -> false;
    has_location(_) -> true.


generate_username(Props, Context) ->
    case proplists:get_value(title, Props) of
        [] ->
            First = proplists:get_value(name_first, Props),
            Last = proplists:get_value(name_surname, Props),
            generate_username1(z_string:nospaces(z_string:to_lower(First) ++ "." ++ z_string:to_lower(Last)), Context);
        Title ->
            generate_username1(z_string:nospaces(z_string:to_lower(Title)), Context)
    end.
    
    generate_username1(Name, Context) ->
        case m_identity:lookup_by_username(Name, Context) of
            undefined -> Name;
            _ -> generate_username2(Name, Context)
        end.
        
    generate_username2(Name, Context) ->
        N = integer_to_list(z_ids:number() rem 1000),
        case m_identity:lookup_by_username(Name++N, Context) of
{'fraction_non_alphanumeric': 0.12421285724171338, 'fraction_numerical': 0.008027126150439415, 'mean_word_length': 3.024505708716235, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 90, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 32, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '136516', 'n_tokens_mistral': 4453, 'n_tokens_neox': 4321, 'n_words': 952}