<commit_before>// DecentCMS (c) 2014 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';

// TODO: Enable shells to be restarted.
// TODO: Build file monitoring events so code that caches parsed files can expire and re-parse entries on-the-fly. Make sure that this can be done as an optional feature.

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var path = require('path');
var fs = require('fs');
var t = require('decent-core-localization').t;

/**
 * @description
 * A shell is the representation of a tenant in the system.
 * A shell has its own enabled services, that can be required by modules
 * running in its context.
 *
 * @constructor
 * @param {Object}  options
 * @param {String}  [options.name]         The name of the tenant.
 * @param {String}  [options.rootPath]     The path to the site's folder.
 * @param {String}  [options.settingsPath] The path to the settings file for this tenant.
 * @param {String}  [options.host]         The host name under which the tenant answers.
 * @param {Number}  [options.port]         The port to which the tenant answers.
 * @param {String}  [options.cert]         The path to the SSL certificate to use with this tenant.
 * @param {String}  [options.key]          The path to the SSL key to use with this tenant.
 * @param {String}  [options.pfx]          The path to the pfx SSL certificate to use with this tenant.
 * @param {Array}   [options.features]     The list of enabled feature names on this tenant.
 * @param {Object}  [options.services]     The enabled services keyed by service name.
 * @param {Boolean} [options.active]       True if the tenant is active.
 */
function Shell(options) {
  options = options || {};
  this.name = options.name;
  this.rootPath = options.rootPath;
  this.settingsPath = options.settingsPath;
  this.host = options.host || 'localhost';
  this.port = options.port || 80;
  this.https = !!options.https;
  this.cert = options.cert;
  this.key = options.key;
  this.pfx = options.pfx;
  this.features = options.features || [];
  this.availableModules = options.availableModules || {};
  this.services = options.services || {};
  this.active = !(options.active === false);
  this.serviceManifests = {};
  this.moduleManifests = {};
  this.modules = [];
  this.loaded = false;
}

util.inherits(Shell, EventEmitter);

/**
 * @description
 * An empty shell that you can use to initialize services if you don't need multitenancy.
 */
Shell.empty = new Shell({
  name: "Empty shell"
});

/**
 * @description
 * The list of tenants.
 */
Shell.list = {};

/**
 * @description
 * Creates a shell from its settings file.
 * @param {String} sitePath The path of the settings file.
 * @param {Object} [defaults] Default settings.
 * @returns {Shell} The new shell
 */
Shell.load = function(sitePath, defaults) {
  defaults = defaults || {};
  var settingsPath = path.join(sitePath, 'settings.json');
  var settings = require(settingsPath);
  settings.settingsPath = settingsPath;
  settings.rootPath = sitePath;
  for (var settingName in defaults) {
    if (!(settingName in settings)) {
      settings[settingName] = defaults[settingName];
    }
  }
  console.log(t('Loaded site settings from %s', settingsPath));
  return new Shell(settings);
}

/**
 * @description
 * Discovers all tenants in the ./sites directory.
 *
 * @param {Object} defaults  Default settings for the shells.
 * @param {String} rootPath  The root path where to look for shell settings files.
 *                           Defaults to ./sites
 */
Shell.discover = function(defaults, rootPath) {
  Shell.discoveryStart = new Date();
  rootPath = rootPath || './sites';
  console.log(t('Discovering tenants in %s', rootPath));
  var siteNames = fs.readdirSync(rootPath);
  siteNames.forEach(function(siteName) {
    var resolvedSitePath = path.resolve(rootPath, siteName);
    try {
      var shell = Shell.load(resolvedSitePath, defaults);
      Shell.list[siteName] = shell;
    }
    catch(ex) {
      ex.path = resolvedSitePath;
      ex.message = t('Failed to load site settings from %s.', settingsPath);
      throw ex;
    }
  });
  var elapsed = new Date() - Shell.discoveryStart;
  console.log(t('All tenants discovered in %s ms.', elapsed))
};

/**
 * @description
 * Returns the shell that should handle this request.
 *
 * @param {IncomingMessage} req The request
 */
Shell.resolve = function(req) {
  for (var shellName in Shell.list) {
    var shell = Shell.list[shellName];

    if (shell.active && shell.canHandle(req)) {
      return shell;
    }
  }
  return null; // Unresolved requests should not go to a default shell
}

/**
 * @description
 * Determines if the shell can handle that request.
 *
 * @param {IncomingMessage} req The request
 * @returns {Boolean} true if the shell can handle the request.
 */
Shell.prototype.canHandle = function(req) {
  var host = req.headers.host;
  return (
    (
      ((this.https && this.port === 443) || this.port === 80)
      && this.host === host
    )
    || (this.host + ':' + this.port === host)
  )
  && (
    !this.path
    || req.url.substr(0, this.path.length) === this.path
  );
}

/**
 * @description
 * Enables or disables the tenant.
 *
 * @param {Boolean} state If provided, sets the enabled state of the tenant. Otherwise, enables the tenant.
 */
Shell.prototype.enable = function(state) {
  this.active = typeof state === 'undefined' || !!state;
};

/**
 * @description
 * Disables the tenant.
 */
Shell.prototype.disable = function() {
  this.active = false;
};

/**
 * @description
 * Loads all enabled services in each module.
 */
Shell.prototype.load = function() {
  if (this.loaded || !this.availableModules) return;
  for (var moduleName in this.availableModules) {
    this.loadModule(moduleName);
  }
  this.loaded = true;
};

// TODO: replace service manifest with meta-data on the services themselves, auto-discover and load
/**
 * @description
 * Loads all the services in the module under the path passed as a parameter.
 * Services are loaded while respecting the dependency order.
 *
 * @param {String} moduleName  The name of the module to load.
 */
Shell.prototype.loadModule = function(moduleName) {
  var self = this;
  var manifest = self.availableModules[moduleName];
  if (self.moduleManifests[moduleName]) return;
  self.moduleManifests[moduleName] = manifest;
  var features = self.features;
  var services = manifest.services;
  var anyEnabledService;
  for (var serviceName in services) {
    var service = services[serviceName];
    var serviceFeature = service.feature;
    if (serviceFeature && features.indexOf(serviceFeature) === -1) continue;
    var servicePath = path.resolve(manifest.physicalPath, service.path + ".js");
    if (self.serviceManifests[servicePath]) continue;
    var dependencies = service.dependencies;
    if (dependencies) {
      dependencies.forEach(function(dependencyPath) {
        self.loadModule(dependencyPath);
      });
    }
    var ServiceClass = require(servicePath);
    if (!self.services[serviceName]) {
      self.services[serviceName] = [ServiceClass];
    }
    else {
      self.services[serviceName].push(ServiceClass);
    }
    ServiceClass.manifest = service;
    self.serviceManifests[servicePath] = service;
    if (ServiceClass.init) {
      ServiceClass.init(self);
    }
    // Wire up declared static event handlers
    if (ServiceClass.on) {
      for (var eventName in ServiceClass.on) {
        (function(ServiceClass, eventName) {
          self.on(eventName, function (payload) {
            ServiceClass.on[eventName](self, payload);
          });
        })(ServiceClass, eventName);
      }
    }
    anyEnabledService = true;
    console.log(t('Loaded service %s from %s', serviceName, servicePath));
  }
  if (anyEnabledService) {
    self.modules.push(moduleName);
  }
};

/**
 * @description
 * Returns an instance of a service implementing the named contract passed as a parameter.
 * If more than one service exists in the tenant for that contract, one instance that
 * has dependencies on any other service for that contract is returned. Do not
 * count on any particular service being returned if that is the case among the ones
 * that have the most dependencies.
 * A new instance is returned every time the function is called.
 *
 * @param {String} service  The name of the contract for which a service instance is required.
 * @param {object} options Options to pass into the service's constructor.
 * @returns {object} An instance of the service, or null if it wasn't found.
 */
Shell.prototype.require = function(service, options) {
  var services = this.services[service];
  var ServiceClass = Array.isArray(services)
    ? (services.length > 0 ? services[services.length - 1] : null) : null;
  return this.construct(ServiceClass, options);
};

/**
 * @description
 * Constructs an instance of the class passed in.
 * If the class is a shell singleton, the same instance
 * is always returned for any given shell.
 * Shell singletons are classes marked with isShellSingleton = true.
 * Otherwise, a new instance is created on each call.
 * Don't call this directly, it should only be internally used by Shell.
 * @param {Function} ServiceClass The class to instantiate.
 *                                This constructor must always take a shell as its first argument.
 *                                It can also take an optional 'options' argument, unless it's a shell singleton.
 * @param {object} options Options to pass into the service's constructor.
 * @returns {object} An instance of the service, or null if it wasn't found.
 */
Shell.prototype.construct = function(ServiceClass, options) {
  return ServiceClass ?
    ServiceClass.isShellSingleton ?
      ServiceClass.instance ?
        ServiceClass.instance :
        ServiceClass.instance = new ServiceClass(this)
      : new ServiceClass(this, options)
    : null;
};

/**
 * @description
 * Returns a list of service instances that are implementing the named contract passed as a parameter.
 * The services are returned in order of dependency: if service A has a dependency
 * on service B, B is guaranteed to appear earlier in the list.
 * New instances are returned every time the function is called.
 *
 * @param {String} service The name of the contract for which service instances are required.
 * @param {object} options Options to pass into the services' constructors.
 * @returns {Array} An array of instances of the service.
 */
Shell.prototype.getServices = function(service, options) {
  var self = this;
  if (!(service in self.services)) return [];
  return self.services[service].map(function(service) {
    return self.construct(service, options);
  });
};

/**
 * @description
 * Handles the request for the tenant
 *
 * @param {http.IncomingMessage} req Request
 * @param {http.ServerResponse}  res Response
 */
Shell.prototype.handleRequest = function(req, res) {
  var self = this;
  req.startTime = new Date();
  var payload = {
    shell: self,
    req: req,
    res: res,
    handled: false
  };
  // Let services register themselves with the request
  self.emit(Shell.startRequestEvent, payload);
  // Does anyone want to handle this?
  self.emit(Shell.handleRouteEvent, payload);
  self.emit(Shell.fetchContentEvent, {
    callback: function(err, data) {
      if (err) {
        self.emit(Shell.renderErrorPage, err);
        return;
      }
      // Let's render stuff
      self.emit(Shell.renderPageEvent, {
        req: payload.req,
        res: payload.res
      });
      // Tear down
      self.emit(Shell.endRequestEvent, payload);
      self
        .removeAllListeners(Shell.endRequestEvent)
        .removeAllListeners(Shell.renderPageEvent);
    }
  });
};

// TODO: document event payloads
/**
 * @description
 * The event that is broadcast at the beginning of request handling.
 * This is a good time to attach a service to the request object.
 * @type {string}
 */
Shell.startRequestEvent = Shell.prototype.startRequestEvent = 'decent-core-shell-start-request';

/**
 * @description
 * The event that is broadcast at the end of request handling.
 * This is a good time to detach services and events.
 * @type {string}
 */
Shell.endRequestEvent = Shell.prototype.endRequestEvent = 'decent-core-shell-end-request';

