#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Handles login/logout pages and dealing with user cookies.

Includes a WSGI application that serves the login page and handles login and
logout HTTP requests. It accepts these GET query parameters:

  continue: URL to redirect to after a login or logout has completed.
  email: Email address to set for the client.
  admin: If 'True', the client should be logged in as an admin.
  action: What action to take ('Login' or 'Logout').

To view the current user information and a form for logging in and out,
supply no parameters.
"""


import cgi
import Cookie
import hashlib
import logging
import urllib

import google
import webapp2

# URL of the login page within the dev appserver.
LOGIN_URL_RELATIVE = '_ah/login'

# CGI parameter constants.
CONTINUE_PARAM = 'continue'
_EMAIL_PARAM = 'email'
_ADMIN_PARAM = 'admin'
ACTION_PARAM = 'action'

# Values for the action parameter.
LOGOUT_ACTION = 'logout'
LOGIN_ACTION = 'login'

# Name of the cookie that stores the user info.
_COOKIE_NAME = 'dev_appserver_login'


def get_user_info(http_cookie, cookie_name=_COOKIE_NAME):
  """Gets the requestor's user info from an HTTP Cookie header.

  Args:
    http_cookie: The value of the 'Cookie' HTTP request header.
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    A tuple (email, admin, user_id) where:
      email: The user's email address, if any.
      admin: True if the user is an admin; False otherwise.
      user_id: The user ID, if any.
  """
  try:
    cookie = Cookie.SimpleCookie(http_cookie)
  except Cookie.CookieError:
    return '', False, ''

  cookie_dict = dict((k, v.value) for k, v in cookie.iteritems())
  return _get_user_info_from_dict(cookie_dict, cookie_name)


def _get_user_info_from_dict(cookie_dict, cookie_name=_COOKIE_NAME):
  """Gets the requestor's user info from a cookie dictionary.

  Args:
    cookie_dict: A dictionary mapping cookie names onto values.
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    A tuple (email, admin, user_id) where:
      email: The user's email address, if any.
      admin: True if the user is an admin; False otherwise.
      user_id: The user ID, if any.
  """
  cookie_value = cookie_dict.get(cookie_name, '')

  email, admin, user_id = (cookie_value.split(':') + ['', '', ''])[:3]
  if '@' not in email:
    if email:
      logging.warning('Ignoring invalid login cookie: %s', cookie_value)
    return '', False, ''
  return email, (admin == 'True'), user_id


def _create_cookie_data(email, admin):
  """Creates cookie payload data.

  Args:
    email: The user's email address.
    admin: True if the user is an admin; False otherwise.

  Returns:
    A string containing the cookie payload.
  """
  if email:
    user_id_digest = hashlib.md5(email.lower()).digest()
    user_id = '1' + ''.join(['%02d' % ord(x) for x in user_id_digest])[:20]
  else:
    user_id = ''
  return '%s:%s:%s' % (email, admin, user_id)


def _set_user_info_cookie(email, admin, cookie_name=_COOKIE_NAME):
  """Creates a cookie to set the user information for the requestor.

  Args:
    email: The email to set for the user.
    admin: True if the user should be admin; False otherwise.
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    Set-Cookie value for setting the user info of the requestor.
  """
  cookie_value = _create_cookie_data(email, admin)
  cookie = Cookie.SimpleCookie()
  cookie[cookie_name] = cookie_value
  cookie[cookie_name]['path'] = '/'
  return cookie[cookie_name].OutputString()


def _clear_user_info_cookie(cookie_name=_COOKIE_NAME):
  """Clears the user info cookie from the requestor, logging them out.

  Args:
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    A Set-Cookie value for clearing the user info of the requestor.
  """
  cookie = Cookie.SimpleCookie()
  cookie[cookie_name] = ''
  cookie[cookie_name]['path'] = '/'
  cookie[cookie_name]['max-age'] = '0'
  return cookie[cookie_name].OutputString()


_LOGIN_TEMPLATE = """<html>
<head>
  <title>Login</title>
</head>
<body>

