/************************************************************************************* 程式功能 : 人機驗證前端(#54295 Google reCAPTCHA V3) 說明 : window._captcha 由 cloudflare_turnstile::scriptTag() 輸出 {mode, siteKey} reCAPTCHA V3 權杖 2 分鐘失效且只能使用一次 → 一律在送出當下才取得,不在進頁時預先取得 取不到權杖(Google 程式未載入/逾時/錯誤)時照樣送出空權杖,由後端擋下並記錄 NO_TOKEN 用法 : 1. 走 fromsAuth 的表單:form 加 data-captcha-action="xxx" 即可(front_formsauth.js/quick_formsauth.js 已整合) 2. 原生送出的表單:form 加 data-captcha-action="xxx" data-captcha-bind="native" 3. 自組 ajax:Captcha.run('xxx').done(function(token){ data['g-recaptcha-response'] = token; ... }) action 名稱須與後端 cloudflare_turnstile::check()/checkAjax()/verify() 帶入的 action 相同 *************************************************************************************/ (function(window, $){ var FIELD = 'g-recaptcha-response'; var TIMEOUT = 8000; var Captcha = { FIELD: FIELD, mode: function(){ return (window._captcha && window._captcha.mode) ? window._captcha.mode : 'off'; }, isRecaptcha: function(){ return this.mode() === 'recaptcha'; }, /** * 取得權杖;非 reCAPTCHA 模式或失敗時回傳空字串(不會 reject) * @return jQuery Promise */ run: function(action){ var d = $.Deferred(); var finished = false; var finish = function(token){ if(finished) return; finished = true; d.resolve(token || ''); }; if(!this.isRecaptcha() || typeof window.grecaptcha === 'undefined' || !window.grecaptcha.ready){ finish(''); return d.promise(); } setTimeout(function(){ finish(''); }, TIMEOUT); try{ window.grecaptcha.ready(function(){ try{ window.grecaptcha.execute(window._captcha.siteKey, {action: action}).then( function(token){ finish(token); }, function(){ finish(''); } ); }catch(e){ finish(''); } }); }catch(e){ finish(''); } return d.promise(); }, /** * 表單是否需要在送出前取得權杖 */ needToken: function($form){ return this.isRecaptcha() && !!$form.attr('data-captcha-action'); }, /** * 取得權杖並寫入表單的隱藏欄位(沒有就建立) * @return jQuery Promise */ fill: function($form){ return this.run($form.attr('data-captcha-action')).then(function(token){ var $input = $form.find('input[name="' + FIELD + '"]'); if(!$input.length){ $input = $('').appendTo($form); } $input.val(token); return token; }); }, /** * 手動渲染 Turnstile 元件(Turnstile 只在頁面初次載入時自動渲染,ajax 載入的元件須手動呼叫) * @param scope 範圍(選擇器或元素),省略為整頁 */ renderTurnstile: function(scope){ if(this.mode() !== 'turnstile' || typeof window.turnstile === 'undefined'){ return; } $(scope || document).find('.cf-turnstile').each(function(){ if(!this.childElementCount){ try{ window.turnstile.render(this); }catch(e){} } }); }, /** * 原生送出(不再觸發 jQuery submit 事件,避免重複驗證) */ nativeSubmit: function(form){ HTMLFormElement.prototype.submit.call(form); } }; window.Captcha = Captcha; //頁面自瀏覽器快取還原(如驗證/必填不通過後 history.back())時,Turnstile 元件仍留著已使用過的權杖(只能用一次)→ 重設以取得新權杖 $(window).on('pageshow', function(e){ if(e.originalEvent && e.originalEvent.persisted && Captcha.mode() === 'turnstile' && typeof window.turnstile !== 'undefined'){ $('.cf-turnstile').each(function(){ try{ window.turnstile.reset(this); }catch(err){} }); } }); //原生送出的表單:攔截 submit → 取得權杖 → 原生送出 $(document).on('submit', 'form[data-captcha-action][data-captcha-bind="native"]', function(e){ var $form = $(this); if(!Captcha.needToken($form)){ return true; } e.preventDefault(); var form = this; Captcha.fill($form).done(function(){ Captcha.nativeSubmit(form); }); return false; }); })(window, jQuery);