Closed zendre4 closed 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)
JS Control TReCaptcha2
JS TReCaptcha2Validator
@camilohaze nice, do you want to open a PR with it?
@ctrlaltca So far it lacks some features such as render methods, and getResponse reset. In the next few hours will post another more stable.
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();
}
});
The control work :+1:
@ctrlaltca PR.
Thanks for fast response !
Can someone open a pull request?
@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.
@ctrlaltca Excellent work ... congratulations!
Waw!! Very nice! Excellent!
Tanks for implement it!
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