/**
 * @description
 * The event that is broadcast when a route needs to be resolved.
 * @type {string}
 */
Shell.handleRouteEvent = Shell.prototype.handleRouteEvent = 'decent.core.shell.handle-route';

/**
 * @description
 * The event that is broadcast when content needs to be fetched from stores.
 * @type {string}
 */
Shell.fetchContentEvent = Shell.prototype.fetchContentEvent = 'decent.core.shell.fetch-content';

/**
 * @description
 * The event that is broadcast when the page is ready to be rendered.
 * @type {string}
 */
Shell.renderPageEvent = Shell.prototype.renderPageEvent = 'decent.core.shell.render-page';

/**
 * @description
 * This event is triggered when an error has happened during the page lifecycle.
 * @type {string}
 */
Shell.renderErrorPage = Shell.prototype.renderErrorPage = 'decent.core.shell.render-error';

module.exports = Shell;<commit_msg>Shell support for content type definitions.<commit_after>// DecentCMS (c) 2014 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';

// TODO: Enable shells to be restarted.
// TODO: allow for purely static services.
// TODO: Build file monitoring events so code that caches parsed files can expire and re-parse entries on-the-fly. Make sure that this can be done as an optional feature.

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var path = require('path');
var fs = require('fs');
var t = require('decent-core-localization').t;

/**
 * @description
 * A shell is the representation of a tenant in the system.
 * A shell has its own enabled services, that can be required by modules
 * running in its context.
 *
 * @constructor
 * @param {Object}  options
 * @param {String}  [options.name]         The name of the tenant.
 * @param {String}  [options.rootPath]     The path to the site's folder.
 * @param {String}  [options.settingsPath] The path to the settings file for this tenant.
 * @param {String}  [options.host]         The host name under which the tenant answers.
 * @param {Number}  [options.port]         The port to which the tenant answers.
 * @param {String}  [options.cert]         The path to the SSL certificate to use with this tenant.
 * @param {String}  [options.key]          The path to the SSL key to use with this tenant.
 * @param {String}  [options.pfx]          The path to the pfx SSL certificate to use with this tenant.
 * @param {Array}   [options.features]     The list of enabled feature names on this tenant.
 * @param {Object}  [options.services]     The enabled services keyed by service name.
 * @param {Object}  [options.types]        The list of content types for this tenant.
 * @param {Boolean} [options.active]       True if the tenant is active.
 */
function Shell(options) {
  options = options || {};
  this.name = options.name;
  this.rootPath = options.rootPath;
  this.settingsPath = options.settingsPath;
  this.host = options.host || 'localhost';
  this.port = options.port || 80;
  this.https = !!options.https;
  this.cert = options.cert;
  this.key = options.key;
  this.pfx = options.pfx;
  this.features = options.features || [];
  this.availableModules = options.availableModules || {};
  this.services = options.services || {};
  this.types = options.types || {};
  this.active = !(options.active === false);
  this.serviceManifests = {};
  this.moduleManifests = {};
  this.modules = [];
  this.loaded = false;
}

util.inherits(Shell, EventEmitter);

/**
 * @description
 * An empty shell that you can use to initialize services if you don't need multitenancy.
 */
Shell.empty = new Shell({
  name: "Empty shell"
});

/**
 * @description
 * The list of tenants.
 */
Shell.list = {};

/**
 * @description
 * Creates a shell from its settings file.
 * @param {String} sitePath The path of the settings file.
 * @param {Object} [defaults] Default settings.
 * @returns {Shell} The new shell
 */
Shell.load = function(sitePath, defaults) {
  defaults = defaults || {};
  var settingsPath = path.join(sitePath, 'settings.json');
  var settings = require(settingsPath);
  settings.settingsPath = settingsPath;
  settings.rootPath = sitePath;
  for (var settingName in defaults) {
    if (!(settingName in settings)) {
      settings[settingName] = defaults[settingName];
    }
  }
  console.log(t('Loaded site settings from %s', settingsPath));
  return new Shell(settings);
}

/**
 * @description
 * Discovers all tenants in the ./sites directory.
 *
 * @param {Object} defaults  Default settings for the shells.
 * @param {String} rootPath  The root path where to look for shell settings files.
 *                           Defaults to ./sites
 */
Shell.discover = function(defaults, rootPath) {
  Shell.discoveryStart = new Date();
  rootPath = rootPath || './sites';
  console.log(t('Discovering tenants in %s', rootPath));
  var siteNames = fs.readdirSync(rootPath);
  siteNames.forEach(function(siteName) {
    var resolvedSitePath = path.resolve(rootPath, siteName);
    try {
      var shell = Shell.load(resolvedSitePath, defaults);
      Shell.list[siteName] = shell;
    }
    catch(ex) {
      ex.path = resolvedSitePath;
      ex.message = t('Failed to load site settings from %s.', settingsPath);
      throw ex;
    }
  });
  var elapsed = new Date() - Shell.discoveryStart;
  console.log(t('All tenants discovered in %s ms.', elapsed))
};

/**
 * @description
 * Returns the shell that should handle this request.
 *
 * @param {IncomingMessage} req The request
 */
Shell.resolve = function(req) {
  for (var shellName in Shell.list) {
    var shell = Shell.list[shellName];

    if (shell.active && shell.canHandle(req)) {
      return shell;
    }
  }
  return null; // Unresolved requests should not go to a default shell
}

/**
 * @description
 * Determines if the shell can handle that request.
 *
 * @param {IncomingMessage} req The request
 * @returns {Boolean} true if the shell can handle the request.
 */
Shell.prototype.canHandle = function(req) {
  var host = req.headers.host;
  return (
    (
      ((this.https && this.port === 443) || this.port === 80)
      && this.host === host
    )
    || (this.host + ':' + this.port === host)
  )
  && (
    !this.path
    || req.url.substr(0, this.path.length) === this.path
  );
}

/**
 * @description
 * Enables or disables the tenant.
 *
 * @param {Boolean} state If provided, sets the enabled state of the tenant. Otherwise, enables the tenant.
 */
Shell.prototype.enable = function(state) {
  this.active = typeof state === 'undefined' || !!state;
};

/**
 * @description
 * Disables the tenant.
 */
Shell.prototype.disable = function() {
  this.active = false;
};

/**
 * @description
 * Loads all enabled services in each module.
 */
Shell.prototype.load = function() {
  if (this.loaded || !this.availableModules) return;
  for (var moduleName in this.availableModules) {
    this.loadModule(moduleName);
  }
  this.loaded = true;
};

// TODO: replace service manifest with meta-data on the services themselves, auto-discover and load
/**
 * @description
 * Loads all the services in the module under the path passed as a parameter.
 * Services are loaded while respecting the dependency order.
 *
 * @param {String} moduleName  The name of the module to load.
 */
Shell.prototype.loadModule = function(moduleName) {
  var self = this;
  var manifest = self.availableModules[moduleName];
  if (self.moduleManifests[moduleName]) return;
  self.moduleManifests[moduleName] = manifest;
  var features = self.features;
  var services = manifest.services;
  var anyEnabledService;
  for (var serviceName in services) {
    var service = services[serviceName];
    var serviceFeature = service.feature;
    if (serviceFeature && features.indexOf(serviceFeature) === -1) continue;
    var servicePath = path.resolve(manifest.physicalPath, service.path + ".js");
    if (self.serviceManifests[servicePath]) continue;
    var dependencies = service.dependencies;
    if (dependencies) {
      dependencies.forEach(function(dependencyPath) {
        self.loadModule(dependencyPath);
      });
    }
    var ServiceClass = require(servicePath);
    if (!self.services[serviceName]) {
      self.services[serviceName] = [ServiceClass];
    }
    else {
      self.services[serviceName].push(ServiceClass);
    }
    ServiceClass.manifest = service;
    self.serviceManifests[servicePath] = service;
    if (ServiceClass.init) {
      ServiceClass.init(self);
    }
    // Wire up declared static event handlers
    if (ServiceClass.on) {
      for (var eventName in ServiceClass.on) {
        (function(ServiceClass, eventName) {
          self.on(eventName, function (payload) {
            ServiceClass.on[eventName](self, payload);
          });
        })(ServiceClass, eventName);
      }
    }
    anyEnabledService = true;
    console.log(t('Loaded service %s from %s', serviceName, servicePath));
  }
  if (anyEnabledService) {
    self.modules.push(moduleName);
  }
};

/**
 * @description
 * Returns an instance of a service implementing the named contract passed as a parameter.
 * If more than one service exists in the tenant for that contract, one instance that
 * has dependencies on any other service for that contract is returned. Do not
 * count on any particular service being returned if that is the case among the ones
 * that have the most dependencies.
 * A new instance is returned every time the function is called.
 *
 * @param {String} service  The name of the contract for which a service instance is required.
 * @param {object} options Options to pass into the service's constructor.
 * @returns {object} An instance of the service, or null if it wasn't found.
 */
Shell.prototype.require = function(service, options) {
  var services = this.services[service];
  var ServiceClass = Array.isArray(services)
    ? (services.length > 0 ? services[services.length - 1] : null) : null;
  return this.construct(ServiceClass, options);
};

/**
 * @description
 * Constructs an instance of the class passed in.
 * If the class is a shell singleton, the same instance
 * is always returned for any given shell.
 * Shell singletons are classes marked with isShellSingleton = true.
 * Otherwise, a new instance is created on each call.
 * Don't call this directly, it should only be internally used by Shell.
 * @param {Function} ServiceClass The class to instantiate.
 *                                This constructor must always take a shell as its first argument.
 *                                It can also take an optional 'options' argument, unless it's a shell singleton.
 * @param {object} options Options to pass into the service's constructor.
 * @returns {object} An instance of the service, or null if it wasn't found.
 */
Shell.prototype.construct = function(ServiceClass, options) {
  return ServiceClass ?
    ServiceClass.isShellSingleton ?
      ServiceClass.instance ?
        ServiceClass.instance :
        ServiceClass.instance = new ServiceClass(this)
      : new ServiceClass(this, options)
    : null;
};

/**
 * @description
 * Returns a list of service instances that are implementing the named contract passed as a parameter.
 * The services are returned in order of dependency: if service A has a dependency
 * on service B, B is guaranteed to appear earlier in the list.
 * New instances are returned every time the function is called.
 *
 * @param {String} service The name of the contract for which service instances are required.
 * @param {object} options Options to pass into the services' constructors.
 * @returns {Array} An array of instances of the service.
 */
Shell.prototype.getServices = function(service, options) {
  var self = this;
  if (!(service in self.services)) return [];
  return self.services[service].map(function(service) {
    return self.construct(service, options);
  });
};

/**
 * @description
 * Handles the request for the tenant
 *
 * @param {http.IncomingMessage} req Request
 * @param {http.ServerResponse}  res Response
 */
