pxlNav/core/CookieManager.js

/////////////////////////////////////
// pxlCookieManager -
//  Written by Kevin Edzenga; 2020, 2025
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
//  Read, Write, Clear, a Check if a cookie exists
//  Will prepend a given name to isolate cookies,
//    That said, cookies should be set per room if this is used in pxlNav
//      This way dynamic rooms can maintain
//        cookies between Networking rooms / Environments
//      This would be your "per room" user data
//    If not used in pxlNav, just provide a unique prefix to the constructor

/**
 * @alias pxlCookie
 * @class
 * @description Cookie management
 */

// This is only used for `CookieManager.log()`
//   Alter the verbose check in `log()` to make this file standalone
import { VERBOSE_LEVEL } from "./Enums.js";

/**
 * @alias pxlCookie
 * @class
 * @description CookieManager class for managing browser cookies with a specific prefix.
 * <br/>Provides methods to read, write, clear, and check the existence of cookies.
 * <br/>Handles value serialization, expiration, and path management.
 * 
 * Use this class if you want to store user data between sessions.
 */
export class CookieManager{
  /**
   * @memberof pxlCookie
   * @constructor
   * @description Creates an instance of CookieManager.
   * @param {boolean} [verbose=false] - Enable verbose logging.
   * @param {string} [prefix="pxlNav-"] - Prefix to prepend to all cookie names.
   * @param {string} [path="/"] - Path for which the cookie is valid.
   * @param {number} [expiration=30] - Number of days until the cookie expires.
   */
  constructor(verbose=false, prefix="pxlNav-", path="/", expiration=30){
    // Suffix name to help searching and avoid cookie name conflictions
    let prepPrepend= prefix.substring(-1)==="-" ? prefix : prefix+"-" ;
    this.prepend=prepPrepend; 
    this.verbose=verbose;
    
    // Days till expiration
    this.exp=expiration; 
    
    // Update this with the folder name from your domain
    this.path="path="+path; 
    
    // Do not edit this--
    //  This forces cookies to be removed when the expiration value
    //    is set prior to the current date
    this.deleteDate="expires=Thu, 01 Jan 1970 00:00:01 GMT;";
    
    // If variables contain a ';'
    //   The variable will break the cookies for the site
    // Substitute ; with _%_
    this.sub="_%_";
  }

  // -- -- --

  // Simple logger with verbose check
  log( ...logger ){
    if( this.verbose >= VERBOSE_LEVEL.INFO ){
      console.log("-- Cookie Manager --");
      logger.forEach( (l)=>{ console.log(l); } );
    }
  }

  // -- -- --

  // Set expiration date for the new cookie
  /**
   * @method
   * @memberof pxlCookie
   * @function getExpiration
   * @description Returns the expiration string for a new cookie.
   * @returns {string} Expiration string for the cookie.
   */
  getExpiration(){ 
    let d = new Date();
    d.setTime( d.getTime() + (this.exp*24*60*60*1000) );
    return "expires=" + d.toGMTString() + ";";
  }

  // Is the cookie value equal to a given input?
  /**
   * @method
   * @memberof pxlCookie
   * @function isEqual
   * @description Checks if the value of a cookie is equal to a given input.
   * @param {string} cName - Cookie name (without prefix).
   * @returns {boolean} True if the cookie value matches the input.
   */
  isEqual( cName ){ 
    if( this.hasCookie(cName) ){
      this.log( cName );
      return this.readCookie( cName ) === this.variableToString( cName );
    }
    return false;
  }

  
  /**
   * @method
   * @memberof pxlCookie
   * @function getRegexp
   * @description Returns a RegExp to match cookies with the given name.
   * @param {string} cName - Cookie name (with or without prefix).
   * @returns {RegExp} Regular expression for matching the cookie.
   */
  getRegexp( cName ){
    return new RegExp( '(?=' + cName + ').*?((?=;)|(?=$))', 'g' );
  }

