Skip to content

Commit 11553b5

Browse files
committed
Extract csrf logic in csrf form helper file
Replicate some of the rails ujs login in csrf_form_helper: - read the csrf meta tags with getCsrfToken() and getCsrfParam() - normalizeOptions sets method and credentials - adds X-CSRF_Token and X-Requested-With and Accept - fetchWithCsrf() -> equivelant to rails ajax call - refreshCSRFTokens() -> updates hidden authenticity token in forms. we hook it with DOMContentLoaded - a public API Orangelight.CsrfFormHelper and window.CsrfFormHelper similar to rails ajax related to #5465
1 parent 2281479 commit 11553b5

2 files changed

Lines changed: 118 additions & 39 deletions

File tree

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// This is a manifest file that'll be compiled into application.js, which will include all the files
22
// listed below.
33
//
4-
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5-
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
64
//
75
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
86
// compiled file.
@@ -19,50 +17,59 @@
1917
// Required by Blacklight
2018
//= require blacklight/blacklight
2119
//= require babel/polyfill
20+
//= require csrf_form_helper
2221

23-
// Wait for the modal to open
2422
document.addEventListener('show.blacklight.blacklight-modal', function () {
25-
// Attach a vanilla submit handler to modal forms that submits via fetch
26-
// and hides the Blacklight modal on a successful response. We mark forms
27-
// that we've attached to so we don't double-bind when the modal reopens.
23+
console.log('Modal is going to be shown');
2824
document.querySelectorAll('.modal_form').forEach(function (form) {
29-
if (form.dataset.vanillaHandlerAdded) return;
30-
form.dataset.vanillaHandlerAdded = 'true';
25+
form.addEventListener(
26+
'submit',
27+
function (e) {
28+
e.preventDefault();
3129

32-
form.addEventListener('submit', function (e) {
33-
e.preventDefault();
30+
var action = form.getAttribute('action') || window.location.href;
31+
var method = (form.getAttribute('method') || 'GET').toUpperCase();
32+
var fetchOptions = { method: method };
3433

35-
var action = form.getAttribute('action') || window.location.href;
36-
var method = (form.getAttribute('method') || 'GET').toUpperCase();
37-
var fetchOptions = { method: method, credentials: 'same-origin' };
34+
var formData = new FormData(form);
35+
console.log(
36+
`method: ${method}, action: ${action}, formData:, ${formData}`
37+
);
38+
console.log(formData);
39+
if (method !== 'GET') {
40+
fetchOptions.body = formData;
41+
}
3842

39-
var formData = new FormData(form);
40-
41-
if (method === 'GET') {
42-
// Append form data to query string for GET
43-
var params = new URLSearchParams(formData);
44-
action += (action.indexOf('?') === -1 ? '?' : '&') + params.toString();
45-
} else {
46-
fetchOptions.body = formData;
47-
}
48-
49-
fetch(action, fetchOptions)
50-
.then(function (response) {
51-
if (response.ok) {
52-
Blacklight.Modal.hide();
53-
} else {
43+
// otherwise we get 422 error due to missing CSRF token
44+
Orangelight.CsrfFormHelper.fetch(action, fetchOptions)
45+
.then(function (response) {
5446
return response.text().then(function (body) {
55-
console.error(
56-
'Modal form submission failed',
57-
response.status,
58-
body
59-
);
47+
if (response.ok) {
48+
var modalEl =
49+
document.querySelector('.modal') ||
50+
document.querySelector('.blacklight-modal') ||
51+
document.getElementById('blacklight-modal');
52+
if (modalEl) {
53+
modalEl.innerHTML = body;
54+
} else {
55+
var wrapper = document.createElement('div');
56+
wrapper.innerHTML = body;
57+
document.body.appendChild(wrapper);
58+
}
59+
} else {
60+
console.error(
61+
'Modal form submission failed',
62+
response.status,
63+
body
64+
);
65+
}
6066
});
61-
}
62-
})
63-
.catch(function (err) {
64-
console.error('Modal form submission error', err);
65-
});
66-
});
67+
})
68+
.catch(function (err) {
69+
console.error('Modal form submission error', err);
70+
});
71+
},
72+
{ once: true }
73+
);
6774
});
6875
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// CSRF form helper to perform fetch requests with CSRF tokens included
2+
(function () {
3+
var Orangelight = window.Orangelight || (window.Orangelight = {});
4+
5+
function getCsrfToken() {
6+
var meta = document.querySelector('meta[name="csrf-token"]');
7+
return meta && meta.getAttribute('content');
8+
}
9+
10+
function getCsrfParam() {
11+
var meta = document.querySelector('meta[name="csrf-param"]');
12+
return meta && meta.getAttribute('content');
13+
}
14+
15+
function normalizeOptions(url, options) {
16+
options = options || {};
17+
options.method = (options.method || 'GET').toUpperCase();
18+
options.credentials = options.credentials || 'same-origin';
19+
options.headers = options.headers || {};
20+
21+
var token = getCsrfToken();
22+
if (token) {
23+
options.headers['X-CSRF-Token'] = token;
24+
}
25+
options.headers['X-Requested-With'] =
26+
options.headers['X-Requested-With'] || 'XMLHttpRequest';
27+
options.headers['Accept'] =
28+
options.headers['Accept'] ||
29+
'text/javascript, text/html, application/json, application/xml';
30+
31+
if (options.method === 'GET' && options.body instanceof FormData) {
32+
var params = new URLSearchParams();
33+
options.body.forEach(function (value, key) {
34+
params.append(key, value);
35+
});
36+
url += (url.indexOf('?') === -1 ? '?' : '&') + params.toString();
37+
delete options.body;
38+
}
39+
40+
return { url: url, options: options };
41+
}
42+
43+
function fetchWithCsrf(url, options) {
44+
var normalized = normalizeOptions(url, options);
45+
return fetch(normalized.url, normalized.options);
46+
}
47+
48+
Orangelight.CsrfFormHelper = Orangelight.CsrfFormHelper || {};
49+
Orangelight.CsrfFormHelper.fetch = fetchWithCsrf;
50+
51+
// Refresh function similar to rails-ujs to update hidden
52+
// authenticity_token inputs in forms after token rotation.
53+
function refreshCSRFTokens() {
54+
var token = getCsrfToken();
55+
var param = getCsrfParam();
56+
if (!token || !param) return;
57+
// Update any inputs matching the csrf param
58+
var selector = 'form input[name="' + param + '"]';
59+
document.querySelectorAll(selector).forEach(function (input) {
60+
input.value = token;
61+
});
62+
}
63+
64+
Orangelight.CsrfFormHelper.refreshCSRFTokens = refreshCSRFTokens;
65+
66+
// Hook to DOMContentLoaded so server-rendered forms have current token
67+
document.addEventListener('DOMContentLoaded', function () {
68+
refreshCSRFTokens();
69+
});
70+
71+
window.CsrfFormHelper = window.CsrfFormHelper || Orangelight.CsrfFormHelper;
72+
})();

0 commit comments

Comments
 (0)