<commit_before>var boom = require('boom')
var braveHapi = require('../brave-hapi')
var bson = require('bson')
var Joi = require('joi')
var underscore = require('underscore')

var v1 = {}

/*
   GET /v1/ad-manifest[? [since={milliseconds since epoch}&] [limit={positive integer}] ]
       defaults to since=0 limit=100

 */

v1.get =
{ handler: function (runtime) {
  return async function (request, reply) {
    var entries, modifiers, query, result
    var limit = parseInt(request.query.limit, 10)
    var timestamp = request.query.timestamp
    var siteInfo = runtime.db.get('site_info')

    try { timestamp = (timestamp || 0) ? bson.Timestamp.fromString(timestamp) : bson.Timestamp.ZERO } catch (ex) {
      return reply(boom.badRequest('invalid value for the timestamp parameter: ' + timestamp))
    }

    if (isNaN(limit) || (limit > 100)) limit = 100
    query = { timestamp: { $gte: timestamp } }
    modifiers = { sort: { timestamp: 1 } }

    entries = await siteInfo.find(query, underscore.extend({ limit: limit }, modifiers))
    result = []
    entries.forEach(entry => {
      if (entry.hostname === '') return
      result.push(underscore.extend(underscore.omit(entry, '_id', 'timestamp'),
                                    { timestamp: entry.timestamp.toString() }))
    })

    reply(result)
  }
},

  description: 'Incrementally returns ad manifests for zero or more sites',
  notes: 'The "timestamp" parameter corresponding to an opaque value, defaulting to "0". The "limit" parameter defaults to "100". The result is a JSON array containing zero or more entries.',
  tags: ['api'],

  validate:
    { query:
      { timestamp: Joi.string().regex(/^[0-9]+$/).optional().description('an opaque, monotonically-increasing value'),
        limit: Joi.number().positive().optional().description('the maximum number of entries to return')
      }
    },

  response: {
    schema: Joi.array().items(Joi.object().keys({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    }))
/*
    status: {
      400: Joi.object({
        boomlet: Joi.string().required().description('invalid value for the timestamp parameter')
      })
    }
 */
  }
}

/*
   GET /v1/ad-manifest/{hostname}
 */