  // TODO : Validate this opposed to `getRegexp()`
  /**
   * @method
   * @memberof pxlCookie
   * @function getClearCookieRegexp
   * @description Returns a RegExp to match cookies for clearing.
   * @param {string} cName - Cookie name (with or without prefix).
   * @returns {RegExp} Regular expression for matching the cookie to clear.
   */
  getClearCookieRegexp( cName ){
    return new RegExp( '(?=' + cName + ').*?(?==)', 'g' );
  }

  // -- -- --
  
  // Read all of CookieManager's controlled cookies
  //   Returns a dictionary of `this.prepend` cookies
  /**
   * @method
   * @memberof pxlCookie
   * @function pullData
   * @description Reads all cookies managed by this CookieManager.
   * @returns {Object} Dictionary of cookie names and their values.
   */
  pullData(){ 
    let cur = document.cookie;
    let reg = this.getRegexp( this.prepend );
    let curCookies = cur.match( reg );
    
    let ret = {};
    if( curCookies ){
      curCookies.forEach( c =>{ 
        let cName = c.split("=")[0].replace(this.prepend,'');
        let cData = c.split("=")[1].replace(this.sub,';');
        ret[cName] = cData;
      });
    }
    
    return ret;
  }

  // -- -- --

  // Convert given value to a string
  
  /**
   * @method
   * @memberof pxlCookie
   * @function valueToString
   * @description Converts a given value to a string representation.
   * @param {any} val - Value to convert.
   * @returns {string} String representation of the value.
   */
  valueToString(val){ 
    let type = typeof(val);
    
    if( !isNaN(Number(val)) ){
      return val;
    }else if( type==="string" ){
      return "'" + val + "'";
    }else if( type==="boolean" ){
      return ( val ? "true" : "false" );
    }else if(val===null || val===undefined){ // Non-Strict Null Check; null===undefined true; null===undefined false
      return "null";
    }else if( isNaN(Number(val) )){
      return "NaN";
    }else{
      return val;
    }
  }

  // Convert a given variable to a string; account for arrays or not
  
  /**
   * @method
   * @memberof pxlCookie
   * @function variableToString
   * @description Converts a variable (including arrays) to a string representation.
   * @param {any} arr - Variable or array to convert.
   * @returns {string} String representation of the variable.
   */
  variableToString(arr){ 
    if( Array.isArray(arr) ){
      let ret = arr.map((x)=>{
        if( Array.isArray(x) ){
          return this.variableToString(x);
        }
        return this.valueToString(x);
      });
      return "[" + ret.join(",") + "]";
    }else{
      return this.valueToString( arr );
    }
  }

  // -- -- --

  // Check if a cookie exists
  
  /**
   * @method
   * @memberof pxlCookie
   * @function hasCookie
   * @description Checks if a cookie exists.
   * @param {string} cName - Cookie name (without prefix).
   * @returns {boolean} True if the cookie exists.
   */
  hasCookie( cName ){ 
    return document.cookie.includes( this.prepend + cName );
  }
  // Read the value of the cookie; returns string
  
  /**
   * @method
   * @memberof pxlCookie
   * @function readCookie
   * @description Reads the value of a cookie as a string.
   * @param {string} cName - Cookie name (without prefix).
   * @returns {string|null} Value of the cookie, or null if not found.
   */
  readCookie( cName ){ 
    if(this.hasCookie(cName)){
      let reg = this.getRegexp( this.prepend + cName );
      
      let val = document.cookie.match( reg )[0].split( "=" )[1].replace( this.prepend, '' ).replace( this.sub, ';' );
      
      return val;
    }
    return null;
  }
  // Read the value of the cookie; returns rectified value
  

  /**
   * @method
   * @memberof pxlCookie
   * @function parseCookie
   * @description Reads and parses the value of a cookie.
   * @param {string} cName - Cookie name (without prefix).
   * @returns {string|number|boolean|null} Parsed value of the cookie.
   */
  parseCookie( cName ){ 
    if( this.hasCookie(cName) ){
      let reg = this.getRegexp( this.prepend + cName );
      
      let val=document.cookie.match(reg)[0].split("=")[1].replace(this.prepend,'').replace(this.sub,';');
      
      if( val==="true" ){
        val = true;
      }else if( val==="false" ){
        val = false;
      }else if( parseInt(val)===val ){
        val=parseInt(val);
      }else if( parseFloat(val)===val ){
        val = parseFloat(val);
      }
      return val;
    }
    return null;
  }

