diff --git a/assets/js/dash/options.js b/assets/js/dash/options.js index 59a0e93..6707058 100644 --- a/assets/js/dash/options.js +++ b/assets/js/dash/options.js @@ -17,9 +17,14 @@ var dash_options_load = function() { $(this.content).find('select.captcha_select').change(this.bind(this, function(){ var captcha_type = $(this.content).find('select.captcha_select').val(); if (captcha_type == 'recaptcha') { + $(this.content).find('.recaptcha3_inputs').hide(); $(this.content).find('.recaptcha_inputs').show(); + } else if (captcha_type == 'recaptcha3') { + $(this.content).find('.recaptcha_inputs').hide(); + $(this.content).find('.recaptcha3_inputs').show(); } else { $(this.content).find('.recaptcha_inputs').hide(); + $(this.content).find('.recaptcha3_inputs').hide(); } })); $(this.content).find('select.captcha_select').trigger('change'); diff --git a/assets/js/holiday/holiday.js b/assets/js/holiday/holiday.js index 2162100..821c29b 100644 --- a/assets/js/holiday/holiday.js +++ b/assets/js/holiday/holiday.js @@ -149,12 +149,30 @@ $('body').append(''); $('.new-year-theme-img-6').css('top','-'+img6.height+'px').show().animate({top:0},1000,function(){ $(this).animate({top:'-5px'},500,function(){ - $(this).animate({top:'0px'},500); + $(this).stop(1,1).animate({top:'0px'},500); }); }); + var img6AnimLock = false; $('.new-year-theme-img-6').click(function(){ + img6AnimLock = false; $(this).animate({top:'-'+img6.height+'px'},1000); }); + $('.new-year-theme-img-6').mouseover(function(){ + if (img6AnimLock) return; + img6AnimLock = true; + $(this).animate({top:'-10px'},500,function(){ + if (!img6AnimLock) return; + $(this).animate({top:'0px'},500, function(){ + if (!img6AnimLock) return; + $(this).animate({top:'-5px'},500, function(){ + if (!img6AnimLock) return; + $(this).animate({top:'0px'},500, function(){ + img6AnimLock = false; + }); + }); + }); + }); + }); }; img6.src = base + '/assets/images/holiday/newyear6.png'; diff --git a/assets/js/zira.js b/assets/js/zira.js index 841b8dc..0709f8e 100644 --- a/assets/js/zira.js +++ b/assets/js/zira.js @@ -311,11 +311,17 @@ zira_init_captcha(); } /** - * reCaptcha + * reCaptcha v2 **/ if ($('.g-recaptcha').length>0) { zira_load_recaptcha(); } + /** + * reCaptcha v3 + **/ + if ($('.g-recaptcha3').length>0) { + zira_load_recaptcha3(); + } /** * User auth popup */ @@ -1163,6 +1169,25 @@ grecaptcha.render($('#zira-auth-dialog .g-recaptcha').get(0)); } catch(e) {} } + } else if ($('#zira-auth-dialog .g-recaptcha3').length>0) { + if (typeof zira_load_recaptcha3.loaded == "undefined") { + zira_load_recaptcha3(); + } else { + try { + var el = $('#zira-auth-dialog .g-recaptcha3'); + var id = 'grecaptcha3-auth-popup'; + $(el).attr('id', id); + $(el).html(''); + $(el).parent().find('.g-recaptcha3-message').attr('id',id+'-message'); + var site_key = $(el).data('sitekey'); + var action = $(el).data('action'); + grecaptcha.execute(site_key, {action: action}).then(function(token){ + $('input#'+id+'-hidden').val(token); + var msgi = $('#'+id+'-message'); + $(msgi).text($(msgi).data('success')); + }); + } catch(e) {} + } } else if ($('.captcha-refresh-btn').length>0) { zira_init_captcha(); } @@ -1495,7 +1520,7 @@ zira_init_xhr_form = function() { $('form.xhr-form').each(function() { - if ($(this).find('.g-recaptcha').length>0 || $(this).find('.captcha-refresh-btn').length>0) return true; + if ($(this).find('.g-recaptcha').length>0 || $(this).find('.g-recaptcha3').length>0 || $(this).find('.captcha-refresh-btn').length>0) return true; $(this).bind('xhr-submit-error', function(e, response){ if (!response) return; if (typeof response.captcha_error != "undefined" && response.captcha_error) { @@ -1558,6 +1583,43 @@ }); }; + zira_load_recaptcha3 = function() { + if (typeof zira_recaptcha3_url == "undefined") return; + if (typeof zira_load_recaptcha3.loaded != 'undefined') return; + zira_load_recaptcha3.loaded = true; + $('body').append(''); + }; + + zira_recaptcha3_onload = function() { + var grecaptcha_co = 0; + $('.g-recaptcha3').each(function(){ + grecaptcha_co++; + var id = 'grecaptcha3-'+grecaptcha_co; + $(this).attr('id', id); + $(this).html(''); + $(this).parent().find('.g-recaptcha3-message').attr('id',id+'-message'); + var site_key = $(this).data('sitekey'); + var action = $(this).data('action'); + grecaptcha.execute(site_key, {action: action}).then(function(token){ + $('input#'+id+'-hidden').val(token); + var msgi = $('#'+id+'-message'); + $(msgi).text($(msgi).data('success')); + }); + $(this).parents('form.xhr-form').submit(function(){ + var grecaptcha_el = $(this).find('.g-recaptcha3'); + window.setTimeout(function(){ + try { + var site_key = $(grecaptcha_el).data('sitekey'); + var action = $(grecaptcha_el).data('action'); + grecaptcha.execute(site_key, {action: action}).then(function(token){ + $('input#'+$(grecaptcha_el).attr('id')+'-hidden').val(token); + }); + } catch(err) {} + }, 3000); + }); + }); + }; + zira_set_dashpanel_dropdown_height = function() { var dropdown = $('#dashpanel-container').find('.open > .dropdown-menu'); if ($(dropdown).length>0) { diff --git a/dash/forms/options.php b/dash/forms/options.php index 1dc705a..d92907e 100644 --- a/dash/forms/options.php +++ b/dash/forms/options.php @@ -71,6 +71,12 @@ class Options extends Form $html .= $this->input(Locale::t('Site key for reCaptcha'), 'recaptcha_site_key'); $html .= $this->input(Locale::t('Secret key for reCaptcha'), 'recaptcha_secret_key'); $html .= Zira\Helper::tag_close('div'); + + $html .= Zira\Helper::tag_open('div', array('class' => 'recaptcha3_inputs')); + $html .= $this->input(Locale::t('Site key for reCaptcha'), 'recaptcha3_site_key'); + $html .= $this->input(Locale::t('Secret key for reCaptcha'), 'recaptcha3_secret_key'); + $html .= Zira\Helper::tag_close('div'); + $html .= $this->checkbox(Locale::t('Sticky top bar'), 'dash_panel_frontend', null, false); $html .= $this->select(Locale::t('Window buttons position'), 'dashwindow_mode', array( '0' => Locale::t('Left'), diff --git a/dash/models/options.php b/dash/models/options.php index ff73ba9..dd15187 100644 --- a/dash/models/options.php +++ b/dash/models/options.php @@ -39,7 +39,9 @@ class Options extends Model { 'check_updates' => 'int', 'captcha_type' => 'string', 'recaptcha_site_key' => 'string', - 'recaptcha_secret_key' => 'string' + 'recaptcha_secret_key' => 'string', + 'recaptcha3_site_key' => 'string', + 'recaptcha3_secret_key' => 'string' ); if (count(Zira\Config::get('languages'))>1) { diff --git a/languages/ru/ru.php b/languages/ru/ru.php index d4c1f05..9f38217 100644 --- a/languages/ru/ru.php +++ b/languages/ru/ru.php @@ -154,5 +154,7 @@ return array( 'Anti-Bot' => 'Анти-Бот', 'Read more' => 'Подробнее', 'DD.MM.YYYY' => 'ДД.ММ.ГГГГ', - 'Permission denied' => 'Нет прав доступа' + 'Permission denied' => 'Нет прав доступа', + 'Anti-Bot is not active.' => 'Анти-Бот не активен.', + 'Anti-Bot is active.' => 'Анти-Бот активен.' ); \ No newline at end of file diff --git a/themes/dark/assets/css/main.css b/themes/dark/assets/css/main.css index f6fcc96..c3aefc7 100644 --- a/themes/dark/assets/css/main.css +++ b/themes/dark/assets/css/main.css @@ -2470,6 +2470,9 @@ ul.vote-results li .vote-result { filter: invert(65%) contrast(300%); opacity: .6; } +.grecaptcha-badge { + filter: invert(65%) contrast(300%); +} /** fields **/ .record_fields_tabs_wrapper .nav-tabs { diff --git a/zira/form/factory.php b/zira/form/factory.php index 0696d94..bcaa5b5 100644 --- a/zira/form/factory.php +++ b/zira/form/factory.php @@ -80,6 +80,7 @@ class Factory { $this->_token = Form::getToken($this->_id, $this->_is_token_unique); $this->_validator = new Validator(); $this->_validator->setToken($this->_token); + $this->_validator->setFormId($this->_id); if ($this->_multipart) $this->_validator->setMultipart(true); } @@ -712,6 +713,7 @@ class Factory { $captcha_type = Zira\Config::get('captcha_type', Zira\Models\Captcha::TYPE_DEFAULT); if ($captcha_type == Zira\Models\Captcha::TYPE_NONE) return ''; else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA) return $this->_captcha_recaptcha(); + else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA_v3) return $this->_captcha_recaptcha3(); else return $this->_captcha_default($label, $description); } @@ -747,6 +749,15 @@ class Factory { ); return $this->wrap($label.$this->wrap($captcha,$this->_input_wrap_class)); } + + protected function _captcha_recaptcha3() { + $label = Form::label(' ', null, array('class'=>$this->_label_class)); + $captcha = Form::recaptcha3( + Zira\Config::get('recaptcha3_site_key',''), + str_replace('-', '_', $this->_id) + ); + return $this->wrap($label.$this->wrap($captcha,$this->_input_wrap_class)); + } public function validate() { if (!$this->getValidator()->validate()) { diff --git a/zira/form/form.php b/zira/form/form.php index 1532964..389bf0e 100644 --- a/zira/form/form.php +++ b/zira/form/form.php @@ -239,6 +239,18 @@ class Form { $html .= Helper::tag_close('div'); return $html; } + + public static function recaptcha3($site_key, $action, $wrapper_class='recaptcha3') { + $html = Helper::tag_open('div',array('class'=>$wrapper_class)); + $html .= Helper::tag('div', null, array( + 'class' => 'g-recaptcha3', + 'data-sitekey' => $site_key, + 'data-action' => $action + )); + $html .= Helper::tag('div', Zira\Locale::t('Anti-Bot is not active.').' '.Zira\Locale::t('Please wait').'...', array('data-error'=>Zira\Locale::t('Anti-Bot is not active.'),'data-success'=>Zira\Locale::t('Anti-Bot is active.'),'class'=>'g-recaptcha3-message')); + $html .= Helper::tag_close('div'); + return $html; + } public static function generateCaptcha() { $token = Request::get('token'); @@ -341,6 +353,32 @@ class Form { } return $result_data['success']; } + + public static function isRecaptcha3Valid($secret_key, $response_value, $action) { + if (!$secret_key || !$response_value || !$action) return false; + $data = http_build_query(array( + 'secret' => $secret_key, + 'response' => $response_value + )); + $options = array( + 'http' => array( + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $data + ) + ); + $context = stream_context_create($options); + try { + $result = file_get_contents(Zira\Models\Captcha::RECAPTCHA_VALIDATE_URL, false, $context); + if (!$result) throw new \Exception('An error occurred'); + $result_data = json_decode($result, true); + if (empty($result_data) || !array_key_exists('success', $result_data)) throw new \Exception('An error occurred'); + if (!array_key_exists('score', $result_data) || !array_key_exists('action', $result_data)) throw new \Exception('An error occurred'); + } catch (\Exception $e) { + return false; + } + return $result_data['success'] && $result_data['action']==$action && floatval($result_data['score'])>=Zira\Models\Captcha::RECAPTCHA_v3_MIN_SCORE; + } public static function getValue($token,$name,$method=Request::POST) { $_name = self::getFieldName($token, $name); diff --git a/zira/form/validator.php b/zira/form/validator.php index be3a32b..522aeb7 100644 --- a/zira/form/validator.php +++ b/zira/form/validator.php @@ -17,6 +17,7 @@ class Validator { protected $_fields = array(); protected $_message = ''; protected $_error_field = ''; + protected $_form_id = ''; const TYPE_STRING = 'string'; const TYPE_NUMBER = 'number'; @@ -42,6 +43,14 @@ class Validator { public function getToken() { return $this->_token; } + + public function setFormId($id) { + $this->_form_id = $id; + } + + public function getFormId() { + return $this->_form_id; + } public function setMethod($method) { $this->_method = $method; @@ -388,6 +397,7 @@ class Validator { $captcha_type = Zira\Config::get('captcha_type', Zira\Models\Captcha::TYPE_DEFAULT); if ($captcha_type == Zira\Models\Captcha::TYPE_NONE) return true; else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA) return $this->_validateCaptchaRecaptcha($field); + else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA_v3) return $this->_validateCaptchaRecaptcha3($field); else return $this->_validateCaptchaDefault($field); } @@ -398,11 +408,16 @@ class Validator { protected function _validateCaptchaRecaptcha(array $field) { return Form::isRecaptchaValid(Zira\Config::get('recaptcha_secret_key', ''), Zira\Request::post(Zira\Models\Captcha::RECAPTCHA_RESPONSE_INPUT)); } + + protected function _validateCaptchaRecaptcha3(array $field) { + return Form::isRecaptcha3Valid(Zira\Config::get('recaptcha3_secret_key', ''), Zira\Request::post(Zira\Models\Captcha::RECAPTCHA_RESPONSE_INPUT), str_replace('-', '_', $this->getFormId())); + } public function registerCaptchaLazy($form_id, $message) { $captcha_type = Zira\Config::get('captcha_type', Zira\Models\Captcha::TYPE_DEFAULT); if ($captcha_type == Zira\Models\Captcha::TYPE_NONE) return; else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA) return $this->_registerCaptchaLazyRecaptcha($form_id, $message); + else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA_v3) return $this->_registerCaptchaLazyRecaptcha3($form_id, $message); else return $this->_registerCaptchaLazyDefault($form_id, $message); } @@ -424,11 +439,21 @@ class Validator { 'message' => $message ); } + + protected function _registerCaptchaLazyRecaptcha3($form_id, $message) { + $this->_fields []= array( + 'type' => self::TYPE_CAPTCHA_LAZY, + 'name' => CAPTCHA_NAME, + 'form_id' => $form_id, + 'message' => $message + ); + } protected function validateCaptchaLazy(array $field) { $captcha_type = Zira\Config::get('captcha_type', Zira\Models\Captcha::TYPE_DEFAULT); if ($captcha_type == Zira\Models\Captcha::TYPE_NONE) return true; else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA) return $this->_validateCaptchaLazyRecaptcha($field); + else if ($captcha_type == Zira\Models\Captcha::TYPE_RECAPTCHA_v3) return $this->_validateCaptchaLazyRecaptcha3($field); else return $this->_validateCaptchaLazyDefault($field); } @@ -451,6 +476,16 @@ class Validator { return Form::isRecaptchaValid(Zira\Config::get('recaptcha_secret_key', ''), Zira\Request::post(Zira\Models\Captcha::RECAPTCHA_RESPONSE_INPUT)); } } + + protected function _validateCaptchaLazyRecaptcha3(array $field) { + if (Zira\Request::post(Zira\Models\Captcha::RECAPTCHA_RESPONSE_INPUT)===null && !Zira\Models\Captcha::isActive($field['form_id'])) { + Zira\Models\Captcha::register($field['form_id']); + return true; + } else { + Zira\Models\Captcha::register($field['form_id']); + return Form::isRecaptcha3Valid(Zira\Config::get('recaptcha3_secret_key', ''), Zira\Request::post(Zira\Models\Captcha::RECAPTCHA_RESPONSE_INPUT), str_replace('-', '_', $this->getFormId())); + } + } public function registerExists($field,$class,$property,$message) { $this->_fields []= array( diff --git a/zira/models/captcha.php b/zira/models/captcha.php index f9927ed..e041b4a 100644 --- a/zira/models/captcha.php +++ b/zira/models/captcha.php @@ -17,10 +17,12 @@ class Captcha extends Orm { const TYPE_NONE = 'none'; const TYPE_DEFAULT = 'default'; const TYPE_RECAPTCHA = 'recaptcha'; + const TYPE_RECAPTCHA_v3 = 'recaptcha3'; const RECAPTCHA_JS_URL = 'https://www.google.com/recaptcha/api.js'; const RECAPTCHA_VALIDATE_URL = 'https://www.google.com/recaptcha/api/siteverify'; const RECAPTCHA_RESPONSE_INPUT = 'g-recaptcha-response'; + const RECAPTCHA_v3_MIN_SCORE = .5; public static function getTable() { return self::$table; @@ -67,7 +69,8 @@ class Captcha extends Orm { return array( self::TYPE_NONE => \Zira\Locale::t('Do not use'), self::TYPE_DEFAULT => \Zira\Locale::t('Default'), - self::TYPE_RECAPTCHA => \Zira\Locale::t('Google reCaptcha') + self::TYPE_RECAPTCHA => \Zira\Locale::t('Google reCaptcha v2'), + self::TYPE_RECAPTCHA_v3 => \Zira\Locale::t('Google reCaptcha v3') ); } } \ No newline at end of file diff --git a/zira/view.php b/zira/view.php index d62228b..d8644db 100644 --- a/zira/view.php +++ b/zira/view.php @@ -363,8 +363,11 @@ class View { if (self::$_render_js_strings) { $js_scripts .= Helper::tag_open('script', array('type'=>'text/javascript')); $js_scripts .= 'zira_base = \''.Helper::baseUrl('').'\';'; - if (Config::get('captcha_type', Models\Captcha::TYPE_DEFAULT)==Models\Captcha::TYPE_RECAPTCHA) { + $captcha_type = Config::get('captcha_type', Models\Captcha::TYPE_DEFAULT); + if ($captcha_type==Models\Captcha::TYPE_RECAPTCHA) { $js_scripts .= 'zira_recaptcha_url = \''.Models\Captcha::RECAPTCHA_JS_URL.'?hl='.(Locale::getLanguage()).'&render=explicit&onload=zira_recaptcha_onload\';'; + } else if ($captcha_type==Models\Captcha::TYPE_RECAPTCHA_v3) { + $js_scripts .= 'zira_recaptcha3_url = \''.Models\Captcha::RECAPTCHA_JS_URL.'?hl='.(Locale::getLanguage()).'&render='.Config::get('recaptcha3_site_key','').'&onload=zira_recaptcha3_onload\';'; } $js_scripts .= 'zira_scroll_effects_enabled = '.(Config::get('site_scroll_effects',1) ? 'true' : 'false').';'; $js_scripts .= 'zira_show_images_description = '.(Config::get('site_parse_images',1) ? 'true' : 'false').';';