routerconsole/jsp/js/csrf.js

/**
 * @module csrf
 * @description CSRF protection wrappers for fetch and XMLHttpRequest.
 * Uses double-submit pattern: reads token from meta tag and cookie,
 * sends both in request headers for defense-in-depth.
 * @author dr|z3d
 * @license AGPL3 or later
 * @description Provides CSRF protection wrappers for fetch and XMLHttpRequest.
 * Uses double-submit cookie pattern: reads token from both meta tag and cookie,
 * then sends both in request headers for defense-in-depth.
 * @requires fetch API
 * @requires XMLHttpRequest
 * @example
 * // Using csrfFetch (drop-in replacement for fetch)
 * csrfFetch('/configupdate', {method: 'POST', body: 'data'})
 *   .then(response => console.log(response));
 * @example
 * // Using csrfXHR (replacement for new XMLHttpRequest)
 * var xhr = csrfXHR();
 * xhr.open('POST', '/endpoint');
 * xhr.send('data');
 */

/**
 * Retrieves CSRF token from meta tag or hidden form input.
 * @function getCsrfTokenFromMeta
 * @returns {string|null} CSRF token string or null if not found
 */
function getCsrfTokenFromMeta() {
  var meta = document.querySelector('meta[name="csrf-token"]');
  if (meta) return meta.getAttribute('content');
  var input = document.querySelector('input[name="nonce"]');
  if (input) return input.value;
  input = document.querySelector('input[name="consoleNonce"]');
  if (input) return input.value;
  return null;
}

/**
 * Retrieves CSRF token from cookie.
 * @function getCsrfTokenFromCookie
 * @returns {string|null} CSRF token from cookie or null if not found
 */
function getCsrfTokenFromCookie() {
  var cookies = document.cookie.split(';');
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i].trim();
    if (cookie.indexOf('csrfToken=') === 0) {
      return cookie.substring('csrfToken='.length);
    }
  }
  return null;
}

/**
 * Fetches resource with CSRF protection headers.
 * @function csrfFetch
 * @param {string} url - The URL to fetch
 * @param {Object} [options] - Fetch options
 * @param {string} [options.method='GET'] - HTTP method
 * @param {Object} [options.headers={}] - Additional headers
 * @param {BodyInit} [options.body] - Request body
 * @param {boolean} [options.credentials] - Include credentials
 * @returns {Promise<Response>} Fetch response promise
 * @example
 * // POST with data
 * csrfFetch('/api endpoint', {
 *   method: 'POST',
 *   body: JSON.stringify({key: 'value'})
 * });
 */
function csrfFetch(url, options) {
  options = options || {};
  options.headers = options.headers || {};
  if (csrfMetaToken) {
    options.headers['X-CSRF-Token'] = csrfMetaToken;
  }
  if (csrfCookieToken && csrfCookieToken !== csrfMetaToken) {
    options.headers['X-CSRF-Token-Cookie'] = csrfCookieToken;
  }
  return fetch(url, options);
}

/**
 * Creates XMLHttpRequest with CSRF protection headers.
 * @function csrfXHR
 * @returns {XMLHttpRequest} XMLHttpRequest instance with CSRF headers
 * @example
 * var xhr = csrfXHR();
 * xhr.open('POST', '/configupdate');
 * xhr.onreadystatechange = function() {
 *   if (xhr.readyState === 4 && xhr.status === 200) {
 *     console.log(xhr.responseText);
 *   }
 * };
 * xhr.send('data=1');
 */
function csrfXHR() {
  var xhr = new XMLHttpRequest();
  var originalOpen = xhr.open;
  xhr.open = function(method, url, async, user, password) {
    arguments[1] = url;
    var result = originalOpen.apply(this, arguments);
    if (csrfMetaToken) {
      this.setRequestHeader('X-CSRF-Token', csrfMetaToken);
    }
    if (csrfCookieToken && csrfCookieToken !== csrfMetaToken) {
      this.setRequestHeader('X-CSRF-Token-Cookie', csrfCookieToken);
    }
    return result;
  };
  return xhr;
}

// Initialize tokens at module load time
var csrfMetaToken = getCsrfTokenFromMeta();
var csrfCookieToken = getCsrfTokenFromCookie();

// Expose functions to global scope
window.csrfFetch = csrfFetch;
window.csrfXHR = csrfXHR;