def fxa_concerts_rsvp(request): if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) fields = ('email', 'is_firefox', 'campaign_id') data = request.POST.dict() if not all(f in data for f in fields): return HttpResponseJSON( { 'status': 'error', 'desc': 'missing required field', 'code': errors.BASKET_USAGE_ERROR, }, 401) is_firefox = 'Y' if data['is_firefox'][0].upper() == 'Y' else 'N' record_fxa_concerts_rsvp.delay( email=data['email'], is_firefox=is_firefox, campaign_id=data['campaign_id'], ) return HttpResponseJSON({'status': 'ok'})
def common_voice_goals(request): if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) form = CommonVoiceForm(request.POST) if form.is_valid(): # don't send empty values and use ISO formatted date strings data = { k: v for k, v in form.cleaned_data.items() if not (v == '' or v is None) } record_common_voice_goals.delay(data) return HttpResponseJSON({'status': 'ok'}) else: # form is invalid return HttpResponseJSON( { 'status': 'error', 'errors': format_form_errors(form.errors), 'errors_by_field': form.errors, }, 400)
def amo_sync(request, post_type): if post_type not in AMO_SYNC_TYPES: return HttpResponseJSON( { 'status': 'error', 'desc': 'API URL not found', 'code': errors.BASKET_USAGE_ERROR, }, 404) if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) try: data = json.loads(request.body) except ValueError: statsd.incr(f'amo_sync.{post_type}.message.json_error') sentry_client.captureException( data={'extra': { 'request.body': request.body }}) return HttpResponseJSON( { 'status': 'error', 'desc': 'JSON error', 'code': errors.BASKET_USAGE_ERROR, }, 400) AMO_SYNC_TYPES[post_type].delay(data) return HttpResponseJSON({'status': 'ok'})
def fxa_concerts_rsvp(request): if not has_valid_api_key(request): return HttpResponseJSON( { "status": "error", "desc": "requires a valid API-key", "code": errors.BASKET_AUTH_ERROR, }, 401, ) fields = ("email", "is_firefox", "campaign_id") data = request.POST.dict() if not all(f in data for f in fields): return HttpResponseJSON( { "status": "error", "desc": "missing required field", "code": errors.BASKET_USAGE_ERROR, }, 401, ) is_firefox = "Y" if data["is_firefox"][0].upper() == "Y" else "N" record_fxa_concerts_rsvp.delay( email=data["email"], is_firefox=is_firefox, campaign_id=data["campaign_id"], ) return HttpResponseJSON({"status": "ok"})
def fxa_activity(request): if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-activity requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) data = json.loads(request.body) if 'fxa_id' not in data: return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-activity requires a Firefox Account ID', 'code': errors.BASKET_USAGE_ERROR, }, 401) if 'user_agent' not in data: return HttpResponseJSON( { 'status': 'error', 'desc': 'fxa-activity requires a device user-agent', 'code': errors.BASKET_USAGE_ERROR, }, 401) add_fxa_activity.delay(data) return HttpResponseJSON({'status': 'ok'})
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_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 common_voice_goals(request): if not has_valid_api_key(request): return HttpResponseJSON( { "status": "error", "desc": "requires a valid API-key", "code": errors.BASKET_AUTH_ERROR, }, 401, ) form = CommonVoiceForm(request.POST) if form.is_valid(): # don't send empty values and use ISO formatted date strings data = { k: v for k, v in form.cleaned_data.items() if not (v == "" or v is None) } if settings.COMMON_VOICE_BATCH_UPDATES: if settings.READ_ONLY_MODE: api_key = request.META["HTTP_X_API_KEY"] # forward to basket with r/w DB requests.post( f"{settings.BASKET_RW_URL}/news/common-voice-goals/", data=request.POST, headers={"x-api-key": api_key}, ) else: CommonVoiceUpdate.objects.create(data=data) else: record_common_voice_update.delay(data) return HttpResponseJSON({"status": "ok"}) else: # form is invalid return HttpResponseJSON( { "status": "error", "errors": format_form_errors(form.errors), "errors_by_field": form.errors, }, 400, )
def common_voice_goals(request): if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) form = CommonVoiceForm(request.POST) if form.is_valid(): # don't send empty values and use ISO formatted date strings data = {k: v for k, v in form.cleaned_data.items() if v} record_common_voice_goals.delay(data) return HttpResponseJSON({'status': 'ok'}) else: # form is invalid return HttpResponseJSON({ 'status': 'error', 'errors': format_form_errors(form.errors), 'errors_by_field': form.errors, }, 400)
def amo_sync(request, post_type): if post_type not in AMO_SYNC_TYPES: return HttpResponseJSON( { "status": "error", "desc": "API URL not found", "code": errors.BASKET_USAGE_ERROR, }, 404, ) if not has_valid_api_key(request): return HttpResponseJSON( { "status": "error", "desc": "requires a valid API-key", "code": errors.BASKET_AUTH_ERROR, }, 401, ) try: data = json.loads(request.body) except ValueError: statsd.incr(f"amo_sync.{post_type}.message.json_error") with sentry_sdk.configure_scope() as scope: scope.set_extra("request.body", request.body) sentry_sdk.capture_exception() return HttpResponseJSON( { "status": "error", "desc": "JSON error", "code": errors.BASKET_USAGE_ERROR, }, 400, ) AMO_SYNC_TYPES[post_type].delay(data) return HttpResponseJSON({"status": "ok"})
def common_voice_goals(request): if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) form = CommonVoiceForm(request.POST) if form.is_valid(): # don't send empty values and use ISO formatted date strings data = { k: v for k, v in form.cleaned_data.items() if not (v == '' or v is None) } if settings.COMMON_VOICE_BATCH_UPDATES: if settings.READ_ONLY_MODE: api_key = request.META['HTTP_X_API_KEY'] # forward to basket with r/w DB requests.post( f'{settings.BASKET_RW_URL}/news/common-voice-goals/', data=request.POST, headers={'x-api-key': api_key}) else: CommonVoiceUpdate.objects.create(data=data) else: record_common_voice_update.delay(data) return HttpResponseJSON({'status': 'ok'}) else: # form is invalid return HttpResponseJSON( { 'status': 'error', 'errors': format_form_errors(form.errors), 'errors_by_field': form.errors, }, 400)
def subhub_post(request): if not has_valid_api_key(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) try: data = json.loads(request.body) except ValueError: statsd.incr('subhub_post.message.json_error') sentry_client.captureException( data={'extra': { 'request.body': request.body }}) return HttpResponseJSON( { 'status': 'error', 'desc': 'JSON error', 'code': errors.BASKET_USAGE_ERROR, }, 400) else: etype = data['event_type'] processor = SUBHUB_EVENT_TYPES.get(etype) if processor: processor.delay(data) return HttpResponseJSON({'status': 'ok'}) else: return HttpResponseJSON( { 'desc': 'unknown event type', 'status': 'error', 'code': errors.BASKET_USAGE_ERROR }, 400)
def fxa_concerts_rsvp(request): if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) fields = ('email', 'is_firefox', 'campaign_id') data = request.POST.dict() if not all(f in data for f in fields): return HttpResponseJSON({ 'status': 'error', 'desc': 'missing required field', 'code': errors.BASKET_USAGE_ERROR, }, 401) is_firefox = 'Y' if data['is_firefox'][0].upper() == 'Y' else 'N' record_fxa_concerts_rsvp.delay( email=data['email'], is_firefox=is_firefox, campaign_id=data['campaign_id'], ) return HttpResponseJSON({'status': 'ok'})
def fxa_activity(request): if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-activity requires a valid API-key', 'code': errors.BASKET_AUTH_ERROR, }, 401) data = json.loads(request.body) if 'fxa_id' not in data: return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-activity requires a Firefox Account ID', 'code': errors.BASKET_USAGE_ERROR, }, 401) if 'user_agent' not in data: return HttpResponseJSON({ 'status': 'error', 'desc': 'fxa-activity requires a device user-agent', 'code': errors.BASKET_USAGE_ERROR, }, 401) add_fxa_activity.delay(data) 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 lookup_user(request): """Lookup a user in Exact Target given email or token (not both). To look up by email, a valid API key are required. If email and token are both provided, an error is returned rather than trying to define all the possible behaviors. SSL is always required when using this call. If no SSL, it'll fail with 401 and an appropriate message in the response body. Response content is always JSON. If user is not found, returns a 404 status and json is:: { 'status': 'error', 'desc': 'No such user' } (If you need to distinguish user not found from an error calling the API, check the response content.) If a required, valid API key is not provided, status is 401 Unauthorized. The API key can be provided either as a GET query parameter ``api-key`` or a request header ``X-api-key``. If it's provided as a query parameter, any request header is ignored. For other errors, similarly response status is 4xx and the json 'desc' says what's wrong. Otherwise, status is 200 and json is the return value from `get_user_data`. See that method for details. Note that because this method always calls Exact Target one or more times, it can be slower than some other Basket APIs, and will fail if ET is down. """ if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # can't return user data during maintenance return HttpResponseJSON({ 'status': 'error', 'desc': 'user data is not available in maintenance mode', 'code': errors.BASKET_NETWORK_FAILURE, }, 400) token = request.GET.get('token', None) email = request.GET.get('email', None) if (not email and not token) or (email and token): return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) if email and not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'Using lookup_user with `email`, you need to pass a ' 'valid `api-key` GET parameter or X-api-key header', 'code': errors.BASKET_AUTH_ERROR, }, 401) if email: email = process_email(email) if not email: return invalid_email_response() try: user_data = get_user_data(token=token, email=email) except NewsletterException as e: return newsletter_exception_response(e) status_code = 200 if not user_data: code = errors.BASKET_UNKNOWN_TOKEN if token else errors.BASKET_UNKNOWN_EMAIL user_data = { 'status': 'error', 'desc': MSG_USER_NOT_FOUND, 'code': code, } status_code = 404 return HttpResponseJSON(user_data, status_code)
def subscribe(request): data = request.POST.dict() newsletters = data.get('newsletters', None) if not newsletters: # request.body causes tests to raise exceptions # while request.read() works. raw_request = request.read() if 'newsletters=' in raw_request: # malformed request from FxOS # Can't use QueryDict since the string is not url-encoded. # It will convert '+' to ' ' for example. data = dict(pair.split('=') for pair in raw_request.split('&') if '=' in pair) statsd.incr('news.views.subscribe.fxos-workaround') else: return HttpResponseJSON({ 'status': 'error', 'desc': 'newsletters is missing', 'code': errors.BASKET_USAGE_ERROR, }, 400) if 'email' not in data: return HttpResponseJSON({ 'status': 'error', 'desc': 'email is required', 'code': errors.BASKET_USAGE_ERROR, }, 401) email = process_email(data['email']) if not email: return invalid_email_response() data['email'] = email if email_is_blocked(data['email']): statsd.incr('news.views.subscribe.email_blocked') # don't let on there's a problem return HttpResponseJSON({'status': 'ok'}) optin = data.pop('optin', 'N').upper() == 'Y' sync = data.pop('sync', 'N').upper() == 'Y' if optin and not has_valid_api_key(request): # for backward compat we just ignore the optin if # no valid API key is sent. optin = False if sync: if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'Using subscribe with sync=Y, you need to pass a ' 'valid `api-key` GET or POST parameter or X-api-key header', 'code': errors.BASKET_AUTH_ERROR, }, 401) # NOTE this is not a typo; Referrer is misspelled in the HTTP spec # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.36 if not data.get('source_url') and request.META.get('HTTP_REFERER'): # try to get it from referrer statsd.incr('news.views.subscribe.use_referrer') data['source_url'] = request.META['HTTP_REFERER'] return update_user_task(request, SUBSCRIBE, data=data, optin=optin, sync=sync)