Source: utils/index.js

/**
 * Utilities
 * @module utils
 * @borrows module:auth_token as generate_auth_token
 */

const crypto = require("crypto");
const querystring = require("querystring");
const urlParse = require("url").parse;

// Functions used internally
const compact = require("lodash/compact");
const defaults = require("lodash/defaults");
const find = require("lodash/find");
const first = require("lodash/first");
const identity = require("lodash/identity");
const isFunction = require("lodash/isFunction");
const isPlainObject = require("lodash/isPlainObject");
const last = require("lodash/last");
const map = require("lodash/map");
const sortBy = require("lodash/sortBy");
const take = require("lodash/take");
const at = require("lodash/at");

// Exposed by the module
const clone = require("lodash/clone");
const extend = require("lodash/extend");
const filter = require("lodash/filter");
const includes = require("lodash/includes");
const isArray = require("lodash/isArray");
const isEmpty = require("lodash/isEmpty");
const isNumber = require("lodash/isNumber");
const isObject = require("lodash/isObject");
const isString = require("lodash/isString");
const isUndefined = require("lodash/isUndefined");
const keys = require("lodash/keys");
const merge = require("lodash/merge");

const config = require("../config");
const generate_token = require("../auth_token");
const utf8_encode = require('./utf8_encode');
const crc32 = require('./crc32');
const ensurePresenceOf = require('./ensurePresenceOf');
const ensureOption = require('./ensureOption').defaults(config());

module.exports = {
  at,
  clone,
  extend,
  filter,
  includes,
  isArray,
  isEmpty,
  isNumber,
  isObject,
  isString,
  isUndefined,
  keys,
  merge,
  ensurePresenceOf,
};
exports = module.exports;
const utils = module.exports;

exports.generate_auth_token = function generate_auth_token(options) {
  let token_options = Object.assign({}, config().auth_token, options);
  return generate_token(token_options);
};

exports.CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net";

exports.OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net";

exports.AKAMAI_SHARED_CDN = "res.cloudinary.com";

exports.SHARED_CDN = exports.AKAMAI_SHARED_CDN;

try {
  exports.VERSION = require('../../package.json').version;
} catch (error) {

}

exports.USER_AGENT = `CloudinaryNodeJS/${exports.VERSION}`;

// Add platform information to the USER_AGENT header
// This is intended for platform information and not individual applications!
exports.userPlatform = "";

exports.getUserAgent = function getUserAgent() {
  if (isEmpty(utils.userPlatform)) {
    return `${utils.USER_AGENT}`;
  } else {
    return `${utils.userPlatform} ${utils.USER_AGENT}`;
  }
};

const DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {
  width: "auto",
  crop: "limit"
};

exports.DEFAULT_POSTER_OPTIONS = {
  format: 'jpg',
  resource_type: 'video'
};

exports.DEFAULT_VIDEO_SOURCE_TYPES = ['webm', 'mp4', 'ogv'];

const CONDITIONAL_OPERATORS = {
  "=": 'eq',
  "!=": 'ne',
  "<": 'lt',
  ">": 'gt',
  "<=": 'lte',
  ">=": 'gte',
  "&&": 'and',
  "||": 'or',
  "*": "mul",
  "/": "div",
  "+": "add",
  "-": "sub"
};

const PREDEFINED_VARS = {
  "aspect_ratio": "ar",
  "aspectRatio": "ar",
  "current_page": "cp",
  "currentPage": "cp",
  "face_count": "fc",
  "faceCount": "fc",
  "height": "h",
  "initial_aspect_ratio": "iar",
  "initial_height": "ih",
  "initial_width": "iw",
  "initialAspectRatio": "iar",
  "initialHeight": "ih",
  "initialWidth": "iw",
  "page_count": "pc",
  "page_x": "px",
  "page_y": "py",
  "pageCount": "pc",
  "pageX": "px",
  "pageY": "py",
  "tags": "tags",
  "width": "w"
};

const LAYER_KEYWORD_PARAMS = {
  font_weight: "normal",
  font_style: "normal",
  text_decoration: "none",
  text_align: null,
  stroke: "none"
};

function textStyle(layer) {
  var attr, attr_value, default_value, font_family, font_size, keywords, letter_spacing, line_spacing;
  font_family = layer["font_family"];
  font_size = layer["font_size"];
  keywords = [];
  for (attr in LAYER_KEYWORD_PARAMS) {
    default_value = LAYER_KEYWORD_PARAMS[attr];
    attr_value = layer[attr] || default_value;
    if (attr_value !== default_value) {
      keywords.push(attr_value);
    }
  }
  letter_spacing = layer["letter_spacing"];
  if (letter_spacing) {
    keywords.push(`letter_spacing_${letter_spacing}`);
  }
  line_spacing = layer["line_spacing"];
  if (line_spacing) {
    keywords.push(`line_spacing_${line_spacing}`);
  }
  if (font_size || font_family || !isEmpty(keywords)) {
    if (!font_family) {
      raise(CloudinaryException, "Must supply font_family for text in overlay/underlay");
    }
    if (!font_size) {
      raise(CloudinaryException, "Must supply font_size for text in overlay/underlay");
    }
    keywords.unshift(font_size);
    keywords.unshift(font_family);
    return compact(keywords).join("_");
  }
}

/**
 * Normalize an offset value
 * @param {String} expression a decimal value which may have a 'p' or '%' postfix. E.g. '35%', '0.4p'
 * @return {Object|String} a normalized String of the input value if possible otherwise the value itself
 */
function normalize_expression(expression) {

  if (!isString(expression) || expression.length === 0 || expression.match(/^!.+!$/)) {
    return expression;
  }
  const operators = "\\|\\||>=|<=|&&|!=|>|=|<|/|-|\\+|\\*";
  const pattern = "((" + operators + ")(?=[ _])|" + Object.keys(PREDEFINED_VARS).join("|") + ")";
  const replaceRE = new RegExp(pattern, "g");
  expression = expression.replace(replaceRE, function(match) {
    return CONDITIONAL_OPERATORS[match] || PREDEFINED_VARS[match];
  });
  return expression.replace(/[ _]+/g, '_');
}