v1.getHostname =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result
    var hostname = request.params.hostname
    var siteInfo = runtime.db.get('site_info')

    result = await siteInfo.findOne({ hostname: hostname })
    if (!result) { return reply(boom.notFound('ad-manifest entry does not exist: ' + hostname)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  description: 'Returns the ad manifest for a particular site',
  tags: ['api'],

  validate:
    { params:
      { hostname: Joi.string().hostname().required().description('the domain name of the site') }
    },

  response: {
    schema: Joi.object({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    })
/*
    status: {
      404: Joi.object({
        boomlet: Joi.string().required().description('ad-manifest entry does not exist')
      })
    }
 */
  }
}

/*
   POST /v1/ad-manifest
        { "hostname": "...", "replacementAd": "..." }
        create (entry MUST not exist)
 */

v1.post =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result
    var debug = braveHapi.debug(module, request)
    var payload = request.payload
    var hostname = payload.hostname
    var siteInfo = runtime.db.get('site_info')

    try {
      await siteInfo.insert(underscore.extend(payload, { timestamp: bson.Timestamp() }))
    } catch (ex) {
      debug('insert failed for siteInfo', ex)
      return reply(boom.badData('ad-manifest entry already exists: ' + hostname))
    }

    result = await siteInfo.findOne({ hostname: hostname })
    if (!result) { return reply(boom.badImplementation('database creation failed: ' + hostname)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  auth:
    { strategy: 'session',
      scope: [ 'devops' ],
      mode: 'required'
    },

  description: 'Creates the ad manifest for a particular site',
  tags: ['api'],

  validate:
    { payload:
      { hostname: Joi.string().hostname().required().description('the domain name of the site'),
        timestamp: Joi.any().forbidden(),
        replacementAd: Joi.array().items(Joi.object().keys({ width: Joi.number().positive().required().description('the ad\'s width in pixels'),
                                                             height: Joi.number().positive().required().description('the ad\'s height in pixels'),
                                                             replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
                                                           })).required()
      }
    },

  response: {
    schema: Joi.object({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    })
/*
    status: {
      422: Joi.object({
        boomlet: Joi.string().required().description('ad-manifest entry already exists')
      }),
      500: Joi.object({
        boomlet: Joi.string().required().description('database creation failed')
      })
    }
 */
  }
}

/*
   PUT /v1/ad-manifest/{hostname}
       { "replacementAd": "..." }
        create/update (entry MAY already exist)
 */

v1.putHostname =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result, state
    var hostname = request.params.hostname
    var siteInfo = runtime.db.get('site_info')

    state = { $currentDate: { timestamp: { $type: 'timestamp' } },
              $set: request.payload
            }
    await siteInfo.update({ hostname: hostname }, state, { upsert: true })

    result = await siteInfo.findOne({ hostname: hostname })
    if (!result) { return reply(boom.badImplementation('database update failed: ' + hostname)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  auth:
    { strategy: 'session',
      scope: [ 'devops' ],
      mode: 'required'
    },

  description: 'Sets the ad manifest for a particular site',
  tags: ['api'],

  validate:
    { params: { hostname: Joi.string().hostname().required().description('the domain name of the site') },
      payload:
      { hostname: Joi.any().forbidden(),
        timestamp: Joi.any().forbidden(),
        replacementAd: Joi.array().items(Joi.object().keys({ width: Joi.number().positive().required(),
                                                             height: Joi.number().positive().required(),
                                                             replaceId: Joi.string().required()
                                                           })).required()
      }
    },

  response: {
    schema: Joi.object({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    })
/*
    status: {
      500: Joi.object({
        boomlet: Joi.string().required().description('database update failed')
      })
    }
 */
  }
}

module.exports.routes =
[ braveHapi.routes.async().path('/v1/ad-manifest').config(v1.get),
  braveHapi.routes.async().path('/v1/ad-manifest/{hostname}').config(v1.getHostname),
  braveHapi.routes.async().post().path('/v1/ad-manifest').config(v1.post),
  braveHapi.routes.async().put().path('/v1/ad-manifest/{hostname}').config(v1.putHostname)
]

module.exports.initialize = async function (debug, runtime) {
  runtime.db.checkIndices(debug,
  [ { category: runtime.db.get('site_info'),
      name: 'site_info',
      property: 'hostname',
      empty: { hostname: '', timestamp: bson.Timestamp.ZERO },
      unique: [ { hostname: 1 } ],
      others: [ { timestamp: 1 } ]
    }
  ])
}
<commit_msg>add crumb to Joi checking<commit_after>var boom = require('boom')
var braveHapi = require('../brave-hapi')
var bson = require('bson')
var Joi = require('joi')
var underscore = require('underscore')

var v1 = {}

/*
   GET /v1/ad-manifest[? [since={milliseconds since epoch}&] [limit={positive integer}] ]
       defaults to since=0 limit=100

 */

v1.get =
{ handler: function (runtime) {
  return async function (request, reply) {
    var entries, modifiers, query, result
    var limit = parseInt(request.query.limit, 10)
    var timestamp = request.query.timestamp
    var siteInfo = runtime.db.get('site_info')

    try { timestamp = (timestamp || 0) ? bson.Timestamp.fromString(timestamp) : bson.Timestamp.ZERO } catch (ex) {
      return reply(boom.badRequest('invalid value for the timestamp parameter: ' + timestamp))
    }

    if (isNaN(limit) || (limit > 100)) limit = 100
    query = { timestamp: { $gte: timestamp } }
    modifiers = { sort: { timestamp: 1 } }

    entries = await siteInfo.find(query, underscore.extend({ limit: limit }, modifiers))
    result = []
    entries.forEach(entry => {
      if (entry.hostname === '') return
      result.push(underscore.extend(underscore.omit(entry, '_id', 'timestamp'),
                                    { timestamp: entry.timestamp.toString() }))
    })

    reply(result)
  }
},

  description: 'Incrementally returns ad manifests for zero or more sites',
  notes: 'The "timestamp" parameter corresponding to an opaque value, defaulting to "0". The "limit" parameter defaults to "100". The result is a JSON array containing zero or more entries.',
  tags: ['api'],

  validate:
    { query:
      { timestamp: Joi.string().regex(/^[0-9]+$/).optional().description('an opaque, monotonically-increasing value'),
        limit: Joi.number().positive().optional().description('the maximum number of entries to return')
      }
    },

  response: {
    schema: Joi.array().items(Joi.object().keys({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    }))
/*
    status: {
      400: Joi.object({
        boomlet: Joi.string().required().description('invalid value for the timestamp parameter')
      })
    }
 */
  }
}

/*
   GET /v1/ad-manifest/{hostname}
 */

v1.getHostname =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result
    var hostname = request.params.hostname
    var siteInfo = runtime.db.get('site_info')

    result = await siteInfo.findOne({ hostname: hostname })
    if (!result) { return reply(boom.notFound('ad-manifest entry does not exist: ' + hostname)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  description: 'Returns the ad manifest for a particular site',
  tags: ['api'],

  validate:
    { params:
      { hostname: Joi.string().hostname().required().description('the domain name of the site') }
    },

  response: {
    schema: Joi.object({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    })
/*
    status: {
      404: Joi.object({
        boomlet: Joi.string().required().description('ad-manifest entry does not exist')
      })
    }
 */
  }
}

/*
   POST /v1/ad-manifest
        { "hostname": "...", "replacementAd": "..." }
        create (entry MUST not exist)
 */

v1.post =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result
    var debug = braveHapi.debug(module, request)
    var payload = request.payload
    var hostname = payload.hostname
    var siteInfo = runtime.db.get('site_info')

    try {
      await siteInfo.insert(underscore.extend(payload, { timestamp: bson.Timestamp() }))
    } catch (ex) {
      debug('insert failed for siteInfo', ex)
      return reply(boom.badData('ad-manifest entry already exists: ' + hostname))
    }

    result = await siteInfo.findOne({ hostname: hostname })
    if (!result) { return reply(boom.badImplementation('database creation failed: ' + hostname)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  auth:
    { strategy: 'session',
      scope: [ 'devops' ],
      mode: 'required'
    },

  description: 'Creates the ad manifest for a particular site',
  tags: ['api'],

  validate:
    { query: { crumb: Joi.string().description('') },
      payload:
      { hostname: Joi.string().hostname().required().description('the domain name of the site'),
        timestamp: Joi.any().forbidden(),
        replacementAd: Joi.array().items(Joi.object().keys({ width: Joi.number().positive().required().description('the ad\'s width in pixels'),
                                                             height: Joi.number().positive().required().description('the ad\'s height in pixels'),
                                                             replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
                                                           })).required()
      }
    },

  response: {
    schema: Joi.object({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    })
/*
    status: {
      422: Joi.object({
        boomlet: Joi.string().required().description('ad-manifest entry already exists')
      }),
      500: Joi.object({
        boomlet: Joi.string().required().description('database creation failed')
      })
    }
 */
  }
}

/*
   PUT /v1/ad-manifest/{hostname}
       { "replacementAd": "..." }
        create/update (entry MAY already exist)
 */

v1.putHostname =
{ handler: function (runtime) {
  return async function (request, reply) {
    var result, state
    var hostname = request.params.hostname
    var siteInfo = runtime.db.get('site_info')

    state = { $currentDate: { timestamp: { $type: 'timestamp' } },
              $set: request.payload
            }
    await siteInfo.update({ hostname: hostname }, state, { upsert: true })

    result = await siteInfo.findOne({ hostname: hostname })
    if (!result) { return reply(boom.badImplementation('database update failed: ' + hostname)) }
    result = underscore.extend(underscore.omit(result, '_id', 'timestamp'), { timestamp: result.timestamp.toString() })

    reply(result)
  }
},

  auth:
    { strategy: 'session',
      scope: [ 'devops' ],
      mode: 'required'
    },

  description: 'Sets the ad manifest for a particular site',
  tags: ['api'],

  validate:
    { params:
      { hostname: Joi.string().hostname().required().description('the domain name of the site'),
        crumb: Joi.string().description('')
      },
      payload:
      { hostname: Joi.any().forbidden(),
        timestamp: Joi.any().forbidden(),
        replacementAd: Joi.array().items(Joi.object().keys({ width: Joi.number().positive().required(),
                                                             height: Joi.number().positive().required(),
                                                             replaceId: Joi.string().required()
                                                           })).required()
      }
    },

  response: {
    schema: Joi.object({
      hostname: Joi.string().hostname().required().description('the domain name of the site'),
      timestamp: Joi.string().required().description('an opaque, monotonically-increasing value'),
      replacementAd: Joi.array().items(Joi.object().keys({
        width: Joi.number().positive().required().description('the ad\'s width in pixels'),
        height: Joi.number().positive().required().description('the ad\'s height in pixels'),
        replaceId: Joi.string().required().description('the ad\'s DOM identifier ')
      }).required())
    })
/*
    status: {
      500: Joi.object({
        boomlet: Joi.string().required().description('database update failed')
      })
    }
 */
  }
}

module.exports.routes =
[ braveHapi.routes.async().path('/v1/ad-manifest').config(v1.get),
  braveHapi.routes.async().path('/v1/ad-manifest/{hostname}').config(v1.getHostname),
  braveHapi.routes.async().post().path('/v1/ad-manifest').config(v1.post),
  braveHapi.routes.async().put().path('/v1/ad-manifest/{hostname}').config(v1.putHostname)
]

module.exports.initialize = async function (debug, runtime) {
  runtime.db.checkIndices(debug,
  [ { category: runtime.db.get('site_info'),
      name: 'site_info',
      property: 'hostname',
      empty: { hostname: '', timestamp: bson.Timestamp.ZERO },
      unique: [ { hostname: 1 } ],
      others: [ { timestamp: 1 } ]
    }
  ])
}
<|endoftext|>


<commit_before>/* jshint laxcomma: true */
var utils = require('./utils');

exports.C14nCanonicalization = C14nCanonicalization;
exports.C14nCanonicalizationWithComments = C14nCanonicalizationWithComments;

function C14nCanonicalization() {
	this.includeComments = false;
};

C14nCanonicalization.prototype.attrCompare = function(a,b) {
  if (!a.namespaceURI && b.namespaceURI) { return -1; }
  if (!b.namespaceURI && a.namespaceURI) { return 1; }

  var left = a.namespaceURI + a.localName
  var right = b.namespaceURI + b.localName

  if (left===right) return 0
  else if (left<right) return -1
  else return 1

};

C14nCanonicalization.prototype.nsCompare = function(a,b) {
  var attr1 = a.prefix;
  var attr2 = b.prefix;
  if (attr1 == attr2) { return 0; }
  return attr1.localeCompare(attr2);
};

C14nCanonicalization.prototype.renderAttrs = function(node, defaultNS) {
  var a, i, attr
    , res = []
    , attrListToRender = [];

  if (node.nodeType===8) { return this.renderComment(node); }

  if (node.attributes) {
    for (i = 0; i < node.attributes.length; ++i) {
      attr = node.attributes[i];
      //ignore namespace definition attributes
      if (attr.name.indexOf("xmlns") === 0) { continue; }
      attrListToRender.push(attr);
    }
  }

  attrListToRender.sort(this.attrCompare);

  for (a in attrListToRender) {
    if (!attrListToRender.hasOwnProperty(a)) { continue; }

    attr = attrListToRender[a];
    res.push(" ", attr.name, '="', utils.encodeSpecialCharactersInAttribute(attr.value), '"');
  }

  return res.join("");
};

/**
 * Create the string of all namespace declarations that should appear on this element
 *
 * @param {Node} node. The node we now render
 * @param {Array} prefixesInScope. The prefixes defined on this node
 *                parents which are a part of the output set
 * @param {String} defaultNs. The current default namespace
 * @param {String} defaultNsForPrefix.
 * @param {String} ancestorNamespaces - Import ancestor namespaces if it is specified
 * @return {String}
 * @api private
 */
C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {
  var a, i, p, attr
    , res = []
    , newDefaultNs = defaultNs
    , nsListToRender = []
    , currNs = node.namespaceURI || "";

  //handle the namespaceof the node itself
  if (node.prefix) {
    if (prefixesInScope.indexOf(node.prefix)==-1) {
      nsListToRender.push({"prefix": node.prefix, "namespaceURI": node.namespaceURI || defaultNsForPrefix[node.prefix]});
      prefixesInScope.push(node.prefix);
    }
  }
  else if (defaultNs!=currNs) {
      //new default ns
      newDefaultNs = node.namespaceURI;
      res.push(' xmlns="', newDefaultNs, '"');
  }

  //handle the attributes namespace
  if (node.attributes) {
    for (i = 0; i < node.attributes.length; ++i) {
      attr = node.attributes[i];

      //handle all prefixed attributes that are included in the prefix list and where
      //the prefix is not defined already. New prefixes can only be defined by `xmlns:`.
      if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) {
        nsListToRender.push({"prefix": attr.localName, "namespaceURI": attr.value});
        prefixesInScope.push(attr.localName);
      }

      //handle all prefixed attributes that are not xmlns definitions and where
      //the prefix is not defined already
      if (attr.prefix && prefixesInScope.indexOf(attr.prefix)==-1 && attr.prefix!="xmlns" && attr.prefix!="xml") {
        nsListToRender.push({"prefix": attr.prefix, "namespaceURI": attr.namespaceURI});
        prefixesInScope.push(attr.prefix);
      }
    }
  }

  if(Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0){
    // Remove namespaces which are already present in nsListToRender
    for(var p1 in ancestorNamespaces){
      if(!ancestorNamespaces.hasOwnProperty(p1)) continue;
      var alreadyListed = false;
      for(var p2 in nsListToRender){
        if(nsListToRender[p2].prefix === ancestorNamespaces[p1].prefix
          && nsListToRender[p2].namespaceURI === ancestorNamespaces[p1].namespaceURI)
        {
          alreadyListed = true;
        }
      }

      if(!alreadyListed){
        nsListToRender.push(ancestorNamespaces[p1]);
      }
    }
  }

  nsListToRender.sort(this.nsCompare);

  //render namespaces
  for (a in nsListToRender) {
    if (!nsListToRender.hasOwnProperty(a)) { continue; }

    p = nsListToRender[a];
    res.push(" xmlns:", p.prefix, '="', p.namespaceURI, '"');
  }

  return {"rendered": res.join(""), "newDefaultNs": newDefaultNs};
};

C14nCanonicalization.prototype.processInner = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {

  if (node.nodeType === 8) { return this.renderComment(node); }
  if (node.data) { return utils.encodeSpecialCharactersInText(node.data); }

  var i, pfxCopy
    , ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces)
    , res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"];

  for (i = 0; i < node.childNodes.length; ++i) {
    pfxCopy = prefixesInScope.slice(0);
    res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, []));
  }

  res.push("</", node.tagName, ">");
  return res.join("");
};

// Thanks to deoxxa/xml-c14n for comment renderer
C14nCanonicalization.prototype.renderComment = function (node) {

  if (!this.includeComments) { return ""; }

  var isOutsideDocument = (node.ownerDocument === node.parentNode),
      isBeforeDocument = null,
      isAfterDocument = null;

  if (isOutsideDocument) {
    var nextNode = node,
        previousNode = node;

    while (nextNode !== null) {
      if (nextNode === node.ownerDocument.documentElement) {
        isBeforeDocument = true;
        break;
      }

      nextNode = nextNode.nextSibling;
    }

    while (previousNode !== null) {
      if (previousNode === node.ownerDocument.documentElement) {
        isAfterDocument = true;
        break;
      }

      previousNode = previousNode.previousSibling;
    }
  }

  return (isAfterDocument ? "\n" : "") + "<!--" + utils.encodeSpecialCharactersInText(node.data) + "-->" + (isBeforeDocument ? "\n" : "");
};

/**
 * Perform canonicalization of the given node
 *
 * @param {Node} node
 * @return {String}
 * @api public
 */
C14nCanonicalization.prototype.process = function(node, options) {
  options = options || {};
  var defaultNs = options.defaultNs || "";
  var defaultNsForPrefix = options.defaultNsForPrefix || {};
  var ancestorNamespaces = options.ancestorNamespaces || [];

  var prefixesInScope = [];
  for (var i = 0; i < ancestorNamespaces.length; i++) {
    prefixesInScope.push(ancestorNamespaces[i].prefix)
  }

  var res = this.processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces);
  return res;
};

C14nCanonicalization.prototype.getAlgorithmName = function() {
  return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
};

// Add c14n#WithComments here (very simple subclass)
exports.C14nCanonicalizationWithComments = C14nCanonicalizationWithComments;

