module Yesod.ReCAPTCHA ( YesodReCAPTCHA(..) , recaptchaAForm , recaptchaMForm , recaptchaOptions , RecaptchaOptions(..) ) where import Control.Applicative import Data.Typeable (Typeable) import Yesod.Core (whamlet) import qualified Control.Exception.Lifted as E import qualified Control.Monad.Trans.Resource as R import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy.Char8 as L8 import qualified Data.Default as D import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding.Error as TEE import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TLE import qualified Network.HTTP.Conduit as H import qualified Network.HTTP.Types as HT import qualified Network.Info as NI import qualified Network.Socket as HS import qualified Network.Wai as W import qualified Yesod.Auth as YA import qualified Yesod.Core as YC import qualified Yesod.Form.Fields as YF import qualified Yesod.Form.Functions as YF import qualified Yesod.Form.Types as YF -- | Class used by @yesod-recaptcha@'s fields. It should be -- fairly easy to implement a barebones instance of this class -- for you foundation data type: -- -- > instance YesodReCAPTCHA MyType where -- > recaptchaPublicKey = return "[your public key]" -- > recaptchaPrivateKey = return "[your private key]" -- -- You may also write a more sophisticated instance. For -- example, you may get these values from your @settings.yml@ -- instead of hardcoding them. Or you may give different keys -- depending on the request (maybe you're serving to two -- different domains in the same application). -- -- The 'YA.YesodAuth' superclass is used only for the HTTP -- request. Please fill a bug report if you think that this -- @YesodReCAPTCHA@ may be useful without @YesodAuth@. -- -- /Minimum complete definition:/ 'recaptchaPublicKey' and -- 'recaptchaPrivateKey'. class YA.YesodAuth site => YesodReCAPTCHA site where -- | Your reCAPTCHA public key. recaptchaPublicKey :: YC.HandlerT site IO T.Text -- | Your reCAPTCHA private key. recaptchaPrivateKey :: YC.HandlerT site IO T.Text -- | A backdoor to the reCAPTCHA mechanism. While doing -- automated tests you may need to fill a form that is -- protected by a CAPTCHA. The whole point of using a -- CAPTCHA is disallowing access to non-humans, which -- hopefully your test suite is. -- -- In order to solve this problem, you may define -- -- > insecureRecaptchaBackdoor = return (Just "") -- -- Now, whenever someone fills @\@ as the -- CAPTCHA, the @yesod-recaptcha@ library will /not/ contact -- reCAPTCHA's servers and instead will blindly accept the -- secret CAPTCHA. -- -- Note that this is a *huge* security hole in the wrong -- hands. We /do not/ recommend using this function on a -- production environment without a good reason. If for -- whatever reason you must use this function on a production -- environment, please make use of its access to 'GHandler' -- in order to return @Just@ only when strictly necessary. -- For example, you may return @Just@ only when the request -- comes from @localhost@ and read its contents from a secret -- file accessible only by SSH which is afterwards removed. -- -- By default, this function returns @Nothing@, which -- completely disables the backdoor. insecureRecaptchaBackdoor :: YC.HandlerT site IO (Maybe T.Text) insecureRecaptchaBackdoor = return Nothing -- | A reCAPTCHA field. This 'YF.AForm' returns @()@ because -- CAPTCHAs give no useful information besides having being typed -- correctly or not. When the user does not type the CAPTCHA -- correctly, this 'YF.AForm' will automatically fail in the same -- way as any other @yesod-form@ widget fails, so you may just -- ignore the @()@ value. recaptchaAForm :: YesodReCAPTCHA site => YF.AForm (YC.HandlerT site IO) () recaptchaAForm = YF.formToAForm recaptchaMForm -- | Same as 'recaptchaAForm', but instead of being an -- 'YF.AForm', it's an 'YF.MForm'. recaptchaMForm :: YesodReCAPTCHA site => YF.MForm (YC.HandlerT site IO) ( YF.FormResult () , [YF.FieldView site] ) recaptchaMForm = do challengeField <- fakeField "recaptcha_challenge_field" responseField <- fakeField "recaptcha_response_field" ret <- maybe (return Nothing) (YC.lift . fmap Just . uncurry check) ((,) <$> challengeField <*> responseField) let view = recaptchaWidget $ case ret of Just (Error err) -> Just err _ -> Nothing formRet = case ret of Nothing -> YF.FormMissing Just Ok -> YF.FormSuccess () Just (Error _) -> YF.FormFailure [] formView = YF.FieldView { YF.fvLabel = "" , YF.fvTooltip = Nothing , YF.fvId = "recaptcha_challenge_field" , YF.fvInput = view , YF.fvErrors = Nothing , YF.fvRequired = True } return (formRet, [formView]) -- | Widget with reCAPTCHA's HTML. recaptchaWidget :: YesodReCAPTCHA site => Maybe T.Text -- ^ Error code, if any. -> YC.WidgetT site IO () recaptchaWidget merr = do publicKey <- YC.handlerToWidget recaptchaPublicKey isSecure <- W.isSecure <$> YC.waiRequest let proto | isSecure = "https" | otherwise = "http" :: T.Text err = maybe "" (T.append "&error=") merr [whamlet|