#
# Copyright (C) 2009 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.


# This module is used for version 2 of the Google Data APIs.


"""Provides auth related token classes and functions for Google Data APIs.

Token classes represent a user's authorization of this app to access their
data. Usually these are not created directly but by a GDClient object.

ClientLoginToken
AuthSubToken
SecureAuthSubToken
OAuthHmacToken
OAuthRsaToken
TwoLeggedOAuthHmacToken
TwoLeggedOAuthRsaToken

Functions which are often used in application code (as opposed to just within
the gdata-python-client library) are the following:

generate_auth_sub_url
authorize_request_token

"""


__author__ = 'j.s@google.com (Jeff Scudder)'

import urllib


PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth='
AUTHSUB_AUTH_LABEL = 'AuthSub token='


# This dict provides the AuthSub and OAuth scopes for all services by service
# name. The service name (key) is used in ClientLogin requests.
AUTH_SCOPES = {
    'cp': ( # Contacts API
        'https://www.google.com/m8/feeds/',
        'http://www.google.com/m8/feeds/')}



class Error(Exception):
  pass


class UnsupportedTokenType(Error):
  """Raised when token to or from blob is unable to convert the token."""
  pass


# ClientLogin functions and classes.
def generate_client_login_request_body(email, password, service, source,
    account_type='HOSTED_OR_GOOGLE', captcha_token=None,
    captcha_response=None):
  """Creates the body of the autentication request

  See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request
  for more details.

  Args:
    email: str
    password: str
    service: str
    source: str
    account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid
        values are 'GOOGLE' and 'HOSTED'
    captcha_token: str (optional)
    captcha_response: str (optional)

  Returns:
    The HTTP body to send in a request for a client login token.
  """
  # Create a POST body containing the user's credentials.
  request_fields = {'Email': email,
                    'Passwd': password,
                    'accountType': account_type,
                    'service': service,
                    'source': source}
  if captcha_token and captcha_response:
    # Send the captcha token and response as part of the POST body if the
    # user is responding to a captch challenge.
    request_fields['logintoken'] = captcha_token
    request_fields['logincaptcha'] = captcha_response
  return urllib.urlencode(request_fields)


GenerateClientLoginRequestBody = generate_client_login_request_body


def get_client_login_token_string(http_body):
  """Returns the token value for a ClientLoginToken.

  Reads the token from the server's response to a Client Login request and
  creates the token value string to use in requests.

  Args:
    http_body: str The body of the server's HTTP response to a Client Login
        request

  Returns:
    The token value string for a ClientLoginToken.
  """
  for response_line in http_body.splitlines():
    if response_line.startswith('Auth='):
      # Strip off the leading Auth= and return the Authorization value.
      return response_line[5:]
  return None


GetClientLoginTokenString = get_client_login_token_string


def get_captcha_challenge(http_body,
    captcha_base_url='http://www.google.com/accounts/'):
  """Returns the URL and token for a CAPTCHA challenge issued by the server.

  Args:
    http_body: str The body of the HTTP response from the server which
        contains the CAPTCHA challenge.
    captcha_base_url: str This function returns a full URL for viewing the
        challenge image which is built from the server's response. This
        base_url is used as the beginning of the URL because the server
        only provides the end of the URL. For example the server provides
        'Captcha?ctoken=Hi...N' and the URL for the image is
        'http://www.google.com/accounts/Captcha?ctoken=Hi...N'

  Returns:
    A dictionary containing the information needed to repond to the CAPTCHA
    challenge, the image URL and the ID token of the challenge. The
    dictionary is in the form:
    {'token': string identifying the CAPTCHA image,
     'url': string containing the URL of the image}
    Returns None if there was no CAPTCHA challenge in the response.
  """
  contains_captcha_challenge = False
  captcha_parameters = {}
  for response_line in http_body.splitlines():
    if response_line.startswith('Error=CaptchaRequired'):
      contains_captcha_challenge = True
    elif response_line.startswith('CaptchaToken='):
      # Strip off the leading CaptchaToken=
      captcha_parameters['token'] = response_line[13:]
    elif response_line.startswith('CaptchaUrl='):
      captcha_parameters['url'] = '%s%s' % (captcha_base_url,
          response_line[11:])
  if contains_captcha_challenge:
    return captcha_parameters
  else:
    return None


