pradosoft / prado

Prado - Component Framework for PHP
Other
186 stars 70 forks source link

TRecapcha version 2 #560

Closed zendre4 closed 8 years ago

zendre4 commented 8 years ago

Hello,

The control TRecaptcha is in version 1 of ReCaptcha.
Would it be possible to implement the version 2 of ReCaptcha ?

Best wishes

Julien Stalder

unnamed

david0 commented 8 years ago

It would be nice to have support for ReCaptcha 2. A pull request for implementing this is welcome.

I guess it would be the best to implement this in a new control called TReCaptcha2. (for backward compatibility and maintainability)

camilohaze commented 8 years ago

TReCaptcha2.txt

JS Control TReCaptcha2

TReCaptcha2.txt

JS TReCaptcha2Validator

TReCaptcha2Validator.txt

ctrlaltca commented 8 years ago

@camilohaze nice, do you want to open a PR with it?

camilohaze commented 8 years ago

@ctrlaltca So far it lacks some features such as render methods, and getResponse reset. In the next few hours will post another more stable.

camilohaze commented 8 years ago

TReCaptcha2, TReCaptcha2Validator

<?php

Prado::using('System.Web.UI.ActiveControls.TCallbackEventParameter');
Prado::using('System.Web.UI.ActiveControls.TActivePanel');

class TReCaptcha2 extends TActivePanel implements ICallbackEventHandler, IValidatable
{
    const ChallengeFieldName = 'g-recaptcha-response';
    private $_widgetId=0;
    private $_isValid=true;

