diff options
author | Fabio Bas <ctrlaltca@gmail.com> | 2016-03-24 11:54:39 +0100 |
---|---|---|
committer | Fabio Bas <ctrlaltca@gmail.com> | 2016-03-24 11:54:39 +0100 |
commit | c7fd3e1167b6f2fa7746edbd0fb8f8c1694c61f9 (patch) | |
tree | f67f61a6fd5a8ce4893663ab0c3a56d1df7aeae0 /framework/Web | |
parent | af129adce0014fc7c71e335804fef4148a057290 (diff) |
Added TReCaptcha2 and wrote doc; fix #560
Diffstat (limited to 'framework/Web')
4 files changed, 556 insertions, 1 deletions
diff --git a/framework/Web/Javascripts/source/prado/validator/validation3.js b/framework/Web/Javascripts/source/prado/validator/validation3.js index 24466599..6dcf02e9 100644 --- a/framework/Web/Javascripts/source/prado/validator/validation3.js +++ b/framework/Web/Javascripts/source/prado/validator/validation3.js @@ -1239,6 +1239,8 @@ Prado.WebUI.TBaseValidator = jQuery.klass(Prado.WebUI.Control, case 'TActiveCheckBox': case 'TActiveRadioButton': return value; + case 'TReCaptcha2': + return document.getElementById(this.options.ResponseFieldName).value; default: if(this.isListControlType()) return value; @@ -1994,3 +1996,83 @@ Prado.WebUI.TReCaptchaValidator = jQuery.klass(Prado.WebUI.TBaseValidator, } }); +/** + * Registry for TReCaptcha2 components + */ +Prado.WebUI.TReCaptcha2Instances = {}; +/** + * Render callback; called by google's js when loaded + */ +TReCaptcha2_onloadCallback = function() +{ + jQuery.each(Prado.WebUI.TReCaptcha2Instances, function(index, item) { + item.build(); + }); +} + +/** + * TReCaptcha2 client-side control. + * + * @class Prado.WebUI.TReCaptcha2 + * @extends Prado.WebUI.Control + */ +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); + + Prado.WebUI.TReCaptcha2Instances[this.element.id] = this; + }, + 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(); + } +}); + +/** + * TReCaptcha2Validator client-side control. + * + * @class Prado.WebUI.TReCaptcha2Validator + * @extends Prado.WebUI.TBaseValidator + */ +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); + } +});
\ No newline at end of file diff --git a/framework/Web/UI/WebControls/TReCaptcha2.php b/framework/Web/UI/WebControls/TReCaptcha2.php new file mode 100644 index 00000000..ed3f9871 --- /dev/null +++ b/framework/Web/UI/WebControls/TReCaptcha2.php @@ -0,0 +1,363 @@ +<?php + +/** + * TReCaptcha2 class file + * + * @author Cristian Camilo Naranjo Valencia + * @link http://icolectiva.co + * @copyright Copyright © 2005-2016 The PRADO Group + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @package System.Web.UI.WebControls + */ + +Prado::using('System.Web.UI.ActiveControls.TCallbackEventParameter'); +Prado::using('System.Web.UI.ActiveControls.TActivePanel'); + +/** + * TReCaptcha2 class. + * + * TReCaptcha2 displays a reCAPTCHA (a token displayed as an image) that can be used + * to determine if the input is entered by a real user instead of some program. It can + * also prevent multiple submits of the same form either by accident, or on purpose (ie. spamming). + * + * The reCAPTCHA to solve (a string consisting of two separate words) displayed is automatically + * generated by the reCAPTCHA system at recaptcha.net. However, in order to use the services + * of the site you will need to register and get a public and a private API key pair, and + * supply those to the reCAPTCHA control through setting the {@link setSecretKey SecretKey} + * and {@link setSiteKey SiteKey} properties. + * + * Currently the reCAPTCHA API supports only one reCAPTCHA field per page, so you MUST make sure that all + * your input is protected and validated by a single reCAPTCHA control. Placing more than one reCAPTCHA + * control on the page will lead to unpredictable results, and the user will most likely unable to solve + * any of them successfully. + * + * Upon postback, user input can be validated by calling {@link validate()}. + * The {@link TReCaptcha2Validator} control can also be used to do validation, which provides + * server-side validation. Calling (@link validate()) will invalidate the token supplied, so all consecutive + * calls to the method - without solving a new captcha - will return false. Therefore if implementing a multi-stage + * input process, you must make sure that you call validate() only once, either at the end of the input process, or + * you store the result till the end of the processing. + * + * The following template shows a typical use of TReCaptcha control: + * <code> + * <com:TReCaptcha2 ID="Captcha" + * SiteKey="..." + * SecretKey="..." + * /> + * <com:TReCaptcha2Validator ControlToValidate="Captcha" + * ErrorMessage="You are challenged!" /> + * </code> + * + * @author Cristian Camilo Naranjo Valencia + * @package System.Web.UI.WebControls + * @since 3.3.1 + */ + +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?onload=TReCaptcha2_onloadCallback&render=explicit&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); + } +} diff --git a/framework/Web/UI/WebControls/TReCaptcha2Validator.php b/framework/Web/UI/WebControls/TReCaptcha2Validator.php new file mode 100644 index 00000000..2cd4b6d1 --- /dev/null +++ b/framework/Web/UI/WebControls/TReCaptcha2Validator.php @@ -0,0 +1,110 @@ +<?php + +/** + * TReCaptcha2Validator class file + * + * @author Cristian Camilo Naranjo Valencia + * @link http://icolectiva.co + * @copyright Copyright © 2005-2016 The PRADO Group + * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT + * @package System.Web.UI.WebControls + */ + +Prado::using('System.Web.UI.WebControls.TBaseValidator'); +Prado::using('System.Web.UI.WebControls.TReCaptcha2'); + +/** + * TReCaptcha2Validator class + * + * TReCaptcha2Validator validates a reCAPTCHA represented by a {@link TReCaptcha} control. + * The input control fails validation if th user did not pass the humanity test. + * + * To use TReCaptcha2Validator, specify the {@link setCaptchaControl CaptchaControl} + * to be the ID path of the {@link TReCaptcha} control. + * + * @author Cristian Camilo Naranjo Valencia + * @package System.Web.UI.WebControls + * @since 3.3.1 + */ + +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");', + '});', + ))); + } + } +} + diff --git a/framework/Web/UI/WebControls/TReCaptchaValidator.php b/framework/Web/UI/WebControls/TReCaptchaValidator.php index de3b42a5..9078354b 100644 --- a/framework/Web/UI/WebControls/TReCaptchaValidator.php +++ b/framework/Web/UI/WebControls/TReCaptchaValidator.php @@ -21,7 +21,7 @@ Prado::using('System.Web.UI.WebControls.TReCaptcha'); * is not the same as the token displayed in reCAPTCHA. Note, if the user does * not enter any thing, it is still considered as failing the validation. * - * To use TReCaptchaValidator, specify the {@link setControlToValidate ControlToValidate} + * To use TReCaptchaValidator, specify the {@link setCaptchaControl CaptchaControl} * to be the ID path of the {@link TReCaptcha} control. * * @author Bérczi Gábor <gabor.berczi@devworx.hu> |