Source: _util.js

'use strict'

var packageJson = require('../package.json')
var chalk = require('chalk')
var boxen = require('boxen')

var crossGlobal =
  typeof window !== 'undefined'
    ? window
    : typeof globalThis !== 'undefined'
    ? globalThis
    : typeof global !== 'undefined'
    ? global
    : self

/**
 * Inherit the prototype methods from one constructor into another.
 * Source: https://github.com/kaelzhang/node-util-inherits
 * @param {function} ctor Constructor function which needs to inherit the prototype.
 * @param {function} superCtor Constructor function to inherit prototype from.
 * @private
 */
function inherits(ctor, superCtor) {
  if (ctor === undefined || ctor === null) {
    throw new TypeError(
      'The constructor to "inherits" must not be null or undefined'
    )
  }

  if (superCtor === undefined || superCtor === null) {
    throw new TypeError(
      'The super constructor to "inherits" must not be null or undefined'
    )
  }

  if (superCtor.prototype === undefined) {
    throw new TypeError(
      'The super constructor to "inherits" must have a prototype'
    )
  }

  ctor.super_ = superCtor
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })
}

/**
 * Determines if the current environment is a NodeJS environment.
 * @private
 */
function isNodeEnv() {
  return (
    typeof window === 'undefined' &&
    typeof process !== 'undefined' &&
    process.versions != null &&
    process.versions.node != null
  )
}

/**
 * Resolves environment variable if available.
 *
 * @param {string} envKey A name of env variable.
 * @return {void|string} Returns requested env variable or void.
 * @private
 */
function getEnvVariable(envKey) {
  var areEnvVarsAvailable = !!(
    typeof process !== 'undefined' &&
    process &&
    process.env
  )

  if (areEnvVarsAvailable && process.env[envKey] != null) {
    return process.env[envKey]
  }
}

/**
 * JavaScript Client Detection
 * @private
 */
function getBrowserDetails() {
  var browser = navigator.appName
  var browserVersion = '' + parseFloat(navigator.appVersion)
  var nameOffset, verOffset, ix

  // Opera
  if ((verOffset = navigator.userAgent.indexOf('Opera')) != -1) {
    browser = 'Opera'
    browserVersion = navigator.userAgent.substring(verOffset + 6)
    if ((verOffset = navigator.userAgent.indexOf('Version')) != -1) {
      browserVersion = navigator.userAgent.substring(verOffset + 8)
    }
  }
  // MSIE
  else if ((verOffset = navigator.userAgent.indexOf('MSIE')) != -1) {
    browser = 'Microsoft Internet Explorer'
    browserVersion = navigator.userAgent.substring(verOffset + 5)
  }

  //IE 11 no longer identifies itself as MS IE, so trap it
  //http://stackoverflow.com/questions/17907445/how-to-detect-ie11
  else if (
    browser == 'Netscape' &&
    navigator.userAgent.indexOf('Trident/') != -1
  ) {
    browser = 'Microsoft Internet Explorer'
    browserVersion = navigator.userAgent.substring(verOffset + 5)
    if ((verOffset = navigator.userAgent.indexOf('rv:')) != -1) {
      browserVersion = navigator.userAgent.substring(verOffset + 3)
    }
  }

  // Chrome
  else if ((verOffset = navigator.userAgent.indexOf('Chrome')) != -1) {
    browser = 'Chrome'
    browserVersion = navigator.userAgent.substring(verOffset + 7)
  }
  // Safari
  else if ((verOffset = navigator.userAgent.indexOf('Safari')) != -1) {
    browser = 'Safari'
    browserVersion = navigator.userAgent.substring(verOffset + 7)
    if ((verOffset = navigator.userAgent.indexOf('Version')) != -1) {
      browserVersion = navigator.userAgent.substring(verOffset + 8)
    }

    // Chrome on iPad identifies itself as Safari. Actual results do not match what Google claims
    //  at: https://developers.google.com/chrome/mobile/docs/user-agent?hl=ja
    //  No mention of chrome in the user agent string. However it does mention CriOS, which presumably
    //  can be keyed on to detect it.
    if (navigator.userAgent.indexOf('CriOS') != -1) {
      //Chrome on iPad spoofing Safari...correct it.
      browser = 'Chrome'
      //Don't believe there is a way to grab the accurate version number, so leaving that for now.
    }
  }
  // Firefox
  else if ((verOffset = navigator.userAgent.indexOf('Firefox')) != -1) {
    browser = 'Firefox'
    browserVersion = navigator.userAgent.substring(verOffset + 8)
  }
  // Other browsers
  else if (
    (nameOffset = navigator.userAgent.lastIndexOf(' ') + 1) <
    (verOffset = navigator.userAgent.lastIndexOf('/'))
  ) {
    browser = navigator.userAgent.substring(nameOffset, verOffset)
    browserVersion = navigator.userAgent.substring(verOffset + 1)
    if (browser.toLowerCase() == browser.toUpperCase()) {
      browser = navigator.appName
    }
  }
  // trim the browser version string
  if ((ix = browserVersion.indexOf(';')) != -1)
    browserVersion = browserVersion.substring(0, ix)
  if ((ix = browserVersion.indexOf(' ')) != -1)
    browserVersion = browserVersion.substring(0, ix)
  if ((ix = browserVersion.indexOf(')')) != -1)
    browserVersion = browserVersion.substring(0, ix)

  return [browser, browserVersion].join('-')
}