  // -- -- --

  // Read all cookie entries with the given suffix
  

  /**
   * @method
   * @memberof pxlCookie
   * @function evalCookie
   * @description Evaluates the existence or value of cookies with the given suffix.
   * @param {string} [cName] - Cookie name (without prefix). If omitted, evaluates all managed cookies.
   * @returns {boolean} True if the cookie(s) exist.
   */
  evalCookie(cName){ 
    if( cName ){
      if( this.hasCookie(cName) ){
        let reg = this.getRegexp( this.prepend + cName );
        this.log("Eval Cookie has been limited, responce is: ");
        this.log(document.cookie.match(reg)[0].replace(this.prepend,'').replace(this.sub,';'));
        return true;
      }
      return false;
    }else{
      let reg = this.getRegexp( this.prepend );
      this.log( "Eval Cookie has been limited, may error." );
      document.cookie.match( reg ).forEach( e =>{ e.replace(this.prepend,'').replace(this.sub,';') });
      return true;
    }
  }

  // Set cookie value; setCookie( string, variable )
  /**
   * @method
   * @memberof pxlCookie
   * @function setCookie
   * @description Sets a cookie value.
   * @param {string} cName - Cookie name (without prefix).
   * @param {any} cData - Value to store in the cookie.
   */
  setCookie(cName, cData){ 
    cData = this.variableToString(cData);
    if( cData.replace ){
      cData.replace( ";", this.sub );
    }
    document.cookie = this.prepend + cName + "=" + cData + ";" + this.getExpiration() + this.path;
  }
  
  // Add dictionary keys and values to cookies
  /**
   * @method
   * @memberof pxlCookie
   * @function addDictionary
   * @description Adds multiple cookies from a dictionary.
   * @param {Object} cDict - Dictionary of key-value pairs to add as cookies.
   */
  addDictionary(cDict){ 
    let cKeys = Object.keys(cDict);
    for( let x=0; x<cKeys.length; ++x ){
      let cData = cDict[ cKeys[x] ];
      cData = this.variableToString(cData);
      if( cData.replace ){
        cData.replace( ";", this.sub );
      }
      document.cookie = this.prepend + cKeys[x] + "=" + cData + ";" + this.getExpiration() + this.path;
    }
  }
  
  // -- -- --

  // Parse Dict Values, Update if they exist
  //   Overwrites Input Dict
  // Returns if any items in the dict were set
  /**
   * @method
   * @memberof pxlCookie
   * @function parseDict
   * @description Parses and updates a dictionary with values from cookies if they exist.
   * @param {Object} cDict - Dictionary to update with cookie values.
   * @returns {boolean} True if any items in the dictionary were set from cookies.
   */
  parseDict(cDict){
    let keys = Object.keys( cDict );
    let ret = false;
    keys.forEach( (k)=>{
      if( this.hasCookie(k) ){
        cDict[k] = this.parseCookie(k);
        ret = true;
      }
    });
    return ret;
  }
  
  // -- -- --

  // Clear specific cookie entry
  /**
   * @method
   * @memberof pxlCookie
   * @function clearCookie
   * @description Clears a specific cookie or all managed cookies.
   * @param {string|string[]} [cName] - Cookie name(s) (without prefix) to clear. If omitted, clears all managed cookies.
   */
  clearCookie( cName ){ 
    if(!cName){
      let reg = this.getClearCookieRegexp( this.prepend );
      let curCookies = document.cookie.match( reg );
      curCookies.forEach( c =>{ document.cookie = c + "=;" + this.deleteDate + this.path; });
    }else{
      if( typeof(cName) === "string" ){
        cName=[cName];
      }
      cName.forEach( c =>{ document.cookie = c + "=;" + this.deleteDate + this.path; });
    }
  }
}