Shell.prototype.handleRequest = function(req, res) {
  var self = this;
  req.startTime = new Date();
  var payload = {
    shell: self,
    req: req,
    res: res,
    handled: false
  };
  // Let services register themselves with the request
  self.emit(Shell.startRequestEvent, payload);
  // Does anyone want to handle this?
  self.emit(Shell.handleRouteEvent, payload);
  self.emit(Shell.fetchContentEvent, {
    callback: function(err, data) {
      if (err) {
        self.emit(Shell.renderErrorPage, err);
        return;
      }
      // Let's render stuff
      self.emit(Shell.renderPageEvent, {
        req: payload.req,
        res: payload.res
      });
      // Tear down
      self.emit(Shell.endRequestEvent, payload);
      self
        .removeAllListeners(Shell.endRequestEvent)
        .removeAllListeners(Shell.renderPageEvent);
    }
  });
};

// TODO: document event payloads
/**
 * @description
 * The event that is broadcast at the beginning of request handling.
 * This is a good time to attach a service to the request object.
 * @type {string}
 */
Shell.startRequestEvent = Shell.prototype.startRequestEvent = 'decent-core-shell-start-request';

/**
 * @description
 * The event that is broadcast at the end of request handling.
 * This is a good time to detach services and events.
 * @type {string}
 */
Shell.endRequestEvent = Shell.prototype.endRequestEvent = 'decent-core-shell-end-request';

/**
 * @description
 * The event that is broadcast when a route needs to be resolved.
 * @type {string}
 */
Shell.handleRouteEvent = Shell.prototype.handleRouteEvent = 'decent.core.shell.handle-route';

/**
 * @description
 * The event that is broadcast when content needs to be fetched from stores.
 * @type {string}
 */
Shell.fetchContentEvent = Shell.prototype.fetchContentEvent = 'decent.core.shell.fetch-content';

/**
 * @description
 * The event that is broadcast when the page is ready to be rendered.
 * @type {string}
 */
Shell.renderPageEvent = Shell.prototype.renderPageEvent = 'decent.core.shell.render-page';

/**
 * @description
 * This event is triggered when an error has happened during the page lifecycle.
 * @type {string}
 */
Shell.renderErrorPage = Shell.prototype.renderErrorPage = 'decent.core.shell.render-error';

module.exports = Shell;<|endoftext|>


<commit_before>// DecentCMS (c) 2014 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';

// TODO: Add configurable timeout to request handling.
// TODO: Enable shells to be restarted.
// TODO: Move module loading and service discovery into a separate service, so that shell can deal purely with handling requests.
// TODO: Build file monitoring events so code that caches parsed files can expire and re-parse entries on-the-fly. Make sure that this can be done as an optional feature.

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var path = require('path');
var fs = require('fs');
var async = require('async');
var scope = require('decent-core-dependency-injection').scope;
var NullServices = require('./null-services');

/**
 * @description
 * A shell is the representation of a tenant in the system.
 * A shell has its own enabled services, that can be required by modules
 * running in its context.
 *
 * @constructor
 * @param {Object}  options
 * @param {String}  [options.name]             The name of the tenant.
 * @param {String}  [options.rootPath]         The path to the site's folder.
 * @param {String}  [options.settingsPath]     The path to the settings file for this tenant.
 * @param {String}  [options.host]             The host name under which the tenant answers.
 * @param {Number}  [options.port]             The port to which the tenant answers.
 * @param {String}  [options.cert]             The path to the SSL certificate to use with this tenant.
 * @param {String}  [options.key]              The path to the SSL key to use with this tenant.
 * @param {String}  [options.pfx]              The path to the pfx SSL certificate to use with this tenant.
 * @param {Array}   [options.features]         The list of enabled feature names on this tenant.
 * @param {Object}  [options.availableModules] The list of available module manifests.
 * @param {Object}  [options.services]         The enabled services keyed by service name.
 * @param {Boolean} [options.active]           True if the tenant is active.
 */
function Shell(options) {
  this.settings = options = options || {};
  this.name = options.name;
  this.rootPath = options.rootPath;
  this.settingsPath = options.settingsPath;
  this.host = options.host || 'localhost';
  this.port = options.port || 80;
  this.https = !!options.https;
  this.cert = options.cert;
  this.key = options.key;
  this.pfx = options.pfx;
  this.features = options.features || {};
  this.availableModules = options.availableModules || {};
  this.services = options.services || {};
  this.active = !(options.active === false);
  this.serviceManifests = {};
  this.moduleManifests = {};
  this.modules = [];
  this.loaded = false;
}

util.inherits(Shell, EventEmitter);

/**
 * @description
 * An empty shell that you can use to initialize services if you don't need multitenancy.
 */
Shell.empty = new Shell({
  name: "Empty shell"
});

/**
 * @description
 * The list of tenants.
 */
Shell.list = {};

/**
 * @description
 * Creates a shell from its settings file.
 * @param {String} sitePath The path of the settings file.
 * @param {Object} [defaults] Default settings.
 * @returns {Shell} The new shell
 */
Shell.load = function(sitePath, defaults) {
  defaults = defaults || {};
  var settingsPath = path.join(sitePath, 'settings.json');
  var settings = require(settingsPath);
  settings.settingsPath = settingsPath;
  settings.rootPath = sitePath;
  for (var settingName in defaults) {
    if (!(settingName in settings)) {
      settings[settingName] = defaults[settingName];
    }
  }
  return new Shell(settings);
};

/**
 * @description
 * Discovers all tenants in the ./sites directory.
 *
 * @param {Object} defaults
 * Default settings for the shells.
 * @param {String} rootPath
 * The root path where to look for shell settings files. Defaults to ./sites
 */
Shell.discover = function(defaults, rootPath) {
  rootPath = rootPath || './sites';
  var siteNames = fs.readdirSync(rootPath);
  siteNames.forEach(function(siteName) {
    var resolvedSitePath = path.resolve(rootPath, siteName);
    try {
      var shell = Shell.load(resolvedSitePath, defaults);
      Shell.list[siteName] = shell;
    }
    catch(ex) {
      ex.path = resolvedSitePath;
      ex.message = 'Failed to load site settings for ' + siteName;
      throw ex;
    }
  });
};

/**
 * @description
 * Returns the shell that should handle this request.
 *
 * @param {IncomingMessage} request The request
 */
Shell.resolve = function(request) {
  var shellNames = Object.getOwnPropertyNames(Shell.list);
  // If there's only one shell, always return that.
  if (shellNames.length === 1) return Shell.list[shellNames[0]];
  // Otherwise let each shell decide if it can handle the request.
  for (var shellName in Shell.list) {
    var shell = Shell.list[shellName];

    if (shell.active && shell.canHandle(request)) {
      return shell;
    }
  }
  // Unresolved requests should not go to a default shell
  // if there's more than one.
  return null;
};

/**
 * @description
 * Determines if the shell can handle that request.
 *
 * @param {IncomingMessage} request The request
 * @returns {Boolean} true if the shell can handle the request.
 */
Shell.prototype.canHandle = function(request) {
  var host = request.headers.host;
  var hosts = Array.isArray(this.host) ? this.host : [this.host];
  for (var i = 0; i < hosts.length; i++) {
    var thisHost = hosts[i];
    if ((
      (
      ((this.https && this.port === 443) || this.port === 80 || this.port === '*')
      && thisHost === host
      )
      || (this.port === '*' && host.substr(0, thisHost.length) === thisHost)
      || (thisHost + ':' + this.port === host)
    ) && (
    !this.path
    || request.url.substr(0, this.path.length) === this.path
    )) return true;
  }
  return false;
};

/**
 * @description
 * Enables or disables the tenant.
 *
 * @param {Boolean} state If provided, sets the enabled state of the tenant. Otherwise, enables the tenant.
 */
Shell.prototype.enable = function(state) {
  this.active = typeof state === 'undefined' || !!state;
};

/**
 * @description
 * Disables the tenant.
 */
Shell.prototype.disable = function() {
  this.active = false;
};

/**
 * @description
 * Scopes the shell, then loads all enabled services in each module.
 */
Shell.prototype.load = function() {
  if (this.loaded || !this.availableModules) return;
  // Make this a scope
  scope('shell', this);
  // Load services from each available module
  for (var moduleName in this.availableModules) {
    this.loadModule(moduleName);
  }
  this.initialize();
  // The shell exposes itself as a service
  this.register('shell', this);
  // Register null services if no better one exists
  for (var nullServiceName in NullServices) {
    if (!this.services.hasOwnProperty(nullServiceName)) {
      var nullService = NullServices[nullServiceName];
      nullService.isStatic = true;
      this.services[nullServiceName] = [nullService];
    }
  }
  // Mark the shell as loaded
  this.loaded = true;
};

/**
 * @description
 * Loads all the services in the module under the path passed as a parameter.
 * Services are loaded while respecting the dependency order.
 *
 * @param {String} moduleName  The name of the module to load.
 */
Shell.prototype.loadModule = function(moduleName) {
  var self = this;
  var manifest = self.availableModules[moduleName];
  // Skip if the module is already loaded
  if (self.moduleManifests[moduleName]) return;
  // Store the manifest, which also flags this module as loaded
  self.moduleManifests[moduleName] = manifest;
  var features = self.features;
  var services = manifest.services;
  var anyEnabledService;
  var moduleServiceClasses = {};
  for (var serviceName in services) {
    var serviceList = services[serviceName];
    if (!Array.isArray(serviceList)) {
      serviceList = [serviceList];
    }
    for (var i = 0; i < serviceList.length ; i++) {
      var service = serviceList[i];
      var serviceFeature = service.feature;
      // Skip if that service is not enabled
      if (serviceFeature && !features.hasOwnProperty(serviceFeature)) continue;
      var servicePath = path.resolve(manifest.physicalPath, service.path + ".js");
      // Skip if that service is already loaded
      if (self.serviceManifests[servicePath]) continue;
      // Dependencies must be loaded first
      var dependencies = service.dependencies;
      if (dependencies) {
        dependencies.forEach(function (dependencyPath) {
          self.loadModule(dependencyPath);
        });
      }
      // Services are obtained through require
      var ServiceClass = moduleServiceClasses[serviceName] = require(servicePath);
      self.register(serviceName, ServiceClass);
      // Add the service's configuration onto the config object
      self.settings[serviceFeature] = features[serviceFeature];
      // Store the manifest on the service class, for reflection, and easy reading of settings
      ServiceClass.manifest = service;
      self.serviceManifests[servicePath] = service;
      anyEnabledService = true;
    }
  }
  // Only add to the modules collection if it has enabled services
  if (anyEnabledService || manifest.theme) {
    self.modules.push(moduleName);
  }
};

/**
 * @description
 * Middleware for use, for example, with Express.
 *
 * @param {http.IncomingMessage} request Request
 * @param {http.ServerResponse}  response Response
 */
Shell.prototype.middleware = function(request, response, next) {
  if (!this.canHandle(request)) {
    next();
    return;
  }
  this.handleRequest(request, response, next);
};

/**
 * @description
 * Handles the request for the tenant
 * Emits the following events and service calls:
 * * decent.core.shell.start-request
 * * 'route-handler'.handle({shell, request, response}, done)
 * * 'storage-manager'.fetchContent({shell, request, response}, done)
 * * 'renderer'.render({shell, request, response}, done)
 * * decent.core.shell.render-error
 * * decent.core.shell.end-request
 *
 * @param {http.IncomingMessage} request Request
 * @param {http.ServerResponse}  response Response
 */
