Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
}
</style>
</noscript>
<script src="https://www.google.com/recaptcha/api.js?render=6LcpTuUnAAAAAD6YCBdr_2-0b1AH8N6nXkYEG5G5"></script>
<!-- Google Tag Manager -->
<script>
(function(w, d, s, l, i) {
Expand Down
3 changes: 3 additions & 0 deletions src/js/utils/recaptcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
define(['js/utils/recaptcha_core'], function(core) {
return core;
});
113 changes: 113 additions & 0 deletions src/js/utils/recaptcha_core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* UMD-style module that works with RequireJS and CommonJS */
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.RecaptchaCore = factory();
}
})(this, function() {
var loadingPromise = null;
var SCRIPT_ID = 'ads-recaptcha-script';

var clearLoading = function() {
loadingPromise = null;
};

var createScript = function(siteKey) {
return new Promise(function(resolve, reject) {
var existing = document.getElementById(SCRIPT_ID);
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing);
}

var script = document.createElement('script');
script.id = SCRIPT_ID;
script.dataset.siteKey = siteKey;
script.async = true;
script.defer = true;
script.src =
'https://www.google.com/recaptcha/api.js?render=' +
encodeURIComponent(siteKey);
script.onload = function() {
if (window.grecaptcha && typeof window.grecaptcha.ready === 'function') {
window.grecaptcha.ready(function() {
resolve(window.grecaptcha);
});
} else {
reject(new Error('reCAPTCHA did not initialize'));
}
};
script.onerror = function() {
reject(new Error('reCAPTCHA failed to load'));
};
document.head.appendChild(script);
})
.then(function(result) {
clearLoading();
return result;
})
.catch(function(err) {
clearLoading();
throw err;
});
};

var loadRecaptcha = function(siteKey) {
var existing = document.getElementById(SCRIPT_ID);
if (
window.grecaptcha &&
typeof window.grecaptcha.execute === 'function' &&
existing &&
existing.dataset.siteKey === siteKey
) {
return Promise.resolve(window.grecaptcha);
}
if (loadingPromise) {
return loadingPromise;
}
loadingPromise = createScript(siteKey);
return loadingPromise;
};

var executeRecaptcha = function(siteKey, action, options) {
var timeoutMs = (options && options.timeoutMs) || 8000;
return new Promise(function(resolve, reject) {
var finished = false;
var timeout = setTimeout(function() {
if (!finished) {
finished = true;
reject(new Error('reCAPTCHA timed out'));
}
}, timeoutMs);

loadRecaptcha(siteKey)
.then(function() {
window.grecaptcha
.execute(siteKey, { action: action })
.then(function(token) {
finished = true;
clearTimeout(timeout);
resolve(token);
})
.catch(function(err) {
finished = true;
clearTimeout(timeout);
reject(err);
});
})
.catch(function(err) {
finished = true;
clearTimeout(timeout);
reject(err);
});
});
};

