from django.conf import settings from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render_to_response from django.template import RequestContext from django.utils.timezone import utc, now from django.core.exceptions import ValidationError from django.contrib.auth.views import login as django_login_page from zephyr.models import Message, UserProfile, Stream, Subscription, \ Recipient, get_display_recipient, get_huddle, Realm, UserMessage, \ do_add_subscription, do_remove_subscription, do_change_password, \ do_change_full_name, do_activate_user, \ create_user, do_send_message, create_mit_user_if_needed, \ create_stream_if_needed, PreregistrationUser, get_client, MitUser, \ User, UserActivity from zephyr.forms import RegistrationForm, HomepageForm, is_unique, \ is_active from django.views.decorators.csrf import csrf_exempt from zephyr.decorator import asynchronous, require_post, \ authenticated_api_view, authenticated_json_view, \ has_request_variables, POST from zephyr.lib.query import last_n from zephyr.lib.avatar import gravatar_hash from zephyr.lib.response import json_success, json_error from confirmation.models import Confirmation import datetime import simplejson import socket import re import urllib import time import requests import os import base64 SERVER_GENERATION = int(time.time()) def to_non_negative_int(x): x = int(x) assert x >= 0 return x def get_stream(stream_name, realm): try: return Stream.objects.get(name__iexact=stream_name, realm=realm) except Stream.DoesNotExist: return None @require_post def accounts_register(request): key = request.POST['key'] confirmation = Confirmation.objects.get(confirmation_key=key) email = confirmation.content_object.email mit_beta_user = isinstance(confirmation.content_object, MitUser) company_name = email.split('@')[-1] try: if mit_beta_user: # MIT users already exist, but are supposed to be inactive. is_active(email) else: # Other users should not already exist at all. is_unique(email) except ValidationError: return HttpResponseRedirect(reverse('django.contrib.auth.views.login') + '?email=' + urllib.quote_plus(email)) if request.POST.get('from_confirmation'): form = RegistrationForm() else: form = RegistrationForm(request.POST) if form.is_valid(): password = form.cleaned_data['password'] full_name = form.cleaned_data['full_name'] short_name = email.split('@')[0] domain = email.split('@')[-1] (realm, _) = Realm.objects.get_or_create(domain=domain) if mit_beta_user: user = User.objects.get(email=email) do_activate_user(user) do_change_password(user, password) do_change_full_name(user.userprofile, full_name) else: # FIXME: sanitize email addresses create_user(email, password, realm, full_name, short_name) login(request, authenticate(username=email, password=password)) return HttpResponseRedirect(reverse('zephyr.views.home')) return render_to_response('zephyr/register.html', { 'form': form, 'company_name': company_name, 'email': email, 'key': key }, context_instance=RequestContext(request)) def login_page(request, **kwargs): template_response = django_login_page(request, **kwargs) try: template_response.context_data['email'] = request.GET['email'] except KeyError: pass return template_response def accounts_home(request): if request.method == 'POST': form = HomepageForm(request.POST) if form.is_valid(): try: email = form.cleaned_data['email'] user = PreregistrationUser.objects.get(email=email) except PreregistrationUser.DoesNotExist: user = PreregistrationUser() user.email = email user.save() Confirmation.objects.send_confirmation(user, user.email) return HttpResponseRedirect(reverse('send_confirm', kwargs={'email':user.email})) try: email = request.POST['email'] is_unique(email) except ValidationError: return HttpResponseRedirect(reverse('django.contrib.auth.views.login') + '?email=' + urllib.quote_plus(email)) return render_to_response('zephyr/accounts_home.html', context_instance=RequestContext(request)) @login_required(login_url = settings.HOME_NOT_LOGGED_IN) def home(request): user_profile = UserProfile.objects.get(user=request.user) num_messages = UserMessage.objects.filter(user_profile=user_profile).count() if user_profile.pointer == -1 and num_messages > 0: # Put the new user's pointer at the bottom # # This improves performance, because we limit backfilling of messages # before the pointer. It's also likely that someone joining an # organization is interested in recent messages more than the very # first messages on the system. max_id = (UserMessage.objects.filter(user_profile=user_profile) .order_by('message') .reverse()[0]).message_id user_profile.pointer = max_id user_profile.last_pointer_updater = request.session.session_key # Populate personals autocomplete list based on everyone in your # realm. Later we might want a 2-layer autocomplete, where we # consider specially some sort of "buddy list" who e.g. you've # talked to before, but for small organizations, the right list is # everyone in your realm. people = [{'email' : profile.user.email, 'full_name' : profile.full_name} for profile in UserProfile.objects.select_related().filter(realm=user_profile.realm) if profile != user_profile] subscriptions = Subscription.objects.select_related().filter(user_profile_id=user_profile, active=True) streams = [get_display_recipient(sub.recipient) for sub in subscriptions if sub.recipient.type == Recipient.STREAM] return render_to_response('zephyr/index.html', {'user_profile': user_profile, 'email_hash' : gravatar_hash(user_profile.user.email), 'people' : people, 'streams' : streams, 'have_initial_messages': 'true' if num_messages > 0 else 'false', 'show_debug': settings.DEBUG and ('show_debug' in request.GET) }, context_instance=RequestContext(request)) @authenticated_api_view @has_request_variables def api_update_pointer(request, user_profile, updater=POST('client_id')): return update_pointer_backend(request, user_profile, updater) @authenticated_json_view def json_update_pointer(request, user_profile): return update_pointer_backend(request, user_profile, request.session.session_key) @has_request_variables def update_pointer_backend(request, user_profile, updater, pointer=POST(converter=int)): if pointer < 0: return json_error("Invalid pointer value") user_profile.pointer = pointer user_profile.last_pointer_updater = updater user_profile.save() if settings.TORNADO_SERVER: requests.post(settings.TORNADO_SERVER + '/notify_pointer_update', data=dict( secret = settings.SHARED_SECRET, user = user_profile.user.id, new_pointer = pointer, pointer_updater = updater)) return json_success() @authenticated_json_view def json_get_old_messages(request, user_profile): return get_old_messages_backend(request, user_profile=user_profile, apply_markdown=True) @authenticated_api_view def api_get_old_messages(request, user_profile): return get_old_messages_backend(request, user_profile=user_profile, apply_markdown=(request.POST.get("apply_markdown") is not None)) @has_request_variables def get_old_messages_backend(request, anchor = POST(converter=to_non_negative_int), num_before = POST(converter=to_non_negative_int), num_after = POST(converter=to_non_negative_int), user_profile=None, apply_markdown=True): query = Message.objects.select_related().filter(usermessage__user_profile = user_profile).order_by('id') # We add 1 to the number of messages requested to ensure that the # resulting list always contains the anchor message if num_before != 0 and num_after == 0: num_before += 1 messages = last_n(num_before, query.filter(id__lte=anchor)) elif num_before == 0 and num_after != 0: num_after += 1 messages = query.filter(id__gte=anchor)[:num_after] else: num_after += 1 messages = (last_n(num_before, query.filter(id__lt=anchor)) + list(query.filter(id__gte=anchor)[:num_after])) ret = {'messages': [message.to_dict(apply_markdown) for message in messages], "result": "success", "msg": ""} return json_success(ret) @asynchronous @authenticated_json_view def json_get_updates(request, user_profile, handler): client_id = request.session.session_key return get_updates_backend(request, user_profile, handler, client_id, apply_markdown=True) @asynchronous @authenticated_api_view @has_request_variables def api_get_messages(request, user_profile, handler, client_id=POST(default=None)): return get_updates_backend(request, user_profile, handler, client_id, apply_markdown=(request.POST.get("apply_markdown") is not None), mirror=request.POST.get("mirror")) def format_updates_response(messages=[], apply_markdown=True, user_profile=None, new_pointer=None, mirror=None, update_types=[]): if mirror is not None: messages = [m for m in messages if m.sending_client.name != mirror] ret = {'messages': [message.to_dict(apply_markdown) for message in messages], "result": "success", "msg": "", 'server_generation': SERVER_GENERATION, 'update_types': update_types} if new_pointer is not None: ret['new_pointer'] = new_pointer return ret def format_delayed_updates_response(request=None, user_profile=None, new_pointer=None, pointer_updater=None, client_id=None, update_types=[], **kwargs): client_pointer = request.POST.get("pointer") client_wants_ptr_updates = False if client_pointer is not None: client_pointer = int(client_pointer) client_wants_ptr_updates = True pointer = None if (client_wants_ptr_updates and str(pointer_updater) != str(client_id) and client_pointer != new_pointer): pointer = new_pointer update_types.append("pointer_update") return format_updates_response(new_pointer=pointer, update_types=update_types, **kwargs) def return_messages_immediately(user_profile, client_id, last, failures, client_server_generation, client_reload_pending, **kwargs): if last is None: # When an API user is first querying the server to subscribe, # there's no reason to reply immediately. # TODO: Make this work with server_generation/failures return None if UserMessage.objects.filter(user_profile=user_profile).count() == 0: # The client has no messages, so we should immediately start long-polling return None if last < 0: return {"msg": "Invalid 'last' argument", "result": "error"} # Pointer sync is disabled for now # client_pointer = request.POST.get("pointer") # Pointer sync is disabled for now # client_wants_ptr_updates = False # if client_pointer is not None: # client_pointer = int(client_pointer) # client_wants_ptr_updates = True new_pointer = None query = Message.objects.select_related().filter(usermessage__user_profile = user_profile).order_by('id') # Pointer sync is disabled for now # ptr = user_profile.pointer messages = query.filter(id__gt=last)[:400] # Filter for mirroring before checking whether there are any # messages to pass on. If we don't do this, when the only message # to forward is one that was sent via the mirroring, the API # client will end up in an endless loop requesting more data from # us. if "mirror" in kwargs: messages = [m for m in messages if m.sending_client.name != kwargs["mirror"]] update_types = [] if messages: update_types.append("new_messages") if (client_server_generation is not None and int(client_server_generation) != SERVER_GENERATION and not client_reload_pending): update_types.append("client_reload") # Pointer sync is disabled for now # if (client_wants_ptr_updates # and str(user_profile.last_pointer_updater) != str(client_id) # and ptr != client_pointer): # new_pointer = ptr # update_types.append("pointer_update") if failures >= 1: update_types.append("reset_failure_counter") if update_types: return format_updates_response(messages=messages, user_profile=user_profile, new_pointer=new_pointer, update_types=update_types, **kwargs) return None def send_with_safety_check(response, handler, apply_markdown=True, **kwargs): # Make sure that Markdown rendering really happened, if requested. # This is a security issue because it's where we escape HTML. # c.f. ticket #64 # # apply_markdown=True is the fail-safe default. if response['result'] == 'success' and apply_markdown: for msg in response['messages']: if msg['content_type'] != 'text/html': handler.set_status(500) handler.finish('Internal error: bad message format') return if response['result'] == 'error': handler.set_status(400) handler.finish(response) @has_request_variables def get_updates_backend(request, user_profile, handler, client_id, last = POST(converter=int, default=None), failures = POST(converter=int, default=None), client_server_generation = POST(whence='server_generation', default=None), client_reload_pending = POST(whence='server_generation', default=None), **kwargs): resp = return_messages_immediately(user_profile, client_id, last, failures, client_server_generation, client_reload_pending, **kwargs) if resp is not None: send_with_safety_check(resp, handler, **kwargs) return # Now we're in long-polling mode def cb(**cb_kwargs): if handler.request.connection.stream.closed(): return try: kwargs.update(cb_kwargs) res = format_delayed_updates_response(request=request, user_profile=user_profile, client_id=client_id, **kwargs) send_with_safety_check(res, handler, **kwargs) except socket.error: pass user_profile.add_receive_callback(handler.async_callback(cb)) user_profile.add_pointer_update_callback(handler.async_callback(cb)) def generate_client_id(): return base64.b16encode(os.urandom(16)).lower() @authenticated_api_view def api_get_profile(request, user_profile): result = dict(pointer = user_profile.pointer, client_id = generate_client_id(), max_message_id = -1) messages = Message.objects.filter(usermessage__user_profile=user_profile).order_by('-id')[:1] if messages: result['max_message_id'] = messages[0].id return json_success(result) @authenticated_api_view @has_request_variables def api_send_message(request, user_profile, client_name=POST("client", default="API")): return send_message_backend(request, user_profile, user_profile, client_name) @authenticated_json_view @has_request_variables def json_send_message(request, user_profile, client_name=POST("client", default="website")): return send_message_backend(request, user_profile, user_profile, client_name) # Currently tabbott/extra@mit.edu is our only superuser. TODO: Make # this a real superuser security check. def is_super_user_api(request): return request.POST.get("api-key") in ["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] def already_sent_mirrored_message(message): if message.recipient.type == Recipient.HUDDLE: # For huddle messages, we use a 10-second window because the # timestamps aren't guaranteed to actually match between two # copies of the same message. time_window = datetime.timedelta(seconds=10) else: time_window = datetime.timedelta(seconds=0) # Since our database doesn't store timestamps with # better-than-second resolution, we should do our comparisons # using objects at second resolution pub_date_lowres = message.pub_date.replace(microsecond=0) return Message.objects.filter( sender=message.sender, recipient=message.recipient, content=message.content, subject=message.subject, sending_client=message.sending_client, pub_date__gte=pub_date_lowres - time_window, pub_date__lte=pub_date_lowres + time_window).exists() # Validte that the passed in object is an email address from the user's realm # TODO: Check that it's a real email address here. def same_realm_email(user_profile, email): try: domain = email.split("@", 1)[1] return user_profile.realm.domain == domain except: return False def extract_recipients(request): raw_recipient = request.POST.get("recipient") try: recipients = simplejson.loads(raw_recipient) except simplejson.decoder.JSONDecodeError: recipients = [raw_recipient] return [recipient.strip().lower() for recipient in set(recipients)] def create_mirrored_message_users(request, user_profile): if "sender" not in request.POST: return (False, None) sender_email = request.POST["sender"].strip().lower() # First, check that the sender is in our realm: if not same_realm_email(user_profile, sender_email): return (False, None) pm_recipients = [] if request.POST['type'] == 'private': if "recipient" not in request.POST: return (False, None) pm_recipients = extract_recipients(request) # Then, check that all private message recipients are in our realm: for recipient in pm_recipients: if not same_realm_email(user_profile, recipient): return (False, None) # Create a user for the sender, if needed sender = create_mit_user_if_needed(user_profile.realm, sender_email) # Create users for private message recipients, if needed. for email in pm_recipients: create_mit_user_if_needed(user_profile.realm, email) return (True, sender) # We do not @require_login for send_message_backend, since it is used # both from the API and the web service. Code calling # send_message_backend should either check the API key or check that # the user is logged in. @has_request_variables def send_message_backend(request, user_profile, sender, client_name, message_type_name = POST('type'), message_content = POST('content')): forged = "forged" in request.POST is_super_user = is_super_user_api(request) if forged and not is_super_user: return json_error("User not authorized for this query") if client_name == "zephyr_mirror": # Here's how security works for non-superuser mirroring: # # The message must be (1) a private message (2) that # is both sent and received exclusively by other users in your # realm which (3) must be the MIT realm and (4) you must have # received the message. # # If that's the case, we let it through, but we still have the # security flaw that we're trusting your Hesiod data for users # you report having sent you a message. if "sender" not in request.POST: return json_error("Missing sender") if message_type_name != "private" and not is_super_user: return json_error("User not authorized for this query") (valid_input, mirror_sender) = create_mirrored_message_users(request, user_profile) if not valid_input: return json_error("Invalid mirrored message") if user_profile.realm.domain != "mit.edu": return json_error("Invalid mirrored realm") sender = mirror_sender if message_type_name == 'stream': if "stream" not in request.POST: return json_error("Missing stream") if "subject" not in request.POST: return json_error("Missing subject") stream_name = request.POST['stream'].strip() subject_name = request.POST['subject'].strip() if stream_name == "": return json_error("Stream can't be empty") if subject_name == "": return json_error("Subject can't be empty") if len(stream_name) > 30: return json_error("Stream name too long") if len(subject_name) > 60: return json_error("Subject too long") if not valid_stream_name(stream_name): return json_error("Invalid stream name") ## FIXME: Commented out temporarily while we figure out what we want # if not valid_stream_name(subject_name): # return json_error("Invalid subject name") try: stream = Stream.objects.get(realm=user_profile.realm, name__iexact=stream_name) except Stream.DoesNotExist: return json_error("Stream does not exist") recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM) elif message_type_name == 'private': if "recipient" not in request.POST: return json_error("Missing recipients") pm_recipients = extract_recipients(request) if client_name == "zephyr_mirror": if user_profile.user.email not in pm_recipients and not forged: return json_error("User not authorized for this query") recipient_profile_ids = set() for recipient in pm_recipients: if recipient == "": continue try: recipient_profile_ids.add(UserProfile.objects.get(user__email=recipient).id) except UserProfile.DoesNotExist: return json_error("Invalid email '%s'" % (recipient,)) # If the private message is just between the sender and # another person, force it to be a personal internally if (len(recipient_profile_ids) == 2 and user_profile.id in recipient_profile_ids): recipient_profile_ids.remove(user_profile.id) if len(recipient_profile_ids) > 1: # Make sure the sender is included in huddle messages recipient_profile_ids.add(sender.id) huddle = get_huddle(list(recipient_profile_ids)) recipient = Recipient.objects.get(type_id=huddle.id, type=Recipient.HUDDLE) else: recipient = Recipient.objects.get(type_id=list(recipient_profile_ids)[0], type=Recipient.PERSONAL) else: return json_error("Invalid message type") message = Message() message.sender = sender message.content = message_content message.recipient = recipient if message_type_name == 'stream': message.subject = subject_name if forged: # Forged messages come with a timestamp message.pub_date = datetime.datetime.utcfromtimestamp(float(request.POST['time'])).replace(tzinfo=utc) else: message.pub_date = now() message.sending_client = get_client(client_name) if client_name == "zephyr_mirror" and already_sent_mirrored_message(message): return json_success() do_send_message(message) return json_success() def validate_notify(request): # Check the shared secret. # Also check the originating IP, at least for now. return (request.META['REMOTE_ADDR'] in ('127.0.0.1', '::1') and request.POST.get('secret') == settings.SHARED_SECRET) @csrf_exempt @require_post def notify_new_message(request): if not validate_notify(request): return json_error("Access denied", status=403) # If a message for some reason has no recipients (e.g. it is sent # by a bot to a stream that nobody is subscribed to), just skip # the message gracefully if request.POST["users"] == "": return json_success() # FIXME: better query users = [UserProfile.objects.get(id=user) for user in request.POST['users'].split(',')] message = Message.objects.get(id=request.POST['message']) # Cause message.to_dict() to return the dicts already rendered in the other process. # # We decode this JSON only to eventually re-encode it as JSON. # This isn't trivial to fix, because we do access some fields in the meantime # (see send_with_safety_check). It's probably not a big deal. message.precomputed_dicts = simplejson.loads(request.POST['rendered']) for user in users: user.receive(message) return json_success() @csrf_exempt @require_post def notify_pointer_update(request): if not validate_notify(request): return json_error("Access denied", status=403) # FIXME: better query user_profile = UserProfile.objects.get(id=request.POST['user']) new_pointer = int(request.POST['new_pointer']) pointer_updater = request.POST['pointer_updater'] user_profile.update_pointer(new_pointer, pointer_updater) return json_success() @authenticated_api_view def api_get_public_streams(request, user_profile): streams = sorted(stream.name for stream in Stream.objects.filter(realm=user_profile.realm)) return json_success({"streams": streams}) def gather_subscriptions(user_profile): subscriptions = Subscription.objects.filter(user_profile=user_profile, active=True) # For now, don't display the subscription for your ability to receive personals. return sorted(get_display_recipient(sub.recipient) for sub in subscriptions if sub.recipient.type == Recipient.STREAM) @authenticated_api_view def api_get_subscriptions(request, user_profile): return json_success({"streams": gather_subscriptions(user_profile)}) @authenticated_json_view def json_list_subscriptions(request, user_profile): return json_success({"subscriptions": gather_subscriptions(user_profile)}) @authenticated_json_view def json_remove_subscription(request, user_profile): if 'subscription' not in request.POST: return json_error("Missing subscriptions") sub_name = request.POST.get('subscription') stream = get_stream(sub_name, user_profile.realm) if not stream: return json_error("Stream does not exist") did_remove = do_remove_subscription(user_profile, stream) if not did_remove: return json_error("Not subscribed, so you can't unsubscribe") return json_success({"data": sub_name}) def valid_stream_name(name): return name != "" @authenticated_api_view def api_subscribe(request, user_profile): return add_subscriptions_backend(request, user_profile) @authenticated_json_view def json_add_subscriptions(request, user_profile): return add_subscriptions_backend(request, user_profile) @has_request_variables def add_subscriptions_backend(request, user_profile, streams_raw = POST('streams', simplejson.loads)): streams = [] for stream_name in streams_raw: stream_name = stream_name.strip() if len(stream_name) > 30: return json_error("Stream name (%s) too long." % (stream_name,)) if not valid_stream_name(stream_name): return json_error("Invalid stream name (%s)." % (stream_name,)) streams.append(stream_name) subscribed = [] already_subscribed = [] for stream_name in set(streams): stream = create_stream_if_needed(user_profile.realm, stream_name) did_subscribe = do_add_subscription(user_profile, stream) if did_subscribe: subscribed.append(stream_name) else: already_subscribed.append(stream_name) return json_success({"subscribed": subscribed, "already_subscribed": already_subscribed}) @authenticated_json_view @has_request_variables def json_change_settings(request, user_profile, full_name=POST, old_password=POST, new_password=POST, confirm_password=POST): if new_password != "": if new_password != confirm_password: return json_error("New password must match confirmation password!") if not authenticate(username=user_profile.user.email, password=old_password): return json_error("Wrong password!") do_change_password(user_profile.user, new_password) result = {} if user_profile.full_name != full_name: do_change_full_name(user_profile, full_name) result['full_name'] = full_name return json_success(result) @authenticated_json_view @has_request_variables def json_stream_exists(request, user_profile, stream=POST): if not valid_stream_name(stream): return json_error("Invalid characters in stream name") exists = bool(get_stream(stream, user_profile.realm)) return json_success({"exists": exists}) @csrf_exempt @require_post @has_request_variables def api_fetch_api_key(request, username=POST, password=POST): user = authenticate(username=username, password=password) if user is None: return json_error("Your username or password is incorrect.", status=403) if not user.is_active: return json_error("Your account has been disabled.", status=403) return json_success({"api_key": user.userprofile.api_key}) @authenticated_json_view @has_request_variables def json_fetch_api_key(request, user_profile, password=POST): if not request.user.check_password(password): return json_error("Your username or password is incorrect.") return json_success({"api_key": user_profile.api_key}) @login_required(login_url = settings.HOME_NOT_LOGGED_IN) def get_activity(request): user_profile = request.user.userprofile if user_profile.realm.domain != "humbughq.com": return HttpResponseRedirect(reverse('zephyr.views.login_page')) def add_activity(activity, url, query_name, client_name): for row in UserActivity.objects.filter(query=url, client__name=client_name): email = row.user_profile.user.email activity.setdefault(email, {}) activity[email]['email'] = email activity[email][query_name + '_count'] = row.count activity[email][query_name + '_last'] = row.last_visit website_activity = {} add_activity(website_activity, "/json/get_updates", "get_updates", "website") add_activity(website_activity, "/json/send_message", "send_message", "website") add_activity(website_activity, "/json/update_pointer", "update_pointer", "website") mirror_activity = {} add_activity(mirror_activity, "/api/v1/get_messages", "get_updates", "zephyr_mirror") add_activity(mirror_activity, "/api/v1/send_message", "send_message", "zephyr_mirror") api_activity = {} add_activity(api_activity, "/api/v1/get_messages", "get_updates", "API") add_activity(api_activity, "/api/v1/send_message", "send_message", "API") return render_to_response('zephyr/activity.html', { 'data': [ ('Website', True, website_activity.values()), ('Mirror', False, mirror_activity.values()), ('API', False, api_activity.values()) ]}, context_instance=RequestContext(request))