from django.contrib import auth
from django.contrib.auth import authenticate, login
from django.core.files.temp import NamedTemporaryFile
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import transaction
from django.db.utils import IntegrityError
import json
from django_facebook import exceptions as facebook_exceptions, \
settings as facebook_settings, signals
from django_facebook.api import get_facebook_graph
from django_facebook.utils import get_registration_backend, get_form_class, \
get_profile_model, to_bool, get_user_model, get_instance_for,\
get_user_attribute, try_get_profile, get_model_for_attribute,\
get_instance_for_attribute, update_user_attributes
from random import randint
import logging
import sys
import urllib
try:
import urllib2
except ImportError:
import urllib.error as urllib2
logger = logging.getLogger(__name__)
class CONNECT_ACTIONS:
class LOGIN:
pass
class CONNECT(LOGIN):
pass
class REGISTER:
pass
[docs]def connect_user(request, access_token=None, facebook_graph=None, connect_facebook=False):
'''
Given a request either
- (if authenticated) connect the user
- login
- register
'''
user = None
graph = facebook_graph or get_facebook_graph(request, access_token)
converter = get_instance_for('user_conversion', graph)
assert converter.is_authenticated()
facebook_data = converter.facebook_profile_data()
force_registration = request.POST.get('force_registration') or \
request.GET.get('force_registration') or \
request.POST.get('force_registration_hard') or \
request.GET.get('force_registration_hard')
logger.debug('force registration is set to %s', force_registration)
if connect_facebook and request.user.is_authenticated() and not force_registration:
# we should only allow connect if users indicate they really want to connect
# only when the request.CONNECT_FACEBOOK = 1
# if this isn't present we just do a login
action = CONNECT_ACTIONS.CONNECT
# default behaviour is not to overwrite old data
user = _connect_user(request, converter, overwrite=True)
else:
email = facebook_data.get('email', False)
email_verified = facebook_data.get('verified', False)
kwargs = {}
if email and email_verified:
kwargs = {'facebook_email': email}
auth_user = authenticate(facebook_id=facebook_data['id'], **kwargs)
if auth_user and not force_registration:
action = CONNECT_ACTIONS.LOGIN
# Has the user registered without Facebook, using the verified FB
# email address?
# It is after all quite common to use email addresses for usernames
update = getattr(auth_user, 'fb_update_required', False)
profile = try_get_profile(auth_user)
current_facebook_id = get_user_attribute(
auth_user, profile, 'facebook_id')
if not current_facebook_id:
update = True
# login the user
user = _login_user(request, converter, auth_user, update=update)
else:
action = CONNECT_ACTIONS.REGISTER
# when force registration is active we should remove the old
# profile
try:
user = _register_user(request, converter,
remove_old_connections=force_registration)
except facebook_exceptions.AlreadyRegistered as e:
# in Multithreaded environments it's possible someone beats us to
# the punch, in that case just login
logger.info(
'parallel register encountered, slower thread is doing a login')
auth_user = authenticate(
facebook_id=facebook_data['id'], **kwargs)
if not auth_user:
# We don't have a valid user so raise
raise e
action = CONNECT_ACTIONS.LOGIN
user = _login_user(request, converter, auth_user, update=False)
_update_likes_and_friends(request, user, converter)
_update_access_token(user, graph)
logger.info('connect finished with action %s', action)
return action, user
def _login_user(request, facebook, authenticated_user, update=False):
login(request, authenticated_user)
if update:
_connect_user(request, facebook)
return authenticated_user
def _connect_user(request, facebook, overwrite=True):
'''
Update the fields on the user model and connects it to the facebook account
'''
if not request.user.is_authenticated():
raise ValueError(
'Connect user can only be used on authenticated users')
if not facebook.is_authenticated():
raise ValueError(
'Facebook needs to be authenticated for connect flows')
data = facebook.facebook_profile_data()
facebook_id = data['id']
# see if we already have profiles connected to this Facebook account
old_connections = _get_old_connections(facebook_id, request.user.id)[:20]
if old_connections and not (request.POST.get('confirm_connect') or
request.GET.get('confirm_connect')):
raise facebook_exceptions.AlreadyConnectedError(list(old_connections))
user = _update_user(request.user, facebook, overwrite=overwrite)
return user
def _update_likes_and_friends(request, user, facebook):
# store likes and friends if configured
sid = transaction.savepoint()
try:
if facebook_settings.FACEBOOK_STORE_LIKES:
facebook.get_and_store_likes(user)
if facebook_settings.FACEBOOK_STORE_FRIENDS:
facebook.get_and_store_friends(user)
transaction.savepoint_commit(sid)
except IntegrityError as e:
logger.warn(u'Integrity error encountered during registration, '
'probably a double submission %s' % e,
exc_info=sys.exc_info(), extra={
'request': request,
'data': {
'body': unicode(e),
}
})
transaction.savepoint_rollback(sid)
def _update_access_token(user, graph):
'''
Conditionally updates the access token in the database
'''
profile = try_get_profile(user)
model_or_profile = get_instance_for_attribute(
user, profile, 'access_token')
# store the access token for later usage if the profile model supports it
if model_or_profile:
# update if not equal to the current token
new_token = graph.access_token != model_or_profile.access_token
token_message = 'a new' if new_token else 'the same'
logger.info(
'found %s token %s', token_message, graph.access_token[:10])
if new_token:
logger.info('access token changed, updating now')
model_or_profile.update_access_token(graph.access_token)
model_or_profile.save()
# see if we can extend the access token
# this runs in a task, after extending the token we fire an event
model_or_profile.extend_access_token()
def _register_user(request, facebook, profile_callback=None,
remove_old_connections=False):
'''
Creates a new user and authenticates
The registration form handles the registration and validation
Other data on the user profile is updates afterwards
if remove_old_connections = True we will disconnect old
profiles from their facebook flow
'''
if not facebook.is_authenticated():
raise ValueError(
'Facebook needs to be authenticated for connect flows')
# get the backend on new registration systems, or none
# if we are on an older version
backend = get_registration_backend()
logger.info('running backend %s for registration', backend)
# gets the form class specified in FACEBOOK_REGISTRATION_FORM
form_class = get_form_class(backend, request)
facebook_data = facebook.facebook_registration_data()
data = request.POST.copy()
for k, v in facebook_data.items():
if not data.get(k):
data[k] = v
if remove_old_connections:
_remove_old_connections(facebook_data['facebook_id'])
if request.POST.get('force_registration_hard') or \
request.GET.get('force_registration_hard'):
data['email'] = data['email'].replace(
'@', '+test%s@' % randint(0, 1000000000))
if not data['email']:
data['email'] = '%s@facebook.com' % data['username']
form = form_class(data=data, files=request.FILES,
initial={'ip': request.META['REMOTE_ADDR']})
if not form.is_valid():
# show errors in sentry
form_errors = form.errors
error = facebook_exceptions.IncompleteProfileError(
'Facebook signup incomplete')
error.form = form
raise error
try:
# for new registration systems use the backends methods of saving
new_user = None
if backend:
new_user = backend.register(request,
form=form, **form.cleaned_data)
# fall back to the form approach
if new_user is None:
raise ValueError(
'new_user is None, note that backward compatability for the older versions of django registration has been dropped.')
except IntegrityError as e:
# this happens when users click multiple times, the first request registers
# the second one raises an error
raise facebook_exceptions.AlreadyRegistered(e)
# update some extra data not yet done by the form
new_user = _update_user(new_user, facebook)
signals.facebook_user_registered.send(sender=get_user_model(),
user=new_user, facebook_data=facebook_data, request=request, converter=facebook)
# IS this the correct way for django 1.3? seems to require the backend
# attribute for some reason
new_user.backend = 'django_facebook.auth_backends.FacebookBackend'
auth.login(request, new_user)
return new_user
def _get_old_connections(facebook_id, current_user_id=None):
'''
Gets other accounts connected to this facebook id, which are not
attached to the current user
'''
user_or_profile_model = get_model_for_attribute('facebook_id')
other_facebook_accounts = user_or_profile_model.objects.filter(
facebook_id=facebook_id)
kwargs = {}
if current_user_id:
# if statement since we need to support both
user_model = get_user_model()
if user_or_profile_model == user_model:
kwargs['id'] = current_user_id
else:
kwargs['user'] = current_user_id
other_facebook_accounts = other_facebook_accounts.exclude(**kwargs)
return other_facebook_accounts
def _remove_old_connections(facebook_id, current_user_id=None):
'''
Removes the facebook id for profiles with the specified facebook id
which arent the current user id
'''
other_facebook_accounts = _get_old_connections(
facebook_id, current_user_id)
other_facebook_accounts.update(facebook_id=None)
def _update_user(user, facebook, overwrite=True):
'''
Updates the user and his/her profile with the data from facebook
'''
# if you want to add fields to ur user model instead of the
# profile thats fine
# partial support (everything except raw_data and facebook_id is included)
facebook_data = facebook.facebook_registration_data(username=False)
facebook_fields = ['facebook_name', 'facebook_profile_url', 'gender',
'date_of_birth', 'about_me', 'website_url', 'first_name', 'last_name']
profile = try_get_profile(user)
# which attributes to update
attributes_dict = {}
# send the signal that we're updating
signals.facebook_pre_update.send(sender=get_user_model(), user=user,
profile=profile, facebook_data=facebook_data)
# set the facebook id and make sure we are the only user with this id
current_facebook_id = get_user_attribute(user, profile, 'facebook_id')
facebook_id_changed = facebook_data['facebook_id'] != current_facebook_id
overwrite_allowed = overwrite or not current_facebook_id
# update the facebook id and access token
facebook_id_overwritten = False
if facebook_id_changed and overwrite_allowed:
# when not overwriting we only update if there is no
# profile.facebook_id
logger.info('profile facebook id changed from %s to %s',
repr(facebook_data['facebook_id']),
repr(current_facebook_id))
attributes_dict['facebook_id'] = facebook_data['facebook_id']
facebook_id_overwritten = True
if facebook_id_overwritten:
_remove_old_connections(facebook_data['facebook_id'], user.id)
# update all fields on both user and profile
for f in facebook_fields:
facebook_value = facebook_data.get(f, False)
current_value = get_user_attribute(user, profile, f, None)
if facebook_value and not current_value:
attributes_dict[f] = facebook_value
# write the raw data in case we missed something
serialized_fb_data = json.dumps(facebook.facebook_profile_data())
current_raw_data = get_user_attribute(user, profile, 'raw_data')
if current_raw_data != serialized_fb_data:
attributes_dict['raw_data'] = serialized_fb_data
image_url = facebook_data['image']
# update the image if we are allowed and have to
if facebook_settings.FACEBOOK_STORE_LOCAL_IMAGE:
image_field = get_user_attribute(user, profile, 'image', True)
if not image_field:
image_name, image_file = _update_image(
facebook_data['facebook_id'], image_url)
image_field.save(image_name, image_file)
# save both models if they changed
update_user_attributes(user, profile, attributes_dict)
if getattr(user, '_fb_is_dirty', False):
user.save()
if getattr(profile, '_fb_is_dirty', False):
profile.save()
signals.facebook_post_update.send(sender=get_user_model(),
user=user, profile=profile, facebook_data=facebook_data)
return user
def _update_image(facebook_id, image_url):
'''
Updates the user profile's image to the given image url
Unfortunately this is quite a pain to get right with Django
Suggestions to improve this are welcome
'''
image_name = 'fb_image_%s.jpg' % facebook_id
image_temp = NamedTemporaryFile()
try:
image_response = urllib2.urlopen(image_url)
except AttributeError:
image_response = urllib.request.urlopen(image_url)
image_content = image_response.read()
image_temp.write(image_content)
http_message = image_response.info()
image_size = len(image_content)
try:
content_type = http_message.type
except AttributeError:
content_type = http_message.get_content_type()
image_file = InMemoryUploadedFile(
file=image_temp, name=image_name, field_name='image',
content_type=content_type, size=image_size, charset=None
)
image_file.seek(0)
image_temp.flush()
return image_name, image_file
[docs]def update_connection(request, graph):
'''
A special purpose view for updating the connection with an existing user
- updates the access token (already done in get_graph)
- sets the facebook_id if nothing is specified
- stores friends and likes if possible
'''
converter = get_instance_for('user_conversion', graph)
user = _connect_user(request, converter, overwrite=False)
_update_likes_and_friends(request, user, converter)
_update_access_token(user, graph)
return user