    public function __construct()
    {
        parent::__construct();
        $this->setAdapter(new TActiveControlAdapter($this));
    }
    public function getActiveControl()
    {
        return $this->getAdapter()->getBaseActiveControl();
    }
    public function getClientSide()
    {
        return $this->getAdapter()->getBaseActiveControl()->getClientSide();
    }
    public function getClientClassName()
    {
        return 'Prado.WebUI.TReCaptcha2';
    }
    public function getTagName()
    {
        return 'div';
    }
    /**
     * Returns true if this control validated successfully. 
     * Defaults to true.
     * @return bool wether this control validated successfully.
     */
    public function getIsValid()
    {
        return $this->_isValid;
    }
    /**
     * @param bool wether this control is valid.
     */
    public function setIsValid($value)
    {
        $this->_isValid=TPropertyValue::ensureBoolean($value);
    }
    public function getValidationPropertyValue()
    {
        return $this->Request[$this->getResponseFieldName()];
    }
    public function getResponseFieldName()
    {
        $captchas = $this->Page->findControlsByType('TReCaptcha2');
        $cont = 0;
        $responseFieldName = self::ChallengeFieldName;
        foreach ($captchas as $captcha)
        {
            if ($this->getClientID() == $captcha->ClientID)
            {
                $responseFieldName .= ($cont > 0) ? '-'.$cont : '';
            }
            $cont++;
        }
        return $responseFieldName;
    }
    /**
     * Returns your site key. 
     * @return string.
     */
    public function getSiteKey()
    {
        return $this->getViewState('SiteKey');
    }
    /**
     * @param string your site key.
     */
    public function setSiteKey($value)
    {
        $this->setViewState('SiteKey', TPropertyValue::ensureString($value));
    }
    /**
     * Returns your secret key. 
     * @return string.
     */
    public function getSecretKey()
    {
        return $this->getViewState('SecretKey');
    }
    /**
     * @param string your secret key.
     */
    public function setSecretKey($value)
    {
        $this->setViewState('SecretKey', TPropertyValue::ensureString($value));
    }
    /**
     * Returns your language.
     * @return string.
     */
    public function getLanguage()
    {
        return $this->getViewState('Language', 'en');
    }
    /**
     * @param string your language.
     */
    public function setLanguage($value)
    {
        $this->setViewState('Language', TPropertyValue::ensureString($value), 'en');
    }
    /**
     * Returns the color theme of the widget. 
     * @return string.
     */
    public function getTheme()
    {
        return $this->getViewState('Theme', 'light');
    }
    /**
     * The color theme of the widget.
     * Default: light
     * @param string the color theme of the widget.
     */
    public function setTheme($value)
    {
        $this->setViewState('Theme', TPropertyValue::ensureString($value), 'light');
    }
    /**
     * Returns the type of CAPTCHA to serve.
     * @return string.
     */
    public function getType()
    {
        return $this->getViewState('Type', 'image');
    }
    /**
     * The type of CAPTCHA to serve.
     * Default: image
     * @param string the type of CAPTCHA to serve.
     */
    public function setType($value)
    {
        $this->setViewState('Type', TPropertyValue::ensureString($value), 'image');
    }
    /**
     * Returns the size of the widget.
     * @return string.
     */
    public function getSize()
    {
        return $this->getViewState('Size', 'normal');
    }
    /**
     * The size of the widget.
     * Default: normal
     * @param string the size of the widget.
     */
    public function setSize($value)
    {
        $this->setViewState('Size', TPropertyValue::ensureString($value), 'normal');
    }
    /**
     * Returns the tabindex of the widget and challenge.
     * If other elements in your page use tabindex, it should be set to make user navigation easier.
     * @return string.
     */
    public function getTabIndex()
    {
        return $this->getViewState('TabIndex', 0);
    }
    /**
     * The tabindex of the widget and challenge.
     * If other elements in your page use tabindex, it should be set to make user navigation easier.
     * Default: 0
     * @param string the tabindex of the widget and challenge.
     */
    public function setTabIndex($value)
    {
        $this->setViewState('TabIndex', TPropertyValue::ensureInteger($value), 0);
    }
    /**
     * Resets the reCAPTCHA widget.
     * Optional widget ID, defaults to the first widget created if unspecified.
     */
    public function reset()
    {
        $this->Page->CallbackClient->callClientFunction('grecaptcha.reset',array(array($this->WidgetId)));
    }
    /**
     * Gets the response for the reCAPTCHA widget.
     */
    public function getResponse()
    {
        return $this->getViewState('Response', '');
    }
    public function setResponse($value)
    {
        $this->setViewState('Response', TPropertyValue::ensureString($value), '');
    }
    public function getWidgetId()
    {
        return $this->getViewState('WidgetId', 0);
    }
    public function setWidgetId($value)
    {
        $this->setViewState('WidgetId', TPropertyValue::ensureInteger($value), 0);
    }
    protected function getClientOptions()
    {
        $options['ID'] = $this->getClientID();
        $options['EventTarget'] = $this->getUniqueID();
        $options['FormID'] = $this->Page->getForm()->getClientID();
        $options['onCallback'] = $this->hasEventHandler('OnCallback');
        $options['onCallbackExpired'] = $this->hasEventHandler('OnCallbackExpired');
        $options['options']['sitekey'] = $this->getSiteKey();
        if ($theme = $this->getTheme()) $options['options']['theme'] = $theme;
        if ($type = $this->getType()) $options['options']['type'] = $type;
        if ($size = $this->getSize()) $options['options']['size'] = $size;
        if ($tabIndex = $this->getTabIndex()) $options['options']['tabindex'] = $tabIndex;

        return $options;
    }
    protected function registerClientScript()
    {
        $id         = $this->getClientID();
        $options    = TJavaScript::encode($this->getClientOptions());
        $className  = $this->getClientClassName();
        $cs         = $this->Page->ClientScript;
        $code       = "new $className($options);";

        $cs->registerPradoScript('ajax');
        $cs->registerEndScript("grecaptcha:$id", $code);
    }
    public function validate()
    {
        if ((is_null($this->getValidationPropertyValue())) || (empty($this->getValidationPropertyValue())))
            return false;

        return true;
    }
    /**
     * Checks for API keys
     * @param mixed event parameter
     */
    public function onPreRender($param)
    {
        parent::onPreRender($param);

        if("" == $this->getSiteKey())
            throw new TConfigurationException('recaptcha_publickey_unknown');
        if("" == $this->getSecretKey())
            throw new TConfigurationException('recaptcha_privatekey_unknown');

        // need to register captcha fields so they will be sent postback
        $this->Page->registerRequiresPostData($this->getResponseFieldName());
        $this->Page->ClientScript->registerHeadScriptFile('grecaptcha2', 'https://www.google.com/recaptcha/api.js?hl=' . $this->getLanguage());
    }
    protected function addAttributesToRender($writer)
    {
        $writer->addAttribute('id',$this->getClientID());
        parent::addAttributesToRender($writer);
    }
    public function raiseCallbackEvent($param)
    {
        $params = $param->getCallbackParameter();
        if ($params instanceof stdClass)
        {
            $callback = property_exists($params, 'onCallback');
            $callbackExpired = property_exists($params, 'onCallbackExpired');

            if ($callback)
            {
                $this->WidgetId = $params->widgetId;
                $this->Response = $params->response;
                $this->Page->CallbackClient->jQuery($params->responseField, 'text',array($params->response));

                if ($params->onCallback)
                {
                    $this->onCallback($param);
                }
            }

            if ($callbackExpired)
            {
                $this->Response = '';
                $this->reset();

                if ($params->onCallbackExpired)
                {
                    $this->onCallbackExpired($param);
                }
            }
        }
    }

    public function onCallback($param)
    {
        $this->raiseEvent('OnCallback', $this, $param);
    }

    public function onCallbackExpired($param)
    {
        $this->raiseEvent('OnCallbackExpired', $this, $param);
    }

    public function render($writer)
    {
        $this->registerClientScript();
        parent::render($writer);
    }
}

Prado::using('System.Web.UI.WebControls.TBaseValidator');

class TReCaptcha2Validator extends TBaseValidator
{
    protected $_isvalid = null;