function C14nCanonicalizationWithComments() {
	C14nCanonicalization.call(this);
	this.includeComments = true;
};

C14nCanonicalizationWithComments.prototype = Object.create(C14nCanonicalization.prototype);

C14nCanonicalizationWithComments.prototype.constructor = C14nCanonicalizationWithComments;

C14nCanonicalizationWithComments.prototype.getAlgorithmName = function() {
  return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
};
<commit_msg>Add `;` for consistency<commit_after>/* jshint laxcomma: true */
var utils = require('./utils');

exports.C14nCanonicalization = C14nCanonicalization;
exports.C14nCanonicalizationWithComments = C14nCanonicalizationWithComments;

function C14nCanonicalization() {
	this.includeComments = false;
};

C14nCanonicalization.prototype.attrCompare = function(a,b) {
  if (!a.namespaceURI && b.namespaceURI) { return -1; }
  if (!b.namespaceURI && a.namespaceURI) { return 1; }

  var left = a.namespaceURI + a.localName
  var right = b.namespaceURI + b.localName

  if (left===right) return 0
  else if (left<right) return -1
  else return 1

};

C14nCanonicalization.prototype.nsCompare = function(a,b) {
  var attr1 = a.prefix;
  var attr2 = b.prefix;
  if (attr1 == attr2) { return 0; }
  return attr1.localeCompare(attr2);
};

C14nCanonicalization.prototype.renderAttrs = function(node, defaultNS) {
  var a, i, attr
    , res = []
    , attrListToRender = [];

  if (node.nodeType===8) { return this.renderComment(node); }

  if (node.attributes) {
    for (i = 0; i < node.attributes.length; ++i) {
      attr = node.attributes[i];
      //ignore namespace definition attributes
      if (attr.name.indexOf("xmlns") === 0) { continue; }
      attrListToRender.push(attr);
    }
  }

  attrListToRender.sort(this.attrCompare);

  for (a in attrListToRender) {
    if (!attrListToRender.hasOwnProperty(a)) { continue; }

    attr = attrListToRender[a];
    res.push(" ", attr.name, '="', utils.encodeSpecialCharactersInAttribute(attr.value), '"');
  }

  return res.join("");
};

/**
 * Create the string of all namespace declarations that should appear on this element
 *
 * @param {Node} node. The node we now render
 * @param {Array} prefixesInScope. The prefixes defined on this node
 *                parents which are a part of the output set
 * @param {String} defaultNs. The current default namespace
 * @param {String} defaultNsForPrefix.
 * @param {String} ancestorNamespaces - Import ancestor namespaces if it is specified
 * @return {String}
 * @api private
 */
C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {
  var a, i, p, attr
    , res = []
    , newDefaultNs = defaultNs
    , nsListToRender = []
    , currNs = node.namespaceURI || "";

  //handle the namespaceof the node itself
  if (node.prefix) {
    if (prefixesInScope.indexOf(node.prefix)==-1) {
      nsListToRender.push({"prefix": node.prefix, "namespaceURI": node.namespaceURI || defaultNsForPrefix[node.prefix]});
      prefixesInScope.push(node.prefix);
    }
  }
  else if (defaultNs!=currNs) {
      //new default ns
      newDefaultNs = node.namespaceURI;
      res.push(' xmlns="', newDefaultNs, '"');
  }

  //handle the attributes namespace
  if (node.attributes) {
    for (i = 0; i < node.attributes.length; ++i) {
      attr = node.attributes[i];

      //handle all prefixed attributes that are included in the prefix list and where
      //the prefix is not defined already. New prefixes can only be defined by `xmlns:`.
      if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) {
        nsListToRender.push({"prefix": attr.localName, "namespaceURI": attr.value});
        prefixesInScope.push(attr.localName);
      }

      //handle all prefixed attributes that are not xmlns definitions and where
      //the prefix is not defined already
      if (attr.prefix && prefixesInScope.indexOf(attr.prefix)==-1 && attr.prefix!="xmlns" && attr.prefix!="xml") {
        nsListToRender.push({"prefix": attr.prefix, "namespaceURI": attr.namespaceURI});
        prefixesInScope.push(attr.prefix);
      }
    }
  }

  if(Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0){
    // Remove namespaces which are already present in nsListToRender
    for(var p1 in ancestorNamespaces){
      if(!ancestorNamespaces.hasOwnProperty(p1)) continue;
      var alreadyListed = false;
      for(var p2 in nsListToRender){
        if(nsListToRender[p2].prefix === ancestorNamespaces[p1].prefix
          && nsListToRender[p2].namespaceURI === ancestorNamespaces[p1].namespaceURI)
        {
          alreadyListed = true;
        }
      }

      if(!alreadyListed){
        nsListToRender.push(ancestorNamespaces[p1]);
      }
    }
  }

  nsListToRender.sort(this.nsCompare);

  //render namespaces
  for (a in nsListToRender) {
    if (!nsListToRender.hasOwnProperty(a)) { continue; }

    p = nsListToRender[a];
    res.push(" xmlns:", p.prefix, '="', p.namespaceURI, '"');
  }

  return {"rendered": res.join(""), "newDefaultNs": newDefaultNs};
};

C14nCanonicalization.prototype.processInner = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {

  if (node.nodeType === 8) { return this.renderComment(node); }
  if (node.data) { return utils.encodeSpecialCharactersInText(node.data); }

  var i, pfxCopy
    , ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces)
    , res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"];

  for (i = 0; i < node.childNodes.length; ++i) {
    pfxCopy = prefixesInScope.slice(0);
    res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, []));
  }

  res.push("</", node.tagName, ">");
  return res.join("");
};

// Thanks to deoxxa/xml-c14n for comment renderer
C14nCanonicalization.prototype.renderComment = function (node) {

  if (!this.includeComments) { return ""; }

  var isOutsideDocument = (node.ownerDocument === node.parentNode),
      isBeforeDocument = null,
      isAfterDocument = null;

  if (isOutsideDocument) {
    var nextNode = node,
        previousNode = node;

    while (nextNode !== null) {
      if (nextNode === node.ownerDocument.documentElement) {
        isBeforeDocument = true;
        break;
      }

      nextNode = nextNode.nextSibling;
    }

    while (previousNode !== null) {
      if (previousNode === node.ownerDocument.documentElement) {
        isAfterDocument = true;
        break;
      }

      previousNode = previousNode.previousSibling;
    }
  }

  return (isAfterDocument ? "\n" : "") + "<!--" + utils.encodeSpecialCharactersInText(node.data) + "-->" + (isBeforeDocument ? "\n" : "");
};

/**
 * Perform canonicalization of the given node
 *
 * @param {Node} node
 * @return {String}
 * @api public
 */
C14nCanonicalization.prototype.process = function(node, options) {
  options = options || {};
  var defaultNs = options.defaultNs || "";
  var defaultNsForPrefix = options.defaultNsForPrefix || {};
  var ancestorNamespaces = options.ancestorNamespaces || [];

  var prefixesInScope = [];
  for (var i = 0; i < ancestorNamespaces.length; i++) {
    prefixesInScope.push(ancestorNamespaces[i].prefix);
  }

  var res = this.processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces);
  return res;
};

C14nCanonicalization.prototype.getAlgorithmName = function() {
  return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
};

// Add c14n#WithComments here (very simple subclass)
exports.C14nCanonicalizationWithComments = C14nCanonicalizationWithComments;

function C14nCanonicalizationWithComments() {
	C14nCanonicalization.call(this);
	this.includeComments = true;
};

C14nCanonicalizationWithComments.prototype = Object.create(C14nCanonicalization.prototype);

C14nCanonicalizationWithComments.prototype.constructor = C14nCanonicalizationWithComments;

C14nCanonicalizationWithComments.prototype.getAlgorithmName = function() {
  return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
};
<|endoftext|>


<commit_before>// Maia Server Implementation
//
// @author balkian
// Grupo de Sistemas Inteligentes
// http://gsi.dit.upm.es
// http://github.com/gsi-upm
//
// Code based on:
// http://martinsikora.com/nodejs-and-websocket-simple-chat-tutorial
// http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/

var webSocketServer = require('websocket').server;
var express = require('express');
var http = require('http');
var path = require('path');
var Logger = require('simple-colourful-logger').Logger;

function MaiaServer(webSocketsServerPort, servestatic, app, levels){
    var self = this;
    if (!app || typeof app === 'undefined' || typeof app === 'null'){
        self.app = express();
    }else{
        self.app = app;
    }
    self.separator = "::";
    self.subscriptions = {};
    self.plugins = [];
    self.clients = [];
    self.logger = new Logger('Maia-Server',['all']);
    if(servestatic){
        self.staticpath = path.resolve(__dirname, '../public');
        self.app.use(express.static(self.staticpath));
    }
    self.app.use(express.bodyParser());
    self.server = http.createServer(self.app);
    self.server.listen(webSocketsServerPort, function() {
        self.logger.info((new Date()) + " Server is listening on port " + webSocketsServerPort);
    });

    var hookHandler = {
        name : 'Hooks Dispatcher',
        sendUTF:  function(message){
            var msg = JSON.parse(message);
            self.logger.debug('Received Hook:',msg.name);
        }
    }
    self.subscribe(['hook','**'],hookHandler);

    /**
     * WebSocket server
     */
    webSocketServer.call(self, {
        // WebSocket server is tied to a HTTP server. WebSocket request is just
        // an enhanced HTTP request. For more info http://tools.ietf.org/html/rfc6455#page-6
        httpServer: self.server
    });

    /*
     * Catch hooks and transform them to events
     */
    self.app.post(/^\/(hook\/?.*)/, function(req, res){
        var htype = [];
        if(req.params[0].length>1){
            htype = req.params[0].split('/');
        }
        var outevent = {
            name: htype,
            origin: 'Hooks Dispatcher',
            data: {
                origin: req.connection.remoteAddress,
                body: req.body,
                header: req.header,
            }
        };
        self.logger.info('Received:',outevent.name);
        self.send(outevent);
        res.end();
    });

    // Handle socket connections
    self.on('request', function(request) {
        self.logger.info((new Date()) + ' Connection from origin ' + request.origin + '.');
        // accept connection - you should check 'request.origin' to make sure that
        // client is connecting from your website
        // (http://en.wikipedia.org/wiki/Same_origin_policy)
        var connection = request.accept(null, request.origin);
        // we need to know client index to remove them on 'close' event
        var index = self.clients.push(connection) - 1;
        self.logger.info((new Date()) + ' Connection accepted.');
        self.logger.info('Connection: ' + connection);
        self.notifyPlugins('connection',connection);
        connection.on('message', function(message) {
                try{
                    self.logger.info((new Date()) + ' Received Message from '
                                + connection.name + ': ' + message.utf8Data);
                    var msg = JSON.parse(message.utf8Data);
                    msg.name = msg.name.split(self.separator);
                    if(msg.name[0] === 'username'){
                        connection.name = msg.data;
                        connection.sendUTF('{"name":"accepted","data":"'+connection.name+'"}')
                    }else if (msg.name[0] === 'subscribe'){
                        var path = msg.data.split(this.separator);
                        self.subscribe(path,self.clients[index]);
                        self.logger.info(self.subscriptions);
                    }else if (msg.name[0] === 'unsubscribe'){
                        var name = msg.data;
                        var path = msg.data.split(this.separator);
                        self.unsubscribe(path,self.clients[index]);
                        self.logger.info(self.subscriptions);
                    }else {
                        var obj = {
                            name: msg.name,
                            data: msg.data,
                            origin: connection.name,
                        };
                        self.send(obj);
                    }
                }catch(err){
                    self.logger.error(err);
                    throw err;
                }
        });
        // user disconnected
        connection.on('close', function(conn) {
            self.notifyPlugins('close',conn);
            self.logger.info((new Date()) + ' Peer '
                + connection.remoteAddress + ' (' + connection.name + ')' + ' disconnected.');
            self.unsubscribeAll(self.clients[index]);
            self.logger.debug('Removed subscriptions. Now: ', self.subscriptions);
            self.clients.splice(index, 1);
        });
    });
}