/**
 * Parse "if" parameter
 * Translates the condition if provided.
 * @private
 * @return {string} "if_" + ifValue
 */
function process_if(ifValue) {
  if (ifValue) {
    return "if_" + normalize_expression(ifValue);
  } else {
    return ifValue;
  }
}

/**
 * Parse layer options
 * @private
 * @param {object|*} layer The layer to parse.
 * @return {string} layer transformation string
 */
function process_layer(layer) {
  var components, format, public_id, re, res, resource_type, start, style, text, textSource, type;
  if (isPlainObject(layer)) {
    if (layer["resource_type"] === "fetch" || (layer["url"] != null)) {
      result = `fetch:${base64EncodeURL(layer['url'])}`;
    } else {
    public_id = layer["public_id"];
    format = layer["format"];
    resource_type = layer["resource_type"] || "image";
    type = layer["type"] || "upload";
    text = layer["text"];
    style = null;
    components = [];
    if (!isEmpty(public_id)) {
      public_id = public_id.replace(new RegExp("/", 'g'), ":");
      if (format != null) {
        public_id = `${public_id}.${format}`;
      }
    }
    if (isEmpty(text) && resource_type !== "text") {
      if (isEmpty(public_id)) {
        throw "Must supply public_id for resource_type layer_parameter";
      }
      if (resource_type === "subtitles") {
        style = textStyle(layer);
      }
    } else {
      resource_type = "text";
      type = null;
        // type is ignored for text layers
      style = textStyle(layer);
      if (!isEmpty(text)) {
        if (!(isEmpty(public_id) ^ isEmpty(style))) {
          throw "Must supply either style parameters or a public_id when providing text parameter in a text overlay/underlay";
        }
        re = /\$\([a-zA-Z]\w*\)/g;
        start = 0;
        textSource = smart_escape(decodeURIComponent(text), /[,\/]/g);
        text = "";
        while (res = re.exec(textSource)) {
          text += smart_escape(textSource.slice(start, res.index));
          text += res[0];
          start = res.index + res[0].length;
        }
        text += encodeURIComponent(textSource.slice(start));
      }
    }
    if (resource_type !== "image") {
      components.push(resource_type);
    }
    if (type !== "upload") {
      components.push(type);
    }
    components.push(style);
    components.push(public_id);
    components.push(text);
      result = compact(components).join(":");
    }
  } else if (/^fetch:.+/.test(layer)) {
    result = `fetch:${base64EncodeURL(layer.substr(6))}`;
  } else {
    result = layer;
  }
  return result;
};

function base64EncodeURL(url) {
  var ignore;
  try {
    url = decodeURI(url);
  } catch (error) {
    ignore = error;
  }
  url = encodeURI(url);
  return base64Encode(url);
};

function base64Encode(input) {
  if (!(input instanceof Buffer)) {
    input = new Buffer.from(String(input), 'binary');
  }
  return input.toString('base64');
};


exports.build_upload_params = function build_upload_params(options) {
  let params = {
    access_mode: options.access_mode,
    allowed_formats: options.allowed_formats && utils.build_array(options.allowed_formats).join(","),
    async: utils.as_safe_bool(options.async),
    backup: utils.as_safe_bool(options.backup),
    callback: options.callback,
    colors: utils.as_safe_bool(options.colors),
    discard_original_filename: utils.as_safe_bool(options.discard_original_filename),
    eager: utils.build_eager(options.eager),
    eager_async: utils.as_safe_bool(options.eager_async),
    eager_notification_url: options.eager_notification_url,
    exif: utils.as_safe_bool(options.exif),
    faces: utils.as_safe_bool(options.faces),
    folder: options.folder,
    format: options.format,
    image_metadata: utils.as_safe_bool(options.image_metadata),
    invalidate: utils.as_safe_bool(options.invalidate),
    moderation: options.moderation,
    notification_url: options.notification_url,
    overwrite: utils.as_safe_bool(options.overwrite),
    phash: utils.as_safe_bool(options.phash),
    proxy: options.proxy,
    public_id: options.public_id,
    responsive_breakpoints: utils.generate_responsive_breakpoints_string(options["responsive_breakpoints"]),
    return_delete_token: utils.as_safe_bool(options.return_delete_token),
    timestamp: exports.timestamp(),
    transformation: utils.generate_transformation_string(clone(options)),
    type: options.type,
    unique_filename: utils.as_safe_bool(options.unique_filename),
    upload_preset: options.upload_preset,
    use_filename: utils.as_safe_bool(options.use_filename),
  };
  return utils.updateable_resource_params(options, params);
};

exports.timestamp = function timestamp() {
  return Math.floor(new Date().getTime() / 1000);
};

/**
 * Deletes `option_name` from `options` and return the value if present.
 * If `options` doesn't contain `option_name` the default value is returned.
 * @param {Object} options a collection
 * @param {String} option_name the name (key) of the desired value
 * @param {*} [default_value] the value to return is option_name is missing
 */
exports.option_consume = function option_consume(options, option_name, default_value) {
  var result;
  result = options[option_name];
  delete options[option_name];
  if (result != null) {
    return result;
  } else {
    return default_value;
  }
};

exports.build_array = function build_array(arg) {
  if (arg == null) {
    return [];
  } else if (isArray(arg)) {
    return arg;
  } else {
    return [arg];
  }
};

exports.encode_double_array = function encode_double_array(array) {
  array = utils.build_array(array);
  if (array.length > 0 && isArray(array[0])) {
    return array.map(function(e) {
      return utils.build_array(e).join(",");
    }).join("|");
  } else {
    return array.join(",");
  }
};

