From cca06da2e4c1e3bfef597994db8ceb72796b50b5 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Thu, 21 Sep 2017 17:53:28 +0000 Subject: Standardize access to CSRF token in JavaScript --- app/assets/javascripts/blob/blob_file_dropzone.js | 5 +- app/assets/javascripts/dropzone_input.js | 9 ++-- app/assets/javascripts/lib/utils/csrf.js | 56 ++++++++++++++++++++++ .../vue_shared/vue_resource_interceptor.js | 5 +- spec/javascripts/lib/utils/csrf_token_spec.js | 49 +++++++++++++++++++ 5 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/csrf.js create mode 100644 spec/javascripts/lib/utils/csrf_token_spec.js diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 26d3419a162..ddd1fea3aca 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -3,6 +3,7 @@ import '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; +import csrf from '../lib/utils/csrf'; function toggleLoading($el, $icon, loading) { if (loading) { @@ -36,9 +37,7 @@ export default class BlobFileDropzone { maxFiles: 1, addRemoveLinks: true, previewsContainer: '.dropzone-previews', - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'), - }, + headers: csrf.headers, init: function () { this.on('addedfile', function () { toggleLoading(submitButton, submitButtonLoadingIcon, false); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 975903159be..1cba65d17cd 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,7 @@ /* global Dropzone */ import _ from 'underscore'; import './preview_markdown'; +import csrf from './lib/utils/csrf'; window.DropzoneInput = (function() { function DropzoneInput(form) { @@ -50,9 +51,7 @@ window.DropzoneInput = (function() { paramName: 'file', maxFilesize: maxFileSize, uploadMultiple: false, - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') - }, + headers: csrf.headers, previewContainer: false, processing: function() { return $('.div-dropzone-alert').alert('close'); @@ -260,9 +259,7 @@ window.DropzoneInput = (function() { dataType: 'json', processData: false, contentType: false, - headers: { - 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') - }, + headers: csrf.headers, beforeSend: function() { showSpinner(); return closeAlertMessage(); diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js new file mode 100644 index 00000000000..ae41cc5e8a8 --- /dev/null +++ b/app/assets/javascripts/lib/utils/csrf.js @@ -0,0 +1,56 @@ +/* +This module provides easy access to the CSRF token and caches +it for re-use. It also exposes some values commonly used in relation +to the CSRF token (header key and headers object). + +If you need to refresh the csrfToken for some reason, just call `init` and +then use the accessors as you would normally. + +If you need to compose a headers object, use the spread operator: + +``` + headers: { + ...csrf.headers, + someOtherHeader: '12345', + } +``` + */ + +const csrf = { + init() { + const tokenEl = document.querySelector('meta[name=csrf-token]'); + + if (tokenEl !== null) { + this.csrfToken = tokenEl.getAttribute('content'); + } else { + this.csrfToken = null; + } + }, + + get token() { + return this.csrfToken; + }, + + get headerKey() { + return 'X-CSRF-Token'; + }, + + get headers() { + if (this.csrfToken !== null) { + return { + [this.headerKey]: this.token, + }; + } + return {}; + }, +}; + +csrf.init(); + +// use our cached token for any $.rails-generated AJAX requests +if ($.rails) { + $.rails.csrfToken = () => csrf.token; +} + +export default csrf; + diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index 7f8e514fda1..b9693892f45 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; +import csrf from '../lib/utils/csrf'; Vue.use(VueResource); @@ -18,9 +19,7 @@ Vue.http.interceptors.push((request, next) => { // New Vue Resource version uses Headers, we are expecting a plain object to render pagination // and polling. Vue.http.interceptors.push((request, next) => { - if ($.rails) { - request.headers.set('X-CSRF-Token', $.rails.csrfToken()); - } + request.headers.set(csrf.headerKey, csrf.token); next((response) => { // Headers object has a `forEach` property that iterates through all values. diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js new file mode 100644 index 00000000000..c484213df8e --- /dev/null +++ b/spec/javascripts/lib/utils/csrf_token_spec.js @@ -0,0 +1,49 @@ +import csrf from '~/lib/utils/csrf'; + +describe('csrf', () => { + beforeEach(() => { + this.tokenKey = 'X-CSRF-Token'; + this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ=='; + }); + + it('returns the correct headerKey', () => { + expect(csrf.headerKey).toBe(this.tokenKey); + }); + + describe('when csrf token is in the DOM', () => { + beforeEach(() => { + setFixtures(` + + `); + + csrf.init(); + }); + + it('returns the csrf token', () => { + expect(csrf.token).toBe(this.token); + }); + + it('returns the csrf headers object', () => { + expect(csrf.headers[this.tokenKey]).toBe(this.token); + }); + }); + + describe('when csrf token is not in the DOM', () => { + beforeEach(() => { + setFixtures(` + + `); + + csrf.init(); + }); + + it('returns null for token', () => { + expect(csrf.token).toBeNull(); + }); + + it('returns empty object for headers', () => { + expect(typeof csrf.headers).toBe('object'); + expect(Object.keys(csrf.headers).length).toBe(0); + }); + }); +}); -- cgit v1.2.3