    protected function getClientClassName()
    {
        return 'Prado.WebUI.TReCaptcha2Validator';
    }
    public function getEnableClientScript()
    {
        return true;
    }
    protected function getCaptchaControl()
    {
        $control = $this->getValidationTarget();
        if (!$control)
            throw new Exception('No target control specified for TReCaptcha2Validator');
        if (!($control instanceof TReCaptcha2))
            throw new Exception('TReCaptcha2Validator only works with TReCaptcha2 controls');
        return $control;
    }
    public function getClientScriptOptions()
    {
        $options = parent::getClientScriptOptions();
        $options['ResponseFieldName'] = $this->getCaptchaControl()->getResponseFieldName();
        return $options;
    }
    /**
     * This method overrides the parent's implementation.
     * The validation succeeds if the input control has the same value
     * as the one displayed in the corresponding RECAPTCHA control.
     *
     * @return boolean whether the validation succeeds
     */
    protected function evaluateIsValid()
    {
        // check validity only once (if trying to evaulate multiple times, all redundant checks would fail)
        if (is_null($this->_isvalid))
        {
            $control = $this->getCaptchaControl();
            $this->_isvalid = $control->validate();
        }
        return ($this->_isvalid==true);
    }
    public function onPreRender($param)
    {
        parent::onPreRender($param);

        $cs = $this->Page->getClientScript();
        $cs->registerPradoScript('validator');

        // communicate validation status to the client side
        $value = $this->_isvalid===false ? '0' : '1';
        $cs->registerHiddenField($this->getClientID().'_1',$value);

        // update validator display
        if ($control = $this->getValidationTarget())
        {
            $fn = 'captchaUpdateValidatorStatus_'.$this->getClientID();

            $cs->registerEndScript($this->getClientID().'::validate', implode(' ',array(
                // this function will be used to update the validator
                'function '.$fn.'(valid)',
                '{',
                '  jQuery('.TJavaScript::quoteString('#'.$this->getClientID().'_1').').val(valid);',
                '  Prado.Validation.validateControl('.TJavaScript::quoteString($control->ClientID).'); ',
                '}',
                '',
                // update the validator to the result if we're in a callback 
                // (if we're in initial rendering or a postback then the result will be rendered directly to the page html anyway)
                $this->Page->IsCallback ? $fn.'('.$value.');' : '',
                '',
                // install event handler that clears the validation error when user changes the captcha response field
                'jQuery("#'.$control->getClientID().'").on("change", '.TJavaScript::quoteString('#'.$control->getResponseFieldName()).', function() { ',
                    $fn.'("1");',
                '});',
            )));
        }
    }
}

TReCaptcha2Validaotr

Prado.WebUI.TReCaptcha2Validator = jQuery.klass(Prado.WebUI.TBaseValidator,
{
    /**
     * Evaluate validation state
     * @function {boolean} ?
     * @return True if the captcha has validate, False otherwise.
     */
    evaluateIsValid : function()
    {
        var a = this.getValidationValue();
        var b = this.trim(this.options.InitialValue);
        return(a != b);
    }
});

Find function getValidationValue on file validation3.js and

getValidationValue : function(control)

And after:

            case 'TActiveRadioButton':
                return value;

Added the following:

            case 'TReCaptcha2':
                return document.getElementById(this.options.ResponseFieldName).value;

Would be as follows:

            case 'TActiveRadioButton':
                return value;
            case 'TReCaptcha2':
                return document.getElementById(this.options.ResponseFieldName).value;

TReCaptcha2

Prado.WebUI.TReCaptcha2 = jQuery.klass(Prado.WebUI.Control,
{
    onInit: function(options)
    {
        for (key in options) { this[key] = options[key]; }
        this.options['callback'] = jQuery.proxy(this.callback,this);
        this.options['expired-callback'] = jQuery.proxy(this.callbackExpired,this);
        this.build();
    },
    build: function()
    {
        if (grecaptcha !== undefined) this.widgetId = grecaptcha.render(this.element, this.options);
    },
    callback: function(response)
    {
        var responseField = jQuery('#' + this.ID + ' textarea').attr('id');
        var params = {
            widgetId: this.widgetId,
            response: response,
            responseField: responseField,
            onCallback: this.onCallback
        };
        var request = new Prado.CallbackRequest(this.EventTarget,this);
        request.setCallbackParameter(params);
        request.dispatch();
    },
    callbackExpired: function()
    {
        var responseField = jQuery('#' + this.ID + ' textarea').attr('id');
        var params = {
            responseField: responseField,
            onCallbackExpired: this.onCallbackExpired
        };
        var request = new Prado.CallbackRequest(this.EventTarget,this);
        request.setCallbackParameter(params);
        request.dispatch();
    }
});
camilohaze commented 8 years ago

The control work :+1:

@ctrlaltca PR.

zendre4 commented 8 years ago

Thanks for fast response !

Can someone open a pull request?

ctrlaltca commented 8 years ago

@camilohaze added, thank you. I had to rework the way the control is rendered, since the build() method was executed before google's js finished loading, resulting in a grecaptcha is not defined javascript error. Now the build() method is called as a result of google's api on load callback.

camilohaze commented 8 years ago

@ctrlaltca Excellent work ... congratulations!

yoisar commented 8 years ago

Waw!! Very nice! Excellent!

zendre4 commented 8 years ago

Tanks for implement it!