<filename>kai/controllers/consumer.py
import logging

from openid import sreg
from openid.consumer import consumer
from pylons import app_globals, cache, request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from pylons.decorators import rest, secure, jsonify

from kai.lib.base import BaseController, render
from kai.lib.decorators import validate
from kai.lib.helpers import failure_flash, success_flash
from kai.model import Human, forms

log = logging.getLogger(__name__)

def proper_abort(end_action):
    if end_action == 'login':
        redirect(url('account_login'))
    else:
        redirect(url('account_register'))


class ConsumerController(BaseController):
    def __before__(self):
        c.active_tab = True
        c.active_sub = True
        self.my_cache = cache.get_cache('ConsumerController.openid_session')
        self.openid_session = self.my_cache.get_value(
            key=session.id, createfunc=lambda: {}, expiretime=300)
    
    @rest.dispatch_on(POST='_handle_create')
    def create(self):
        return render('/accounts/register.mako')
    
    @validate(form=forms.openid_login_form, error_handler='create')
    def _handle_create(self):
        return self._openid_login('register')
    
    @rest.dispatch_on(POST='_handle_login')
    def login(self):
        return render('/accounts/login.mako')
    
    @validate(form=forms.openid_login_form, error_handler='login')
    @secure.authenticate_form
    def _handle_login(self):
        return self._openid_login()
    
    def _openid_login(self, end_action='login'):
        openid_url = self.form_result['openid_identifier']
        if not openid_url:
            redirect(url('account_login'))

        oidconsumer = consumer.Consumer(self.openid_session, app_globals.openid_store)
        try:
            authrequest = oidconsumer.begin(openid_url)
        except consumer.DiscoveryFailure, exc:
            err_msg = str(exc[0])
            if 'No usable OpenID' in err_msg:
                failure_flash('Invalid OpenID URL')
            else:
                failure_flash('Timeout for OpenID URL')
            proper_abort(end_action)
        
        if authrequest is None:
            failure_flash('No OpenID services found for <code>%s</code>' % openid_url)
            proper_abort(end_action)
        
        if end_action == 'register':
            sreg_request = sreg.SRegRequest(
                required=['fullname', 'email', 'timezone'],
                optional=['language']
            )
            authrequest.addExtension(sreg_request)
            session['openid_action'] = 'register'
        elif 'openid_action' in session:
            del session['openid_action']
        
        trust_root = url('home', qualified=True)
        return_to = url('openid_process', qualified=True)
        
        # OpenID 2.0 lets Providers request POST instead of redirect, this
        # checks for such a request.
        if authrequest.shouldSendRedirect():
            redirect_url = authrequest.redirectURL(realm=trust_root, 
                                                   return_to=return_to, 
                                                   immediate=False)
            self.my_cache.set_value(key=session.id, value=self.openid_session, expiretime=300)
            redirect(url(redirect_url))
        else:
            form_html = authrequest.formMarkup(
                realm=trust_root, return_to=return_to, immediate=False,
                form_tag_attrs={'id':'openid_message'})
            self.my_cache.set_value(key=session.id, value=self.openid_session, expiretime=300)
            return form_html
    
    def process(self):
        """Handle incoming redirect from OpenID Provider"""
        end_action = session.get('openid_action', 'login')
        
        oidconsumer = consumer.Consumer(self.openid_session, app_globals.openid_store)
        info = oidconsumer.complete(request.params, url('openid_process', qualified=True))
        if info.status == consumer.FAILURE and info.identity_url:
            fmt = "Verification of %s failed: %s"
            failure_flash(fmt % (info.identity_url, info.message))
        elif info.status == consumer.SUCCESS:
            openid_identity = info.identity_url
            if info.endpoint.canonicalID:
                # If it's an i-name, use the canonicalID as its secure even if
                # the old one is compromised
                openid_identity = info.endpoint.canonicalID
            
            # We've now verified a successful OpenID login, time to determine
            # how to dispatch it
            
            # First save their identity in the session, as several pages use 
            # this data
            session['openid_identity'] = openid_identity
            
            # Save off the session
            session.save()
            
            # First, if someone already has this openid, we log them in if
            # they've verified their email, otherwise we inform them to verify
            # their email address first
            users = list(Human.by_openid(self.db)[openid_identity])
            if users:
                user = users[0]
                if user.email_token:
                    failure_flash('You must verify your email before signing in.')
                    redirect(url('account_login'))
                else:
                    user.process_login()
                    success_flash('You have logged into PylonsHQ')
                    if session.get('redirect'):
                        redir_url = session.pop('redirect')
                        session.save()
                        redirect(url(redir_url))
                    redirect(url('home'))
            
            # Second, if this is a registration request, present the rest of
            # registration process
            if session.get('openid_action', '') == 'register':
                sreg_response = sreg.SRegResponse.fromSuccessResponse(info)
                results = {}
                
                # Just in case the user didn't provide sreg details
                if sreg_response:
                    results['displayname'] = sreg_response.get('fullname')
                    results['timezone'] = sreg_response.get('timezone')
                    results['email_address'] = sreg_response.get('email')
                c.defaults = results
                c.openid = openid_identity
                return render('/accounts/register.mako')
            
            # The last option possible, is that the user is associating this
            # OpenID account with an existing account
            c.openid = openid_identity
            return render('/accounts/associate.mako')
        elif info.status == consumer.CANCEL:
            failure_flash('Verification cancelled')
        elif info.status == consumer.SETUP_NEEDED:
            if info.setup_url:
                c.message = '<a href=%s>Setup needed</a>' % info.setup_url
            else:
                # This means auth didn't succeed, but you're welcome to try
                # non-immediate mode.
                failure_flash('Setup needed')
        else:
            failure_flash('Verification failed.')
        session.delete()
        proper_abort(end_action)