exports.encode_key_value = function encode_key_value(arg) {
  var k, pairs, v;
  if (isObject(arg)) {
    pairs = (function() {
      var results;
      results = [];
      for (k in arg) {
        v = arg[k];
        results.push(`${k}=${v}`);
      }
      return results;
    })();
    return pairs.join("|");
  } else {
    return arg;
  }
};

exports.encode_context = function encode_context(arg) {
  var k, pairs, v;
  if (isObject(arg)) {
    pairs = (function() {
      var results;
      results = [];
      for (k in arg) {
        v = arg[k];
        v = v.replace(/([=|])/g, function(match) {
          return `\\${match}`;
        });
        results.push(`${k}=${v}`);
      }
      return results;
    })();
    return pairs.join("|");
  } else {
    return arg;
  }
};

exports.build_eager = function build_eager(transformations) {
  return utils.build_array(transformations).map(
    transformation => [
      utils.generate_transformation_string(clone(transformation)),
      transformation.format
    ].filter(utils.present).join('/')
  ).join('|');
};

/**
 * Build the custom headers for the request
 * @private
 * @param headers
 * @return {Array<string>|object|string} An object of name and value,
 *         an array of header strings, or a string of headers
 */
exports.build_custom_headers = function build_custom_headers(headers) {
  if (headers == null) {
    return void 0;
  } else if (isArray(headers)) {
    return headers.join("\n");
  } else if (isObject(headers)) {
    return Object.entries(headers).map(([k,v])=>`${k}:${v}`).join("\n");
  } else {
    return headers;
  }
};

const TRANSFORMATION_PARAMS = [
  'angle',
  'aspect_ratio',
  'audio_codec',
  'audio_frequency',
  'background',
  'bit_rate',
  'border',
  'color',
  'color_space',
  'crop',
  'default_image',
  'delay',
  'density',
  'dpr',
  'duration',
  'effect',
  'end_offset',
  'fetch_format',
  'flags',
  'gravity',
  'height',
  'if',
  'keyframe_interval',
  'offset',
  'opacity',
  'overlay',
  'page',
  'prefix',
  'quality',
  'radius',
  'raw_transformation',
  'responsive_width',
  'size',
  'start_offset',
  'streaming_profile',
  'transformation',
  'underlay',
  'variables',
  'video_codec',
  'video_sampling',
  'width',
  'x',
  'y',
  'zoom' // + any key that starts with '$'
];

