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 send_recovery_message(request): """ Send a recovery message to an email address. required form parameter: email If email not provided or not syntactically correct, returns 400. If email not known, returns 404. Otherwise, queues a task to send the message and returns 200. """ email = process_email(request.POST.get("email")) if not email: return invalid_email_response() if email_is_blocked(email): # don't let on there's a problem return HttpResponseJSON({"status": "ok"}) try: user_data = get_user_data(email=email) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: return HttpResponseJSON( { "status": "error", "desc": "Email address not known", "code": errors.BASKET_UNKNOWN_EMAIL, }, 404, ) # Note: Bedrock looks for this 404 send_recovery_message_task.delay(email) 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 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 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 subscribe_sms(request): mobile = request.POST.get("mobile_number") if not mobile: return HttpResponseJSON( { "status": "error", "desc": "mobile_number is missing", "code": errors.BASKET_USAGE_ERROR, }, 400, ) country = request.POST.get("country", "us") language = request.POST.get("lang", "en-US") msg_name = request.POST.get("msg_name", "SMS_Android") vendor_id = get_sms_vendor_id(msg_name, country, language) if not vendor_id: if language != "en-US": # if not available in the requested language, try the default language = "en-US" vendor_id = get_sms_vendor_id(msg_name, country, language) if not vendor_id: return HttpResponseJSON( { "status": "error", "desc": "Invalid msg_name + country + language", "code": errors.BASKET_USAGE_ERROR, }, 400, ) mobile = parse_phone_number(mobile, country) if not mobile: return HttpResponseJSON( { "status": "error", "desc": "mobile_number is invalid", "code": errors.BASKET_USAGE_ERROR, }, 400, ) # only rate limit numbers here so we don't rate limit errors. if is_ratelimited( request, group="basket.news.views.subscribe_sms", key=lambda x, y: "%s-%s" % (msg_name, mobile), rate=PHONE_NUMBER_RATE_LIMIT, increment=True, ): raise Ratelimited() optin = request.POST.get("optin", "N") == "Y" add_sms_user.delay(msg_name, mobile, optin, vendor_id=vendor_id) return HttpResponseJSON({"status": "ok"})
def custom_unsub_reason(request): """Update the reason field for the user, which logs why the user unsubscribed from all newsletters.""" if 'token' not in request.POST or 'reason' not in request.POST: return HttpResponseJSON({ 'status': 'error', 'desc': 'custom_unsub_reason requires the `token` ' 'and `reason` POST parameters', 'code': errors.BASKET_USAGE_ERROR, }, 400) update_custom_unsub.delay(request.POST['token'], request.POST['reason']) return HttpResponseJSON({'status': 'ok'})
def subscribe_sms(request): mobile = request.POST.get('mobile_number') if not mobile: return HttpResponseJSON( { 'status': 'error', 'desc': 'mobile_number is missing', 'code': errors.BASKET_USAGE_ERROR, }, 400) country = request.POST.get('country', 'us') language = request.POST.get('lang', 'en-US') msg_name = request.POST.get('msg_name', 'SMS_Android') vendor_id = get_sms_vendor_id(msg_name, country, language) if not vendor_id: if language != 'en-US': # if not available in the requested language, try the default language = 'en-US' vendor_id = get_sms_vendor_id(msg_name, country, language) if not vendor_id: return HttpResponseJSON( { 'status': 'error', 'desc': 'Invalid msg_name + country + language', 'code': errors.BASKET_USAGE_ERROR, }, 400) mobile = parse_phone_number(mobile, country) if not mobile: return HttpResponseJSON( { 'status': 'error', 'desc': 'mobile_number is invalid', 'code': errors.BASKET_USAGE_ERROR, }, 400) # only rate limit numbers here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.subscribe_sms', key=lambda x, y: '%s-%s' % (msg_name, mobile), rate=PHONE_NUMBER_RATE_LIMIT, increment=True): raise Ratelimited() optin = request.POST.get('optin', 'N') == 'Y' add_sms_user.delay(msg_name, mobile, optin, vendor_id=vendor_id) return HttpResponseJSON({'status': 'ok'})
def respond_error(request, form, message, code, template_name="news/formerror.html"): """ Return either a JSON or HTML error response @param request: the request @param form: the bound form object @param message: the error message @param code: the HTTP status code @param template_name: the template name in case of HTML response @return: HttpResponse object """ if request.is_ajax(): return HttpResponseJSON( { "status": "error", "errors": [message], "errors_by_field": { NON_FIELD_ERRORS: [message] }, }, code, ) else: form.add_error(None, message) return render(request, template_name, {"form": form}, status=code)
def user_meta(request, token): """Only update user metadata, not newsletters""" form = UpdateUserMeta(request.POST) if form.is_valid(): # don't send empty values data = {k: v for k, v in form.cleaned_data.items() if v} # don't change subscriber status data['_set_subscriber'] = False update_user_meta.delay(token, data) return HttpResponseJSON({'status': 'ok'}) return HttpResponseJSON({ 'status': 'error', 'desc': 'data is invalid', 'code': errors.BASKET_USAGE_ERROR, }, 400)
def confirm(request, token): if is_ratelimited(request, group='basket.news.views.confirm', key=lambda x, y: token, rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() confirm_user.delay(token, start_time=time()) return HttpResponseJSON({'status': 'ok'})
def debug_user(request): return HttpResponseJSON( { 'status': 'error', 'desc': 'method removed. use lookup-user and an API key.', 'code': errors.BASKET_USAGE_ERROR, }, 404)
def custom_unsub_reason(request): """Update the reason field for the user, which logs why the user unsubscribed from all newsletters.""" if "token" not in request.POST or "reason" not in request.POST: return HttpResponseJSON( { "status": "error", "desc": "custom_unsub_reason requires the `token` " "and `reason` POST parameters", "code": errors.BASKET_USAGE_ERROR, }, 400, ) update_custom_unsub.delay(request.POST["token"], request.POST["reason"]) return HttpResponseJSON({"status": "ok"})
def invalid_email_response(): resp_data = { 'status': 'error', 'code': errors.BASKET_INVALID_EMAIL, 'desc': 'Invalid email address', } statsd.incr('news.views.invalid_email_response') return HttpResponseJSON(resp_data, 400)
def invalid_email_response(): resp_data = { "status": "error", "code": errors.BASKET_INVALID_EMAIL, "desc": "Invalid email address", } statsd.incr("news.views.invalid_email_response") return HttpResponseJSON(resp_data, 400)
def debug_user(request): return HttpResponseJSON( { "status": "error", "desc": "method removed. use lookup-user and an API key.", "code": errors.BASKET_USAGE_ERROR, }, 404, )
def subscribe_main(request): """Subscription view for use with client side JS""" form = SubscribeForm(request.POST) if form.is_valid(): data = form.cleaned_data if email_is_blocked(data['email']): statsd.incr('news.views.subscribe_main.email_blocked') # don't let on there's a problem return respond_ok(request, data) data['format'] = data.pop('fmt') or 'H' if data['lang']: if not language_code_is_valid(data['lang']): data['lang'] = 'en' # 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 else: del data['lang'] # if source_url not provided we should store the referrer header # 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['source_url'] and request.META.get('HTTP_REFERER'): referrer = request.META['HTTP_REFERER'] if SOURCE_URL_RE.match(referrer): statsd.incr('news.views.subscribe_main.use_referrer') data['source_url'] = referrer if is_ratelimited(request, group='basket.news.views.subscribe_main', key=lambda x, y: '%s-%s' % (':'.join(data['newsletters']), data['email']), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): statsd.incr('subscribe.ratelimited') return respond_error(request, form, 'Rate limit reached', 429) try: upsert_user.delay(SUBSCRIBE, data, start_time=time()) except Exception: return respond_error(request, form, 'Unknown error', 500) return respond_ok(request, data) else: # form is invalid if request.is_ajax(): return HttpResponseJSON({ 'status': 'error', 'errors': format_form_errors(form.errors), 'errors_by_field': form.errors, }, 400) else: return render(request, 'news/formerror.html', {'form': form}, status=400)
def ratelimited(request, e): parts = [x.strip() for x in request.path.split('/') if x.strip()] # strip out tokens in the urls parts = [x for x in parts if not is_token(x)] statsd.incr('.'.join(parts + ['ratelimited'])) return HttpResponseJSON({ 'status': 'error', 'desc': 'rate limit reached', 'code': errors.BASKET_USAGE_ERROR, }, 429)
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 newsletters(request): # Get the newsletters as a dictionary of dictionaries that are # easily jsonified result = {} for newsletter in Newsletter.objects.all().values(): newsletter["languages"] = newsletter["languages"].split(",") result[newsletter["slug"]] = newsletter del newsletter["id"] # caller doesn't need to know our pkey del newsletter["slug"] # or our slug return HttpResponseJSON({"status": "ok", "newsletters": result})
def user_meta(request, token): """Only update user metadata, not newsletters""" token = str(token) form = UpdateUserMeta(request.POST) if form.is_valid(): # don't send empty values data = {k: v for k, v in form.cleaned_data.items() if v} # don't change subscriber status data["_set_subscriber"] = False update_user_meta.delay(token, data) return HttpResponseJSON({"status": "ok"}) return HttpResponseJSON( { "status": "error", "desc": "data is invalid", "code": errors.BASKET_USAGE_ERROR, }, 400, )
def confirm(request, token): token = str(token) if is_ratelimited( request, group="basket.news.views.confirm", key=lambda x, y: token, rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True, ): raise Ratelimited() confirm_user.delay(token, start_time=time()) 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 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 respond_ok(request, data, template_name="news/thankyou.html"): """ Return either a JSON or HTML success response @param request: the request @param data: the incoming request data @param template_name: the template name in case of HTML response @return: HttpResponse object """ if request.is_ajax(): return HttpResponseJSON({"status": "ok"}) else: return render(request, template_name, data)
def ratelimited(request, e): parts = [x.strip() for x in request.path.split("/") if x.strip()] # strip out tokens in the urls parts = [x for x in parts if not is_token(x)] statsd.incr(".".join(parts + ["ratelimited"])) return HttpResponseJSON( { "status": "error", "desc": "rate limit reached", "code": errors.BASKET_USAGE_ERROR, }, 429, )
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 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 newsletters(request): # Get the newsletters as a dictionary of dictionaries that are # easily jsonified result = {} for newsletter in Newsletter.objects.all().values(): newsletter['languages'] = newsletter['languages'].split(",") result[newsletter['slug']] = newsletter del newsletter['id'] # caller doesn't need to know our pkey del newsletter['slug'] # or our slug return HttpResponseJSON({ 'status': 'ok', 'newsletters': result, })