def fxa_verified(data): """Add new FxA users to an SFMC data extension""" # used to be handled by the fxa_register view email = data['email'] fxa_id = data['uid'] locale = data.get('locale') subscribe = data.get('marketingOptIn') metrics = data.get('metricsContext', {}) if not locale: statsd.incr('fxa_verified.ignored.no_locale') return # if we're not using the sandbox ignore testing domains if email_is_testing(email): return lang = get_best_language(get_accept_languages(locale)) if not lang: return _update_fxa_info(email, lang, fxa_id) if subscribe: upsert_user.delay( SUBSCRIBE, { 'email': email, 'lang': lang, 'newsletters': settings.FXA_REGISTER_NEWSLETTER, 'source_url': fxa_source_url(metrics), })
def fxa_verified(data): """Add new FxA users to an SFMC data extension""" # used to be handled by the fxa_register view email = data['email'] fxa_id = data['uid'] create_date = data.get('createDate') if create_date: create_date = datetime.fromtimestamp(create_date) locale = data.get('locale') subscribe = data.get('marketingOptIn') metrics = data.get('metricsContext', {}) service = data.get('service', '') if not locale: statsd.incr('fxa_verified.ignored.no_locale') return # if we're not using the sandbox ignore testing domains if email_is_testing(email): return lang = get_best_language(get_accept_languages(locale)) if not lang: return _update_fxa_info(email, lang, fxa_id, service, create_date) if subscribe: upsert_user.delay(SUBSCRIBE, { 'email': email, 'lang': lang, 'newsletters': settings.FXA_REGISTER_NEWSLETTER, 'source_url': fxa_source_url(metrics), })
def fxa_register(request): if settings.FXA_EVENTS_QUEUE_ENABLE: # When this setting is true these requests will be handled by # a queue via which we receive various events from FxA. See process_fxa_queue.py. # This is still here to avoid errors during the transition to said queue. # TODO remove after complete transistion to queue return HttpResponseJSON({'status': 'ok'}) if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-register requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) data = request.POST.dict() if 'email' not in data: return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-register requires an email address', 'code': errors.BASKET_USAGE_ERROR, }, 401) email = process_email(data['email']) if not email: return invalid_email_response() if 'fxa_id' not in data: return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-register requires a Firefox Account ID', 'code': errors.BASKET_USAGE_ERROR, }, 401) if 'accept_lang' not in data: return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-register requires accept_lang', 'code': errors.BASKET_USAGE_ERROR, }, 401) lang = get_best_language(get_accept_languages(data['accept_lang'])) if lang is None: return HttpResponseJSON( { 'status': 'error', 'desc': 'invalid language', 'code': errors.BASKET_INVALID_LANGUAGE, }, 400) update_fxa_info.delay(email, lang, data['fxa_id']) return HttpResponseJSON({'status': 'ok'})
def fxa_verified(data): """Add new FxA users to an SFMC data extension""" # used to be handled by the fxa_register view email = data['email'] fxa_id = data['uid'] create_date = data.get('createDate') if create_date: create_date = datetime.fromtimestamp(create_date) locale = data.get('locale') subscribe = data.get('marketingOptIn') newsletters = data.get('newsletters') metrics = data.get('metricsContext', {}) service = data.get('service', '') country = data.get('countryCode', '') if not locale: statsd.incr('fxa_verified.ignored.no_locale') return # if we're not using the sandbox ignore testing domains if email_is_testing(email): return lang = get_best_language(get_accept_languages(locale)) if not lang: return _update_fxa_info(email, lang, fxa_id, service, create_date) add_news = None if newsletters: if settings.FXA_REGISTER_NEWSLETTER not in newsletters: newsletters.append(settings.FXA_REGISTER_NEWSLETTER) add_news = ','.join(newsletters) elif subscribe: add_news = settings.FXA_REGISTER_NEWSLETTER if add_news: upsert_user.delay( SUBSCRIBE, { 'email': email, 'lang': lang, 'newsletters': add_news, 'source_url': fxa_source_url(metrics), 'country': country, }) else: record_source_url(email, fxa_source_url(metrics), 'fxa-no-optin')
def fxa_register(request): if settings.FXA_EVENTS_QUEUE_ENABLE: # When this setting is true these requests will be handled by # a queue via which we receive various events from FxA. See process_fxa_queue.py. # This is still here to avoid errors during the transition to said queue. # TODO remove after complete transistion to queue return HttpResponseJSON({'status': 'ok'}) if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-register requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) data = request.POST.dict() if 'email' not in data: return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-register requires an email address', 'code': errors.BASKET_USAGE_ERROR, }, 401) email = process_email(data['email']) if not email: return invalid_email_response() if 'fxa_id' not in data: return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-register requires a Firefox Account ID', 'code': errors.BASKET_USAGE_ERROR, }, 401) if 'accept_lang' not in data: return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-register requires accept_lang', 'code': errors.BASKET_USAGE_ERROR, }, 401) lang = get_best_language(get_accept_languages(data['accept_lang'])) if lang is None: return HttpResponseJSON({ 'status': 'error', 'desc': 'invalid language', 'code': errors.BASKET_INVALID_LANGUAGE, }, 400) update_fxa_info.delay(email, lang, data['fxa_id']) return HttpResponseJSON({'status': 'ok'})
def fxa_verified(data): """Add new FxA users to SFDC""" # if we're not using the sandbox ignore testing domains if email_is_testing(data["email"]): return lang = get_best_language(get_accept_languages(data.get("locale"))) if not lang or lang not in newsletter_languages(): lang = "other" email = data["email"] fxa_id = data["uid"] create_date = data.get("createDate", data.get("ts")) newsletters = data.get("newsletters") metrics = data.get("metricsContext", {}) new_data = { "email": email, "source_url": fxa_source_url(metrics), "country": data.get("countryCode", ""), "fxa_lang": data.get("locale"), "fxa_service": data.get("service", ""), "fxa_id": fxa_id, "optin": True, "format": "H", } if create_date: new_data["fxa_create_date"] = iso_format_unix_timestamp(create_date) newsletters = newsletters or [] newsletters.append(settings.FXA_REGISTER_NEWSLETTER) new_data["newsletters"] = newsletters user_data = get_fxa_user_data(fxa_id, email) # don't overwrite the user's language if already set if not (user_data and user_data.get("lang")): new_data["lang"] = lang upsert_contact(SUBSCRIBE, new_data, user_data)
def fxa_callback(request): # remove state from session to prevent multiple attempts error_url = f"https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/fxa-error/" sess_state = request.session.pop("fxa_state", None) if sess_state is None: statsd.incr("news.views.fxa_callback.error.no_state") return HttpResponseRedirect(error_url) code = request.GET.get("code") state = request.GET.get("state") if not (code and state): statsd.incr("news.views.fxa_callback.error.no_code_state") return HttpResponseRedirect(error_url) if sess_state != state: statsd.incr("news.views.fxa_callback.error.no_state_match") return HttpResponseRedirect(error_url) fxa_oauth, fxa_profile = get_fxa_clients() try: access_token = fxa_oauth.trade_code( code, ttl=settings.FXA_OAUTH_TOKEN_TTL)["access_token"] user_profile = fxa_profile.get_profile(access_token) except Exception: statsd.incr("news.views.fxa_callback.error.fxa_comm") sentry_sdk.capture_exception() return HttpResponseRedirect(error_url) email = user_profile["email"] try: user_data = get_user_data(email=email) except SalesforceError: statsd.incr("news.views.fxa_callback.error.get_user_data") sentry_sdk.capture_exception() return HttpResponseRedirect(error_url) if user_data: token = user_data["token"] else: new_user_data = { "email": email, "optin": True, "format": "H", "newsletters": [settings.FXA_REGISTER_NEWSLETTER], "source_url": f"{settings.FXA_REGISTER_SOURCE_URL}?utm_source=basket-fxa-oauth", } locale = user_profile.get("locale") if locale: new_user_data["fxa_lang"] = locale lang = get_best_language(get_accept_languages(locale)) if lang not in newsletter_languages(): lang = "other" new_user_data["lang"] = lang try: token = upsert_contact(SUBSCRIBE, new_user_data, None)[0] except SalesforceError: statsd.incr("news.views.fxa_callback.error.upsert_contact") sentry_sdk.capture_exception() return HttpResponseRedirect(error_url) redirect_to = ( f"https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/existing/{token}/?fxa=1" ) return HttpResponseRedirect(redirect_to)
def update_user_task(request, api_call_type, data=None, optin=False, sync=False): """Call the update_user task async with the right parameters. If sync==True, be sure to include the token in the response. Otherwise, basket can just do everything in the background. """ data = data or request.POST.dict() newsletters = parse_newsletters_csv(data.get("newsletters")) if newsletters: if api_call_type == SUBSCRIBE: all_newsletters = (newsletter_and_group_slugs() + get_transactional_message_ids()) else: all_newsletters = newsletter_slugs() private_newsletters = newsletter_private_slugs() for nl in newsletters: if nl not in all_newsletters: return HttpResponseJSON( { "status": "error", "desc": "invalid newsletter", "code": errors.BASKET_INVALID_NEWSLETTER, }, 400, ) if api_call_type != UNSUBSCRIBE and nl in private_newsletters: if not is_authorized(request, data.get("email")): return HttpResponseJSON( { "status": "error", "desc": "private newsletter subscription requires a valid API key or OAuth", "code": errors.BASKET_AUTH_ERROR, }, 401, ) if "lang" in data: if not language_code_is_valid(data["lang"]): data["lang"] = "en" elif "accept_lang" in data: lang = get_best_language(get_accept_languages(data["accept_lang"])) if lang: data["lang"] = lang del data["accept_lang"] # if lang not provided get the best one from the accept-language header else: lang = get_best_request_lang(request) if lang: data["lang"] = lang # now ensure that if we do have a lang that it's a supported one if "lang" in data: data["lang"] = get_best_supported_lang(data["lang"]) email = data.get("email") token = data.get("token") if not (email or token): return HttpResponseJSON( { "status": "error", "desc": MSG_EMAIL_OR_TOKEN_REQUIRED, "code": errors.BASKET_USAGE_ERROR, }, 400, ) if optin: data["optin"] = True if api_call_type == SUBSCRIBE and email and data.get("newsletters"): # only rate limit here so we don't rate limit errors. if is_ratelimited( request, group="basket.news.views.update_user_task.subscribe", key=lambda x, y: "%s-%s" % (data["newsletters"], email), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True, ): raise Ratelimited() if api_call_type == SET and token and data.get("newsletters"): # only rate limit here so we don't rate limit errors. if is_ratelimited( request, group="basket.news.views.update_user_task.set", key=lambda x, y: "%s-%s" % (data["newsletters"], token), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True, ): raise Ratelimited() if sync: statsd.incr("news.views.subscribe.sync") if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # save what we can upsert_user.delay(api_call_type, data, start_time=time()) # have to error since we can't return a token return HttpResponseJSON( { "status": "error", "desc": "sync is not available in maintenance mode", "code": errors.BASKET_NETWORK_FAILURE, }, 400, ) try: user_data = get_user_data(email=email, token=token) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: if not email: # must have email to create a user return HttpResponseJSON( { "status": "error", "desc": MSG_EMAIL_OR_TOKEN_REQUIRED, "code": errors.BASKET_USAGE_ERROR, }, 400, ) token, created = upsert_contact(api_call_type, data, user_data) return HttpResponseJSON({ "status": "ok", "token": token, "created": created }) else: upsert_user.delay(api_call_type, data, start_time=time()) return HttpResponseJSON({"status": "ok"})
def update_user_task(request, api_call_type, data=None, optin=False, sync=False): """Call the update_user task async with the right parameters. If sync==True, be sure to include the token in the response. Otherwise, basket can just do everything in the background. """ data = data or request.POST.dict() newsletters = parse_newsletters_csv(data.get('newsletters')) if newsletters: if api_call_type == SUBSCRIBE: all_newsletters = newsletter_and_group_slugs() + get_transactional_message_ids() else: all_newsletters = newsletter_slugs() private_newsletters = newsletter_private_slugs() for nl in newsletters: if nl not in all_newsletters: return HttpResponseJSON({ 'status': 'error', 'desc': 'invalid newsletter', 'code': errors.BASKET_INVALID_NEWSLETTER, }, 400) if api_call_type != UNSUBSCRIBE and nl in private_newsletters: if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'private newsletter subscription requires a valid API key', 'code': errors.BASKET_AUTH_ERROR, }, 401) if 'lang' in data: if not language_code_is_valid(data['lang']): data['lang'] = 'en' elif 'accept_lang' in data: lang = get_best_language(get_accept_languages(data['accept_lang'])) if lang: data['lang'] = lang del data['accept_lang'] else: data['lang'] = 'en' # if lang not provided get the best one from the accept-language header else: data['lang'] = get_best_request_lang(request) or 'en' email = data.get('email') token = data.get('token') if not (email or token): return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) if optin: data['optin'] = True if api_call_type == SUBSCRIBE and email and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.subscribe', key=lambda x, y: '%s-%s' % (data['newsletters'], email), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if api_call_type == SET and token and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.set', key=lambda x, y: '%s-%s' % (data['newsletters'], token), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if sync: statsd.incr('news.views.subscribe.sync') if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # save what we can upsert_user.delay(api_call_type, data, start_time=time()) # have to error since we can't return a token return HttpResponseJSON({ 'status': 'error', 'desc': 'sync is not available in maintenance mode', 'code': errors.BASKET_NETWORK_FAILURE, }, 400) try: user_data = get_user_data(email=email, token=token) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: if not email: # must have email to create a user return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) token, created = upsert_contact(api_call_type, data, user_data) return HttpResponseJSON({ 'status': 'ok', 'token': token, 'created': created, }) else: upsert_user.delay(api_call_type, data, start_time=time()) return HttpResponseJSON({ 'status': 'ok', })
def update_user_task(request, api_call_type, data=None, optin=False, sync=False): """Call the update_user task async with the right parameters. If sync==True, be sure to include the token in the response. Otherwise, basket can just do everything in the background. """ data = data or request.POST.dict() newsletters = parse_newsletters_csv(data.get('newsletters')) if newsletters: if api_call_type == SUBSCRIBE: all_newsletters = newsletter_and_group_slugs() + get_transactional_message_ids() else: all_newsletters = newsletter_slugs() private_newsletters = newsletter_private_slugs() for nl in newsletters: if nl not in all_newsletters: return HttpResponseJSON({ 'status': 'error', 'desc': 'invalid newsletter', 'code': errors.BASKET_INVALID_NEWSLETTER, }, 400) if api_call_type != UNSUBSCRIBE and nl in private_newsletters: if not is_authorized(request, data.get('email')): return HttpResponseJSON({ 'status': 'error', 'desc': 'private newsletter subscription requires a valid API key or OAuth', 'code': errors.BASKET_AUTH_ERROR, }, 401) if 'lang' in data: if not language_code_is_valid(data['lang']): data['lang'] = 'en' elif 'accept_lang' in data: lang = get_best_language(get_accept_languages(data['accept_lang'])) if lang: data['lang'] = lang del data['accept_lang'] # if lang not provided get the best one from the accept-language header else: lang = get_best_request_lang(request) if lang: data['lang'] = lang email = data.get('email') token = data.get('token') if not (email or token): return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) if optin: data['optin'] = True if api_call_type == SUBSCRIBE and email and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.subscribe', key=lambda x, y: '%s-%s' % (data['newsletters'], email), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if api_call_type == SET and token and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.set', key=lambda x, y: '%s-%s' % (data['newsletters'], token), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if sync: statsd.incr('news.views.subscribe.sync') if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # save what we can upsert_user.delay(api_call_type, data, start_time=time()) # have to error since we can't return a token return HttpResponseJSON({ 'status': 'error', 'desc': 'sync is not available in maintenance mode', 'code': errors.BASKET_NETWORK_FAILURE, }, 400) try: user_data = get_user_data(email=email, token=token) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: if not email: # must have email to create a user return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) token, created = upsert_contact(api_call_type, data, user_data) return HttpResponseJSON({ 'status': 'ok', 'token': token, 'created': created, }) else: upsert_user.delay(api_call_type, data, start_time=time()) return HttpResponseJSON({ 'status': 'ok', })
def fxa_callback(request): # remove state from session to prevent multiple attempts error_url = f'https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/fxa-error/' sess_state = request.session.pop('fxa_state', None) if sess_state is None: statsd.incr('news.views.fxa_callback.error.no_state') return HttpResponseRedirect(error_url) code = request.GET.get('code') state = request.GET.get('state') if not (code and state): statsd.incr('news.views.fxa_callback.error.no_code_state') return HttpResponseRedirect(error_url) if sess_state != state: statsd.incr('news.views.fxa_callback.error.no_state_match') return HttpResponseRedirect(error_url) fxa_oauth, fxa_profile = get_fxa_clients() try: access_token = fxa_oauth.trade_code( code, ttl=settings.FXA_OAUTH_TOKEN_TTL)['access_token'] user_profile = fxa_profile.get_profile(access_token) except Exception: statsd.incr('news.views.fxa_callback.error.fxa_comm') sentry_client.captureException() return HttpResponseRedirect(error_url) email = user_profile['email'] try: user_data = get_user_data(email=email) except SalesforceError: statsd.incr('news.views.fxa_callback.error.get_user_data') sentry_client.captureException() return HttpResponseRedirect(error_url) if user_data: token = user_data['token'] else: new_user_data = { 'email': email, 'optin': True, 'format': 'H', 'newsletters': [settings.FXA_REGISTER_NEWSLETTER], 'source_url': f'{settings.FXA_REGISTER_SOURCE_URL}?utm_source=basket-fxa-oauth', } lang = user_profile.get('locale') if lang: lang = get_best_language(get_accept_languages(lang)) new_user_data['lang'] = lang try: token = upsert_contact(SUBSCRIBE, new_user_data, None)[0] except SalesforceError: statsd.incr('news.views.fxa_callback.error.upsert_contact') sentry_client.captureException() return HttpResponseRedirect(error_url) redirect_to = f'https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/existing/{token}/?fxa=1' return HttpResponseRedirect(redirect_to)
def _test(self, langs_list, expected_lang): self.assertEqual(get_best_language(langs_list), expected_lang)