function getBrowserOsDetails() {
  var os = 'unknown'
  var clientStrings = [
    { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ },
    { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ },
    { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ },
    { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ },
    { s: 'Windows Vista', r: /Windows NT 6.0/ },
    { s: 'Windows Server 2003', r: /Windows NT 5.2/ },
    { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ },
    { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ },
    { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ },
    { s: 'Windows 98', r: /(Windows 98|Win98)/ },
    { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ },
    { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ },
    { s: 'Windows CE', r: /Windows CE/ },
    { s: 'Windows 3.11', r: /Win16/ },
    { s: 'Android', r: /Android/ },
    { s: 'Open BSD', r: /OpenBSD/ },
    { s: 'Sun OS', r: /SunOS/ },
    { s: 'Chrome OS', r: /CrOS/ },
    { s: 'Linux', r: /(Linux|X11(?!.*CrOS))/ },
    { s: 'iOS', r: /(iPhone|iPad|iPod)/ },
    { s: 'Mac OS X', r: /Mac OS X/ },
    { s: 'Mac OS', r: /(Mac OS|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ },
    { s: 'QNX', r: /QNX/ },
    { s: 'UNIX', r: /UNIX/ },
    { s: 'BeOS', r: /BeOS/ },
    { s: 'OS/2', r: /OS\/2/ },
    {
      s: 'Search Bot',
      r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/,
    },
  ]
  for (var id in clientStrings) {
    var cs = clientStrings[id]
    if (cs.r.test(navigator.userAgent)) {
      os = cs.s
      break
    }
  }

  var osVersion = 'unknown'

  if (/Windows/.test(os)) {
    osVersion = /Windows (.*)/.exec(os)[1]
    os = 'Windows'
  }

  switch (os) {
    case 'Mac OS':
    case 'Mac OS X':
    case 'Android':
      osVersion = /(?:Android|Mac OS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh) ([\.\_\d]+)/.exec(
        navigator.userAgent
      )[1]
      break

    case 'iOS':
      osVersion = /OS (\d+)_(\d+)_?(\d+)?/.exec(navigator.appVersion)
      osVersion = osVersion[1] + '.' + osVersion[2] + '.' + (osVersion[3] | 0)
      break
  }
  return [os, osVersion].join('-')
}

/**
 * For checking process.env always use `hasOwnProperty`
 * Some providers could throw an error when trying to access env variables that does not exists
 * @private */
function getNodeRuntimeEnv() {
  var runtimeEnvs = [
    {
      name: 'Netlify',
      check: function() {
        return process.env.hasOwnProperty('NETLIFY_IMAGES_CDN_DOMAIN')
      },
    },
    {
      name: 'Vercel',
      check: function() {
        return process.env.hasOwnProperty('VERCEL')
      },
    },
    {
      name: 'Heroku',
      check: function() {
        return (
          process.env.hasOwnProperty('PATH') &&
          process.env.PATH.indexOf('.heroku') !== -1
        )
      },
    },
    {
      name: 'AWS Lambda',
      check: function() {
        return process.env.hasOwnProperty('AWS_LAMBDA_FUNCTION_VERSION')
      },
    },
    {
      name: 'GCP Cloud Functions',
      check: function() {
        return (
          process.env.hasOwnProperty('_') &&
          process.env._.indexOf('google') !== -1
        )
      },
    },
    {
      name: 'GCP Compute Instances',
      check: function() {
        return process.env.hasOwnProperty('GOOGLE_CLOUD_PROJECT')
      },
    },
    {
      name: 'Azure Cloud Functions',
      check: function() {
        return process.env.hasOwnProperty(
          'WEBSITE_FUNCTIONS_AZUREMONITOR_CATEGORIES'
        )
      },
    },
    {
      name: 'Azure Compute',
      check: function() {
        return (
          process.env.hasOwnProperty('ORYX_ENV_TYPE') &&
          process.env.hasOwnProperty('WEBSITE_INSTANCE_ID') &&
          process.env.ORYX_ENV_TYPE === 'AppService'
        )
      },
    },
    {
      name: 'Mongo Stitch',
      check: function() {
        return typeof crossGlobal.StitchError === 'function'
      },
    },
    {
      name: 'Render',
      check: function() {
        return process.env.hasOwnProperty('RENDER_SERVICE_ID')
      },
    },
    {
      name: 'Begin',
      check: function() {
        return process.env.hasOwnProperty('BEGIN_DATA_SCOPE_ID')
      },
    },
  ]
  var detectedEnv = runtimeEnvs.find(env => env.check())

  return detectedEnv ? detectedEnv.name : 'unknown'
}

/**
 * If defined, returns the given value. Otherwise, returns the default value.
 * @param {any} obj The given value.
 * @param {any} def The default value.
 * @private
 */
function defaults(obj, def) {
  if (obj === undefined) {
    return def
  } else {
    return obj
  }
}

/**
 * Used for functions that take an options objects.
 * Fills in defaults for options not provided.
 * Throws errors for provided options that aren't recognized.
 * A default value of `undefined` is used to indicate that the option must be provided.
 * @private
 */
function applyDefaults(provided, defaults) {
  var out = {}

  for (var providedKey in provided) {
    if (!(providedKey in defaults)) {
      throw new Error('No such option ' + providedKey)
    }
    out[providedKey] = provided[providedKey]
  }

  for (var defaultsKey in defaults) {
    if (!(defaultsKey in out)) {
      out[defaultsKey] = defaults[defaultsKey]
    }
  }

  return out
}

/**
 * Returns a new object without any keys where the value would be null or undefined.
 * @private
 * */
function removeNullAndUndefinedValues(object) {
  var res = {}
  for (var key in object) {
    var val = object[key]
    if (val !== null && val !== undefined) {
      res[key] = val
    }
  }
  return res
}

/**
 * Returns a new object without any keys where the value would be undefined.
 * @private
 * */
function removeUndefinedValues(object) {
  var res = {}
  for (var key in object) {
    var val = object[key]
    if (val !== undefined) {
      res[key] = val
    }
  }
  return res
}

/**
 * Returns a boolean stating if the given object has a given property
 * @private
 * */
function checkInstanceHasProperty(obj, prop) {
  return typeof obj === 'object' && obj !== null && Boolean(obj[prop])
}

function formatUrl(base, path, query) {
  query = typeof query === 'object' ? querystringify(query) : query
  return [
    base,
    path ? (path.charAt(0) === '/' ? '' : '/' + path) : '',
    query ? (query.charAt(0) === '?' ? '' : '?' + query) : '',
  ].join('')
}

/**
 * Transform a query string to an object.
 *
 * @param {Object} obj Object that should be transformed.
 * @param {String} prefix Optional prefix.
 * @returns {String}
 * @api public
 */
function querystringify(obj, prefix) {
  prefix = prefix || ''

  var pairs = [],
    value,
    key

  //
  // Optionally prefix with a '?' if needed
  //
  if ('string' !== typeof prefix) prefix = '?'

  for (key in obj) {
    if (checkInstanceHasProperty(obj, key)) {
      value = obj[key]

      //
      // Edge cases where we actually want to encode the value to an empty
      // string instead of the stringified value.
      //
      if (!value && (value === null || value === undefined || isNaN(value))) {
        value = ''
      }

      key = encode(key)
      value = encode(value)

      //
      // If we failed to encode the strings, we should bail out as we don't
      // want to add invalid strings to the query.
      //
      if (key === null || value === null) continue
      pairs.push(key + '=' + value)
    }
  }

  return pairs.length ? prefix + pairs.join('&') : ''
}

/**
 * Attempts to encode a given input.
 *
 * @param {String} input The string that needs to be encoded.
 * @returns {String|Null} The encoded string.
 * @api private
 */
function encode(input) {
  try {
    return encodeURIComponent(input)
  } catch (e) {
    return null
  }
}

/**
 * Merge two objects into one
 * @param obj1
 * @param obj2
 * @returns obj3 a new object based on obj1 and obj2
 */
function mergeObjects(obj1, obj2) {
  var obj3 = {}
  for (var attrname in obj1) {
    obj3[attrname] = obj1[attrname]
  }
  for (var attrname in obj2) {
    obj3[attrname] = obj2[attrname]
  }
  return obj3
}

/**
 * Resolves which Fetch API compatible function to use. If an override is
 * provided, returns the override. If no override and the global (window) has
 * "fetch" property, return the native fetch. Otherwise returns the cross-fetch polyfill.
 *
 * @param {?function} fetchOverride An Fetch API compatible function to use.
 * @returns {function} A Fetch API compatible function.
 * @private
 */
function resolveFetch(fetchOverride) {
  if (typeof fetchOverride === 'function') {
    return fetchOverride
  }

  if (typeof crossGlobal.fetch === 'function') {
    // NB. Rebinding to global is needed for Safari
    return crossGlobal.fetch.bind(crossGlobal)
  }

  return require('cross-fetch')
}

module.exports = {
  crossGlobal: crossGlobal,
  mergeObjects: mergeObjects,
  formatUrl: formatUrl,
  querystringify: querystringify,
  inherits: inherits,
  isNodeEnv: isNodeEnv,
  getEnvVariable: getEnvVariable,
  defaults: defaults,
  applyDefaults: applyDefaults,
  removeNullAndUndefinedValues: removeNullAndUndefinedValues,
  removeUndefinedValues: removeUndefinedValues,
  checkInstanceHasProperty: checkInstanceHasProperty,
  getBrowserDetails: getBrowserDetails,
  getBrowserOsDetails: getBrowserOsDetails,
  getNodeRuntimeEnv: getNodeRuntimeEnv,
  resolveFetch: resolveFetch,
}