Shell.prototype.handleRequest = function(request, response, next) {
  var self = this;
  var log = self.require('log');
  var profileId = 'shell-handle-request-'
    + (this._profileIndex = (this._profileIndex || 0) + 1);
  log.profile(profileId);
  // Most events use the same context structure
  var context = {
    shell: self,
    request: request,
    response: response
  };
  // Mix-in scope into request
  scope('request', request, self.services, self);
  // Let services register themselves with the request
  self.emit(Shell.startRequestEvent, context);

  var lifecycle = request.lifecycle(
    'route-handler', 'handle',
    'storage-manager', 'fetchContent',
    'renderer', 'render'
  );
  lifecycle(context, function lifecycleDone(err) {
    if (err) {
      self.emit(Shell.renderErrorPage, err);
      log.error(err.message);
      if (next) next(err);
    }
    // Tear down
    self.emit(Shell.endRequestEvent, context);
    if (request.tearDown) request.tearDown();
    log.profile(profileId, 'Handled request', {
      tenant: self.name,
      url: request.url,
      status: response.statusCode
    });
    if (next) next();
  });
};

/**
 * @description
 * The event that is broadcast at the beginning of request handling.
 * This is a good time to attach a service to the request object.
 * @type {string}
 */
Shell.startRequestEvent = 'decent.core.shell.start-request';
Shell.startRequestEvent_payload = {
  shell: 'Shell',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast at the end of request handling.
 * This is a good time to detach services and events.
 * @type {string}
 */
Shell.endRequestEvent = 'decent.core.shell.end-request';
Shell.endRequestEvent_payload = {
  shell: 'Shell',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast when the page is ready to be rendered.
 * @type {string}
 */
Shell.renderPageEvent = 'decent.core.shell.render-page';
Shell.renderPageEvent_payload = {
  shell: 'Shell',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * This event is triggered when an error has happened during the page lifecycle.
 * @type {string}
 */
Shell.renderErrorPage = 'decent.core.shell.render-error';
/**
 * @description
 * The payload is the error object.
 */
Shell.renderErrorPage_payload = {};

module.exports = Shell;<commit_msg>Skip MacOS junk directories when scanning for sites.<commit_after>// DecentCMS (c) 2014 Bertrand Le Roy, under MIT. See LICENSE.txt for licensing details.
'use strict';

// TODO: Add configurable timeout to request handling.
// TODO: Enable shells to be restarted.
// TODO: Move module loading and service discovery into a separate service, so that shell can deal purely with handling requests.
// TODO: Build file monitoring events so code that caches parsed files can expire and re-parse entries on-the-fly. Make sure that this can be done as an optional feature.

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var path = require('path');
var fs = require('fs');
var async = require('async');
var scope = require('decent-core-dependency-injection').scope;
var NullServices = require('./null-services');

/**
 * @description
 * A shell is the representation of a tenant in the system.
 * A shell has its own enabled services, that can be required by modules
 * running in its context.
 *
 * @constructor
 * @param {Object}  options
 * @param {String}  [options.name]             The name of the tenant.
 * @param {String}  [options.rootPath]         The path to the site's folder.
 * @param {String}  [options.settingsPath]     The path to the settings file for this tenant.
 * @param {String}  [options.host]             The host name under which the tenant answers.
 * @param {Number}  [options.port]             The port to which the tenant answers.
 * @param {String}  [options.cert]             The path to the SSL certificate to use with this tenant.
 * @param {String}  [options.key]              The path to the SSL key to use with this tenant.
 * @param {String}  [options.pfx]              The path to the pfx SSL certificate to use with this tenant.
 * @param {Array}   [options.features]         The list of enabled feature names on this tenant.
 * @param {Object}  [options.availableModules] The list of available module manifests.
 * @param {Object}  [options.services]         The enabled services keyed by service name.
 * @param {Boolean} [options.active]           True if the tenant is active.
 */
function Shell(options) {
  this.settings = options = options || {};
  this.name = options.name;
  this.rootPath = options.rootPath;
  this.settingsPath = options.settingsPath;
  this.host = options.host || 'localhost';
  this.port = options.port || 80;
  this.https = !!options.https;
  this.cert = options.cert;
  this.key = options.key;
  this.pfx = options.pfx;
  this.features = options.features || {};
  this.availableModules = options.availableModules || {};
  this.services = options.services || {};
  this.active = !(options.active === false);
  this.serviceManifests = {};
  this.moduleManifests = {};
  this.modules = [];
  this.loaded = false;
}

util.inherits(Shell, EventEmitter);

/**
 * @description
 * An empty shell that you can use to initialize services if you don't need multitenancy.
 */
Shell.empty = new Shell({
  name: "Empty shell"
});

/**
 * @description
 * The list of tenants.
 */
Shell.list = {};

/**
 * @description
 * Creates a shell from its settings file.
 * @param {String} sitePath The path of the settings file.
 * @param {Object} [defaults] Default settings.
 * @returns {Shell} The new shell
 */
Shell.load = function(sitePath, defaults) {
  defaults = defaults || {};
  var settingsPath = path.join(sitePath, 'settings.json');
  var settings = require(settingsPath);
  settings.settingsPath = settingsPath;
  settings.rootPath = sitePath;
  for (var settingName in defaults) {
    if (!(settingName in settings)) {
      settings[settingName] = defaults[settingName];
    }
  }
  return new Shell(settings);
};

/**
 * @description
 * Discovers all tenants in the ./sites directory.
 *
 * @param {Object} defaults
 * Default settings for the shells.
 * @param {String} rootPath
 * The root path where to look for shell settings files. Defaults to ./sites
 */
Shell.discover = function(defaults, rootPath) {
  rootPath = rootPath || './sites';
  var siteNames = fs.readdirSync(rootPath);
  siteNames.forEach(function(siteName) {
    if (siteName[0] === '.') return;
    var resolvedSitePath = path.resolve(rootPath, siteName);
    try {
      var shell = Shell.load(resolvedSitePath, defaults);
      Shell.list[siteName] = shell;
    }
    catch(ex) {
      ex.path = resolvedSitePath;
      ex.message = 'Failed to load site settings for ' + siteName;
      throw ex;
    }
  });
};

/**
 * @description
 * Returns the shell that should handle this request.
 *
 * @param {IncomingMessage} request The request
 */
Shell.resolve = function(request) {
  var shellNames = Object.getOwnPropertyNames(Shell.list);
  // If there's only one shell, always return that.
  if (shellNames.length === 1) return Shell.list[shellNames[0]];
  // Otherwise let each shell decide if it can handle the request.
  for (var shellName in Shell.list) {
    var shell = Shell.list[shellName];

    if (shell.active && shell.canHandle(request)) {
      return shell;
    }
  }
  // Unresolved requests should not go to a default shell
  // if there's more than one.
  return null;
};

/**
 * @description
 * Determines if the shell can handle that request.
 *
 * @param {IncomingMessage} request The request
 * @returns {Boolean} true if the shell can handle the request.
 */
Shell.prototype.canHandle = function(request) {
  var host = request.headers.host;
  var hosts = Array.isArray(this.host) ? this.host : [this.host];
  for (var i = 0; i < hosts.length; i++) {
    var thisHost = hosts[i];
    if ((
      (
      ((this.https && this.port === 443) || this.port === 80 || this.port === '*')
      && thisHost === host
      )
      || (this.port === '*' && host.substr(0, thisHost.length) === thisHost)
      || (thisHost + ':' + this.port === host)
    ) && (
    !this.path
    || request.url.substr(0, this.path.length) === this.path
    )) return true;
  }
  return false;
};

/**
 * @description
 * Enables or disables the tenant.
 *
 * @param {Boolean} state If provided, sets the enabled state of the tenant. Otherwise, enables the tenant.
 */
Shell.prototype.enable = function(state) {
  this.active = typeof state === 'undefined' || !!state;
};

/**
 * @description
 * Disables the tenant.
 */
Shell.prototype.disable = function() {
  this.active = false;
};

/**
 * @description
 * Scopes the shell, then loads all enabled services in each module.
 */
Shell.prototype.load = function() {
  if (this.loaded || !this.availableModules) return;
  // Make this a scope
  scope('shell', this);
  // Load services from each available module
  for (var moduleName in this.availableModules) {
    this.loadModule(moduleName);
  }
  this.initialize();
  // The shell exposes itself as a service
  this.register('shell', this);
  // Register null services if no better one exists
  for (var nullServiceName in NullServices) {
    if (!this.services.hasOwnProperty(nullServiceName)) {
      var nullService = NullServices[nullServiceName];
      nullService.isStatic = true;
      this.services[nullServiceName] = [nullService];
    }
  }
  // Mark the shell as loaded
  this.loaded = true;
};

/**
 * @description
 * Loads all the services in the module under the path passed as a parameter.
 * Services are loaded while respecting the dependency order.
 *
 * @param {String} moduleName  The name of the module to load.
 */
Shell.prototype.loadModule = function(moduleName) {
  var self = this;
  var manifest = self.availableModules[moduleName];
  // Skip if the module is already loaded
  if (self.moduleManifests[moduleName]) return;
  // Store the manifest, which also flags this module as loaded
  self.moduleManifests[moduleName] = manifest;
  var features = self.features;
  var services = manifest.services;
  var anyEnabledService;
  var moduleServiceClasses = {};
  for (var serviceName in services) {
    var serviceList = services[serviceName];
    if (!Array.isArray(serviceList)) {
      serviceList = [serviceList];
    }
    for (var i = 0; i < serviceList.length ; i++) {
      var service = serviceList[i];
      var serviceFeature = service.feature;
      // Skip if that service is not enabled
      if (serviceFeature && !features.hasOwnProperty(serviceFeature)) continue;
      var servicePath = path.resolve(manifest.physicalPath, service.path + ".js");
      // Skip if that service is already loaded
      if (self.serviceManifests[servicePath]) continue;
      // Dependencies must be loaded first
      var dependencies = service.dependencies;
      if (dependencies) {
        dependencies.forEach(function (dependencyPath) {
          self.loadModule(dependencyPath);
        });
      }
      // Services are obtained through require
      var ServiceClass = moduleServiceClasses[serviceName] = require(servicePath);
      self.register(serviceName, ServiceClass);
      // Add the service's configuration onto the config object
      self.settings[serviceFeature] = features[serviceFeature];
      // Store the manifest on the service class, for reflection, and easy reading of settings
      ServiceClass.manifest = service;
      self.serviceManifests[servicePath] = service;
      anyEnabledService = true;
    }
  }
  // Only add to the modules collection if it has enabled services
  if (anyEnabledService || manifest.theme) {
    self.modules.push(moduleName);
  }
};

/**
 * @description
 * Middleware for use, for example, with Express.
 *
 * @param {http.IncomingMessage} request Request
 * @param {http.ServerResponse}  response Response
 */
Shell.prototype.middleware = function(request, response, next) {
  if (!this.canHandle(request)) {
    next();
    return;
  }
  this.handleRequest(request, response, next);
};

/**
 * @description
 * Handles the request for the tenant
 * Emits the following events and service calls:
 * * decent.core.shell.start-request
 * * 'route-handler'.handle({shell, request, response}, done)
 * * 'storage-manager'.fetchContent({shell, request, response}, done)
 * * 'renderer'.render({shell, request, response}, done)
 * * decent.core.shell.render-error
 * * decent.core.shell.end-request
 *
 * @param {http.IncomingMessage} request Request
 * @param {http.ServerResponse}  response Response
 */
Shell.prototype.handleRequest = function(request, response, next) {
  var self = this;
  var log = self.require('log');
  var profileId = 'shell-handle-request-'
    + (this._profileIndex = (this._profileIndex || 0) + 1);
  log.profile(profileId);
  // Most events use the same context structure
  var context = {
    shell: self,
    request: request,
    response: response
  };
  // Mix-in scope into request
  scope('request', request, self.services, self);
  // Let services register themselves with the request
  self.emit(Shell.startRequestEvent, context);

  var lifecycle = request.lifecycle(
    'route-handler', 'handle',
    'storage-manager', 'fetchContent',
    'renderer', 'render'
  );
  lifecycle(context, function lifecycleDone(err) {
    if (err) {
      self.emit(Shell.renderErrorPage, err);
      log.error(err.message);
      if (next) next(err);
    }
    // Tear down
    self.emit(Shell.endRequestEvent, context);
    if (request.tearDown) request.tearDown();
    log.profile(profileId, 'Handled request', {
      tenant: self.name,
      url: request.url,
      status: response.statusCode
    });
    if (next) next();
  });
};

/**
 * @description
 * The event that is broadcast at the beginning of request handling.
 * This is a good time to attach a service to the request object.
 * @type {string}
 */
Shell.startRequestEvent = 'decent.core.shell.start-request';
Shell.startRequestEvent_payload = {
  shell: 'Shell',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast at the end of request handling.
 * This is a good time to detach services and events.
 * @type {string}
 */
Shell.endRequestEvent = 'decent.core.shell.end-request';
Shell.endRequestEvent_payload = {
  shell: 'Shell',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * The event that is broadcast when the page is ready to be rendered.
 * @type {string}
 */
Shell.renderPageEvent = 'decent.core.shell.render-page';
Shell.renderPageEvent_payload = {
  shell: 'Shell',
  request: 'IncomingMessage',
  response: 'ServerResponse'
};

/**
 * @description
 * This event is triggered when an error has happened during the page lifecycle.
 * @type {string}
 */
Shell.renderErrorPage = 'decent.core.shell.render-error';
/**
 * @description
 * The payload is the error object.
 */
Shell.renderErrorPage_payload = {};

module.exports = Shell;<|endoftext|>


<commit_before>/**
 * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
 *
 * @author John Molakvoæ <skjnldsv@protonmail.com>
 *
 * @license AGPL-3.0-or-later
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

import { getCurrentUser } from '@nextcloud/auth'
import { getRootPath } from '../utils/davUtils'
import axios from '@nextcloud/axios'
import client from './DavClient'

/**
 * Retrieve the comments list
 *
 * @param {string} commentsType the ressource type
 * @param {number} ressourceId the ressource ID
 * @param {string} message the message
 * @return {object} the new comment
 */
export default async function(commentsType, ressourceId, message) {
	const ressourcePath = ['', commentsType, ressourceId].join('/')

	const response = await axios.post(getRootPath() + ressourcePath, {
		actorDisplayName: getCurrentUser().displayName,
		actorId: getCurrentUser().uid,
		actorType: 'users',
		creationDateTime: (new Date()).toUTCString(),
		message,
		objectType: 'files',
		verb: 'comment',
	})

	// Retrieve comment id from ressource location
	const commentId = parseInt(response.headers['content-location'].split('/').pop())
	const commentPath = ressourcePath + '/' + commentId

	// Fetch newly created comment data
	const comment = await client.stat(commentPath, {
		details: true,
	})

	return comment.data
}
<commit_msg>Fix HTML entities not decoded in comment just added<commit_after>/**
 * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
 *
 * @author John Molakvoæ <skjnldsv@protonmail.com>
 *
 * @license AGPL-3.0-or-later
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

import { getCurrentUser } from '@nextcloud/auth'
import { getRootPath } from '../utils/davUtils'
import { decodeHtmlEntities } from '../utils/decodeHtmlEntities'
import axios from '@nextcloud/axios'
import client from './DavClient'

/**
 * Retrieve the comments list
 *
 * @param {string} commentsType the ressource type
 * @param {number} ressourceId the ressource ID
 * @param {string} message the message
 * @return {object} the new comment
 */
export default async function(commentsType, ressourceId, message) {
	const ressourcePath = ['', commentsType, ressourceId].join('/')

	const response = await axios.post(getRootPath() + ressourcePath, {
		actorDisplayName: getCurrentUser().displayName,
		actorId: getCurrentUser().uid,
		actorType: 'users',
		creationDateTime: (new Date()).toUTCString(),
		message,
		objectType: 'files',
		verb: 'comment',
	})

	// Retrieve comment id from ressource location
	const commentId = parseInt(response.headers['content-location'].split('/').pop())
	const commentPath = ressourcePath + '/' + commentId

	// Fetch newly created comment data
	const comment = await client.stat(commentPath, {
		details: true,
	})

	const props = comment.data.props
	// Decode twice to handle potentially double-encoded entities
	// FIXME Remove this once https://github.com/nextcloud/server/issues/29306
	// is resolved
	props.actorDisplayName = decodeHtmlEntities(props.actorDisplayName, 2)
	props.message = decodeHtmlEntities(props.message, 2)

	return comment.data
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import {autobind} from 'core-decorators';
import yubikiri from 'yubikiri';

import RemotePrController from './remote-pr-controller';
import GithubLoginModel from '../models/github-login-model';
import ObserveModel from '../views/observe-model';
import {RemotePropType} from '../prop-types';

class RemoteSelector extends React.Component {
  static propTypes = {
    remotes: PropTypes.arrayOf(RemotePropType).isRequired,
    currentBranchName: PropTypes.string.isRequired,
    selectRemote: PropTypes.func.isRequired,
  }

  render() {
    const {remotes, currentBranchName, selectRemote} = this.props;
    return (

          This repository has multiple remotes hosted at GitHub.com.
          Select a remote to see pull requests associated
          with the {currentBranchName} branch.

          {remotes.map(remote => (

               selectRemote(e, remote)}>
                {remote.getName()} ({remote.getOwner()}/{remote.getRepo()})

          ))}

    );
  }
}

export default class GithubTabController extends React.Component {
  static propTypes = {
    repository: PropTypes.object,
    loginModel: PropTypes.instanceOf(GithubLoginModel),
  }

  fetchModelData(repo) {
    return yubikiri({
      remotes: repo.getRemotes().then(remotes => remotes.filter(remote => remote.isGithubRepo())),
      currentBranch: repo.getCurrentBranch(),
      selectedRemoteName: repo.getConfig('atomGithub.currentRemote'),
      selectedPrUrl: async query => {
        const branch = await query.currentBranch;
        if (!branch.isPresent() || branch.isDetached()) { return null; }
        return repo.getConfig(`branch.${branch.getName()}.atomPrUrl`);
      },
      aheadCount: async query => repo.getAheadCount((await query.currentBranch).getName()),
      isUnpublished: async query => {
        const branch = await query.currentBranch;
        const remote = await repo.getRemoteForBranch(branch.getName());
        return !remote.isPresent();
      },
    });
  }

  serialize() {
    return {
      deserializer: 'GithubDockItem',
      uri: this.getURI(),
    };
  }

  render() {
    return (
      <ObserveModel model={this.props.repository} fetchData={this.fetchModelData}>
        {data => { return data ? this.renderWithData(data) : null; } }
      </ObserveModel>
    );
  }

  renderWithData({remotes, currentBranch, selectedRemoteName, selectedPrUrl, aheadCount, isUnpublished}) {
    if (!this.props.repository.isPresent() || !remotes) {
      return null;
    }

    if (!currentBranch.isPresent() || currentBranch.isDetached()) {
      return null;
    }

    let remote = remotes.find(r => r.getName() === selectedRemoteName);
    let manyRemotesAvailable = false;
    if (!remote && remotes.length === 1) {
      remote = remotes[0];
    } else if (!remote && remotes.length > 1) {
      manyRemotesAvailable = true;
    }

    return (

 { this.root = c; }} className="github-GithubTabController">

          {/* only supporting GH.com for now, hardcoded values */}
          {remote &&
            <RemotePrController
              host="https://api.github.com"
              loginModel={this.props.loginModel}
              onSelectPr={prUrl => this.handleSelectPrByUrl(prUrl, currentBranch)}
              selectedPrUrl={selectedPrUrl}
              onUnpinPr={() => this.handleUnpinPr(currentBranch)}
              remote={remote}
              currentBranchName={currentBranch.getName()}
              aheadCount={aheadCount}
              isUnpublished={isUnpublished}
            />
          }
          {!remote && manyRemotesAvailable &&
            <RemoteSelector
              remotes={remotes}
              currentBranchName={currentBranch.getName()}
              selectRemote={this.handleRemoteSelect}
            />
          }
          {!remote && !manyRemotesAvailable && this.renderNoRemotes()}

    );
  }

  handleSelectPrByUrl(prUrl, currentBranch) {
    return this.props.repository.setConfig(`branch.${currentBranch.getName()}.atomPrUrl`, prUrl);
  }

  handleUnpinPr(currentBranch) {
    return this.props.repository.unsetConfig(`branch.${currentBranch.getName()}.atomPrUrl`);
  }

  getTitle() {
    return 'GitHub (preview)';
  }

  getIconName() {
    return 'octoface';
  }

  getDefaultLocation() {
    return 'right';
  }

  getPreferredWidth() {
    return 400;
  }

  getURI() {
    return 'atom-github://dock-item/github';
  }

  getWorkingDirectory() {
    return this.props.repository.getWorkingDirectoryPath();
  }

  renderNoRemotes() {
    return (

        This repository does not have any remotes hosted at GitHub.com.

    );
  }

  @autobind
  handleRemoteSelect(e, remote) {
    e.preventDefault();
    this.props.repository.setConfig('atomGithub.currentRemote', remote.getName());
  }

  hasFocus() {
    return this.root && this.root.contains(document.activeElement);
  }

  restoreFocus() {
    // No-op
  }
}
<commit_msg>Branch pushing action in the GithubTabController<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import {autobind} from 'core-decorators';
import yubikiri from 'yubikiri';

import RemotePrController from './remote-pr-controller';
import GithubLoginModel from '../models/github-login-model';
import ObserveModel from '../views/observe-model';
import {RemotePropType} from '../prop-types';

class RemoteSelector extends React.Component {
  static propTypes = {
    remotes: PropTypes.arrayOf(RemotePropType).isRequired,
    currentBranchName: PropTypes.string.isRequired,
    selectRemote: PropTypes.func.isRequired,
  }

  render() {
    const {remotes, currentBranchName, selectRemote} = this.props;
    return (

          This repository has multiple remotes hosted at GitHub.com.
          Select a remote to see pull requests associated
          with the {currentBranchName} branch.

          {remotes.map(remote => (

               selectRemote(e, remote)}>
                {remote.getName()} ({remote.getOwner()}/{remote.getRepo()})

          ))}

    );
  }
}

export default class GithubTabController extends React.Component {
  static propTypes = {
    repository: PropTypes.object,
    loginModel: PropTypes.instanceOf(GithubLoginModel),
  }

  fetchModelData(repo) {
    return yubikiri({
      remotes: repo.getRemotes().then(remotes => remotes.filter(remote => remote.isGithubRepo())),
      currentBranch: repo.getCurrentBranch(),
      selectedRemoteName: repo.getConfig('atomGithub.currentRemote'),
      selectedPrUrl: async query => {
        const branch = await query.currentBranch;
        if (!branch.isPresent() || branch.isDetached()) { return null; }
        return repo.getConfig(`branch.${branch.getName()}.atomPrUrl`);
      },
      aheadCount: async query => repo.getAheadCount((await query.currentBranch).getName()),
      isUnpublished: async query => {
        const branch = await query.currentBranch;
        const remote = await repo.getRemoteForBranch(branch.getName());
        return !remote.isPresent();
      },
    });
  }

  serialize() {
    return {
      deserializer: 'GithubDockItem',
      uri: this.getURI(),
    };
  }

  render() {
    return (
      <ObserveModel model={this.props.repository} fetchData={this.fetchModelData}>
        {data => { return data ? this.renderWithData(data) : null; } }
      </ObserveModel>
    );
  }

  renderWithData({remotes, currentBranch, selectedRemoteName, selectedPrUrl, aheadCount, isUnpublished}) {
    if (!this.props.repository.isPresent() || !remotes) {
      return null;
    }

    if (!currentBranch.isPresent() || currentBranch.isDetached()) {
      return null;
    }

    let remote = remotes.find(r => r.getName() === selectedRemoteName);
    let manyRemotesAvailable = false;
    if (!remote && remotes.length === 1) {
      remote = remotes[0];
    } else if (!remote && remotes.length > 1) {
      manyRemotesAvailable = true;
    }

    return (

 { this.root = c; }} className="github-GithubTabController">

          {/* only supporting GH.com for now, hardcoded values */}
          {remote &&
            <RemotePrController
              host="https://api.github.com"
              loginModel={this.props.loginModel}
              onSelectPr={prUrl => this.handleSelectPrByUrl(prUrl, currentBranch)}
              selectedPrUrl={selectedPrUrl}
              onUnpinPr={() => this.handleUnpinPr(currentBranch)}
              onPushBranch={() => this.handlePushBranch(currentBranch, remote)}
              remote={remote}
              currentBranchName={currentBranch.getName()}
              aheadCount={aheadCount}
              isUnpublished={isUnpublished}
            />
          }
          {!remote && manyRemotesAvailable &&
            <RemoteSelector
              remotes={remotes}
              currentBranchName={currentBranch.getName()}
              selectRemote={this.handleRemoteSelect}
            />
          }
          {!remote && !manyRemotesAvailable && this.renderNoRemotes()}

    );
  }

  handleSelectPrByUrl(prUrl, currentBranch) {
    return this.props.repository.setConfig(`branch.${currentBranch.getName()}.atomPrUrl`, prUrl);
  }

  handleUnpinPr(currentBranch) {
    return this.props.repository.unsetConfig(`branch.${currentBranch.getName()}.atomPrUrl`);
  }

  handlePushBranch(currentBranch, targetRemote) {
    return this.props.repository.push(currentBranch.getName(), {
      remote: targetRemote,
      setUpstream: true,
    });
  }

  getTitle() {
    return 'GitHub (preview)';
  }

  getIconName() {
    return 'octoface';
  }

  getDefaultLocation() {
    return 'right';
  }

  getPreferredWidth() {
    return 400;
  }

  getURI() {
    return 'atom-github://dock-item/github';
  }

  getWorkingDirectory() {
    return this.props.repository.getWorkingDirectoryPath();
  }

  renderNoRemotes() {
    return (

        This repository does not have any remotes hosted at GitHub.com.

    );
  }

  @autobind
  handleRemoteSelect(e, remote) {
    e.preventDefault();
    this.props.repository.setConfig('atomGithub.currentRemote', remote.getName());
  }

  hasFocus() {
    return this.root && this.root.contains(document.activeElement);
  }

  restoreFocus() {
    // No-op
  }
}
<|endoftext|>


<commit_before><commit_msg>Fix bug #95758778 - Under new full-network routing, router should not stop simulating on disconnect<commit_after><|endoftext|>


<commit_before>(function () {
    'use strict';

    angular
        .module('dpShared')
        .factory('sharedConfig', sharedConfigFactory);

    sharedConfigFactory.$inject = ['environment'];

    function sharedConfigFactory (environment) {
        let globalConfig,
            environmentConfig;

        globalConfig = {
            RADIUS: 50,
            THUMBNAIL_WIDTH: 240,
            STRAATBEELD_THUMB_URL: 'panorama/thumbnail/',
            AUTH_HEADER_PREFIX: 'Bearer '
        };

        environmentConfig = {
            PRODUCTION: {
                API_ROOT: 'https://api.datapunt.amsterdam.nl/'
            },
            ACCEPTATION: {
                API_ROOT: 'https://api-acc.datapunt.amsterdam.nl/'
            },
            DEVELOPMENT: {
                API_ROOT: 'https://api-acc.datapunt.amsterdam.nl/'
            }
        };

        return angular.merge(globalConfig, environmentConfig[environment.NAME]);
    }
})();
<commit_msg>Revert "Let new-login communicate with acc for now, instead of production"<commit_after>(function () {
    'use strict';

    angular
        .module('dpShared')
        .factory('sharedConfig', sharedConfigFactory);

    sharedConfigFactory.$inject = ['environment'];

    function sharedConfigFactory (environment) {
        let globalConfig,
            environmentConfig;

        globalConfig = {
            RADIUS: 50,
            THUMBNAIL_WIDTH: 240,
            STRAATBEELD_THUMB_URL: 'panorama/thumbnail/',
            AUTH_HEADER_PREFIX: 'Bearer '
        };

        environmentConfig = {
            PRODUCTION: {
                API_ROOT: 'https://api.datapunt.amsterdam.nl/'
            },
            ACCEPTATION: {
                API_ROOT: 'https://api-acc.datapunt.amsterdam.nl/'
            },
            DEVELOPMENT: {
                API_ROOT: 'https://api.datapunt.amsterdam.nl/'
            }
        };

        return angular.merge(globalConfig, environmentConfig[environment.NAME]);
    }
})();
<|endoftext|>


<commit_before>define("ace/theme/idle_fingers",["require","exports","module","ace/lib/dom"], function(require, exports, module) {

  exports.isDark = true;
  exports.cssClass = "ace-idle-fingers";
  exports.cssText = ".ace-idle-fingers .ace_gutter {\
background: #3A3A3A;\
color: rgb(153,153,153)\
}\
.ace-idle-fingers .ace_print-margin {\
width: 1px;\
background: #3b3b3b\
}\
.ace-idle-fingers {\
background-color: #4d4d4d;\
color: #FFFFFF\
}\
.ace-idle-fingers .ace_cursor {\
color: #ffffff\
}\
.ace-idle-fingers .ace_marker-layer .ace_selection {\
background: rgba(90, 100, 126, 0.88)\
}\
.ace-idle-fingers.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #4d4d4d;\
border-radius: 2px\
}\
.ace-idle-fingers .ace_marker-layer .ace_step {\
background: rgb(102, 82, 0)\
}\
.ace-idle-fingers .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid #404040\
}\
.ace-idle-fingers .ace_marker-layer .ace_active-line {\
background: #000000;\
opacity: 0.4;\
}\
.ace-idle-fingers .ace_gutter-active-line {\
background-color: #000000;\
opacity: 0.4;\
}\
.ace-idle-fingers .ace_marker-layer .ace_selected-word {\
border: 1px solid rgba(90, 100, 126, 0.88)\
}\
.ace-idle-fingers .ace_invisible {\
color: #404040\
}\
.ace-idle-fingers .ace_keyword,\
.ace-idle-fingers .ace_meta {\
color: #CC7833\
}\
.ace-idle-fingers .ace_constant,\
.ace-idle-fingers .ace_constant.ace_character,\
.ace-idle-fingers .ace_constant.ace_character.ace_escape,\
.ace-idle-fingers .ace_constant.ace_other,\
.ace-idle-fingers .ace_support.ace_constant {\
color: #72cdf4\
}\
.ace-idle-fingers .ace_boolean {\
color: #ff6532\
}\
.ace-idle-fingers .ace_invalid {\
color: #FFFFFF;\
background-color: #FF0000\
}\
.ace-idle-fingers .ace_fold {\
background-color: #CC7833;\
border-color: #FFFFFF\
}\
.ace-idle-fingers .ace_support.ace_function {\
color: #B83426\
}\
.ace-idle-fingers .ace_variable.ace_parameter {\
font-style: italic\
}\
.ace-idle-fingers .ace_string {\
color: #29be9d\
}\
.ace-idle-fingers .ace_string.ace_regexp {\
color: #CCCC33\
}\
.ace-idle-fingers .ace_comment {\
font-style: italic;\
color: #BC9458\
}\
.ace-idle-fingers .ace_meta.ace_tag {\
color: #FFE5BB\
}\
.ace-idle-fingers .ace_entity.ace_name {\
color: #FFC66D\
}\
.ace-idle-fingers .ace_collab.ace_user1 {\
color: #4d4d4d;\
background-color: #FFF980\
}\
.ace-idle-fingers .ace_indent-guide {\
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMwMjLyZYiPj/8PAAreAwAI1+g0AAAAAElFTkSuQmCC) right repeat-y\
}";

  var dom = require("../lib/dom");
  dom.importCssString(exports.cssText, exports.cssClass);
});
<commit_msg>Style fix for clicking on doc line nums<commit_after>define("ace/theme/idle_fingers",["require","exports","module","ace/lib/dom"], function(require, exports, module) {

  exports.isDark = true;
  exports.cssClass = "ace-idle-fingers";
  exports.cssText = ".ace-idle-fingers .ace_gutter {\
background: #3A3A3A;\
color: rgb(153,153,153)\
}\
.ace-idle-fingers .ace_print-margin {\
width: 1px;\
background: #3b3b3b\
}\
.ace-idle-fingers {\
background-color: #4d4d4d;\
color: #FFFFFF\
}\
.ace-idle-fingers .ace_cursor {\
color: #ffffff\
}\
.ace-idle-fingers .ace_marker-layer .ace_selection {\
background: #000000;\
opacity: 0.4;\
left: 0px !important;\
}\
.ace-idle-fingers.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #4d4d4d;\
border-radius: 2px\
}\
.ace-idle-fingers .ace_marker-layer .ace_step {\
background: rgb(102, 82, 0)\
}\
.ace-idle-fingers .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid #404040\
}\
.ace-idle-fingers .ace_marker-layer .ace_active-line {\
background: #000000;\
opacity: 0.4;\
}\
.ace-idle-fingers .ace_gutter-active-line {\
background-color: #000000;\
opacity: 0.4;\
}\
.ace-idle-fingers .ace_marker-layer .ace_selected-word {\
border: 1px solid rgba(90, 100, 126, 0.88)\
}\
.ace-idle-fingers .ace_invisible {\
color: #404040\
}\
.ace-idle-fingers .ace_keyword,\
.ace-idle-fingers .ace_meta {\
color: #CC7833\
}\
.ace-idle-fingers .ace_constant,\
.ace-idle-fingers .ace_constant.ace_character,\
.ace-idle-fingers .ace_constant.ace_character.ace_escape,\
.ace-idle-fingers .ace_constant.ace_other,\
.ace-idle-fingers .ace_support.ace_constant {\
color: #72cdf4\
}\
.ace-idle-fingers .ace_boolean {\
color: #ff6532\
}\
.ace-idle-fingers .ace_invalid {\
color: #FFFFFF;\
background-color: #FF0000\
}\
.ace-idle-fingers .ace_fold {\
background-color: #CC7833;\
border-color: #FFFFFF\
}\
.ace-idle-fingers .ace_support.ace_function {\
color: #B83426\
}\
.ace-idle-fingers .ace_variable.ace_parameter {\
font-style: italic\
}\
.ace-idle-fingers .ace_string {\
color: #29be9d\
}\
.ace-idle-fingers .ace_string.ace_regexp {\
color: #CCCC33\
}\
.ace-idle-fingers .ace_comment {\
font-style: italic;\
color: #BC9458\
}\
.ace-idle-fingers .ace_meta.ace_tag {\
color: #FFE5BB\
}\
.ace-idle-fingers .ace_entity.ace_name {\
color: #FFC66D\
}\
.ace-idle-fingers .ace_collab.ace_user1 {\
color: #4d4d4d;\
background-color: #FFF980\
}\
.ace-idle-fingers .ace_indent-guide {\
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMwMjLyZYiPj/8PAAreAwAI1+g0AAAAAElFTkSuQmCC) right repeat-y\
}";

  var dom = require("../lib/dom");
  dom.importCssString(exports.cssText, exports.cssClass);
});
<|endoftext|>


<commit_before>var fs = require('fs'),
    path = require('path'),
    URL = require('url');

if (!path.relative) {
    // Add path.relative for node.js < 0.6 (taken from 0.6.13)
    // path.relative(from, to)
    path.relative = function(from, to) {
        from = path.resolve(from).substr(1);
        to = path.resolve(to).substr(1);

        function trim(arr) {
            var start = 0;
            for (; start < arr.length; start++) {
                if (arr[start] !== '') break;
            }

            var end = arr.length - 1;
            for (; end >= 0; end--) {
                if (arr[end] !== '') break;
            }

            if (start > end) return [];
            return arr.slice(start, end - start + 1);
        }

        var fromParts = trim(from.split('/'));
        var toParts = trim(to.split('/'));

        var length = Math.min(fromParts.length, toParts.length);
        var samePartsLength = length;
        for (var i = 0; i < length; i++) {
            if (fromParts[i] !== toParts[i]) {
                samePartsLength = i;
                break;
            }
        }

        var outputParts = [];
        for (var i = samePartsLength; i < fromParts.length; i++) {
            outputParts.push('..');
        }

        outputParts = outputParts.concat(toParts.slice(samePartsLength));

        return outputParts.join('/');
    };
}

function clientToString(client) {
    return client.handshake.address.address + ':' + client.handshake.address.port;
}

module.exports = function (app, options, sio) {
    var io = sio.listen(app),
        clientsByRootRelativeUrl = {},
        rootRelativeUrlsByFileName = {}, // Map to translate file names reported by fs.watch into known root relative urls
        isWatchedByDirName = {},
        mtimeByFileName = {};

    function notifyWatchers(rootRelativeUrl) {
        if (rootRelativeUrl in clientsByRootRelativeUrl) {
            if (options.debug && clientsByRootRelativeUrl[rootRelativeUrl].length) {
                console.warn('  Notifying ' + clientsByRootRelativeUrl[rootRelativeUrl].length + ' watcher(s):\n  ' +
                             clientsByRootRelativeUrl[rootRelativeUrl].map(clientToString).join('\n  '));
            }
            clientsByRootRelativeUrl[rootRelativeUrl].forEach(function (client) {
                client.emit('change', rootRelativeUrl);
            });
        }
    }

    function notifyWatchersIfMtimeIncreased(rootRelativeUrl, fileName) {
        if (rootRelativeUrl in clientsByRootRelativeUrl) {
            if (!(fileName in mtimeByFileName)) {
                mtimeByFileName[fileName] = Infinity;
                notifyWatchers(rootRelativeUrl);
            } else {
                fs.stat(fileName, function (err, stats) {
                    if (err) {
                        return;
                    }
                    if (stats.mtime > mtimeByFileName[fileName]) {
                        notifyWatchers(rootRelativeUrl);
                    } else if (options.debug) {
                        console.warn('Suppressing notification for ' + fileName + ', the mtime of the file is still the same');
                    }
                    mtimeByFileName[fileName] = stats.mtime;
                });
            }
        }
    }

    if (!options.debug) {
        io.set('log level', 1);
    }
    io.sockets.on('connection', function (client) {
        if (options.debug) {
            console.warn('Client ' + clientToString(client) + ' connected');
        }
        client.on('watch', function (rootRelativeUrls) {
            if (options.debug) {
                console.warn('Client subscribed to ' + rootRelativeUrls.length + ' file(s):');
            }
            rootRelativeUrls.forEach(function (rootRelativeUrl) {
                if (options.debug) {
                    console.warn('  ' + rootRelativeUrl);
                }
                var mappedUrl = rootRelativeUrl.replace(/\?.*$/, '');
                if (options.mappings) {
                    Object.keys(options.mappings).every(function (url) {
                        if (mappedUrl.indexOf(url) === 0) {
                            mappedUrl = mappedUrl.replace(url, options.mappings[url]);
                            if (options.debug) {
                                console.warn('    => rewritten to ' + mappedUrl);
                            }
                            return false;
                        }
                        return true;
                    });
                }

                if (/\/$/.test(mappedUrl)) {
                    mappedUrl += 'index.html';
                }

                // Trim the leading slash if it exists
                mappedUrl = mappedUrl[0] === '/' ? mappedUrl.substr(1) : mappedUrl;

                var rootRelativePath = decodeURIComponent(mappedUrl),
                    fileName = path.resolve(options.root, rootRelativePath);

                rootRelativeUrlsByFileName[fileName] = rootRelativeUrlsByFileName[fileName] || [];
                if (rootRelativeUrlsByFileName[fileName].indexOf(rootRelativeUrl) === -1) {
                    rootRelativeUrlsByFileName[fileName].push(rootRelativeUrl);
                }
                if (rootRelativeUrl in clientsByRootRelativeUrl) {
                    clientsByRootRelativeUrl[rootRelativeUrl].push(client);
                } else {
                    clientsByRootRelativeUrl[rootRelativeUrl] = [client];
                    if (fs.watch && !options.watchfile) {
                        if (process.platform === 'linux' || process.platform === 'win32') {
                            var dirName = path.dirname(fileName);
                            if (!isWatchedByDirName[dirName]) {
                                isWatchedByDirName[dirName] = true;
                                if (options.debug) {
                                    console.warn('Starting to watch ' + dirName + ' using fs.watch');
                                }
                                try {
                                    fs.watch(dirName, function (eventName, fileName) {
                                        fileName = path.resolve(dirName, fileName); // Absolutify fileName
                                        if (options.debug) {
                                            console.warn('fs.watch: ' + eventName + ' ' + fileName);
                                        }
                                        (rootRelativeUrlsByFileName[fileName] || []).forEach(function (rootRelativeUrl) {
                                            if (options.mtime) {
                                                notifyWatchersIfMtimeIncreased(rootRelativeUrl, fileName);
                                            } else {
                                                notifyWatchers(rootRelativeUrl);
                                            }
                                        });
                                    });
                                } catch (err) {
                                    console.error("Error trying to watch " + dirName + " using fs.watch: " + err.code);
                                    if (options.debug) {
                                        console.error(err.stack);
                                    }
                                }
                            }
                        } else {
                            if (options.debug) {
                                console.warn('Starting to watch ' + fileName + ' using fs.watch');
                            }
                            try {
                                var fsWatcher = fs.watch(fileName, function handleWatchNotification(eventName) {
                                    // Resubscribe if the file is renamed (Mac OSX):
                                    if (eventName === 'rename') {
                                        fsWatcher.close();
                                        fsWatcher = fs.watch(fileName, handleWatchNotification);
                                    }
                                    if (options.mtime) {
                                        notifyWatchersIfMtimeIncreased(rootRelativeUrl, fileName);
                                    } else {
                                        notifyWatchers(rootRelativeUrl);
                                    }
                                });
                            } catch (err) {
                                console.error("Error trying to watch " + fileName + " using fs.watch: " + err.code);
                                if (options.debug) {
                                    console.error(err.stack);
                                }
                            }
                        }
                    } else {
                        // TODO: Poll the upstream server if the file isn't found on disc (or no root is specified)
                        if (options.debug) {
                            console.warn('Starting to watch ' + fileName + ' using fs.watchFile');
                        }
                        try {
                            fs.watchFile(fileName, function (currStat, prevStat) {
                                if (options.debug) {
                                    console.warn('fs.watchFile: ' + fileName + ' changed on disc');
                                }
                                if (currStat.mtime.getTime() !== prevStat.mtime.getTime()) {
                                    notifyWatchers(rootRelativeUrl);
                                }
                            });
                        } catch (err) {
                            console.error("Error trying to watch " + fileName + " using fs.watchFile: " + err.code);
                            if (options.debug) {
                                console.error(err.stack);
                            }
                        }
                    }
                }
            });
        }).on('disconnect', function () {
            if (options.debug) {
                console.warn('client disconnected');
            }
            Object.keys(clientsByRootRelativeUrl).forEach(function (rootRelativeUrl) {
                var clients = clientsByRootRelativeUrl[rootRelativeUrl],
                    clientIndex = clients.indexOf(client);
                if (clientIndex !== -1) {
                    clients.splice(clientIndex, 1);
                }
            });
        });
    });
};
<commit_msg>add silent flag to socket.io<commit_after>var fs = require('fs'),
    path = require('path'),
    URL = require('url');

if (!path.relative) {
    // Add path.relative for node.js < 0.6 (taken from 0.6.13)
    // path.relative(from, to)
    path.relative = function(from, to) {
        from = path.resolve(from).substr(1);
        to = path.resolve(to).substr(1);

        function trim(arr) {
            var start = 0;
            for (; start < arr.length; start++) {
                if (arr[start] !== '') break;
            }

            var end = arr.length - 1;
            for (; end >= 0; end--) {
                if (arr[end] !== '') break;
            }

            if (start > end) return [];
            return arr.slice(start, end - start + 1);
        }

        var fromParts = trim(from.split('/'));
        var toParts = trim(to.split('/'));

        var length = Math.min(fromParts.length, toParts.length);
        var samePartsLength = length;
        for (var i = 0; i < length; i++) {
            if (fromParts[i] !== toParts[i]) {
                samePartsLength = i;
                break;
            }
        }

        var outputParts = [];
        for (var i = samePartsLength; i < fromParts.length; i++) {
            outputParts.push('..');
        }

        outputParts = outputParts.concat(toParts.slice(samePartsLength));

        return outputParts.join('/');
    };
}

function clientToString(client) {
    return client.handshake.address.address + ':' + client.handshake.address.port;
}

module.exports = function (app, options, sio) {
    var io = sio.listen(app, { log: !options.silentSocketIo }),
        clientsByRootRelativeUrl = {},
        rootRelativeUrlsByFileName = {}, // Map to translate file names reported by fs.watch into known root relative urls
        isWatchedByDirName = {},
        mtimeByFileName = {};

    function notifyWatchers(rootRelativeUrl) {
        if (rootRelativeUrl in clientsByRootRelativeUrl) {
            if (options.debug && clientsByRootRelativeUrl[rootRelativeUrl].length) {
                console.warn('  Notifying ' + clientsByRootRelativeUrl[rootRelativeUrl].length + ' watcher(s):\n  ' +
                             clientsByRootRelativeUrl[rootRelativeUrl].map(clientToString).join('\n  '));
            }
            clientsByRootRelativeUrl[rootRelativeUrl].forEach(function (client) {
                client.emit('change', rootRelativeUrl);
            });
        }
    }

    function notifyWatchersIfMtimeIncreased(rootRelativeUrl, fileName) {
        if (rootRelativeUrl in clientsByRootRelativeUrl) {
            if (!(fileName in mtimeByFileName)) {
                mtimeByFileName[fileName] = Infinity;
                notifyWatchers(rootRelativeUrl);
            } else {
                fs.stat(fileName, function (err, stats) {
                    if (err) {
                        return;
                    }
                    if (stats.mtime > mtimeByFileName[fileName]) {
                        notifyWatchers(rootRelativeUrl);
                    } else if (options.debug) {
                        console.warn('Suppressing notification for ' + fileName + ', the mtime of the file is still the same');
                    }
                    mtimeByFileName[fileName] = stats.mtime;
                });
            }
        }
    }

    if (!options.debug) {
        io.set('log level', 1);
    }
    io.sockets.on('connection', function (client) {
        if (options.debug) {
            console.warn('Client ' + clientToString(client) + ' connected');
        }
        client.on('watch', function (rootRelativeUrls) {
            if (options.debug) {
                console.warn('Client subscribed to ' + rootRelativeUrls.length + ' file(s):');
            }
            rootRelativeUrls.forEach(function (rootRelativeUrl) {
                if (options.debug) {
                    console.warn('  ' + rootRelativeUrl);
                }
                var mappedUrl = rootRelativeUrl.replace(/\?.*$/, '');
                if (options.mappings) {
                    Object.keys(options.mappings).every(function (url) {
                        if (mappedUrl.indexOf(url) === 0) {
                            mappedUrl = mappedUrl.replace(url, options.mappings[url]);
                            if (options.debug) {
                                console.warn('    => rewritten to ' + mappedUrl);
                            }
                            return false;
                        }
                        return true;
                    });
                }

                if (/\/$/.test(mappedUrl)) {
                    mappedUrl += 'index.html';
                }

                // Trim the leading slash if it exists
                mappedUrl = mappedUrl[0] === '/' ? mappedUrl.substr(1) : mappedUrl;

                var rootRelativePath = decodeURIComponent(mappedUrl),
                    fileName = path.resolve(options.root, rootRelativePath);

                rootRelativeUrlsByFileName[fileName] = rootRelativeUrlsByFileName[fileName] || [];
                if (rootRelativeUrlsByFileName[fileName].indexOf(rootRelativeUrl) === -1) {
                    rootRelativeUrlsByFileName[fileName].push(rootRelativeUrl);
                }
                if (rootRelativeUrl in clientsByRootRelativeUrl) {
                    clientsByRootRelativeUrl[rootRelativeUrl].push(client);
                } else {
                    clientsByRootRelativeUrl[rootRelativeUrl] = [client];
                    if (fs.watch && !options.watchfile) {
                        if (process.platform === 'linux' || process.platform === 'win32') {
                            var dirName = path.dirname(fileName);
                            if (!isWatchedByDirName[dirName]) {
                                isWatchedByDirName[dirName] = true;
                                if (options.debug) {
                                    console.warn('Starting to watch ' + dirName + ' using fs.watch');
                                }
                                try {
                                    fs.watch(dirName, function (eventName, fileName) {
                                        fileName = path.resolve(dirName, fileName); // Absolutify fileName
                                        if (options.debug) {
                                            console.warn('fs.watch: ' + eventName + ' ' + fileName);
                                        }
                                        (rootRelativeUrlsByFileName[fileName] || []).forEach(function (rootRelativeUrl) {
                                            if (options.mtime) {
                                                notifyWatchersIfMtimeIncreased(rootRelativeUrl, fileName);
                                            } else {
                                                notifyWatchers(rootRelativeUrl);
                                            }
                                        });
                                    });
                                } catch (err) {
                                    console.error("Error trying to watch " + dirName + " using fs.watch: " + err.code);
                                    if (options.debug) {
                                        console.error(err.stack);
                                    }
                                }
                            }
                        } else {
                            if (options.debug) {
                                console.warn('Starting to watch ' + fileName + ' using fs.watch');
                            }
                            try {
                                var fsWatcher = fs.watch(fileName, function handleWatchNotification(eventName) {
                                    // Resubscribe if the file is renamed (Mac OSX):
                                    if (eventName === 'rename') {
                                        fsWatcher.close();
                                        fsWatcher = fs.watch(fileName, handleWatchNotification);
                                    }
                                    if (options.mtime) {
                                        notifyWatchersIfMtimeIncreased(rootRelativeUrl, fileName);
                                    } else {
                                        notifyWatchers(rootRelativeUrl);
                                    }
                                });
                            } catch (err) {
                                console.error("Error trying to watch " + fileName + " using fs.watch: " + err.code);
                                if (options.debug) {
                                    console.error(err.stack);
                                }
                            }
                        }
                    } else {
                        // TODO: Poll the upstream server if the file isn't found on disc (or no root is specified)
                        if (options.debug) {
                            console.warn('Starting to watch ' + fileName + ' using fs.watchFile');
                        }
                        try {
                            fs.watchFile(fileName, function (currStat, prevStat) {
                                if (options.debug) {
                                    console.warn('fs.watchFile: ' + fileName + ' changed on disc');
                                }
                                if (currStat.mtime.getTime() !== prevStat.mtime.getTime()) {
                                    notifyWatchers(rootRelativeUrl);
                                }
                            });
                        } catch (err) {
                            console.error("Error trying to watch " + fileName + " using fs.watchFile: " + err.code);
                            if (options.debug) {
                                console.error(err.stack);
                            }
                        }
                    }
                }
            });
        }).on('disconnect', function () {
            if (options.debug) {
                console.warn('client disconnected');
            }
            Object.keys(clientsByRootRelativeUrl).forEach(function (rootRelativeUrl) {
                var clients = clientsByRootRelativeUrl[rootRelativeUrl],
                    clientIndex = clients.indexOf(client);
                if (clientIndex !== -1) {
                    clients.splice(clientIndex, 1);
                }
            });
        });
    });
};
<|endoftext|>


<commit_before>/*
 * Auth* provider and manager
 **/
module.exports = rbac

function rbac(api, wes){
var rbac_api = { can: null, roles: null, users: null, mw: mwRBAC }
   ,fs = require('fs') ,dir

    default_access()

    qs = require('connect/node_modules/qs')
    dir = process.cwd() + api.cfg.app.modules.userman.data + '/rbac'

    fs.stat(dir,
        function stat_um_data_rbac_dir(err, d){
            if(err){
                require('mkdirp').mkdirp(dir,
                    function mkdirp_um_data_rbac_dir(err){
                        if(err) throw(err)
                        //load_api()
                    }
                )
                return
            }
            if(!d.isDirectory()){
                throw new Error('Is not a directory: ' + dir)
            }
            //load_api()// init api
        }
    )

    return rbac_api

    function default_access(){
        rbac_api.can = {
            backend:{
                'App.view.desktop.BackendTools': true
               ,'App.backend.JS': true
            }
           ,Static: { }// deny access to class or other files by `initAuthStatic()`
        }
        rbac_api.roles = {// 'role': new Array(of `can`s)
            'developer.local': [ rbac_api.can.backend ]// can do all from specified `can`
           ,'admin.local': [ 'App.view.desktop.BackendTools' ]// single true-permissions
           ,'developer': [ 'App.backend.JS' ]
        }
        rbac_api.users = {
            devka:{
                id: 'devka',
                // require('crypto').createHash('sha1').update(pass).digest('hex')
                pass: '9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684',
                roles: [ 'developer.local' ],
                name:'default login'
            }
        }
    }

    function mwRBAC(req, res, next){
    var ret = { success: false, data: null }//                               \..../
        res.json(ret)// sync
    }
}
<commit_msg>userman default login: give 'admin.local' role (for auth testing)<commit_after>/*
 * Auth* provider and manager
 **/
module.exports = rbac

function rbac(api, wes){
var rbac_api = { can: null, roles: null, users: null, mw: mwRBAC }
   ,fs = require('fs') ,dir

    default_access()

    qs = require('connect/node_modules/qs')
    dir = process.cwd() + api.cfg.app.modules.userman.data + '/rbac'

    fs.stat(dir,
        function stat_um_data_rbac_dir(err, d){
            if(err){
                require('mkdirp').mkdirp(dir,
                    function mkdirp_um_data_rbac_dir(err){
                        if(err) throw(err)
                        //load_api()
                    }
                )
                return
            }
            if(!d.isDirectory()){
                throw new Error('Is not a directory: ' + dir)
            }
            //load_api()// init api
        }
    )

    return rbac_api

    function default_access(){
        rbac_api.can = {
            backend:{
                'App.view.desktop.BackendTools': true
               ,'App.backend.JS': true
            }
           ,Static: { }// deny access to class or other files by `initAuthStatic()`
        }
        rbac_api.roles = {// 'role': new Array(of `can`s)
            'developer.local': [ rbac_api.can.backend ]// can do all from specified `can`
           ,'admin.local': [ 'App.view.desktop.BackendTools' ]// single true-permissions
           ,'developer': [ 'App.backend.JS' ]
        }
        rbac_api.users = {
            devka:{
                id: 'devka',
                // require('crypto').createHash('sha1').update(pass).digest('hex')
                pass: '9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684',
                roles: [ 'developer.local', 'admin.local' ],
                name:'default login'
            }
        }
    }

    function mwRBAC(req, res, next){
    var ret = { success: false, data: null }//                               \..../
        res.json(ret)// sync
    }
}
<|endoftext|>