<form method="get" action="%(login_url)s"
      style="text-align:center; font: 13px sans-serif">
  <div style="width: 20em; margin: 1em auto;
              text-align:left;
              padding: 0 2em 1.25em 2em;
              background-color: #d6e9f8;
              border: 2px solid #67a7e3">
    <h3>%(login_message)s</h3>
    <p style="padding: 0; margin: 0">
      <label for="email" style="width: 3em">Email:</label>
      <input name="email" type="email" value="%(email)s" id="email"/>
    </p>
    <p style="margin: .5em 0 0 3em; font-size:12px">
      <input name="admin" type="checkbox" value="True"
       %(admin_checked)s id="admin"/>
        <label for="admin">Sign in as Administrator</label>
    </p>
    <p style="margin-left: 3em">
      <input name="action" value="Login" type="submit"
             id="submit-login" />
      <input name="action" value="Logout" type="submit"
             id="submit-logout" />
    </p>
  </div>
  <input name="continue" type="hidden" value="%(continue_url)s"/>
</form>

</body>
</html>
"""


def _render_login_template(login_url, continue_url, email, admin):
  """Renders the login page.

  Args:
    login_url: The parameter to _login_response.
    continue_url: The parameter to _login_response.
    email: The email address of the current user, if any.
    admin: True if the user is currently an admin; False otherwise.

  Returns:
    A string containing the contents of the login page.
  """
  if email:
    login_message = 'Logged in'
  else:
    login_message = 'Not logged in'

    email = 'test\x40example.com'
  admin_checked = 'checked' if admin else ''

  template_dict = {
      'email': cgi.escape(email, quote=True),
      'admin_checked': admin_checked,
      'login_message': login_message,
      'login_url': cgi.escape(login_url, quote=True),
      'continue_url': cgi.escape(continue_url, quote=True),
  }

  return _LOGIN_TEMPLATE % template_dict


def login_redirect(application_url, continue_url, start_response):
  """Writes a login redirection URL to a user.

  This redirects to login_url with a continue parameter to return to
  continue_url. The login_url should be on the canonical front-end server,
  regardless of the host:port the user connected to.

  Args:
    application_url: The URL of the dev appserver domain
      (e.g., 'http://localhost:8080').
    continue_url: The URL to continue to after the user logs in.
    start_response: A WSGI start_response function.

  Returns:
    An (empty) iterable over strings containing the body of the HTTP response.
  """
  if not application_url.endswith('/'):
    application_url += '/'
  redirect_url = '%s%s?%s=%s' % (application_url, LOGIN_URL_RELATIVE,
                                 CONTINUE_PARAM, urllib.quote(continue_url))
  start_response('302 Requires login',
                 [('Location', redirect_url)])
  return []


class Handler(webapp2.RequestHandler):
  """The request handler for the login and logout pages."""

  def get(self):
    action = self.request.get(ACTION_PARAM)
    set_email = self.request.get(_EMAIL_PARAM)
    set_admin = self.request.get(_ADMIN_PARAM).lower() == 'true'
    continue_url = self.request.get(CONTINUE_PARAM)

    login_url = self.request.path_url

    if action:
      # Perform the action and then redirect
      if action.lower() == LOGOUT_ACTION.lower():
        self.response.headers['Set-Cookie'] = _clear_user_info_cookie()
      elif action.lower() == LOGIN_ACTION.lower() and set_email:
        self.response.headers['Set-Cookie'] = _set_user_info_cookie(set_email,
                                                                    set_admin)

      redirect_url = continue_url or login_url
      # URLs should be ASCII-only byte strings.
      if isinstance(redirect_url, unicode):
        redirect_url = redirect_url.encode('ascii')

      self.response.status = 302
      self.response.status_message = 'Redirecting to continue URL'
      self.response.headers['Location'] = redirect_url
    else:
      email, admin, _ = _get_user_info_from_dict(self.request.cookies)
      self.response.status = 200
      self.response.headers['Content-Type'] = 'text/html'
      body = _render_login_template(login_url, continue_url, email, admin)
      self.response.write(body)


application = webapp2.WSGIApplication([('/.*', Handler)], debug=True)
