123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- 'use strict'
- const { BerReader, BerWriter } = require('@ldapjs/asn1')
- const Attribute = require('@ldapjs/attribute')
- /**
- * Implements an LDAP CHANGE sequence as described in
- * https://www.rfc-editor.org/rfc/rfc4511.html#section-4.6.
- */
- class Change {
- #operation
- #modification
- /**
- * @typedef {object} ChangeParameters
- * @property {string | number} operation One of `add` (0), `delete` (1), or
- * `replace` (2). Default: `add`.
- * @property {object | import('@ldapjs/attribute')} modification An attribute
- * instance or an object that is shaped like an attribute.
- */
- /**
- * @param {ChangeParameters} input
- *
- * @throws When the `modification` parameter is invalid.
- */
- constructor ({ operation = 'add', modification }) {
- this.operation = operation
- this.modification = modification
- }
- get [Symbol.toStringTag] () {
- return 'LdapChange'
- }
- /**
- * The attribute that will be modified by the {@link Change}.
- *
- * @returns {import('@ldapjs/attribute')}
- */
- get modification () {
- return this.#modification
- }
- /**
- * Define the attribute to be modified by the {@link Change}.
- *
- * @param {object|import('@ldapjs/attribute')} mod
- *
- * @throws When `mod` is not an instance of `Attribute` or is not an
- * `Attribute` shaped object.
- */
- set modification (mod) {
- if (Attribute.isAttribute(mod) === false) {
- throw Error('modification must be an Attribute')
- }
- if (Object.prototype.toString.call(mod) !== '[object LdapAttribute]') {
- mod = new Attribute(mod)
- }
- this.#modification = mod
- }
- /**
- * Get a plain JavaScript object representation of the change.
- *
- * @returns {object}
- */
- get pojo () {
- return {
- operation: this.operation,
- modification: this.modification.pojo
- }
- }
- /**
- * The string name of the operation that will be performed.
- *
- * @returns {string} One of `add`, `delete`, or `replace`.
- */
- get operation () {
- switch (this.#operation) {
- case 0x00: {
- return 'add'
- }
- case 0x01: {
- return 'delete'
- }
- case 0x02: {
- return 'replace'
- }
- }
- }
- /**
- * Define the operation that the {@link Change} represents.
- *
- * @param {string|number} op May be one of `add` (0), `delete` (1),
- * or `replace` (2).
- *
- * @throws When the `op` is not recognized.
- */
- set operation (op) {
- if (typeof op === 'string') {
- op = op.toLowerCase()
- }
- switch (op) {
- case 0x00:
- case 'add': {
- this.#operation = 0x00
- break
- }
- case 0x01:
- case 'delete': {
- this.#operation = 0x01
- break
- }
- case 0x02:
- case 'replace': {
- this.#operation = 0x02
- break
- }
- default: {
- const type = Number.isInteger(op)
- ? '0x' + Number(op).toString(16)
- : op
- throw Error(`invalid operation type: ${type}`)
- }
- }
- }
- /**
- * Serialize the instance to a BER.
- *
- * @returns {import('@ldapjs/asn1').BerReader}
- */
- toBer () {
- const writer = new BerWriter()
- writer.startSequence()
- writer.writeEnumeration(this.#operation)
- const attrBer = this.#modification.toBer()
- writer.appendBuffer(attrBer.buffer)
- writer.endSequence()
- return new BerReader(writer.buffer)
- }
- /**
- * See {@link pojo}.
- *
- * @returns {object}
- */
- toJSON () {
- return this.pojo
- }
- /**
- * Applies a {@link Change} to a `target` object.
- *
- * @example
- * const change = new Change({
- * operation: 'add',
- * modification: {
- * type: 'cn',
- * values: ['new']
- * }
- * })
- * const target = {
- * cn: ['old']
- * }
- * Change.apply(change, target)
- * // target = { cn: ['old', 'new'] }
- *
- * @param {Change} change The change to apply.
- * @param {object} target The object to modify. This object will be mutated
- * by the function. It should have properties that match the `modification`
- * of the change.
- * @param {boolean} scalar When `true`, will convert single-item arrays
- * to scalar values. Default: `false`.
- *
- * @returns {object} The mutated `target`.
- *
- * @throws When the `change` is not an instance of {@link Change}.
- */
- static apply (change, target, scalar = false) {
- if (Change.isChange(change) === false) {
- throw Error('change must be an instance of Change')
- }
- const type = change.modification.type
- const values = change.modification.values
- let data = target[type]
- if (data === undefined) {
- data = []
- } else if (Array.isArray(data) === false) {
- data = [data]
- }
- switch (change.operation) {
- case 'add': {
- // Add only new unique entries.
- const newValues = values.filter(v => data.indexOf(v) === -1)
- Array.prototype.push.apply(data, newValues)
- break
- }
- case 'delete': {
- data = data.filter(v => values.indexOf(v) === -1)
- if (data.length === 0) {
- // An empty list indicates the attribute should be removed
- // completely.
- delete target[type]
- return target
- }
- break
- }
- case 'replace': {
- if (values.length === 0) {
- // A new value set that is empty is a delete.
- delete target[type]
- return target
- }
- data = values
- break
- }
- }
- if (scalar === true && data.length === 1) {
- // Replace array value with a scalar value if the modified set is
- // single valued and the operation calls for a scalar.
- target[type] = data[0]
- } else {
- target[type] = data
- }
- return target
- }
- /**
- * Determines if an object is an instance of {@link Change}, or at least
- * resembles the shape of a {@link Change} object. A plain object will match
- * if it has a `modification` property that matches an `Attribute`,
- * an `operation` property that is a string or number, and has a `toBer`
- * method. An object that resembles a {@link Change} does not guarantee
- * compatibility. A `toString` check is much more accurate.
- *
- * @param {Change|object} change
- *
- * @returns {boolean}
- */
- static isChange (change) {
- if (Object.prototype.toString.call(change) === '[object LdapChange]') {
- return true
- }
- if (Object.prototype.toString.call(change) !== '[object Object]') {
- return false
- }
- if (
- Attribute.isAttribute(change.modification) === true &&
- (typeof change.operation === 'string' || typeof change.operation === 'number')
- ) {
- return true
- }
- return false
- }
- /**
- * Compares two {@link Change} instance to determine the priority of the
- * changes relative to each other.
- *
- * @param {Change} change1
- * @param {Change} change2
- *
- * @returns {number} -1 for lower priority, 1 for higher priority, and 0
- * for equal priority in relation to `change1`, e.g. -1 would mean `change`
- * has lower priority than `change2`.
- *
- * @throws When neither parameter resembles a {@link Change} object.
- */
- static compare (change1, change2) {
- if (Change.isChange(change1) === false || Change.isChange(change2) === false) {
- throw Error('can only compare Change instances')
- }
- if (change1.operation < change2.operation) {
- return -1
- }
- if (change1.operation > change2.operation) {
- return 1
- }
- return Attribute.compare(change1.modification, change2.modification)
- }
- /**
- * Parse a BER into a new {@link Change} object.
- *
- * @param {import('@ldapjs/asn1').BerReader} ber The BER to process. It must
- * be at an offset that starts a new change sequence. The reader will be
- * advanced to the end of the change sequence by this method.
- *
- * @returns {Change}
- *
- * @throws When there is an error processing the BER.
- */
- static fromBer (ber) {
- ber.readSequence()
- const operation = ber.readEnumeration()
- const modification = Attribute.fromBer(ber)
- return new Change({ operation, modification })
- }
- }
- module.exports = Change
|