1/** 2 * Prevents double form submissions across the frontend: the first click on a submit 3 * button disables that form's submit buttons, so a visitor cannot fire the same 4 * request - and the same reCAPTCHA token, which Google rejects on the second use - 5 * by clicking twice. Every click is also logged, see logClick() below. 6 * 7 * Hooked on the button click and not on the form's submit event on purpose: when a 8 * page's own submit handler throws (a missing reCAPTCHA widget does exactly that), 9 * the event never reaches a delegated submit handler, while the click still does. 10 * 11 * The buttons come back after a short window, or right away when a request failed, 12 * so no form is left with a dead button. Forms failing HTML5 validation are not 13 * touched at all - nothing is being sent, so there is nothing to guard. 14 */ 15(function ($) { 16 'use strict'; 17 18 var LOCK_TIME = 5000; // long enough to swallow repeated clicks on one submission 19 var guarded = []; // forms locked by this script, newest last 20 21 function unlock(entry) { 22 if (!entry || entry.done) { 23 return; 24 } 25 entry.done = true; 26 clearTimeout(entry.timer); 27 entry.$buttons.prop('disabled', false).removeClass('disabled'); 28 29 var index = guarded.indexOf(entry); 30 if (index > -1) { 31 guarded.splice(index, 1); 32 } 33 } 34 35 // sent with fetch, not jQuery, to keep this beacon out of the page's own ajax 36 // handlers and out of the ajaxError hook below 37 function logClick(formId) { 38 var url = window.formSubmitLogUrl; 39 if (!url) { 40 return; 41 } 42 43 try { 44 fetch(url, { 45 method: 'POST', 46 headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': window.csrf_token }, 47 body: JSON.stringify({ s: { form: formId } }), 48 keepalive: true 49 })['catch'](function () {}); 50 } catch (e) {} 51 } 52 53 function lock($form, $button) { 54 var $buttons = $form.find('button[type="submit"], input[type="submit"]').not(':disabled'); 55 56 if (!$buttons.length) { 57 return; 58 } 59 60 // HTML5 validation stops the submission itself - leave the buttons alone 61 var form = $form.get(0); 62 if (!$button.is('[formnovalidate]') && form && form.checkValidity && !form.checkValidity()) { 63 return; 64 } 65 66 var entry = { $buttons: $buttons, done: false }; 67 guarded.push(entry); 68 69 // disabled on the next tick: a control disabled while the click is still being 70 // handled is dropped from the posted data (and from serializeArray) 71 setTimeout(function () { 72 $buttons.prop('disabled', true).addClass('disabled'); 73 entry.timer = setTimeout(function () { 74 unlock(entry); 75 }, LOCK_TIME); 76 }, 0); 77 } 78 79 $(document).on('click', 'button[type="submit"], input[type="submit"]', function () { 80 var $button = $(this); 81 var $form = $button.closest('form'); 82 83 logClick($form.attr('id') || ''); 84 85 if ($form.length) { 86 lock($form, $button); 87 } 88 }); 89 90 // a failed request means nothing was submitted - let the visitor retry at once 91 $(document).ajaxError(function () { 92 for (var i = guarded.length - 1; i >= 0; i--) { 93 unlock(guarded[i]); 94 } 95 }); 96})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.