return {
loadRecaptcha: loadRecaptcha,
executeRecaptcha: executeRecaptcha,
SCRIPT_ID: SCRIPT_ID,
};
});
38 changes: 30 additions & 8 deletions src/js/widgets/authentication/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ define([
'analytics',
'backbone-validation',
'backbone.stickit',
'js/utils/recaptcha',
], function(
Marionette,
BaseWidget,
Expand All @@ -29,7 +30,8 @@ define([
ResetPassword2Template,
ResendVerificationEmail,
User,
analytics
analytics,
recaptchaUtils
) {
// Creating module level variable since I can't figure out best way to pass this value into a subview from the model
// This value should be always available, and unchanging, so should be safe to set like this here
Expand Down Expand Up @@ -71,17 +73,37 @@ define([
triggerSubmit: function(ev) {
ev.preventDefault();
const formName = ev.currentTarget.dataset.formName;
if (!window.grecaptcha) {
this.showError('Sorry reCAPTCHA did not load properly. Please try refreshing the page.');
return;
}
if (typeof formName === 'string') {
window.grecaptcha.ready(() =>
window.grecaptcha.execute(siteKey, { action: `auth/${formName}` }).then((token) => {
recaptchaUtils
.executeRecaptcha(siteKey, `auth/${formName}`)
.then((token) => {
this.model.set('g-recaptcha-response', token);
FormFunctions.triggerSubmit.apply(this, arguments);
})
);
.catch((err) => {
analytics('send', 'event', 'auth', 'error', 'recaptcha');
const whenReady =
typeof window.whenSentryReady === 'function'
? window.whenSentryReady()
: Promise.resolve(window.Sentry);
whenReady
.then((sentry) => {
if (sentry && typeof sentry.captureMessage === 'function') {
sentry.captureMessage('auth-recaptcha-error', {
level: 'error',
extra: {
message:
(err && err.message) ||
(typeof err === 'string' ? err : 'unknown'),
},
});
}
})
.catch(() => {});
this.showError(
'Sorry reCAPTCHA did not load properly. Please try refreshing the page.'
);
});
} else {
FormFunctions.triggerSubmit.apply(this, arguments);
}
Expand Down
98 changes: 81 additions & 17 deletions src/js/widgets/navbar/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ define([
'js/components/api_request',
'js/components/api_targets',
'utils',
'analytics',
'js/utils/recaptcha',
'bootstrap',
], function(
_,
Expand All @@ -20,7 +22,9 @@ define([
ApiQuery,
ApiRequest,
ApiTargets,
utils
utils,
analytics,
recaptchaUtils
) {
var NavView;
var NavModel;
Expand Down Expand Up @@ -360,6 +364,17 @@ define([
},

submitForm: function($form, $modal) {
const showRecaptchaError = (message) => {
let $error = $form.find('.recaptcha-inline-error');
if (!$error.length) {
$error = $(
'<div class="alert alert-danger recaptcha-inline-error" role="alert"></div>'
);
$form.find('.g-recaptcha').prepend($error);
}
$error.text(message);
};

const submit = () => {
this._sendFeedbackToSentry($form);
var data = $form.serialize();
Expand All @@ -386,14 +401,18 @@ define([
}, 500);
}

function fail(err) {
const fail = (err) => {
this._trackFeedbackIssue('submit-fail', {
status: err && err.status,
statusText: err && err.statusText,
});
$form
.find('button[type=submit]')
.addClass('btn-danger')
.html(
'<i class="icon-danger" aria-hidden="true"></i> There was an error!'
);
}
};

var request = new ApiRequest({
target: ApiTargets.FEEDBACK,
Expand All @@ -411,21 +430,66 @@ define([
.request(request);
};

const siteKey = this.getBeeHive().getObject('AppStorage').getConfigCopy().recaptchaKey;
window.grecaptcha.ready(() => {
window.grecaptcha
.execute(siteKey, { action: 'feedback/general' })
.then((token) => {
console.log('called', token);
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'g-recaptcha-response';
input.value = token;
$form.append(input);
console.log('form', $form.html());
submit();
const siteKey = this.getBeeHive()
.getObject('AppStorage')
.getConfigCopy().recaptchaKey;

recaptchaUtils
.executeRecaptcha(siteKey, 'feedback/general')
.then((token) => {
$form.find('input[name="g-recaptcha-response"]').remove();

const input = document.createElement('input');
input.type = 'hidden';
input.name = 'g-recaptcha-response';
input.value = token;
$form.append(input);

submit();
})
.catch((err) => {
this._trackFeedbackIssue('recaptcha', {
action: 'feedback/general',
message: err && err.message ? err.message : err,
});
});
showRecaptchaError(
'reCAPTCHA could not run. Please allow Google reCAPTCHA or email adshelp@cfa.harvard.edu to send your feedback.'
);
$form
.find('button[type=submit]')
.addClass('btn-danger')
.html(
'<i class="icon-danger" aria-hidden="true"></i> There was an error!'
);
});
},

_trackFeedbackIssue: function(reason, extra) {
if (reason) {
analytics('send', 'event', 'feedback', 'error', reason);
}
const message =
(extra && extra.message) || (typeof extra === 'string' ? extra : 'unknown');
const whenReady =
typeof window.whenSentryReady === 'function'
? window.whenSentryReady()
: Promise.resolve(window.Sentry);

whenReady
.then((sentry) => {
if (!sentry || typeof sentry.captureMessage !== 'function') {
return;
}
sentry.captureMessage('feedback-issue', {
level: 'error',
extra: {
reason,
message,
extra,
},
});
})
.catch(() => {});
},

_sendFeedbackToSentry: function($form) {
Expand Down