{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE RecordWildCards    #-}

-- |
-- Module      : Network.OAuth.ThreeLegged
-- Copyright   : (c) Joseph Abrahamson 2013
-- License     : MIT
--
-- Maintainer  : me@jspha.com
-- Stability   : experimental
-- Portability : non-portable
--
-- The \"Three-legged OAuth\" protocol implementing RFC 5849's
-- /Redirection-Based Authorization/.

module Network.OAuth.ThreeLegged (
  -- * Configuration types
  ThreeLegged (..), parseThreeLegged, Callback (..),

  -- * Actions
  requestTemporaryToken, buildAuthorizationUrl, requestPermanentToken,

  -- ** Raw forms
  requestTemporaryTokenRaw, requestPermanentTokenRaw,

  -- * Example system
  requestTokenProtocol
  ) where

import           Control.Applicative
import           Control.Monad.Trans
import           Control.Monad.Trans.Maybe
import qualified Data.ByteString.Lazy            as SL
import qualified Data.ByteString                 as S
import           Data.Data
import           Network.HTTP.Client             (httpLbs)
import           Network.HTTP.Client.Request     (getUri,parseUrl)
import           Network.HTTP.Client.Types       (Request (..), Response (..), HttpException)
import           Network.HTTP.Types              (renderQuery)
import           Network.OAuth
import           Network.OAuth.MuLens
import           Network.OAuth.Stateful
import           Network.OAuth.Types.Credentials
import           Network.OAuth.Types.Params
import           Network.URI

-- | Data parameterizing the \"Three-legged OAuth\" redirection-based
-- authorization protocol. These parameters cover the protocol as described
-- in the community editions /OAuth Core 1.0/ and /OAuth Core 1.0a/ as well
-- as RFC 5849.
data ThreeLegged =
  ThreeLegged { temporaryTokenRequest :: Request
              -- ^ Base 'Request' for the \"endpoint used by the client to
              -- obtain a set of 'Temporary' 'Cred'entials\" in the form of
              -- a 'Temporary' 'Token'. This request is automatically
              -- instantiated and performed during the first leg of the
              -- 'ThreeLegged' authorization protocol.
              , resourceOwnerAuthorization :: Request
              -- ^ Base 'Request' for the \"endpoint to which the resource
              -- owner is redirected to grant authorization\". This request
              -- must be performed by the user granting token authorization
              -- to the client. Transmitting the parameters of this request
              -- to the user is out of scope of @oauthenticated@, but
              -- functions are provided to make it easier.
              , permanentTokenRequest      :: Request
              -- ^ Base 'Request' for the \"endpoint used by the client to
              -- request a set of token credentials using the set of
              -- 'Temporary' 'Cred'entials\". This request is also
              -- instantiated and performed by @oauthenticated@ in order to
              -- produce a 'Permanent' 'Token'.
              , callback                   :: Callback
              -- ^ The 'Callback' parameter configures how the user is
              -- intended to communicate the 'Verifier' back to the client.
              }
    deriving ( Show, Typeable )

-- | Convenience method for creating a 'ThreeLegged' configuration from
-- a trio of URLs and a 'Callback'.
parseThreeLegged :: String -> String -> String -> Callback -> Either HttpException ThreeLegged
parseThreeLegged a b c d = ThreeLegged <$> parseUrl a <*> parseUrl b <*> parseUrl c <*> pure d

-- | Request a 'Temporary' 'Token' based on the parameters of
-- a 'ThreeLegged' protocol. This returns the raw response which should be
-- encoded as @www-form-urlencoded@.
requestTemporaryTokenRaw :: MonadIO m => ThreeLegged -> OAuthT Client m SL.ByteString
requestTemporaryTokenRaw (ThreeLegged {..}) = do
  oax  <- newParams
  req  <- sign (oax { workflow = TemporaryTokenRequest callback }) temporaryTokenRequest
  resp <- withManager (liftIO . httpLbs req)
  return $ responseBody resp

-- | Returns 'Nothing' if the response could not be decoded as a 'Token'.
-- Importantly, in RFC 5849 compliant modes this requires that the token
-- response includes @callback_confirmed=true@. See also
-- 'requestTemporaryTokenRaw'.
requestTemporaryToken :: MonadIO m => ThreeLegged -> OAuthT Client m (Maybe (Token Temporary))
requestTemporaryToken tl = do
  raw <- requestTemporaryTokenRaw tl
  s   <- getServer
  let mayToken = fromUrlEncoded $ SL.toStrict raw
  return $ do
    (confirmed, tok) <- mayToken
    case oAuthVersion s of
      OAuthCommunity1 -> return tok
      _               -> if confirmed then return tok else fail "Must be confirmed"

-- | Produce a 'URI' which the user should be directed to in order to
-- authorize a set of 'Temporary' 'Cred's.
buildAuthorizationUrl :: Monad m => ThreeLegged -> OAuthT Temporary m URI
buildAuthorizationUrl (ThreeLegged {..}) = do
  c <- getCredentials
  return $ getUri $ resourceOwnerAuthorization {
    queryString = renderQuery True [ ("oauth_token", Just (c ^. resourceToken . key)) ]
  }

-- | Request a 'Permanent 'Token' based on the parameters of
-- a 'ThreeLegged' protocol. This returns the raw response which should be
-- encoded as @www-form-urlencoded@.
requestPermanentTokenRaw :: MonadIO m => ThreeLegged -> Verifier -> OAuthT Temporary m SL.ByteString
requestPermanentTokenRaw (ThreeLegged {..}) verifier = do
  oax  <- newParams
  req  <- sign (oax { workflow = PermanentTokenRequest verifier }) permanentTokenRequest
  resp <- withManager (liftIO . httpLbs req)
  return $ responseBody resp

-- | Returns 'Nothing' if the response could not be decoded as a 'Token'.
-- See also 'requestPermanentTokenRaw'.
requestPermanentToken :: MonadIO m => ThreeLegged -> Verifier -> OAuthT Temporary m (Maybe (Token Permanent))
requestPermanentToken tl verifier = do
  raw <- requestPermanentTokenRaw tl verifier
  return $ fmap snd $ fromUrlEncoded $ SL.toStrict raw

-- | Performs an interactive token request over stdin assuming that the
-- verifier code is acquired out-of-band.
requestTokenProtocol :: MonadIO m => ThreeLegged -> OAuthT Client m (Maybe (Token Permanent))
requestTokenProtocol threeLegged = runMaybeT $ do
  cCred <- lift getCredentials
  tok <- MaybeT (requestTemporaryToken threeLegged)
  MaybeT $ withCred (temporaryCred tok cCred) $ do
    url <- buildAuthorizationUrl threeLegged
    code <- liftIO $ do 
      putStr "Please direct the user to the following address\n\n"
      putStr "    " >> print url >> putStr "\n\n"
      putStrLn "... then enter the verification code below (no spaces)\n"
      S.getLine
    requestPermanentToken threeLegged code