MaiaServer.prototype = Object.create(webSocketServer.prototype);

/**
 * Adds an event to be sent, and processes it in each plugin.
 * A plugin may modify the event, generate more events, or even
 * silent the event (prevent it from being sent).
 */
MaiaServer.prototype.send = function(obj){
    var res = Array(obj);
    for(plugin in this.plugins){
        this.logger.debug('Processing for plugin:',this.plugins[plugin].name);
        res = this.plugins[plugin].process(res);
    }
    // broadcast message to all connected clients
    for(i in res){
        this.logger.debug('Sending:',res[i].name);
        this.sendToSubscribed(res[i]);
    }
}

/**
 * Helper function to stringify an event
 */
MaiaServer.prototype.stringify = function(msg) {
    var firstTime = true;
    return JSON.stringify(msg, function(key,value){
        if( firstTime && key == 'name'){
            firstTime = false;
            return value.join(this.separator);
        }else{
            return value;
        }
    });
}

/**
 * Helper function for escaping input strings.
 */
MaiaServer.prototype.htmlEntities = function(str) {
    return String(str).replace(/&/g, '&').replace(/</g, '<')
                      .replace(/>/g, '>').replace(/"/g, '"');
}

/**
 * Remove all the subscriptions for a certain connection.
 *
 */
MaiaServer.prototype.unsubscribeAll = function(connection){
    var finished = false;
    var stack = [];
    stack.push([]);
    while(!finished){
        var path = stack.pop();
        if(!path){
            finished=true;
            break;
        }
        var n = this.getNode(path);
        this.unsubscribe(path,connection);
        for(var ix in n){
            if(ix !== '_subscribers'){
                stack.push(path.concat(Array(ix)));
            }
        }
    }
}

/**
 * Delete a specific subscription for a connection (user).
 *
 */
MaiaServer.prototype.unsubscribe = function(path,connection){
    var subs = this.getSubscriptions(path);
    if(subs){
        var ix = subs.indexOf(connection);
        if(ix>-1){
            subs.splice(ix,1);
        }
        var emptyChildren = false;
        for(var depth=path.length-1;depth>=0;depth--){
            var node = this.getNode(path.slice(0,depth+1));
            if(emptyChildren){
                delete node[path[depth+1]];
            }
            if(node._subscribers && node._subscribers.length<1){
                delete node._subscribers;
            }
            if(Object.keys(node).length == 0 || (Object.keys(node) == 1 && node._subscribers.length == 0)){
                emptyChildren = true;
            }else{
                emptyChildren = false;
            }
        }
        if(emptyChildren){
            delete this.subscriptions[path[0]];
        }
    }
}

/**
 * Helper function to get a node in the subscriptions tree, given an array path.
 *
 */

MaiaServer.prototype.getNode = function(path){
    var leaf = this.subscriptions;
    if(typeof path === 'string'){
        path=Array(path);
    }
    for(var token in path){
        var key = path[token];
        if(!leaf[key]){
            return null;
        }else{
            leaf = leaf[key];
        }
    }
    return leaf;
}

/**
 * Get all the subscriptions to an event namespace.
 *
 */
MaiaServer.prototype.getSubscriptions = function(path){
        return this.getNode(path)._subscribers;
}

/**
 * Search for subscriptions that match a certain pattern/namespace.
 *
 */
MaiaServer.prototype.recursiveSearch = function(tokens, tree, parentK, parentT){
    var matches = [];
    var key = tokens[0];
    if(tree === undefined){
        this.logger.error('Tree undefined');
        return [];
    }
    if(!key){
        if(Array(tree).length == 0 || (Array(tree).length == 1 && tree['_subscribers'])){
            return Array([parentT]);
        }else if(!tree['**']){
            return [];
        }
    }
    if( key == '*'){
        for(var tkey in tree){
            if(tkey !== '_subscribers'){
                matches.push([tkey,1]);
            }
        }
    }else if( key == '**'){
        for(var tkey in tree){
            if(tkey !== '_subscribers'){
                matches.push([tkey,1]);
                matches.push([tkey,0]);
            }
        }
        matches.push([null,1]);
    }else if( tree[key] ){
        matches.push([key,1]);
    }else if(tree['*']){
        matches.push(['*',1]);
    }
    if(tree['**']){
        matches.push(['**',1]);
    }
    if(parentT === '**'){
        matches.push([null,1]);
    }
    var results = [];
    var set = {};
    for(var match in matches){
        var moveTokens = matches[match][1];
        var moveTree = matches[match][0] != null;
        var nextT = parentT;
        var nextK = parentK;
        var nextTokens = tokens;
        var nextTree = tree;
        if(moveTokens){
            nextK = tokens[0];
            nextTokens = tokens.slice(1);
        }
        if(moveTree){
            nextT = matches[match][0];
            nextTree = tree[nextT];
        }
        tempResults = this.recursiveSearch(nextTokens, nextTree, nextK, nextT);
        if(tempResults.length > 0){
            set[nextT]=true;
            for(var ix in tempResults){
                if(parentT && moveTree){
                    tempResults[ix].unshift(parentT);
                }
                if(!set[JSON.stringify(tempResults[ix])]){
                    results.push(tempResults[ix])
                    set[JSON.stringify(tempResults[ix])] = true;
                }
            }
        }
    }
    return results;
}

/**
 * Add a subscription.
 *
 */

MaiaServer.prototype.subscribe = function(path,connection){
    var leaf = this.subscriptions;
    for(var token in path){
        var key = path[token];
        if(!leaf[key]){
            leaf[key] = {};
        }
        leaf = leaf[key]
    }
    if(!leaf._subscribers){
        leaf._subscribers = [connection,];
    } else {
        leaf._subscribers.push(connection);
    }
    this.logger.debug('Subscriptions: ', this.subscriptions);
    this.notifyPlugins('subscription',path,connection);
}

/**
 * Notify all the subscribers of a specific subscription
 *
 */
MaiaServer.prototype.pokeSubscribers = function(tokens ,event){
    var subs = this.getSubscriptions(tokens);
    event['forSubscription'] = tokens.join(this.separator);
    for(var subscriber in subs){
        this.logger.debug('Poking subscriber: '+subs[subscriber].name);
        subs[subscriber].sendUTF(this.stringify(event));
    }
    delete event['forSubscription'];
}

/**
 * Send a notification to all the possible subscribers
 *
 */
MaiaServer.prototype.sendToSubscribed = function(event){
    event.time = (new Date()).getTime();
    var results = this.recursiveSearch(event.name,this.subscriptions)
    this.logger.debug('Found subscriptions: ',results);
    this.logger.debug('For event: ',event.name);
    for(var result in results){
        this.pokeSubscribers(results[result],event);
    }
}

MaiaServer.prototype.addPlugin = function(plug){
    plug.server = this;
    var subs = plug.getSubscriptions();
    this.plugins.push(plug);
};

MaiaServer.prototype.notifyPlugins = function(){
    for(var plugin in this.plugins){
        this.logger.debug('Notifying plugin:', this.plugins[plugin].name, arguments[0]);
        this.plugins[plugin].emit.apply(this.plugins[plugin],arguments);
    }
}

exports.MaiaServer = MaiaServer;
exports.MaiaPlugin = require('./maia-plugin').MaiaPlugin;
<commit_msg>Not throwing exceptions anymore<commit_after>// Maia Server Implementation
//
// @author balkian
// Grupo de Sistemas Inteligentes
// http://gsi.dit.upm.es
// http://github.com/gsi-upm
//
// Code based on:
// http://martinsikora.com/nodejs-and-websocket-simple-chat-tutorial
// http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/

var webSocketServer = require('websocket').server;
var express = require('express');
var http = require('http');
var path = require('path');
var Logger = require('simple-colourful-logger').Logger;

function MaiaServer(webSocketsServerPort, servestatic, app, levels){
    var self = this;
    if (!app || typeof app === 'undefined' || typeof app === 'null'){
        self.app = express();
    }else{
        self.app = app;
    }
    self.separator = "::";
    self.subscriptions = {};
    self.plugins = [];
    self.clients = [];
    self.logger = new Logger('Maia-Server',['all']);
    if(servestatic){
        self.staticpath = path.resolve(__dirname, '../public');
        self.app.use(express.static(self.staticpath));
    }
    self.app.use(express.bodyParser());
    self.server = http.createServer(self.app);
    self.server.listen(webSocketsServerPort, function() {
        self.logger.info((new Date()) + " Server is listening on port " + webSocketsServerPort);
    });

    var hookHandler = {
        name : 'Hooks Dispatcher',
        sendUTF:  function(message){
            var msg = JSON.parse(message);
            self.logger.debug('Received Hook:',msg.name);
        }
    }
    self.subscribe(['hook','**'],hookHandler);

    /**
     * WebSocket server
     */
    webSocketServer.call(self, {
        // WebSocket server is tied to a HTTP server. WebSocket request is just
        // an enhanced HTTP request. For more info http://tools.ietf.org/html/rfc6455#page-6
        httpServer: self.server
    });

    /*
     * Catch hooks and transform them to events
     */
    self.app.post(/^\/(hook\/?.*)/, function(req, res){
        var htype = [];
        if(req.params[0].length>1){
            htype = req.params[0].split('/');
        }
        var outevent = {
            name: htype,
            origin: 'Hooks Dispatcher',
            data: {
                origin: req.connection.remoteAddress,
                body: req.body,
                header: req.header,
            }
        };
        self.logger.info('Received:',outevent.name);
        self.send(outevent);
        res.end();
    });

    // Handle socket connections
    self.on('request', function(request) {
        self.logger.info((new Date()) + ' Connection from origin ' + request.origin + '.');
        // accept connection - you should check 'request.origin' to make sure that
        // client is connecting from your website
        // (http://en.wikipedia.org/wiki/Same_origin_policy)
        var connection = request.accept(null, request.origin);
        // we need to know client index to remove them on 'close' event
        var index = self.clients.push(connection) - 1;
        self.logger.info((new Date()) + ' Connection accepted.');
        self.logger.info('Connection: ' + connection);
        self.notifyPlugins('connection',connection);
        connection.on('message', function(message) {
                try{
                    self.logger.info((new Date()) + ' Received Message from '
                                + connection.name + ': ' + message.utf8Data);
                    var msg = JSON.parse(message.utf8Data);
                    msg.name = msg.name.split(self.separator);
                    if(msg.name[0] === 'username'){
                        connection.name = msg.data;
                        connection.sendUTF('{"name":"accepted","data":"'+connection.name+'"}')
                    }else if (msg.name[0] === 'subscribe'){
                        var path = msg.data.split(this.separator);
                        self.subscribe(path,self.clients[index]);
                        self.logger.info(self.subscriptions);
                    }else if (msg.name[0] === 'unsubscribe'){
                        var name = msg.data;
                        var path = msg.data.split(this.separator);
                        self.unsubscribe(path,self.clients[index]);
                        self.logger.info(self.subscriptions);
                    }else {
                        var obj = {
                            name: msg.name,
                            data: msg.data,
                            origin: connection.name,
                        };
                        self.send(obj);
                    }
                }catch(err){
                    self.logger.error(err);
                }
        });
        // user disconnected
        connection.on('close', function(conn) {
            self.notifyPlugins('close',conn);
            self.logger.info((new Date()) + ' Peer '
                + connection.remoteAddress + ' (' + connection.name + ')' + ' disconnected.');
            self.unsubscribeAll(self.clients[index]);
            self.logger.debug('Removed subscriptions. Now: ', self.subscriptions);
            self.clients.splice(index, 1);
        });
    });
}

MaiaServer.prototype = Object.create(webSocketServer.prototype);

/**
 * Adds an event to be sent, and processes it in each plugin.
 * A plugin may modify the event, generate more events, or even
 * silent the event (prevent it from being sent).
 */
MaiaServer.prototype.send = function(obj){
    var res = Array(obj);
    for(plugin in this.plugins){
        this.logger.debug('Processing for plugin:',this.plugins[plugin].name);
        res = this.plugins[plugin].process(res);
    }
    // broadcast message to all connected clients
    for(i in res){
        this.logger.debug('Sending:',res[i].name);
        this.sendToSubscribed(res[i]);
    }
}

/**
 * Helper function to stringify an event
 */
MaiaServer.prototype.stringify = function(msg) {
    var firstTime = true;
    return JSON.stringify(msg, function(key,value){
        if( firstTime && key == 'name'){
            firstTime = false;
            return value.join(this.separator);
        }else{
            return value;
        }
    });
}

/**
 * Helper function for escaping input strings.
 */
MaiaServer.prototype.htmlEntities = function(str) {
    return String(str).replace(/&/g, '&').replace(/</g, '<')
                      .replace(/>/g, '>').replace(/"/g, '"');
}

/**
 * Remove all the subscriptions for a certain connection.
 *
 */
MaiaServer.prototype.unsubscribeAll = function(connection){
    var finished = false;
    var stack = [];
    stack.push([]);
    while(!finished){
        var path = stack.pop();
        if(!path){
            finished=true;
            break;
        }
        var n = this.getNode(path);
        this.unsubscribe(path,connection);
        for(var ix in n){
            if(ix !== '_subscribers'){
                stack.push(path.concat(Array(ix)));
            }
        }
    }
}

/**
 * Delete a specific subscription for a connection (user).
 *
 */
MaiaServer.prototype.unsubscribe = function(path,connection){
    var subs = this.getSubscriptions(path);
    if(subs){
        var ix = subs.indexOf(connection);
        if(ix>-1){
            subs.splice(ix,1);
        }
        var emptyChildren = false;
        for(var depth=path.length-1;depth>=0;depth--){
            var node = this.getNode(path.slice(0,depth+1));
            if(emptyChildren){
                delete node[path[depth+1]];
            }
            if(node._subscribers && node._subscribers.length<1){
                delete node._subscribers;
            }
            if(Object.keys(node).length == 0 || (Object.keys(node) == 1 && node._subscribers.length == 0)){
                emptyChildren = true;
            }else{
                emptyChildren = false;
            }
        }
        if(emptyChildren){
            delete this.subscriptions[path[0]];
        }
    }
}

/**
 * Helper function to get a node in the subscriptions tree, given an array path.
 *
 */

MaiaServer.prototype.getNode = function(path){
    var leaf = this.subscriptions;
    if(typeof path === 'string'){
        path=Array(path);
    }
    for(var token in path){
        var key = path[token];
        if(!leaf[key]){
            return null;
        }else{
            leaf = leaf[key];
        }
    }
    return leaf;
}

/**
 * Get all the subscriptions to an event namespace.
 *
 */
MaiaServer.prototype.getSubscriptions = function(path){
        return this.getNode(path)._subscribers;
}

/**
 * Search for subscriptions that match a certain pattern/namespace.
 *
 */
MaiaServer.prototype.recursiveSearch = function(tokens, tree, parentK, parentT){
    var matches = [];
    var key = tokens[0];
    if(tree === undefined){
        this.logger.error('Tree undefined');
        return [];
    }
    if(!key){
        if(Array(tree).length == 0 || (Array(tree).length == 1 && tree['_subscribers'])){
            return Array([parentT]);
        }else if(!tree['**']){
            return [];
        }
    }
    if( key == '*'){
        for(var tkey in tree){
            if(tkey !== '_subscribers'){
                matches.push([tkey,1]);
            }
        }
    }else if( key == '**'){
        for(var tkey in tree){
            if(tkey !== '_subscribers'){
                matches.push([tkey,1]);
                matches.push([tkey,0]);
            }
        }
        matches.push([null,1]);
    }else if( tree[key] ){
        matches.push([key,1]);
    }else if(tree['*']){
        matches.push(['*',1]);
    }
    if(tree['**']){
        matches.push(['**',1]);
    }
    if(parentT === '**'){
        matches.push([null,1]);
    }
    var results = [];
    var set = {};
    for(var match in matches){
        var moveTokens = matches[match][1];
        var moveTree = matches[match][0] != null;
        var nextT = parentT;
        var nextK = parentK;
        var nextTokens = tokens;
        var nextTree = tree;
        if(moveTokens){
            nextK = tokens[0];
            nextTokens = tokens.slice(1);
        }
        if(moveTree){
            nextT = matches[match][0];
            nextTree = tree[nextT];
        }
        tempResults = this.recursiveSearch(nextTokens, nextTree, nextK, nextT);
        if(tempResults.length > 0){
            set[nextT]=true;
            for(var ix in tempResults){
                if(parentT && moveTree){
                    tempResults[ix].unshift(parentT);
                }
                if(!set[JSON.stringify(tempResults[ix])]){
                    results.push(tempResults[ix])
                    set[JSON.stringify(tempResults[ix])] = true;
                }
            }
        }
    }
    return results;
}

/**
 * Add a subscription.
 *
 */

MaiaServer.prototype.subscribe = function(path,connection){
    var leaf = this.subscriptions;
    for(var token in path){
        var key = path[token];
        if(!leaf[key]){
            leaf[key] = {};
        }
        leaf = leaf[key]
    }
    if(!leaf._subscribers){
        leaf._subscribers = [connection,];
    } else {
        leaf._subscribers.push(connection);
    }
    this.logger.debug('Subscriptions: ', this.subscriptions);
    this.notifyPlugins('subscription',path,connection);
}

/**
 * Notify all the subscribers of a specific subscription
 *
 */
MaiaServer.prototype.pokeSubscribers = function(tokens ,event){
    var subs = this.getSubscriptions(tokens);
    event['forSubscription'] = tokens.join(this.separator);
    for(var subscriber in subs){
        this.logger.debug('Poking subscriber: '+subs[subscriber].name);
        subs[subscriber].sendUTF(this.stringify(event));
    }
    delete event['forSubscription'];
}

/**
 * Send a notification to all the possible subscribers
 *
 */
MaiaServer.prototype.sendToSubscribed = function(event){
    event.time = (new Date()).getTime();
    var results = this.recursiveSearch(event.name,this.subscriptions)
    this.logger.debug('Found subscriptions: ',results);
    this.logger.debug('For event: ',event.name);
    for(var result in results){
        this.pokeSubscribers(results[result],event);
    }
}

MaiaServer.prototype.addPlugin = function(plug){
    plug.server = this;
    var subs = plug.getSubscriptions();
    this.plugins.push(plug);
};

MaiaServer.prototype.notifyPlugins = function(){
    for(var plugin in this.plugins){
        this.logger.debug('Notifying plugin:', this.plugins[plugin].name, arguments[0]);
        this.plugins[plugin].emit.apply(this.plugins[plugin],arguments);
    }
}

exports.MaiaServer = MaiaServer;
exports.MaiaPlugin = require('./maia-plugin').MaiaPlugin;
<|endoftext|>


<commit_before><commit_msg>CR Feedback<commit_after><|endoftext|>


<commit_before>(function(h){function s(d){for(var e={__KEYS:[]},f=0,c=d.length;f<c;f++)for(var j=d[f].replace("://","__").split("/"),a=e,b=0,g=j.length;b<g;b++){var l=j[b];a[l]||(a[l]={__KEYS:[]},a.__KEYS.push(l));a=a[l]}d=[];f=e.__KEYS;c=0;for(j=f.length;c<j;c++){a=b=f[c];b=e[b];for(g=b.__KEYS;1===g.length;)a+="/"+g[0],b=b[g[0]],g=b.__KEYS;g.length&&d.push([a.replace("__","://"),t(b)])}return d}function t(d){for(var e=[],f=d.__KEYS,c=0,j=f.length;c<j;c++){var a=f[c],b=t(d[a]),g=b.length;if(g)for(var l=0;l<g;l++)e.push(a+
"/"+b[l]);else e.push(a)}return e}function u(d){for(var e=0,f=d.length;e<f;e++){for(var c=d[e],j=c[0]+"/",a=c[1],c=[],b={},g=0,l=a.length;g<l;g++){var k=a[g],h;h=k;var m=h.lastIndexOf(".");(h=0<=m?h.substring(m):"")&&(b[h]||(b[h]=[])).push(k)}a=void 0;for(a in b)b.hasOwnProperty(a)&&c.push(b[a]);b=0;for(a=c.length;b<a;b++)p(j,c[b])}return n}function p(d,e){var f=d+m[0]+e.join(m[1]),c=f.length>q;if(1<e.length&&c){a:{for(var c=q-(d+m[0]).length,f=m[1],j=e[0],a=1,b=e.length;a<b;a++)if(j+=f+e[a],j.length>
c){c=[e.splice(0,a),e];break a}c=void 0}p(d,c[0]);p(d,c[1])}else{if(c)throw Error("The combo url is too long: "+f);c=r.length;r[c]=f;f=0;for(j=e.length;f<j;f++)n[d+e[f]]=c}}var v=h.Module,y=v.STATUS.FETCHING,w=h.cache,k=h.data,n=k.comboHash={},r=k.comboHashValues=[],m=["??",","],q=2E3;h.on("load",function(d){var e=d.length;if(!(2>e)){k.comboSyntax&&(m=k.comboSyntax);k.comboMaxLength&&(q=k.comboMaxLength);for(var f=k.comboExcludes,c=[],j=0;j<e;j++){var a=d[j];if(!n[a]){var b;if(b=(w[a]||(w[a]=new v(a))).status<
y)if(b=!f||!f.test(a)){b=a;var g=k.comboSyntax||["??",","],h=g[0],g=g[1];b=!(h&&0<b.indexOf(h)||g&&0<b.indexOf(g))}b&&c.push(a)}}1<c.length&&u(s(c))}});h.on("fetch",function(d){d.requestUri=r[n[d.uri]]||d.uri});if(k.test){var x=h.test||(h.test={});x.uris2paths=s;x.paths2hash=u}define(h.data.dir+"plugin-combo",[],{})})(seajs);
<commit_msg>Rebuild packages<commit_after>(function(j){function r(d){for(var e={__KEYS:[]},f=0,c=d.length;f<c;f++)for(var h=d[f].replace("://","__").split("/"),b=e,a=0,g=h.length;a<g;a++){var l=h[a];b[l]||(b[l]={__KEYS:[]},b.__KEYS.push(l));b=b[l]}d=[];f=e.__KEYS;c=0;for(h=f.length;c<h;c++){b=a=f[c];a=e[a];for(g=a.__KEYS;1===g.length;)b+="/"+g[0],a=a[g[0]],g=a.__KEYS;g.length&&d.push([b.replace("__","://"),s(a)])}return d}function s(d){for(var e=[],f=d.__KEYS,c=0,h=f.length;c<h;c++){var b=f[c],a=s(d[b]),g=a.length;if(g)for(var l=0;l<g;l++)e.push(b+
"/"+a[l]);else e.push(b)}return e}function t(d){for(var e=0,f=d.length;e<f;e++){for(var c=d[e],h=c[0]+"/",b=c[1],c=[],a={},g=0,l=b.length;g<l;g++){var k=b[g],j;j=k;var m=j.lastIndexOf(".");(j=0<=m?j.substring(m):"")&&(a[j]||(a[j]=[])).push(k)}b=void 0;for(b in a)a.hasOwnProperty(b)&&c.push(a[b]);a=0;for(b=c.length;a<b;a++)p(h,c[a])}return n}function p(d,e){var f=d+m[0]+e.join(m[1]),c=f.length>q;if(1<e.length&&c){a:{for(var f=q-(d+m[0]).length,c=m[1],h=e[0],b=1,a=e.length;b<a;b++)if(h+=c+e[b],h.length>
f){f=[e.splice(0,b),e];break a}f=void 0}p(d,f[0]);p(d,f[1])}else{if(c)throw Error("The combo url is too long: "+f);c=0;for(h=e.length;c<h;c++)n[d+e[c]]=f}}var u=j.Module,w=u.STATUS.FETCHING,k=j.data,n=k.comboHash={},m=["??",","],q=2E3;j.on("load",function(d){var e=d.length;if(!(2>e)){k.comboSyntax&&(m=k.comboSyntax);k.comboMaxLength&&(q=k.comboMaxLength);for(var f=k.comboExcludes,c=[],h=0;h<e;h++){var b=d[h];if(!n[b]){var a;if(a=u.get(b).status<w)if(a=!f||!f.test(b)){a=b;var g=k.comboSyntax||["??",
","],j=g[0],g=g[1];a=!(j&&0<a.indexOf(j)||g&&0<a.indexOf(g))}a&&c.push(b)}}1<c.length&&t(r(c))}});j.on("fetch",function(d){d.requestUri=n[d.uri]||d.uri});if(k.test){var v=j.test||(j.test={});v.uris2paths=r;v.paths2hash=t}define(j.data.dir+"plugin-combo",[],{})})(seajs);
<|endoftext|>


<commit_before>/**

  A polyfill for the enhanced context.measureText() in HTML <canvas> v5 / Canvas
  2D Context Level 2. This API returns the 2D bounding box (and the vertical
  positions corresponding to various textBaseline settings) for a given text,
  taking into account the <canvas>'s font, textAlign, textBaseline and
  direction properties.

  Substantially based on fontmetrics.js version 1-2012.0121.1300 by Mike "Pomax"
  Kamermans, found at https://github.com/Pomax/fontmetrics.js .

  This library is licensed under the MIT (Expat) license,
  the text for which is included below.

** -----------------------------------------------------------------------------

  canvas-text-metrics-polyfill changelog:
	2014-12-24 - BREAKING CHANGE:
				 Flipped sign of baseline metrics (emHeightAscent,
				 emHeightDescent, hangingBaseline, alphabeticBaseline,
				 ideographicBaseline) to comply with standard.
				 This also breaks compatibility with Chrome Canary's buggy
				 implementation.

	2014-11-19 - Initial release.
				 Changes on top of fontmetrics.js:
				 * Reworked API to work as a polyfill for <canvas> v5.
				 * Optimized pixel scanning using Uint32Array where available.
				 * Added SVG measurement module. This turns out to be of
				   limited use, as browsers calculate a slightly different
				   bounding box for <svg:text> than what canvas.measureText
				   returns (at least in the one available native implementation
				   -  Google Chrome Canary). However this is now used for the
				   fontBoundingBox(A|Des)cent metrics.
				 * Extended API for increased performance: A second argument to
				   measureText() now lets the caller request specific metrics.
				 * Split out the code into multiple files, AMD-style, and added
				   package.json, Gruntfile.

  fontmetrics.js changelog:

    2012-01-21 - Whitespace handling added by Joe Turner
                 (https://github.com/oampo)

** -----------------------------------------------------------------------------

  fontmetrics.js Copyright (C) 2011 by Mike "Pomax" Kamermans

  Additions and repackaging as canvas-text-metrics-polyfill -
	Copyright (C) 2014 by Moti Zilberman

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
**/
/* jshint node: true */
(function() {
    'use strict';

    var CanvasMeasurementContext = require('./CanvasMeasurementContext');
    var SVGMeasurementContext = require('./SVGMeasurementContext');
    var sniffSVG = require('./sniffsvg');
    var BaselineCache = require('./BaselineCache');

    function isNativeImplementationAvailable() {
        var metrics = document.createElement("canvas")
            .getContext("2d")
            .measureText("");

        if (typeof metrics.actualBoundingBoxAscent === "undefined")
            return false;

        if (metrics.alphabeticBaseline > metrics.hangingBaseline)
        // nothing to polyfill
            return false;

        if (!metrics.emHeightAscent && !metrics.emHeightDescent)
            return false;

        return true;
    }

    if (isNativeImplementationAvailable())
        return;

    var sniff = sniffSVG(document);

    function TextMetrics(metrics) {
        this.width =
            this.actualBoundingBoxLeft =
            this.actualBoundingBoxRight =
            this.fontBoundingBoxAscent =
            this.fontBoundingBoxDescent =
            this.actualBoundingBoxAscent =
            this.actualBoundingBoxDescent =
            this.emHeightAscent =
            this.emHeightDescent =
            this.hangingBaseline =
            this.alphabeticBaseline =
            this.ideographicBaseline = undefined;

        this.raw = metrics;

        for (var prop in metrics)
            if (this.hasOwnProperty(prop))
                this[prop] = metrics[prop];
    }

    function convertToStandardMetrics(metrics) {
        return new TextMetrics({
            width: metrics.width,
            actualBoundingBoxAscent: metrics.ascent,
            actualBoundingBoxDescent: metrics.descent,
            actualBoundingBoxLeft: metrics.bounds.minx,
            actualBoundingBoxRight: metrics.bounds.maxx
        });
    }

    // store the old text metrics function on the Canvas2D prototype
    global.CanvasRenderingContext2D.prototype.measureTextWidth = global.CanvasRenderingContext2D.prototype.measureText;

    /**
     *  shortcut function for getting computed CSS values
     */

    function getDirection(el) {
        var dir = 'ltr';
        if (el.currentStyle)
            dir = el.currentStyle.direction;
        else if (global.getComputedStyle)
            dir = el.ownerDocument.defaultView.getComputedStyle(el, null).getPropertyValue('direction');
        return dir;
    }

    var canvasMeasuringContext = null,
        svgMeasuringContext = null;

    /**
     * The new text metrics function
     */
    global.CanvasRenderingContext2D.prototype.measureText = function measureText(textstring, model) {
        if (typeof(model) === 'string')
            switch (model) {
                case 'native':
                    return this.measureTextWidth.apply(this, arguments);
                case 'clear-baselines':
                    BaselineCache.clear();
                    break;
            }

        var requestedProperties;

        if ((typeof(model) === 'object') && model) {
            requestedProperties = {
                width: true,
                alphabeticBaseline: false,
                hangingBaseline: false,
                ideographicBaseline: false,
                emHeightAscent: false,
                emHeightDescent: false,
                actualBoundingBoxLeft: false,
                actualBoundingBoxRight: false,
                actualBoundingBoxAscent: false,
                actualBoundingBoxDescent: false,
                fontBoundingBoxAscent: false,
                fontBoundingBoxDescent: false
            };

            for (var prop in model)
                requestedProperties[prop] = model[prop] !== undefined;
        } else
            requestedProperties = {
                width: true,
                alphabeticBaseline: true,
                hangingBaseline: true,
                ideographicBaseline: true,
                emHeightAscent: true,
                emHeightDescent: true,
                actualBoundingBoxLeft: true,
                actualBoundingBoxRight: true,
                actualBoundingBoxAscent: true,
                actualBoundingBoxDescent: true,
                fontBoundingBoxAscent: true,
                fontBoundingBoxDescent: true
            };

        var doc = (this.canvas && this.canvas.ownerDocument) ? this.canvas.ownerDocument : document;
        if (!sniff)
            sniff = sniffSVG(this.doc);

        var isSpace = !(/\S/.test(textstring)),
            isDirRtl = (this.direction == 'rtl') || (this.direction == 'inherit' && getDirection(this.canvas) == 'rtl');

        var curStyle = {
            textBaseline: this.textBaseline,
            textAlign: this.textAlign,
            direction: (isDirRtl ? 'rtl' : 'ltr')
        };

        var collectedMetrics = {};

        var baselineProperties = {
            alphabeticBaseline: 'alphabetic',
            hangingBaseline: 'hanging',
            ideographicBaseline: 'ideographic',
            emHeightAscent: 'top',
            emHeightDescent: 'bottom'
        };
        var fontGlobalProperties = {
            alphabeticBaseline: true,
            hangingBaseline: true,
            ideographicBaseline: true,
            emHeightAscent: true,
            emHeightDescent: true,
            fontBoundingBoxAscent: true,
            fontBoundingBoxDescent: true
        };

        if (isSpace) {
            collectedMetrics.width = this.measureTextWidth(textstring).width;
            collectedMetrics.actualBoundingBoxLeft = 0;
            collectedMetrics.actualBoundingBoxRight = collectedMetrics.width;
            var dx = 0;
            switch (curStyle.textAlign) {
                case 'end':
                    dx = (!isDirRtl) ? collectedMetrics.width : 0;
                    break;
                case 'left':
                    dx = 0;
                    break;
                case 'center':
                    dx = collectedMetrics.width / 2;
                    break;
                case 'right':
                    dx = collectedMetrics.width;
                    break;
                case 'start':
                    /* falls through */
                default:
                    dx = (!isDirRtl) ? 0 : collectedMetrics.width;
                    break;
            }
            collectedMetrics.actualBoundingBoxLeft -= dx;
            collectedMetrics.actualBoundingBoxRight -= dx;

            var fontGlobalModel = {};
            var anyFontGlobalsRequested = false;
            for (var requestedProperty in requestedProperties) {
                if (requestedProperties[requestedProperty]) {
                    if (fontGlobalProperties.hasOwnProperty(requestedProperty))
                        anyFontGlobalsRequested = fontGlobalModel[requestedProperty] = true;
                    else if ((requestedProperty == 'actualBoundingBoxAscent') || (requestedProperty == 'actualBoundingBoxDescent'))
                        anyFontGlobalsRequested = fontGlobalModel.alphabeticBaseline = true;
                }
            }

            if (anyFontGlobalsRequested) {
                var nonspace = '_';
                var nonspaceMetrics = measureText.call(this, nonspace, fontGlobalModel);

                for (var fontGlobalProperty in nonspaceMetrics) {
                    if (requestedProperties[fontGlobalProperty] && fontGlobalProperties.hasOwnProperty(fontGlobalProperty) && nonspaceMetrics[fontGlobalProperty] !== undefined)
                        collectedMetrics[fontGlobalProperty] = nonspaceMetrics[fontGlobalProperty];
                }
                collectedMetrics.actualBoundingBoxAscent = -nonspaceMetrics.alphabeticBaseline;
                collectedMetrics.actualBoundingBoxDescent = nonspaceMetrics.alphabeticBaseline;
            }

            return new TextMetrics(collectedMetrics);
        }
        //debugger;

        if (!canvasMeasuringContext)
            canvasMeasuringContext = new CanvasMeasurementContext();

        canvasMeasuringContext.document(doc).font(this.font).text(textstring);

        var edges = {
            top: true,
            left: true,
            right: true,
            bottom: true
        };
        edges.left = requestedProperties.actualBoundingBoxLeft;
        edges.right = requestedProperties.actualBoundingBoxRight;
        edges.top = requestedProperties.actualBoundingBoxAscent || requestedProperties.alphabeticBaseline || requestedProperties.hangingBaseline || requestedProperties.ideographicBaseline || requestedProperties.emHeightAscent || requestedProperties.emHeightDescent;
        edges.bottom = requestedProperties.actualBoundingBoxDescent;

        var cachedBaselines = BaselineCache.fetch(curStyle);

        var partialMetrics = canvasMeasuringContext.measure(curStyle, edges);

        var actualBoundingBoxAscent = partialMetrics.actualBoundingBoxAscent; // save it separately so we don't mask it out if it wasn't requested

        for (var metric in partialMetrics) {
            if (requestedProperties[metric] && partialMetrics[metric] !== undefined) {
                collectedMetrics[metric] = partialMetrics[metric];
            }
        }

        var baselineMetrics = {};

        var firstBaseline = true;

        for (var baselineProperty in baselineProperties) {
            if (!requestedProperties[baselineProperty])
                continue;
            if (baselineProperties[baselineProperty] == this.textBaseline) {
                collectedMetrics[baselineProperty] = 0;
                continue;
            }
            if (cachedBaselines.hasOwnProperty(baselineProperty)) {
                collectedMetrics[baselineProperty] = cachedBaselines[baselineProperty];
                continue;
            }
            if (collectedMetrics.hasOwnProperty(baselineProperty))
                continue;
            var baselineValue = baselineProperties[baselineProperty];

            if (firstBaseline) {
                if (textstring.length > 1) // fudge the text string because we're only looking for font metrics independent of the text
                {
                    actualBoundingBoxAscent = canvasMeasuringContext.text('_').measure(curStyle, {
                        top: true
                    }).actualBoundingBoxAscent;
                }
                firstBaseline = false;
            }

            var altmeas = canvasMeasuringContext.measure({
                textBaseline: baselineValue,
                textAlign: this.textAlign,
                direction: (isDirRtl ? 'rtl' : 'ltr')
            }, {
                top: true
            });
            collectedMetrics[baselineProperty] = actualBoundingBoxAscent - altmeas.actualBoundingBoxAscent;
        }
        canvasMeasuringContext.end();

        if (requestedProperties.width && !collectedMetrics.hasOwnProperty('width'))
            collectedMetrics.width = this.measureTextWidth(textstring).width;

        if (requestedProperties.fontBoundingBoxAscent || requestedProperties.fontBoundingBoxDescent) {
            if (sniff.svg && sniff.svgText11) {
                if (!svgMeasuringContext)
                    svgMeasuringContext = new SVGMeasurementContext();
                svgMeasuringContext.document(doc).font(this.font).text(textstring);
                var svgmeas = svgMeasuringContext.measure(curStyle, {
                    top: true,
                    bottom: true
                });
                if (requestedProperties.fontBoundingBoxAscent)
                    collectedMetrics.fontBoundingBoxAscent = svgmeas.fontBoundingBoxAscent;
                if (requestedProperties.fontBoundingBoxDescent)
                    collectedMetrics.fontBoundingBoxDescent = svgmeas.fontBoundingBoxDescent;
                svgMeasuringContext.end();
            }
        }
        BaselineCache.store(curStyle, collectedMetrics);

        return new TextMetrics(collectedMetrics);
    };

})();<commit_msg>Make compatible with nw.js (node-webkit)<commit_after>/**

  A polyfill for the enhanced context.measureText() in HTML <canvas> v5 / Canvas
  2D Context Level 2. This API returns the 2D bounding box (and the vertical
  positions corresponding to various textBaseline settings) for a given text,
  taking into account the <canvas>'s font, textAlign, textBaseline and
  direction properties.

  Substantially based on fontmetrics.js version 1-2012.0121.1300 by Mike "Pomax"
  Kamermans, found at https://github.com/Pomax/fontmetrics.js .

  This library is licensed under the MIT (Expat) license,
  the text for which is included below.

** -----------------------------------------------------------------------------

  canvas-text-metrics-polyfill changelog:
	2014-12-24 - BREAKING CHANGE:
				 Flipped sign of baseline metrics (emHeightAscent,
				 emHeightDescent, hangingBaseline, alphabeticBaseline,
				 ideographicBaseline) to comply with standard.
				 This also breaks compatibility with Chrome Canary's buggy
				 implementation.

	2014-11-19 - Initial release.
				 Changes on top of fontmetrics.js:
				 * Reworked API to work as a polyfill for <canvas> v5.
				 * Optimized pixel scanning using Uint32Array where available.
				 * Added SVG measurement module. This turns out to be of
				   limited use, as browsers calculate a slightly different
				   bounding box for <svg:text> than what canvas.measureText
				   returns (at least in the one available native implementation
				   -  Google Chrome Canary). However this is now used for the
				   fontBoundingBox(A|Des)cent metrics.
				 * Extended API for increased performance: A second argument to
				   measureText() now lets the caller request specific metrics.
				 * Split out the code into multiple files, AMD-style, and added
				   package.json, Gruntfile.

  fontmetrics.js changelog:

    2012-01-21 - Whitespace handling added by Joe Turner
                 (https://github.com/oampo)

** -----------------------------------------------------------------------------

  fontmetrics.js Copyright (C) 2011 by Mike "Pomax" Kamermans

  Additions and repackaging as canvas-text-metrics-polyfill -
	Copyright (C) 2014 by Moti Zilberman

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
**/
/* jshint node: true */
(function() {
    'use strict';

    var CanvasMeasurementContext = require('./CanvasMeasurementContext');
    var SVGMeasurementContext = require('./SVGMeasurementContext');
    var sniffSVG = require('./sniffsvg');
    var BaselineCache = require('./BaselineCache');

    function isNativeImplementationAvailable() {
        var metrics = document.createElement("canvas")
            .getContext("2d")
            .measureText("");

        if (typeof metrics.actualBoundingBoxAscent === "undefined")
            return false;

        if (metrics.alphabeticBaseline > metrics.hangingBaseline)
        // nothing to polyfill
            return false;

        if (!metrics.emHeightAscent && !metrics.emHeightDescent)
            return false;

        return true;
    }

    if (isNativeImplementationAvailable())
        return;

    var sniff = sniffSVG(document);

    function TextMetrics(metrics) {
        this.width =
            this.actualBoundingBoxLeft =
            this.actualBoundingBoxRight =
            this.fontBoundingBoxAscent =
            this.fontBoundingBoxDescent =
            this.actualBoundingBoxAscent =
            this.actualBoundingBoxDescent =
            this.emHeightAscent =
            this.emHeightDescent =
            this.hangingBaseline =
            this.alphabeticBaseline =
            this.ideographicBaseline = undefined;

        this.raw = metrics;

        for (var prop in metrics)
            if (this.hasOwnProperty(prop))
                this[prop] = metrics[prop];
    }

    function convertToStandardMetrics(metrics) {
        return new TextMetrics({
            width: metrics.width,
            actualBoundingBoxAscent: metrics.ascent,
            actualBoundingBoxDescent: metrics.descent,
            actualBoundingBoxLeft: metrics.bounds.minx,
            actualBoundingBoxRight: metrics.bounds.maxx
        });
    }

    // store the old text metrics function on the Canvas2D prototype
    CanvasRenderingContext2D.prototype.measureTextWidth = CanvasRenderingContext2D.prototype.measureText;

    /**
     *  shortcut function for getting computed CSS values
     */

    function getDirection(el) {
        var dir = 'ltr';
        if (el.currentStyle)
            dir = el.currentStyle.direction;
        else if (window.getComputedStyle)
            dir = el.ownerDocument.defaultView.getComputedStyle(el, null).getPropertyValue('direction');
        return dir;
    }

    var canvasMeasuringContext = null,
        svgMeasuringContext = null;

    /**
     * The new text metrics function
     */
    CanvasRenderingContext2D.prototype.measureText = function measureText(textstring, model) {
        if (typeof(model) === 'string')
            switch (model) {
                case 'native':
                    return this.measureTextWidth.apply(this, arguments);
                case 'clear-baselines':
                    BaselineCache.clear();
                    break;
            }

        var requestedProperties;

        if ((typeof(model) === 'object') && model) {
            requestedProperties = {
                width: true,
                alphabeticBaseline: false,
                hangingBaseline: false,
                ideographicBaseline: false,
                emHeightAscent: false,
                emHeightDescent: false,
                actualBoundingBoxLeft: false,
                actualBoundingBoxRight: false,
                actualBoundingBoxAscent: false,
                actualBoundingBoxDescent: false,
                fontBoundingBoxAscent: false,
                fontBoundingBoxDescent: false
            };

            for (var prop in model)
                requestedProperties[prop] = model[prop] !== undefined;
        } else
            requestedProperties = {
                width: true,
                alphabeticBaseline: true,
                hangingBaseline: true,
                ideographicBaseline: true,
                emHeightAscent: true,
                emHeightDescent: true,
                actualBoundingBoxLeft: true,
                actualBoundingBoxRight: true,
                actualBoundingBoxAscent: true,
                actualBoundingBoxDescent: true,
                fontBoundingBoxAscent: true,
                fontBoundingBoxDescent: true
            };

        var doc = (this.canvas && this.canvas.ownerDocument) ? this.canvas.ownerDocument : document;
        if (!sniff)
            sniff = sniffSVG(this.doc);

        var isSpace = !(/\S/.test(textstring)),
            isDirRtl = (this.direction == 'rtl') || (this.direction == 'inherit' && getDirection(this.canvas) == 'rtl');

        var curStyle = {
            textBaseline: this.textBaseline,
            textAlign: this.textAlign,
            direction: (isDirRtl ? 'rtl' : 'ltr')
        };

        var collectedMetrics = {};

        var baselineProperties = {
            alphabeticBaseline: 'alphabetic',
            hangingBaseline: 'hanging',
            ideographicBaseline: 'ideographic',
            emHeightAscent: 'top',
            emHeightDescent: 'bottom'
        };
        var fontGlobalProperties = {
            alphabeticBaseline: true,
            hangingBaseline: true,
            ideographicBaseline: true,
            emHeightAscent: true,
            emHeightDescent: true,
            fontBoundingBoxAscent: true,
            fontBoundingBoxDescent: true
        };

        if (isSpace) {
            collectedMetrics.width = this.measureTextWidth(textstring).width;
            collectedMetrics.actualBoundingBoxLeft = 0;
            collectedMetrics.actualBoundingBoxRight = collectedMetrics.width;
            var dx = 0;
            switch (curStyle.textAlign) {
                case 'end':
                    dx = (!isDirRtl) ? collectedMetrics.width : 0;
                    break;
                case 'left':
                    dx = 0;
                    break;
                case 'center':
                    dx = collectedMetrics.width / 2;
                    break;
                case 'right':
                    dx = collectedMetrics.width;
                    break;
                case 'start':
                    /* falls through */
                default:
                    dx = (!isDirRtl) ? 0 : collectedMetrics.width;
                    break;
            }
            collectedMetrics.actualBoundingBoxLeft -= dx;
            collectedMetrics.actualBoundingBoxRight -= dx;

            var fontGlobalModel = {};
            var anyFontGlobalsRequested = false;
            for (var requestedProperty in requestedProperties) {
                if (requestedProperties[requestedProperty]) {
                    if (fontGlobalProperties.hasOwnProperty(requestedProperty))
                        anyFontGlobalsRequested = fontGlobalModel[requestedProperty] = true;
                    else if ((requestedProperty == 'actualBoundingBoxAscent') || (requestedProperty == 'actualBoundingBoxDescent'))
                        anyFontGlobalsRequested = fontGlobalModel.alphabeticBaseline = true;
                }
            }

            if (anyFontGlobalsRequested) {
                var nonspace = '_';
                var nonspaceMetrics = measureText.call(this, nonspace, fontGlobalModel);

                for (var fontGlobalProperty in nonspaceMetrics) {
                    if (requestedProperties[fontGlobalProperty] && fontGlobalProperties.hasOwnProperty(fontGlobalProperty) && nonspaceMetrics[fontGlobalProperty] !== undefined)
                        collectedMetrics[fontGlobalProperty] = nonspaceMetrics[fontGlobalProperty];
                }
                collectedMetrics.actualBoundingBoxAscent = -nonspaceMetrics.alphabeticBaseline;
                collectedMetrics.actualBoundingBoxDescent = nonspaceMetrics.alphabeticBaseline;
            }

            return new TextMetrics(collectedMetrics);
        }
        //debugger;

        if (!canvasMeasuringContext)
            canvasMeasuringContext = new CanvasMeasurementContext();

        canvasMeasuringContext.document(doc).font(this.font).text(textstring);

        var edges = {
            top: true,
            left: true,
            right: true,
            bottom: true
        };
        edges.left = requestedProperties.actualBoundingBoxLeft;
        edges.right = requestedProperties.actualBoundingBoxRight;
        edges.top = requestedProperties.actualBoundingBoxAscent || requestedProperties.alphabeticBaseline || requestedProperties.hangingBaseline || requestedProperties.ideographicBaseline || requestedProperties.emHeightAscent || requestedProperties.emHeightDescent;
        edges.bottom = requestedProperties.actualBoundingBoxDescent;

        var cachedBaselines = BaselineCache.fetch(curStyle);

        var partialMetrics = canvasMeasuringContext.measure(curStyle, edges);

        var actualBoundingBoxAscent = partialMetrics.actualBoundingBoxAscent; // save it separately so we don't mask it out if it wasn't requested

        for (var metric in partialMetrics) {
            if (requestedProperties[metric] && partialMetrics[metric] !== undefined) {
                collectedMetrics[metric] = partialMetrics[metric];
            }
        }

        var baselineMetrics = {};

        var firstBaseline = true;

        for (var baselineProperty in baselineProperties) {
            if (!requestedProperties[baselineProperty])
                continue;
            if (baselineProperties[baselineProperty] == this.textBaseline) {
                collectedMetrics[baselineProperty] = 0;
                continue;
            }
            if (cachedBaselines.hasOwnProperty(baselineProperty)) {
                collectedMetrics[baselineProperty] = cachedBaselines[baselineProperty];
                continue;
            }
            if (collectedMetrics.hasOwnProperty(baselineProperty))
                continue;
            var baselineValue = baselineProperties[baselineProperty];

            if (firstBaseline) {
                if (textstring.length > 1) // fudge the text string because we're only looking for font metrics independent of the text
                {
                    actualBoundingBoxAscent = canvasMeasuringContext.text('_').measure(curStyle, {
                        top: true
                    }).actualBoundingBoxAscent;
                }
                firstBaseline = false;
            }

            var altmeas = canvasMeasuringContext.measure({
                textBaseline: baselineValue,
                textAlign: this.textAlign,
                direction: (isDirRtl ? 'rtl' : 'ltr')
            }, {
                top: true
            });
            collectedMetrics[baselineProperty] = actualBoundingBoxAscent - altmeas.actualBoundingBoxAscent;
        }
        canvasMeasuringContext.end();

        if (requestedProperties.width && !collectedMetrics.hasOwnProperty('width'))
            collectedMetrics.width = this.measureTextWidth(textstring).width;

        if (requestedProperties.fontBoundingBoxAscent || requestedProperties.fontBoundingBoxDescent) {
            if (sniff.svg && sniff.svgText11) {
                if (!svgMeasuringContext)
                    svgMeasuringContext = new SVGMeasurementContext();
                svgMeasuringContext.document(doc).font(this.font).text(textstring);
                var svgmeas = svgMeasuringContext.measure(curStyle, {
                    top: true,
                    bottom: true
                });
                if (requestedProperties.fontBoundingBoxAscent)
                    collectedMetrics.fontBoundingBoxAscent = svgmeas.fontBoundingBoxAscent;
                if (requestedProperties.fontBoundingBoxDescent)
                    collectedMetrics.fontBoundingBoxDescent = svgmeas.fontBoundingBoxDescent;
                svgMeasuringContext.end();
            }
        }
        BaselineCache.store(curStyle, collectedMetrics);

        return new TextMetrics(collectedMetrics);
    };

})();<|endoftext|>