diff --git a/composer.json b/composer.json index e9c3927..e89c27b 100644 --- a/composer.json +++ b/composer.json @@ -69,11 +69,26 @@ "bin": [ "yii/yiic" ], + "repositories": [ + { + "type": "package", + "package": { + "name": "bestiejs/punycode.js", + "version": "1.2.1", + "source": { + "url": "git://github.com/bestiejs/punycode.js.git", + "type": "git", + "reference": "1.2.1" + } + } + } + ], "require": { "php": ">=5.3.0", "michelf/php-markdown": "1.3", "twig/twig": "1.12.*", "smarty/smarty": "3.1.*", - "ezyang/htmlpurifier": "v4.5.0" + "ezyang/htmlpurifier": "v4.5.0", + "bestiejs/punycode.js": "1.2.1" } } diff --git a/composer.lock b/composer.lock index 1cae3d4..18a46ee 100644 --- a/composer.lock +++ b/composer.lock @@ -3,9 +3,19 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "7d46ce9c4d8d5f4ecae1611ea8f0b49c", + "hash": "a8f949e337a229a4cfb41496a0071ef6", "packages": [ { + "name": "bestiejs/punycode.js", + "version": "1.2.1", + "source": { + "type": "git", + "url": "git://github.com/bestiejs/punycode.js.git", + "reference": "1.2.1" + }, + "type": "library" + }, + { "name": "ezyang/htmlpurifier", "version": "v4.5.0", "source": { diff --git a/yii/assets.php b/yii/assets.php index 7ee177d..746bf30 100644 --- a/yii/assets.php +++ b/yii/assets.php @@ -26,7 +26,7 @@ return array( 'js' => array( 'yii.activeForm.js', ), - 'depends' => array('yii', 'yii/validation'), + 'depends' => array('yii'), ), 'yii/captcha' => array( 'sourcePath' => __DIR__ . '/assets', @@ -42,4 +42,10 @@ return array( ), 'depends' => array('yii'), ), + 'punycode' => array( + 'sourcePath' => __DIR__ . '/vendor/bestiejs/punycode.js', + 'js' => array( + 'punycode.min.js', + ), + ), ); diff --git a/yii/assets/yii.validation.js b/yii/assets/yii.validation.js index 5fa8492..c9d62bd 100644 --- a/yii/assets/yii.validation.js +++ b/yii/assets/yii.validation.js @@ -110,9 +110,19 @@ yii.validation = (function ($) { return; } - var valid = value.match(options.pattern) && (!options.allowName || value.match(options.fullPattern)); + var valid = true; + + if (options.enableIDN) { + var regexp = /^(.*)@(.*)$/, + matches = regexp.exec(value); + if (matches === null) { + valid = false; + } else { + value = punycode.toASCII(matches[1]) + '@' + punycode.toASCII(matches[2]); + } + } - if (!valid) { + if (!valid || !(value.match(options.pattern) && (!options.allowName || value.match(options.fullPattern)))) { messages.push(options.message); } }, @@ -126,7 +136,19 @@ yii.validation = (function ($) { value = options.defaultScheme + '://' + value; } - if (!value.match(options.pattern)) { + var valid = true; + + if (options.enableIDN) { + var regexp = /^([^:]+):\/\/([^\/]+)(.*)?/, + matches = regexp.exec(value); + if (matches === null) { + valid = false; + } else { + value = matches[1] + '://' + punycode.toASCII(matches[2]) + matches[3]; + } + } + + if (!valid || !value.match(options.pattern)) { messages.push(options.message); } }, diff --git a/yii/validators/BooleanValidator.php b/yii/validators/BooleanValidator.php index 2929dfd..67a64f6 100644 --- a/yii/validators/BooleanValidator.php +++ b/yii/validators/BooleanValidator.php @@ -79,9 +79,11 @@ class BooleanValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array( 'trueValue' => $this->trueValue, @@ -100,6 +102,7 @@ class BooleanValidator extends Validator $options['strict'] = 1; } + $view->registerAssetBundle('yii/validation'); return 'yii.validation.boolean(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/CaptchaValidator.php b/yii/validators/CaptchaValidator.php index c49ffdb..dbc263e 100644 --- a/yii/validators/CaptchaValidator.php +++ b/yii/validators/CaptchaValidator.php @@ -91,9 +91,11 @@ class CaptchaValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $captcha = $this->getCaptchaAction(); $code = $captcha->getVerifyCode(false); @@ -111,6 +113,7 @@ class CaptchaValidator extends Validator $options['skipOnEmpty'] = 1; } + $view->registerAssetBundle('yii/validation'); return 'yii.validation.captcha(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/CompareValidator.php b/yii/validators/CompareValidator.php index 7f7318f..b8e8a50 100644 --- a/yii/validators/CompareValidator.php +++ b/yii/validators/CompareValidator.php @@ -178,9 +178,11 @@ class CompareValidator extends Validator * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated * @return string the client-side validation script + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @throws InvalidConfigException if CompareValidator::operator is invalid */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array('operator' => $this->operator); @@ -203,6 +205,7 @@ class CompareValidator extends Validator '{compareValue}' => $compareValue, ))); + $view->registerAssetBundle('yii/validation'); return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/EmailValidator.php b/yii/validators/EmailValidator.php index dce6c37..4297f12 100644 --- a/yii/validators/EmailValidator.php +++ b/yii/validators/EmailValidator.php @@ -47,6 +47,12 @@ class EmailValidator extends Validator * Defaults to false. */ public $checkPort = false; + /** + * @var boolean whether validation process should take into account IDN (internationalized domain + * names). Defaults to false meaning that validation of emails containing IDN will always fail. + */ + public $enableIDN = false; + /** * Initializes the validator. @@ -81,10 +87,18 @@ class EmailValidator extends Validator public function validateValue($value) { // make sure string length is limited to avoid DOS attacks - $valid = is_string($value) && strlen($value) <= 254 - && (preg_match($this->pattern, $value) || $this->allowName && preg_match($this->fullPattern, $value)); + if (!is_string($value) || strlen($value) >= 255) { + return false; + } + if (($atPosition = strpos($value, '@')) === false) { + return false; + } + $domain = rtrim(substr($value, $atPosition + 1), '>'); + if ($this->enableIDN) { + $value = idn_to_ascii(ltrim(substr($value, 0, $atPosition), '<')) . '@' . idn_to_ascii($domain); + } + $valid = preg_match($this->pattern, $value) || $this->allowName && preg_match($this->fullPattern, $value); if ($valid) { - $domain = rtrim(substr($value, strpos($value, '@') + 1), '>'); if ($this->checkMX && function_exists('checkdnsrr')) { $valid = checkdnsrr($domain, 'MX'); } @@ -99,9 +113,11 @@ class EmailValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array( 'pattern' => new JsExpression($this->pattern), @@ -111,11 +127,16 @@ class EmailValidator extends Validator '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, ))), + 'enableIDN' => (boolean)$this->enableIDN, ); if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; } + $view->registerAssetBundle('yii/validation'); + if ($this->enableIDN) { + $view->registerAssetBundle('punycode'); + } return 'yii.validation.email(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/yii/validators/InlineValidator.php b/yii/validators/InlineValidator.php index 8af5bbc..dd951aa 100644 --- a/yii/validators/InlineValidator.php +++ b/yii/validators/InlineValidator.php @@ -79,12 +79,14 @@ class InlineValidator extends Validator * * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. Null if the validator does not support * client-side validation. * @see enableClientValidation * @see \yii\web\ActiveForm::enableClientValidation */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { if ($this->clientValidate !== null) { $method = $this->clientValidate; diff --git a/yii/validators/NumberValidator.php b/yii/validators/NumberValidator.php index 33822bf..3bcc6ce 100644 --- a/yii/validators/NumberValidator.php +++ b/yii/validators/NumberValidator.php @@ -114,9 +114,11 @@ class NumberValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $label = $object->getAttributeLabel($attribute); $value = $object->$attribute; @@ -149,6 +151,7 @@ class NumberValidator extends Validator $options['skipOnEmpty'] = 1; } + $view->registerAssetBundle('yii/validation'); return 'yii.validation.number(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/yii/validators/RangeValidator.php b/yii/validators/RangeValidator.php index 4bed303..a915275 100644 --- a/yii/validators/RangeValidator.php +++ b/yii/validators/RangeValidator.php @@ -81,9 +81,11 @@ class RangeValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $range = array(); foreach ($this->range as $value) { @@ -101,6 +103,7 @@ class RangeValidator extends Validator $options['skipOnEmpty'] = 1; } + $view->registerAssetBundle('yii/validation'); return 'yii.validation.range(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/RegularExpressionValidator.php b/yii/validators/RegularExpressionValidator.php index 505812f..417f2bc 100644 --- a/yii/validators/RegularExpressionValidator.php +++ b/yii/validators/RegularExpressionValidator.php @@ -79,10 +79,12 @@ class RegularExpressionValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. * @throws InvalidConfigException if the "pattern" is not a valid regular expression */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $pattern = $this->pattern; $pattern = preg_replace('/\\\\x\{?([0-9a-fA-F]+)\}?/', '\u$1', $pattern); @@ -110,6 +112,7 @@ class RegularExpressionValidator extends Validator $options['skipOnEmpty'] = 1; } + $view->registerAssetBundle('yii/validation'); return 'yii.validation.regularExpression(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/yii/validators/RequiredValidator.php b/yii/validators/RequiredValidator.php index 424f94c..aedbe05 100644 --- a/yii/validators/RequiredValidator.php +++ b/yii/validators/RequiredValidator.php @@ -102,9 +102,11 @@ class RequiredValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $options = array(); if ($this->requiredValue !== null) { @@ -124,6 +126,7 @@ class RequiredValidator extends Validator '{value}' => $object->$attribute, ))); + $view->registerAssetBundle('yii/validation'); return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/StringValidator.php b/yii/validators/StringValidator.php index 110619e..abe4634 100644 --- a/yii/validators/StringValidator.php +++ b/yii/validators/StringValidator.php @@ -126,9 +126,11 @@ class StringValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { $label = $object->getAttributeLabel($attribute); $value = $object->$attribute; @@ -168,6 +170,7 @@ class StringValidator extends Validator $options['skipOnEmpty'] = 1; } + $view->registerAssetBundle('yii/validation'); return 'yii.validation.string(value, messages, ' . json_encode($options) . ');'; } } diff --git a/yii/validators/UrlValidator.php b/yii/validators/UrlValidator.php index 6917d01..bbd8883 100644 --- a/yii/validators/UrlValidator.php +++ b/yii/validators/UrlValidator.php @@ -37,6 +37,12 @@ class UrlValidator extends Validator * contain the scheme part. **/ public $defaultScheme; + /** + * @var boolean whether validation process should take into account IDN (internationalized + * domain names). Defaults to false meaning that validation of URLs containing IDN will always + * fail. + */ + public $enableIDN = false; /** @@ -87,6 +93,12 @@ class UrlValidator extends Validator $pattern = $this->pattern; } + if ($this->enableIDN) { + $value = preg_replace_callback('/:\/\/([^\/]+)/', function($matches) { + return '://' . idn_to_ascii($matches[1]); + }, $value); + } + if (preg_match($pattern, $value)) { return true; } @@ -98,10 +110,12 @@ class UrlValidator extends Validator * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. * @see \yii\Web\ActiveForm::enableClientValidation */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { if (strpos($this->pattern, '{schemes}') !== false) { $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); @@ -115,6 +129,7 @@ class UrlValidator extends Validator '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, ))), + 'enableIDN' => (boolean)$this->enableIDN, ); if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; @@ -123,7 +138,10 @@ class UrlValidator extends Validator $options['defaultScheme'] = $this->defaultScheme; } + $view->registerAssetBundle('yii/validation'); + if ($this->enableIDN) { + $view->registerAssetBundle('punycode'); + } return 'yii.validation.url(value, messages, ' . Json::encode($options) . ');'; } } - diff --git a/yii/validators/Validator.php b/yii/validators/Validator.php index 677191b..6b103bf 100644 --- a/yii/validators/Validator.php +++ b/yii/validators/Validator.php @@ -211,11 +211,13 @@ abstract class Validator extends Component * * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. + * @param \yii\base\View $view the view object that is going to be used to render views or view files + * containing a model form with this validator applied. * @return string the client-side validation script. Null if the validator does not support * client-side validation. * @see \yii\web\ActiveForm::enableClientValidation */ - public function clientValidateAttribute($object, $attribute) + public function clientValidateAttribute($object, $attribute, $view) { return null; } diff --git a/yii/widgets/ActiveField.php b/yii/widgets/ActiveField.php index 55357a3..45faf9d 100644 --- a/yii/widgets/ActiveField.php +++ b/yii/widgets/ActiveField.php @@ -138,7 +138,7 @@ class ActiveField extends Component $validators = array(); foreach ($this->model->getActiveValidators($attribute) as $validator) { /** @var \yii\validators\Validator $validator */ - $js = $validator->clientValidateAttribute($this->model, $attribute); + $js = $validator->clientValidateAttribute($this->model, $attribute, $this->form->getView()); if ($validator->enableClientValidation && $js != '') { $validators[] = $js; }