GetCaptchaChallenge = get_captcha_challenge


class ClientLoginToken(object):

  def __init__(self, token_string):
    self.token_string = token_string

  def modify_request(self, http_request):
    http_request.headers['Authorization'] = '%s%s' % (PROGRAMMATIC_AUTH_LABEL,
        self.token_string)

  ModifyRequest = modify_request



def _join_token_parts(*args):
  """"Escapes and combines all strings passed in.

  Used to convert a token object's members into a string instead of
  using pickle.

  Note: A None value will be converted to an empty string.

  Returns:
    A string in the form 1x|member1|member2|member3...
  """
  return '|'.join([urllib.quote_plus(a or '') for a in args])


def _split_token_parts(blob):
  """Extracts and unescapes fields from the provided binary string.

  Reverses the packing performed by _join_token_parts. Used to extract
  the members of a token object.

  Note: An empty string from the blob will be interpreted as None.

  Args:
    blob: str A string of the form 1x|member1|member2|member3 as created
        by _join_token_parts

  Returns:
    A list of unescaped strings.
  """
  return [urllib.unquote_plus(part) or None for part in blob.split('|')]


def token_to_blob(token):
  """Serializes the token data as a string for storage in a datastore.

  Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken,
  OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken,
  TwoLeggedOAuthHmacToken.

  Args:
    token: A token object which must be of one of the supported token classes.

  Raises:
    UnsupportedTokenType if the token is not one of the supported token
    classes listed above.

  Returns:
    A string represenging this token. The string can be converted back into
    an equivalent token object using token_from_blob. Note that any members
    which are set to '' will be set to None when the token is deserialized
    by token_from_blob.
  """
  if isinstance(token, ClientLoginToken):
    return _join_token_parts('1c', token.token_string)
  else:
    raise UnsupportedTokenType(
        'Unable to serialize token of type %s' % type(token))


TokenToBlob = token_to_blob


def token_from_blob(blob):
  """Deserializes a token string from the datastore back into a token object.

  Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken,
  OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken,
  TwoLeggedOAuthHmacToken.

  Args:
    blob: string created by token_to_blob.

  Raises:
    UnsupportedTokenType if the token is not one of the supported token
    classes listed above.

  Returns:
    A new token object with members set to the values serialized in the
    blob string. Note that any members which were set to '' in the original
    token will now be None.
  """
  parts = _split_token_parts(blob)
  if parts[0] == '1c':
    return ClientLoginToken(parts[1])
  else:
    raise UnsupportedTokenType(
        'Unable to deserialize token with type marker of %s' % parts[0])


TokenFromBlob = token_from_blob


def dump_tokens(tokens):
  return ','.join([token_to_blob(t) for t in tokens])


def load_tokens(blob):
  return [token_from_blob(s) for s in blob.split(',')]


def find_scopes_for_services(service_names=None):
  """Creates a combined list of scope URLs for the desired services.

  This method searches the AUTH_SCOPES dictionary.
  
  Args:
    service_names: list of strings (optional) Each name must be a key in the
                   AUTH_SCOPES dictionary. If no list is provided (None) then
                   the resulting list will contain all scope URLs in the
                   AUTH_SCOPES dict.

  Returns:
    A list of URL strings which are the scopes needed to access these services
    when requesting a token using AuthSub or OAuth.
  """
  result_scopes = []
  if service_names is None:
    for service_name, scopes in AUTH_SCOPES.iteritems():
      result_scopes.extend(scopes)
  else:
    for service_name in service_names:
      result_scopes.extend(AUTH_SCOPES[service_name])
  return result_scopes


FindScopesForServices = find_scopes_for_services