exports.generate_transformation_string = function generate_transformation_string(options) {
  if (isArray(options)) {
    return options.map(t=>utils.generate_transformation_string(clone(t))).filter(utils.present).join('/');
  }
  let responsive_width = utils.option_consume(options, "responsive_width", config().responsive_width);
  let width = options["width"];
  let height = options["height"];
  let size = utils.option_consume(options, "size");
  if (size) {
    [options["width"], options["height"]] = [width, height] = size.split("x");
  }
  let has_layer = options.overlay || options.underlay;
  let crop = utils.option_consume(options, "crop");
  let angle = utils.build_array(utils.option_consume(options, "angle")).join(".");
  let no_html_sizes = has_layer || utils.present(angle) || crop === "fit" || crop === "limit" || responsive_width;
  if (width && (width.toString().indexOf("auto") === 0 || no_html_sizes || parseFloat(width) < 1)) {
    delete options["width"];
  }
  if (height && (no_html_sizes || parseFloat(height) < 1)) {
    delete options["height"];
  }
  let background = utils.option_consume(options, "background");
  background = background && background.replace(/^#/, "rgb:");
  let color = utils.option_consume(options, "color");
  color = color && color.replace(/^#/, "rgb:");
  let base_transformations = utils.build_array(utils.option_consume(options, "transformation", []));
  let named_transformation = [];
  if (base_transformations.length !== 0 && filter(base_transformations, isObject).length > 0) {
    base_transformations = map(base_transformations, function(base_transformation) {
      if (isObject(base_transformation)) {
        return utils.generate_transformation_string(clone(base_transformation));
      } else {
        return utils.generate_transformation_string({
          transformation: base_transformation
        });
      }
    });
  } else {
    named_transformation = base_transformations.join(".");
    base_transformations = [];
  }
  let effect = utils.option_consume(options, "effect");
  if (isArray(effect)) {
    effect = effect.join(":");
  } else if (isObject(effect)) {
    effect = Object.entries(effect).map(
      ([key, value])=> `${key}:${value}`
    );
  }
  let border = utils.option_consume(options, "border");
  if (isObject(border)) {
    border = `${border.width != null ? border.width : 2}px_solid_${(border.color != null ? border.color : "black").replace(/^#/, 'rgb:')}`;
  } else if (/^\d+$/.exec(border)) { //fallback to html border attributes
    options.border = border;
    border = void 0;
  }
  let flags = utils.build_array(utils.option_consume(options, "flags")).join(".");
  let dpr = utils.option_consume(options, "dpr", config().dpr);
  if (options["offset"] != null) {
    [options["start_offset"], options["end_offset"]] = split_range(utils.option_consume(options, "offset"));
  }
  let overlay = process_layer(utils.option_consume(options, "overlay"));
  let underlay = process_layer(utils.option_consume(options, "underlay"));
  let ifValue = process_if(utils.option_consume(options, "if"));
  let params = {
    a: normalize_expression(angle),
    ar: normalize_expression(utils.option_consume(options, "aspect_ratio")),
    b: background,
    bo: border,
    c: crop,
    co: color,
    dpr: normalize_expression(dpr),
    e: normalize_expression(effect),
    fl: flags,
    h: normalize_expression(height),
    ki: normalize_expression(utils.option_consume(options, "keyframe_interval")),
    l: overlay,
    o: normalize_expression(utils.option_consume(options, "opacity")),
    q: normalize_expression(utils.option_consume(options, "quality")),
    r: normalize_expression(utils.option_consume(options, "radius")),
    t: named_transformation,
    u: underlay,
    w: normalize_expression(width),
    x: normalize_expression(utils.option_consume(options, "x")),
    y: normalize_expression(utils.option_consume(options, "y")),
    z: normalize_expression(utils.option_consume(options, "zoom"))
  };
  let simple_params = {
    audio_codec: "ac",
    audio_frequency: "af",
    bit_rate: 'br',
    color_space: "cs",
    default_image: "d",
    delay: "dl",
    density: "dn",
    duration: "du",
    end_offset: "eo",
    fetch_format: "f",
    gravity: "g",
    page: "pg",
    prefix: "p",
    start_offset: "so",
    streaming_profile: "sp",
    video_codec: "vc",
    video_sampling: "vs"
  };

  for (let param in simple_params) {
    let short = simple_params[param];
    let value = utils.option_consume(options, param);
    if(value !== undefined){
      params[short] = value;
    }
  }
  if (params["vc"] != null) {
    params["vc"] = process_video_params(params["vc"]);
  }
  ["so", "eo", "du"].forEach(short=>{
    if(params[short] !== undefined){
      params[short] = norm_range_value(params[short]);
    }
  });

  let variablesParam = utils.option_consume(options, "variables", []);
  let variables = Object.entries(options)
    .filter(([key,value])=>key.startsWith('$'))
    .map(([key,value])=>{
      delete options[key];
      return `${key}_${normalize_expression(value)}`;
    }).sort().concat(
      variablesParam.map(([name,value])=>`${name}_${normalize_expression(value)}`)
    ).join(',');

  let transformations = Object.entries(params)
    .filter(([key,value])=>utils.present(value))
    .map(([key,value])=> key + '_' + value)
    .sort()
    .join(',');

  let raw_transformation = utils.option_consume(options, 'raw_transformation');
  transformations = compact([ifValue, variables, transformations, raw_transformation]).join(",");
  base_transformations.push(transformations);
  transformations = base_transformations;
  if (responsive_width) {
    let responsive_width_transformation = config().responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION;
    transformations.push(utils.generate_transformation_string(clone(responsive_width_transformation)));
  }
  if (width != null && width.toString().indexOf("auto")  === 0 || responsive_width) {
    options.responsive = true;
  }
  if (dpr === "auto") {
    options.hidpi = true;
  }
  return filter(transformations, utils.present).join("/");
};

exports.updateable_resource_params = function updateable_resource_params(options, params = {}) {
  if (options.access_control != null) {
    params.access_control = utils.jsonArrayParam(options.access_control);
  }
  if (options.auto_tagging != null) {
    params.auto_tagging = options.auto_tagging;
  }
  if (options.background_removal != null) {
    params.background_removal = options.background_removal;
  }
  if (options.categorization != null) {
    params.categorization = options.categorization;
  }
  if (options.context != null) {
    params.context = utils.encode_context(options.context);
  }
  if (options.custom_coordinates != null) {
    params.custom_coordinates = utils.encode_double_array(options.custom_coordinates);
  }
  if (options.detection != null) {
    params.detection = options.detection;
  }
  if (options.face_coordinates != null) {
    params.face_coordinates = utils.encode_double_array(options.face_coordinates);
  }
  if (options.headers != null) {
    params.headers = utils.build_custom_headers(options.headers);
  }
  if (options.notification_url != null) {
    params.notification_url = options.notification_url;
  }
  if (options.ocr != null) {
    params.ocr = options.ocr;
  }
  if (options.raw_convert != null) {
    params.raw_convert = options.raw_convert;
  }
  if (options.similarity_search != null) {
    params.similarity_search = options.similarity_search;
  }
  if (options.tags != null) {
    params.tags = utils.build_array(options.tags).join(",");
  }
  return params;
};

/**
 * A list of keys used by the url() function.
 * @private
 */
const URL_KEYS = ['api_secret', 'auth_token', 'cdn_subdomain', 'cloud_name', 'cname', 'format', 'private_cdn', 'resource_type', 'secure', 'secure_cdn_subdomain', 'secure_distribution', 'shorten', 'sign_url', 'ssl_detected', 'type', 'url_suffix', 'use_root_path', 'version'];

/**
 * Create a new object with only URL parameters
 * @param {object} options The source object
 * @return {Object} An object containing only URL parameters
 */
exports.extractUrlParams = function extractUrlParams(options) {
  return utils.only(options, ...URL_KEYS);
};

/**
 * Create a new object with only transformation parameters
 * @param {object} options The source object
 * @return {Object} An object containing only transformation parameters
 */
exports.extractTransformationParams = function extractTransformationParams(options){
  return utils.only(options, ...TRANSFORMATION_PARAMS);
};

/**
 * Handle the format parameter for fetch urls
 * @private
 * @param options url and transformation options. This argument may be changed by the function!
 */
exports.patchFetchFormat = function patchFetchFormat(options={}) {
  if (options.type === "fetch") {
    if (options.fetch_format == null) {
      options.fetch_format = utils.option_consume(options, "format");
    }
  }
};

exports.url = function url(public_id, options = {}) {
  let signature, source_to_sign;
  utils.patchFetchFormat(options);
  let type = utils.option_consume(options, "type", null);
  let transformation = utils.generate_transformation_string(options);
  let resource_type = utils.option_consume(options, "resource_type", "image");
  let version = utils.option_consume(options, "version");
  let format = utils.option_consume(options, "format");
  let cloud_name = utils.option_consume(options, "cloud_name", config().cloud_name);
  if (!cloud_name) {
    throw "Unknown cloud_name";
  }
  let private_cdn = utils.option_consume(options, "private_cdn", config().private_cdn);
  let secure_distribution = utils.option_consume(options, "secure_distribution", config().secure_distribution);
  let secure = utils.option_consume(options, "secure", null);
  let ssl_detected = utils.option_consume(options, "ssl_detected", config().ssl_detected);
  if (secure === null) {
    secure = ssl_detected || config().secure;
  }
  let cdn_subdomain = utils.option_consume(options, "cdn_subdomain", config().cdn_subdomain);
  let secure_cdn_subdomain = utils.option_consume(options, "secure_cdn_subdomain", config().secure_cdn_subdomain);
  let cname = utils.option_consume(options, "cname", config().cname);
  let shorten = utils.option_consume(options, "shorten", config().shorten);
  let sign_url = utils.option_consume(options, "sign_url", config().sign_url);
  let api_secret = utils.option_consume(options, "api_secret", config().api_secret);
  let url_suffix = utils.option_consume(options, "url_suffix");
  let use_root_path = utils.option_consume(options, "use_root_path", config().use_root_path);
  let auth_token = utils.option_consume(options, "auth_token");
  if (auth_token !== false) {
    auth_token = exports.merge(config().auth_token, auth_token);
  }
  let preloaded = /^(image|raw)\/([a-z0-9_]+)\/v(\d+)\/([^#]+)$/.exec(public_id);
  if (preloaded) {
    resource_type = preloaded[1];
    type = preloaded[2];
    version = preloaded[3];
    public_id = preloaded[4];
  }
  let original_source = public_id;
  if (public_id == null) {
    return original_source;
  }
  public_id = public_id.toString();
  if (type === null && public_id.match(/^https?:\//i)) {
    return original_source;
  }
  [resource_type, type] = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten);
  [public_id, source_to_sign] = finalize_source(public_id, format, url_suffix);
  if (source_to_sign.indexOf("/") > 0 && !source_to_sign.match(/^v[0-9]+/) && !source_to_sign.match(/^https?:\//)) {
    if (version == null) {
      version = 1;
    }
  }
  if (version != null) {
    version = `v${version}`;
  }
  transformation = transformation.replace(/([^:])\/\//g, '$1/');
  if (sign_url && isEmpty(auth_token)) {
    let to_sign = [transformation, source_to_sign].filter(function(part) {
      return (part != null) && part !== '';
    }).join('/');
    try {
      for (let i=0; to_sign !== decodeURIComponent(to_sign) && i < 10; i++) {
        to_sign = decodeURIComponent(to_sign);
      }
    } catch (error) {
    }
    let shasum = crypto.createHash('sha1');
    shasum.update(utf8_encode(to_sign + api_secret), 'binary');
    signature = shasum.digest('base64').replace(/\//g, '_').replace(/\+/g, '-').substring(0, 8);
    signature = `s--${signature}--`;
  }
  let prefix = unsigned_url_prefix(public_id, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution);
  let resultUrl = [prefix, resource_type, type, signature, transformation, version, public_id].filter(function(part) {
    return (part != null) && part !== '';
  }).join('/');
  if (sign_url && !isEmpty(auth_token)) {
    auth_token.url = urlParse(resultUrl).path;
    let token = generate_token(auth_token);
    resultUrl += `?${token}`;
  }
  return resultUrl;
};

exports.video_url = function video_url(public_id, options) {
  options = extend({
    resource_type: 'video'
  }, options);
  return utils.url(public_id, options);
};

function finalize_source(source, format, url_suffix) {
  var source_to_sign;
  source = source.replace(/([^:])\/\//g, '$1/');
  if (source.match(/^https?:\//i)) {
    source = smart_escape(source);
    source_to_sign = source;
  } else {
    source = encodeURIComponent(decodeURIComponent(source)).replace(/%3A/g, ":").replace(/%2F/g, "/");
    source_to_sign = source;
    if (!!url_suffix) {
      if (url_suffix.match(/[\.\/]/)) {
        throw new Error('url_suffix should not include . or /');
      }
      source = source + '/' + url_suffix;
    }
    if (format != null) {
      source = source + '.' + format;
      source_to_sign = source_to_sign + '.' + format;
    }
  }
  return [source, source_to_sign];
}
exports.video_thumbnail_url = function video_thumbnail_url(public_id, options) {
  options = extend({}, exports.DEFAULT_POSTER_OPTIONS, options);
  return utils.url(public_id, options);
};

function finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten) {
  if (type == null) {
    type = 'upload';
  }
  if (url_suffix != null) {
    if (resource_type === 'image' && type === 'upload') {
      resource_type = "images";
      type = null;
    } else if (resource_type === 'image' && type === 'private') {
      resource_type = 'private_images';
      type = null;
    } else if (resource_type === 'image' && type === 'authenticated') {
      resource_type = 'authenticated_images';
      type = null;
    } else if (resource_type === 'raw' && type === 'upload') {
      resource_type = 'files';
      type = null;
    } else if (resource_type === 'video' && type === 'upload') {
      resource_type = 'videos';
      type = null;
    } else {
      throw new Error("URL Suffix only supported for image/upload, image/private, image/authenticated, video/upload and raw/upload");
    }
  }
  if (use_root_path) {
    if ((resource_type === 'image' && type === 'upload') || (resource_type === 'images' && (type == null))) {
      resource_type = null;
      type = null;
    } else {
      throw new Error("Root path only supported for image/upload");
    }
  }
  if (shorten && resource_type === 'image' && type === 'upload') {
    resource_type = 'iu';
    type = null;
  }
  return [resource_type, type];
}
// cdn_subdomain and secure_cdn_subdomain
// 1) Customers in shared distribution (e.g. res.cloudinary.com)
//   if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https.
// 2) Customers with private cdn
//   if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http
//   if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this)
// 3) Customers with cname
//   if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.

function unsigned_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution) {
  let prefix;
  if (cloud_name.indexOf("/") === 0) {
    return '/res' + cloud_name;
  }
  let shared_domain = !private_cdn;
  if (secure) {
    if ((secure_distribution == null) || secure_distribution === exports.OLD_AKAMAI_SHARED_CDN) {
      secure_distribution = private_cdn ? cloud_name + "-res.cloudinary.com" : exports.SHARED_CDN;
    }
    if (shared_domain == null) {
      shared_domain = secure_distribution === exports.SHARED_CDN;
    }
    if ((secure_cdn_subdomain == null) && shared_domain) {
      secure_cdn_subdomain = cdn_subdomain;
    }
    if (secure_cdn_subdomain) {
      secure_distribution = secure_distribution.replace('res.cloudinary.com', 'res-' + ((crc32(source) % 5) + 1 + '.cloudinary.com'));
    }
    prefix = 'https://' + secure_distribution;
  } else if (cname) {
    let subdomain = cdn_subdomain ? 'a' + ((crc32(source) % 5) + 1) + '.' : '';
    prefix = 'http://' + subdomain + cname;
  } else {
    let cdn_part = private_cdn ? cloud_name + '-' : '';
    let subdomain_part = cdn_subdomain ? '-' + ((crc32(source) % 5) + 1) : '';
    let host = [cdn_part, 'res', subdomain_part, '.cloudinary.com'].join('');
    prefix = 'http://' + host;
  }
  if (shared_domain) {
    prefix += '/' + cloud_name;
  }
  return prefix;
}
// Based on CGI::unescape. In addition does not escape / :
//smart_escape = (string)->
//  encodeURIComponent(string).replace(/%3A/g, ":").replace(/%2F/g, "/")
function smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/g) {
  return string.replace(unsafe, function(match) {
    return match.split("").map(function(c) {
      return "%" + c.charCodeAt(0).toString(16).toUpperCase();
    }).join("");
  });
}
exports.api_url = function api_url(action = 'upload', options = {}) {
  let cloudinary = ensureOption(options,  "upload_prefix", "https://api.cloudinary.com");
  let cloud_name = ensureOption(options, "cloud_name");
  let resource_type = options["resource_type"] || "image";
  return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/");
};

exports.random_public_id = function random_public_id() {
  return crypto.randomBytes(12).toString('base64').replace(/[^a-z0-9]/g, "");
};

exports.signed_preloaded_image = function signed_preloaded_image(result) {
  return `${result.resource_type}/upload/v${result.version}/${filter([result.public_id, result.format], utils.present).join(".")}#${result.signature}`;
};

exports.api_sign_request = function api_sign_request(params_to_sign, api_secret) {
  let to_sign = Object.entries( params_to_sign).filter(
    ([k,v])=> utils.present(v)
  ).map(
    ([k,v])=>`${k}=${utils.build_array(v).join(",")}`
  ).sort().join("&");
  let shasum = crypto.createHash('sha1');
  shasum.update(utf8_encode(to_sign + api_secret), 'binary');
  return shasum.digest('hex');
};

exports.clear_blank = function clear_blank(hash) {
  let filtered_hash = {};
  Object.entries(hash).filter(
    ([k, v])=>utils.present(v)
  ).forEach(
    ([k, v])=> {filtered_hash[k] = v;}
  );
  return filtered_hash;
};

exports.merge = function merge(hash1, hash2) {
  return {...hash1, ...hash2};
};

exports.sign_request = function sign_request(params, options = {}) {
  let apiKey = ensureOption(options, 'api_key');
  let apiSecret = ensureOption(options, 'api_secret');
  params = exports.clear_blank(params);
  params.signature = exports.api_sign_request(params, apiSecret);
  params.api_key = apiKey;
  return params;
};

exports.webhook_signature = function webhook_signature(data, timestamp, options = {}) {
  ensurePresenceOf({data, timestamp});

  let api_secret = ensureOption(options, 'api_secret');
  let shasum = crypto.createHash('sha1');
  shasum.update(data + timestamp + api_secret, 'binary');
  return shasum.digest('hex');
};

exports.process_request_params = function process_request_params(params, options) {
  if ((options.unsigned != null) && options.unsigned) {
    params = exports.clear_blank(params);
    delete params["timestamp"];
  } else {
    params = exports.sign_request(params, options);
  }
  return params;
};

exports.private_download_url = function private_download_url(public_id, format, options = {}) {
  let params = exports.sign_request({
    timestamp: exports.timestamp(),
    public_id: public_id,
    format: format,
    type: options.type,
    attachment: options.attachment,
    expires_at: options.expires_at
  }, options);
  return exports.api_url("download", options) + "?" + querystring.stringify(params);
};

/**
 * Utility method that uses the deprecated ZIP download API.
 * @deprecated Replaced by {download_zip_url} that uses the more advanced and robust archive generation and download API
 */
exports.zip_download_url = function zip_download_url(tag, options = {}) {
  let params = exports.sign_request({
    timestamp: exports.timestamp(),
    tag: tag,
    transformation: utils.generate_transformation_string(options)
  }, options);
  return exports.api_url("download_tag.zip", options) + "?" + hashToQuery(params);
};

/**
 * Returns a URL that when invokes creates an archive and returns it.
 * @param {object} options
 * @param {string} [options.resource_type="image"]  The resource type of files to include in the archive. Must be one of :image | :video | :raw
 * @param {string} [options.type="upload"] The specific file type of resources: :upload|:private|:authenticated
 * @param {string|Array} [options.tags] list of tags to include in the archive
 * @param {string|Array<string>} [options.public_ids] list of public_ids to include in the archive
 * @param {string|Array<string>} [options.prefixes]  list of prefixes of public IDs (e.g., folders).
 * @param {string|Array<string>} [options.transformations]  list of transformations.
 *   The derived images of the given transformations are included in the archive. Using the string representation of
 *   multiple chained transformations as we use for the 'eager' upload parameter.
 * @param {string} [options.mode="create"] return the generated archive file or to store it as a raw resource and
 *   return a JSON with URLs for accessing the archive. Possible values: :download, :create
 * @param {string} [options.target_format="zip"]
 * @param {string} [options.target_public_id]  public ID of the generated raw resource.
 *   Relevant only for the create mode. If not specified, random public ID is generated.
 * @param {boolean} [options.flatten_folders=false] If true, flatten public IDs with folders to be in the root of the archive.
 *   Add numeric counter to the file name in case of a name conflict.
 * @param {boolean} [options.flatten_transformations=false] If true, and multiple transformations are given,
 *   flatten the folder structure of derived images and store the transformation details on the file name instead.
 * @param {boolean} [options.use_original_filename] Use the original file name of included images (if available) instead of the public ID.
 * @param {boolean} [options.async=false] If true, return immediately and perform the archive creation in the background.
 *   Relevant only for the create mode.
 * @param {string} [options.notification_url]  URL to send an HTTP post request (webhook) when the archive creation is completed.
 * @param {string|Array<string>} [options.target_tags=]  array. Allows assigning one or more tag to the generated archive file (for later housekeeping via the admin API).
 * @param {string} [options.keep_derived=false] keep the derived images used for generating the archive
 * @return {String} archive url
 */
exports.download_archive_url = function download_archive_url(options = {}) {
  let cloudinary_params = exports.sign_request(exports.archive_params(merge(options, {
    mode: "download"
  })), options);
  return exports.api_url("generate_archive", options) + "?" + hashToQuery(cloudinary_params);
};

/**
 * Returns a URL that when invokes creates an zip archive and returns it.
 * @see download_archive_url
 */
exports.download_zip_url = function download_zip_url(options = {}) {
  return exports.download_archive_url(merge(options, {
    target_format: "zip"
  }));
};

/**
 * Render the key/value pair as an HTML tag attribute
 * @private
 * @param {string} key
 * @param {string|boolean|number} [value]
 * @return {string} A string representing the HTML attribute
 */
function join_pair(key, value) {
  if (!value) {
    return void 0;
  } else if (value === true) {
    return key;
  } else {
    return key + "='" + value + "'";
  }
}

/**
 *
 * @param attrs
 * @return {*}
 */
exports.html_attrs = function html_attrs(attrs) {
  return filter(map(attrs, function(value, key) {
    return join_pair(key, value);
  })).sort().join(" ");
};

const CLOUDINARY_JS_CONFIG_PARAMS = ['api_key', 'cloud_name', 'private_cdn', 'secure_distribution', 'cdn_subdomain'];

exports.cloudinary_js_config = function cloudinary_js_config() {
  let params = utils.only(config(), ...CLOUDINARY_JS_CONFIG_PARAMS);
  return `<script type='text/javascript'>\n$.cloudinary.config(${JSON.stringify(params)});\n</script>`;
};

function v1_result_adapter(callback) {
  if (callback != null) {
    return function(result) {
      if (result.error != null) {
        return callback(result.error);
      } else {
        return callback(void 0, result);
      }
    };
  } else {
    return undefined;
  }
}
function v1_adapter(name, num_pass_args, v1) {
  return function(...args) {
    let pass_args = take(args, num_pass_args);
    let options = args[num_pass_args];
    let callback = args[num_pass_args + 1];
    if ((callback == null) && isFunction(options)) {
      callback = options;
      options = {};
    }
    callback = v1_result_adapter(callback);
    args = pass_args.concat([callback, options]);
    return v1[name].apply(this, args);
  };
}
exports.v1_adapters = function v1_adapters(exports, v1, mapping) {
  var name, num_pass_args, results;
  results = [];
  for (name in mapping) {
    num_pass_args = mapping[name];
    results.push(exports[name] = v1_adapter(name, num_pass_args, v1));
  }
  return results;
};

exports.as_safe_bool = function as_safe_bool(value) {
  if (value == null) {
    return void 0;
  }
  if (value === true || value === 'true' || value === '1') {
    value = 1;
  }
  if (value === false || value === 'false' || value === '0') {
    value = 0;
  }
  return value;
};

const NUMBER_PATTERN = "([0-9]*)\\.([0-9]+)|([0-9]+)";

const OFFSET_ANY_PATTERN = `(${NUMBER_PATTERN})([%pP])?`;
const RANGE_VALUE_RE = RegExp(`^${OFFSET_ANY_PATTERN}$`);
const OFFSET_ANY_PATTERN_RE = RegExp(`(${OFFSET_ANY_PATTERN})\\.\\.(${OFFSET_ANY_PATTERN})`);

// Split a range into the start and end values
function split_range(range) { // :nodoc:
  switch (range.constructor) {
    case String:
      if (OFFSET_ANY_PATTERN_RE.test(range)) {
        return range.split("..");
      }
      break;
    case Array:
      return [first(range), last(range)];
    default:
      return [null, null];
  }
}

function norm_range_value(value) { // :nodoc:
  let offset = String(value).match(RANGE_VALUE_RE);
  if (offset) {
    let modifier = offset[5] ? 'p' : '';
    value = `${offset[1] || offset[4]}${modifier}`;
  }
  return value;
}

/**
 * A video codec parameter can be either a String or a Hash.
 * @param {Object} param <code>vc_<codec>[ : <profile> : [<level>]]</code>
 *                       or <code>{ codec: 'h264', profile: 'basic', level: '3.1' }</code>
 * @return {String} <code><codec> : <profile> : [<level>]]</code> if a Hash was provided
 *                   or the param if a String was provided.
 *                   Returns null if param is not a Hash or String
 */
function process_video_params(param) {
  switch (param.constructor) {
    case Object:
      let video = "";
      if ('codec' in param) {
        video = param['codec'];
        if ('profile' in param) {
          video += ":" + param['profile'];
          if ('level' in param) {
            video += ":" + param['level'];
          }
        }
      }
      return video;
    case String:
      return param;
    default:
      return null;
  }
}
/**
 * Returns a Hash of parameters used to create an archive
 * @private
 * @param {object} options
 * @return {object} Archive API parameters
 */
exports.archive_params = function archive_params(options = {}) {
  return {
    allow_missing: exports.as_safe_bool(options.allow_missing),
    async: exports.as_safe_bool(options.async),
    expires_at: options.expires_at,
    flatten_folders: exports.as_safe_bool(options.flatten_folders),
    flatten_transformations: exports.as_safe_bool(options.flatten_transformations),
    keep_derived: exports.as_safe_bool(options.keep_derived),
    mode: options.mode,
    notification_url: options.notification_url,
    prefixes: options.prefixes && exports.build_array(options.prefixes),
    public_ids: options.public_ids && exports.build_array(options.public_ids),
    skip_transformation_name: exports.as_safe_bool(options.skip_transformation_name),
    tags: options.tags && exports.build_array(options.tags),
    target_format: options.target_format,
    target_public_id: options.target_public_id,
    target_tags: options.target_tags && exports.build_array(options.target_tags),
    timestamp: options.timestamp ? options.timestamp : exports.timestamp(),
    transformations: utils.build_eager(options.transformations),
    type: options.type,
    use_original_filename: exports.as_safe_bool(options.use_original_filename)
  };
};

exports.build_explicit_api_params = function build_explicit_api_params(public_id, options = {}) {
  return [exports.build_upload_params(extend({}, {public_id}, options))];
};

exports.generate_responsive_breakpoints_string = function generate_responsive_breakpoints_string(breakpoints) {
  if (breakpoints == null) {
    return;
  }
  breakpoints = clone(breakpoints);
  if (!isArray(breakpoints)) {
    breakpoints = [breakpoints];
  }
  for (let j = 0; j < breakpoints.length; j++) {
    let breakpoint_settings = breakpoints[j];
    if (breakpoint_settings != null) {
      if (breakpoint_settings.transformation) {
        breakpoint_settings.transformation = utils.generate_transformation_string(clone(breakpoint_settings.transformation));
      }
    }
  }
  return JSON.stringify(breakpoints);
};

exports.build_streaming_profiles_param = function build_streaming_profiles_param(options = {}) {
  let params = utils.only(options, "display_name", "representations");
  if (isArray(params["representations"])) {
    params["representations"] = JSON.stringify(params["representations"].map(
      r=> ({
        transformation: utils.generate_transformation_string(r.transformation)
      })
    ));
  }
  return params;
};

/**
 * Convert a hash of values to a URI query string.
 * Array values are spread as individual parameters.
 * @param {object} hash Key-value parameters
 * @return {string} A URI query string.
 */
function hashToQuery(hash) {
  return Object.entries(hash).reduce((entries, [key, value]) => {
    if (isArray(value)) {
      key = key.endsWith('[]') ? key : key + '[]';
      const items = value.map(v => [key, v]);
      entries = entries.concat(items);
    } else {
      entries.push([key, value]);
    }
    return entries;
  }, []).map(
    ([key, value]) => `${querystring.escape(key)}=${querystring.escape(value)}`
  ).join('&');
}

/**
 * Verify that the parameter `value` is defined and it's string value is not zero.
 * <br>This function should not be confused with `isEmpty()`.
 * @private
 * @param {string|number} value The value to check.
 * @return {boolean} True if the value is defined and not empty.
 */
exports.present = function present(value) {
  return value != null && ("" + value).length > 0;
};

/**
 * Returns a new object with key values from source based on the keys.
 * `null` or `undefined` values are not copied.
 * @private
 * @param {object} source The object to pick values from.
 * @param {...string} keys One or more keys to copy from source.
 * @return {object} A new object with the required keys and values.
 */
exports.only = function only(source, ...keys) {
  let result = {};
  if(source) {
    for (let j = 0; j < keys.length; j++) {
      let key = keys[j];
      if (source[key] != null) {
        result[key] = source[key];
      }
    }
  }
  return result;
};

/**
 * Returns a JSON array as String.
 * Yields the array before it is converted to JSON format
 * @private
 * @param {object|String|Array<object>} data
 * @param {function(*):*} [modifier] called with the array before the array is stringified
 * @return {String|null} a JSON array string or `null` if data is `null`
 */
exports.jsonArrayParam = function jsonArrayParam(data, modifier) {
  if (!data) {
    return null;
  }
  if (isString(data)) {
    data = JSON.parse(data);
  }
  if (!isArray(data)) {
    data = [data];
  }
  if (isFunction(modifier)) {
    data = modifier(data);
  }
  return JSON.stringify(data);
};

/**
 * Empty function - do nothing
 *
 */
exports.NOP = function () {
};

/**
 * Create a single source entry for the srcset attribute
 * @private
 * @param publicId The public ID of the resource
 * @param {object} options The transformation parameters, configuration parameters, and standard HTML &lt;img&gt; tag attributes to apply to the image tag.
 * @param {string|number} width The breakpoint (width specification) of the current entry
 * @return {string} a single srcset entry
 */
function singleSource(publicId, options, width) {
  let o = clone(options);
  let transformation = utils.generate_transformation_string([o, {crop: 'scale', width}]);
  const url = utils.url(publicId, extend(o, {raw_transformation: transformation}));
  return `${url} ${width}w`;
}

/**
 * Create the srcset attribute
 * @private
 * @param {string} publicId The public ID of the resource.
 * @param {Array<string|number>} stops A list of breakpoints (width specifications).
 * @param {object} options The transformation parameters, configuration parameters, and standard HTML &lt;img&gt; tag attributes to apply to the image tag.
 * @return {string} The srcset attribute
 */
function createSrcset(publicId, stops = [], options) {
  return stops.map(w(function() {
    return singleSource(publicId, options, w);
  })).join(", ");
}