access-mode.js

/**
 * @file Access control model.
 *
 * @copyright 2015-2022 Tinode LLC.
 */
'use strict';

// NOTE TO DEVELOPERS:
// Localizable strings should be double quoted "строка на другом языке",
// non-localizable strings should be single quoted 'non-localized'.

/**
 * Helper class for handling access mode.
 *
 * @class AccessMode
 * @memberof Tinode
 *
 * @param {AccessMode|Object=} acs - AccessMode to copy or access mode object received from the server.
 */
export default class AccessMode {
  constructor(acs) {
    if (acs) {
      this.given = typeof acs.given == 'number' ? acs.given : AccessMode.decode(acs.given);
      this.want = typeof acs.want == 'number' ? acs.want : AccessMode.decode(acs.want);
      this.mode = acs.mode ? (typeof acs.mode == 'number' ? acs.mode : AccessMode.decode(acs.mode)) :
        (this.given & this.want);
    }
  }

  static #checkFlag(val, side, flag) {
    side = side || 'mode';
    if (['given', 'want', 'mode'].includes(side)) {
      return ((val[side] & flag) != 0);
    }
    throw new Error(`Invalid AccessMode component '${side}'`);
  }
  /**
   * Parse string into an access mode value.
   * @memberof Tinode.AccessMode
   * @static
   *
   * @param {string | Number} mode - either a String representation of the access mode to parse or a set of bits to assign.
   * @returns {number} - Access mode as a numeric value.
   */
  static decode(str) {
    if (!str) {
      return null;
    } else if (typeof str == 'number') {
      return str & AccessMode._BITMASK;
    } else if (str === 'N' || str === 'n') {
      return AccessMode._NONE;
    }

    const bitmask = {
      'J': AccessMode._JOIN,
      'R': AccessMode._READ,
      'W': AccessMode._WRITE,
      'P': AccessMode._PRES,
      'A': AccessMode._APPROVE,
      'S': AccessMode._SHARE,
      'D': AccessMode._DELETE,
      'O': AccessMode._OWNER
    };

    let m0 = AccessMode._NONE;

    for (let i = 0; i < str.length; i++) {
      const bit = bitmask[str.charAt(i).toUpperCase()];
      if (!bit) {
        // Unrecognized bit, skip.
        continue;
      }
      m0 |= bit;
    }
    return m0;
  }
  /**
   * Convert numeric representation of the access mode into a string.
   *
   * @memberof Tinode.AccessMode
   * @static
   *
   * @param {number} val - access mode value to convert to a string.
   * @returns {string} - Access mode as a string.
   */
  static encode(val) {
    if (val === null || val === AccessMode._INVALID) {
      return null;
    } else if (val === AccessMode._NONE) {
      return 'N';
    }

    const bitmask = ['J', 'R', 'W', 'P', 'A', 'S', 'D', 'O'];
    let res = '';
    for (let i = 0; i < bitmask.length; i++) {
      if ((val & (1 << i)) != 0) {
        res = res + bitmask[i];
      }
    }
    return res;
  }
  /**
   * Update numeric representation of access mode with the new value. The value
   * is one of the following:
   *  - a string starting with <code>'+'</code> or <code>'-'</code> then the bits to add or remove, e.g. <code>'+R-W'</code> or <code>'-PS'</code>.
   *  - a new value of access mode
   *
   * @memberof Tinode.AccessMode
   * @static
   *
   * @param {number} val - access mode value to update.
   * @param {string} upd - update to apply to val.
   * @returns {number} - updated access mode.
   */
  static update(val, upd) {
    if (!upd || typeof upd != 'string') {
      return val;
    }

    let action = upd.charAt(0);
    if (action == '+' || action == '-') {
      let val0 = val;
      // Split delta-string like '+ABC-DEF+Z' into an array of parts including + and -.
      const parts = upd.split(/([-+])/);
      // Starting iteration from 1 because String.split() creates an array with the first empty element.
      // Iterating by 2 because we parse pairs +/- then data.
      for (let i = 1; i < parts.length - 1; i += 2) {
        action = parts[i];
        const m0 = AccessMode.decode(parts[i + 1]);
        if (m0 == AccessMode._INVALID) {
          return val;
        }
        if (m0 == null) {
          continue;
        }
        if (action === '+') {
          val0 |= m0;
        } else if (action === '-') {
          val0 &= ~m0;
        }
      }
      val = val0;
    } else {
      // The string is an explicit new value 'ABC' rather than delta.
      const val0 = AccessMode.decode(upd);
      if (val0 != AccessMode._INVALID) {
        val = val0;
      }
    }

    return val;
  }
  /**
   * Bits present in a1 but missing in a2.
   *
   * @static
   * @memberof Tinode
   *
   * @param {number | string} a1 - access mode to subtract from.
   * @param {number | string} a2 - access mode to subtract.
   * @returns {number} access mode with bits present in <code>a1</code> but missing in <code>a2</code>.
   */
  static diff(a1, a2) {
    a1 = AccessMode.decode(a1);
    a2 = AccessMode.decode(a2);

    if (a1 == AccessMode._INVALID || a2 == AccessMode._INVALID) {
      return AccessMode._INVALID;
    }
    return a1 & ~a2;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Custom formatter
   */
  toString() {
    return '{"mode": "' + AccessMode.encode(this.mode) +
      '", "given": "' + AccessMode.encode(this.given) +
      '", "want": "' + AccessMode.encode(this.want) + '"}';
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Converts numeric values to strings.
   */
  jsonHelper() {
    return {
      mode: AccessMode.encode(this.mode),
      given: AccessMode.encode(this.given),
      want: AccessMode.encode(this.want)
    };
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Assign value to 'mode'.
   * @memberof Tinode.AccessMode
   *
   * @param {string | Number} m - either a string representation of the access mode or a set of bits.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  setMode(m) {
    this.mode = AccessMode.decode(m);
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Update <code>mode</code> value.
   * @memberof Tinode.AccessMode
   *
   * @param {string} u - string representation of the changes to apply to access mode.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  updateMode(u) {
    this.mode = AccessMode.update(this.mode, u);
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Get <code>mode</code> value as a string.
   * @memberof Tinode.AccessMode
   *
   * @returns {string} - <code>mode</code> value.
   */
  getMode() {
    return AccessMode.encode(this.mode);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Assign <code>given</code>  value.
   * @memberof Tinode.AccessMode
   *
   * @param {string | Number} g - either a string representation of the access mode or a set of bits.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  setGiven(g) {
    this.given = AccessMode.decode(g);
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Update 'given' value.
   * @memberof Tinode.AccessMode
   *
   * @param {string} u - string representation of the changes to apply to access mode.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  updateGiven(u) {
    this.given = AccessMode.update(this.given, u);
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Get 'given' value as a string.
   * @memberof Tinode.AccessMode
   *
   * @returns {string} - <b>given</b> value.
   */
  getGiven() {
    return AccessMode.encode(this.given);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Assign 'want' value.
   * @memberof Tinode.AccessMode
   *
   * @param {string | Number} w - either a string representation of the access mode or a set of bits.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  setWant(w) {
    this.want = AccessMode.decode(w);
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Update 'want' value.
   * @memberof Tinode.AccessMode
   *
   * @param {string} u - string representation of the changes to apply to access mode.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  updateWant(u) {
    this.want = AccessMode.update(this.want, u);
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Get 'want' value as a string.
   * @memberof Tinode.AccessMode
   *
   * @returns {string} - <b>want</b> value.
   */
  getWant() {
    return AccessMode.encode(this.want);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Get permissions present in 'want' but missing in 'given'.
   * Inverse of {@link Tinode.AccessMode#getExcessive}
   *
   * @memberof Tinode.AccessMode
   *
   * @returns {string} permissions present in <b>want</b> but missing in <b>given</b>.
   */
  getMissing() {
    return AccessMode.encode(this.want & ~this.given);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Get permissions present in 'given' but missing in 'want'.
   * Inverse of {@link Tinode.AccessMode#getMissing}
   * @memberof Tinode.AccessMode
   *
   * @returns {string} permissions present in <b>given</b> but missing in <b>want</b>.
   */
  getExcessive() {
    return AccessMode.encode(this.given & ~this.want);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Update 'want', 'give', and 'mode' values.
   * @memberof Tinode.AccessMode
   *
   * @param {AccessMode} val - new access mode value.
   * @returns {AccessMode} - <code>this</code> AccessMode.
   */
  updateAll(val) {
    if (val) {
      this.updateGiven(val.given);
      this.updateWant(val.want);
      this.mode = this.given & this.want;
    }
    return this;
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Owner (O) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isOwner(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._OWNER);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Presence (P) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isPresencer(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._PRES);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Presence (P) flag is NOT set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isMuted(side) {
    return !this.isPresencer(side);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Join (J) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isJoiner(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._JOIN);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Reader (R) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isReader(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._READ);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Writer (W) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isWriter(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._WRITE);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Approver (A) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isApprover(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._APPROVE);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if either one of Owner (O) or Approver (A) flags is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isAdmin(side) {
    return this.isOwner(side) || this.isApprover(side);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if either one of Owner (O), Approver (A), or Sharer (S) flags is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isSharer(side) {
    return this.isAdmin(side) || AccessMode.#checkFlag(this, side, AccessMode._SHARE);
  }
  /**
   * AccessMode is a class representing topic access mode.
   *
   * @memberof Tinode
   * @class AccessMode
   */
  /**
   * Check if Deleter (D) flag is set.
   * @memberof Tinode.AccessMode
   * @param {string=} side - which permission to check: given, want, mode; default: mode.
   * @returns {boolean} - <code>true</code> if flag is set.
   */
  isDeleter(side) {
    return AccessMode.#checkFlag(this, side, AccessMode._DELETE);
  }
}

AccessMode._NONE = 0x00;
AccessMode._JOIN = 0x01;
AccessMode._READ = 0x02;
AccessMode._WRITE = 0x04;
AccessMode._PRES = 0x08;
AccessMode._APPROVE = 0x10;
AccessMode._SHARE = 0x20;
AccessMode._DELETE = 0x40;
AccessMode._OWNER = 0x80;

AccessMode._BITMASK = AccessMode._JOIN | AccessMode._READ | AccessMode._WRITE | AccessMode._PRES |
  AccessMode._APPROVE | AccessMode._SHARE | AccessMode._DELETE | AccessMode._OWNER;
AccessMode._INVALID = 0x100000;