<commit_before>'use strict';
var nodemailer  = require('nodemailer'),
    express     = require('express'),
    flatten     = require('flatten.js').flatten,
    path        = require('path'),
    app         = express(),
    hbs         = require('./hbs'),
    options     = require('./config'),
    store       = require('./store')(options.store),
    undefsafe   = require('undefsafe'),
    models      = require('./models').createModels(store),
    routes      = require('./routes'),
    handlers    = require('./handlers'),
    middleware  = require('./middleware'),
    helpers     = require('./helpers'),
    metrics     = require('./metrics'),
    github      = require('./github')(options), // if used, contains github.id
    dropbox     = require('./dropbox')(options),
    _           = require('underscore'),
    crypto      = require('crypto'),
    filteredCookieSession = require('express-cookie-blacklist'),
    sessionVersion = require('./session-version'),
    stripeRoutes  = require('./stripe')(options),
    flattened;

function generateSessionSecret() {
  console.log('Warning: Generating a session key - please see http://jsbin.com/help/session-secret');
  return crypto.createHash('md5').update(Math.random() + '').digest('hex');
}

/**
 * JS Bin configuration
 */

app.store  = store;
app.sessionVersion = sessionVersion;
// Create model singletons
// models.createModels(store);

app.mailer = (function (mail) {
  var mailTransport = null,
      method   = mail && mail.adapter,
      settings = mail && mail[method];

  if (method && options) {
    mailTransport = nodemailer.createTransport(method, settings);
  }

  return new handlers.MailHandler(mailTransport, app.render.bind(app));
})(options.mail);

app.PRODUCTION  = 'production';
app.DEVELOPMENT = 'development';

// Set the NODE_ENV variable as this is used by Express, we want the
// environment to take precedence but allow it to be set using the config file
// too.
if (process.env.NODE_ENV) {
  options.env = process.env.NODE_ENV;
}
process.env.NODE_ENV = options.env;

// Need to set the node environment to run in the same timezone as the database
// This will ideally be UTC in both cases but if not this can be set either
// using the TZ environment variable or the "timezone" option.
// This is because the mysql library coerces MySQL dates (without timezones)
// into JavaScript date objects.
if (!process.env.TZ && options.timezone) {
  process.env.TZ = options.timezone;
}

// Sort out the port.
(function () {
  var port = process.env.PORT;

  // if we're running from the bin/jsbin file, then we modify
  // both the listen port AND the options.url
  if (process.env.JSBIN_PORT) {
    if (options.url.host.indexOf(':') === -1) {
      options.url.host += ':' + process.env.JSBIN_PORT;
    } else {
      options.url.host = options.url.host.replace(/\:\d+$/, function () {
        return ':' + process.env.JSBIN_PORT;
      });
    }
  }

  if (!port) {
    options.url.host.replace(/\:(\d+)$/, function (m, p) {
      if (p.length) {
        port = p;
      }
    });
  }

  if (!port) {
    port = 80;
  }

  // NOTE: this is *only* used for running the server.listen()
  // it's not used in the urls to access assets, etc.
  options.port = port;
})();

// Strip trailing slash from the prefix
options.url.prefix = options.url.prefix.replace(/\/$/, '');

// Apply the keys from the config file. All nested properties are
// space delimited to match the express style.
//
// For example, app.set('url prefix'); //=> '/'
flattened = flatten(options, ' ');

Object.getOwnPropertyNames(flattened).forEach(function (key) {
  app.set(key, flattened[key]);
});

// in live, we run behind a proxy - so this will give us our IPs again:
// http://expressjs.com/guide.html#proxies
if (process.env.NODE_ENV === app.PRODUCTION || process.env.JSBIN_PROXY) {
  app.enable('trust proxy');
}

app.set('root', path.resolve(path.join(__dirname, '..')));

// ensure jsbin is running from the expected root
process.chdir(app.get('root'));

app.set('version', require('../package').version);

app.set('url prefix', options.url.prefix);
app.set('url ssl', options.url.ssl);
app.set('url full', 'http://' + app.get('url host') + app.get('url prefix'));
app.set('basepath', app.get('url prefix'));
app.set('is_production', app.get('env') === app.PRODUCTION);

if (options.url.static) {
  app.set('static url', 'http://' + app.get('url static'));
} else {
  app.set('static url', app.get('url full'));
}

if (options.url.runner) {
  // strip trailing slash, just in case
  options.url.runner = options.url.runner.replace(/\/$/, '');
  app.set('url runner', options.url.runner);
} else {
  app.set('url runner', app.get('url full'));
}

app.set('views', 'views');
app.set('view engine', 'html');

app.engine('html', hbs.express3({
  extname: '.html',
  // defaultLayout: path.resolve(__dirname + '/../views/layout.html'),
  partialsDir: [
    path.resolve(__dirname + '/../views/partials')
  ]
}));

app.engine('txt', hbs.express3({
  extname: '.txt',
  // defaultLayout: path.resolve(__dirname + '/../views/layout.txt'),
  partialsDir: [
    path.resolve(__dirname + '/../views/partials')
  ]
}));

// Define some global template variables.
var helpers = helpers.createHelpers(app);
app.locals.version = app.get('version');
app.locals.client = app.get('client');
Object.keys(helpers).forEach(function (key) {
  app.locals[key] = helpers[key];
});

// set these as global template variables since they keep getting missed
app.locals.is_production = app.get('is_production');
app.locals.cachebust = app.locals.cacheBust = app.get('is_production') ? '?' + app.get('version') : '';

app.connect = function (callback) {
  app.emit('before:connect', app);

  // Register all the middleware.
  app.configure(function () {
    var mount = app.set('url prefix') || '/',
        logger;

    logger = process.env.JSBIN_LOGGER || app.get('server logger') || 'tiny';

    if (logger !== 'none') {
      app.use(express.logger(logger));
    }

    // favicon
    app.use(express.favicon(path.join(__dirname, '../', 'public','images','favicon.png')));

    app.use(function (req, res, next) {
      // used for timings
      req.start = Date.now();
      next();
    });
    app.emit('before:middleware');

    app.use(middleware.flash());

    // this middleware says:
    // a) if we have a static url in our config, i.e. static.jsbin.com
    // b) only serve static assets IF AND ONLY IF, the full url is requested,
    //    i.e. static.jsbin.com/css/style.css
    // c) otherwise, skip the static handler, and go straight to routes.js
    //
    // If the request hits routes.js and doesn't match a defined route (or doesn't
    // look like a bin url) then it gets 404'd - i.e. you can't request /css/style.css
    // because it'll return a 404.
    app.use(mount, (function () {
      var staticRouter = express.static(path.join(app.get('root'), 'public'));

      return function (req, res, next) {
        // construct the full request url
        var url = req.protocol + '://' + req.get('host') + req.url;

        // *if* the config has a static url path (i.e. different from jsbin.com),
        // i.e. static.jsbin.com
        if (options.url.static) {
          // and the url requested is NOT static, then continue with node
          if (url.indexOf(options.url.static) === -1) {
            return next();
          }
        }
        // otherwise serve it as a static asset...
        return staticRouter(req, res, next);
      };
    })());

    app.use(middleware.limitContentLength({limit: app.get('max-request-size')}));

    app.emit('before:cookies', { app: app });

    app.use(express.cookieParser(app.get('session secret') || generateSessionSecret()));
    app.use(filteredCookieSession(['user.settings', 'passport', 'user.email', 'user.github_id', 'user.github_token', 'user.dropbox_token', 'user.dropbox_id', 'user.flagged', 'user.bincount', 'user.api_key', 'user.key', 'user.id']));
    app.use(express.cookieSession({
      key: 'jsbin',
      cookie: {
        maxAge: 365 * 24 * 60 * 60 * 1000,
        // the domain must contain a dot and should not have a port
        domain: app.get('url host').indexOf('.') === -1 ? undefined : '.' + app.get('url host').replace(/:\d+$/, '')
      }
    }));

    app.emit('before:session', {app: app});

    // memcached sessions
    if (options.session.memcached && options.session.memcached.connection) {
      require('./addons/memcached')(app, options.session.memcached.connection);
    }

    app.emit('after:session', {app: app});

    app.use(function (req, res, next) {
      // Deletes cookies using the old domain (without the leading '.')
      // and we know this because they don't have a version in their cookie.
      if (undefsafe(req, 'session.version') === undefined) {
        var oldSess = _.extend({ version: app.get('version') }, req.session);
        res.clearCookie('jsbin');
        req.session = oldSess;
      }

      next();
    });

    // Passport
    if (options.github && options.github.id) {
      github.initialize(app);
    }
    if (options.dropbox && options.dropbox.id) {
      dropbox.initialize();
    }
    app.use(express.urlencoded());
    app.use(express.json());
    app.use(middleware.csrf({ ignore: ['/', /^\/api\//, /^\/hooks\//, /^\/subscribe\//] }));
    app.use(middleware.api({ app: app }));
    app.use(middleware.subdomain(app));
    app.use(middleware.noslashes());
    app.use(middleware.protectServiceWorker());
    app.use(middleware.ajax());
    // app.use(middleware.cors());
    app.use(middleware.jsonp());
    app.use(middleware.protocolCheck);
    app.use(middleware.decompressBody);
    app.use(middleware.notices());
    app.use(middleware.nofollow({
      allowed: ['/']
    }));

    app.emit('before:router', app);
    app.use(mount, app.router);

    // All routes must be registered after middleware
    // Register stripe webhooks
    if (options.payment && options.payment.stripe && options.store.adapter === 'mysql') {
      stripeRoutes(app);
    }
    // Register all routes
    routes(app);

  });

  app.emit('after', app);

  store.connect(function (err) {
    if (err) {
      metrics.increment('error.store.connect');
      throw err;
    }

    var port = app.set('port');
    app.listen(port);

    if (typeof callback === 'function') {
      callback();
    }
    app.emit('connected');

    process.stdout.write('JS Bin v' + app.get('version') + ' is up and running on port ' + app.get('port') + '. Now point your browser at ' + app.get('url full') + '\n');
  });
};

// Export the application to allow it to be included.
module.exports = { app: app };

// Run a local development server if this file is called directly.
if (require.main === module) {
  app.connect();
}
<commit_msg>Disable 'X-Powered-By' header exposed by Express<commit_after>'use strict';
var nodemailer  = require('nodemailer'),
    express     = require('express'),
    flatten     = require('flatten.js').flatten,
    path        = require('path'),
    app         = express(),
    hbs         = require('./hbs'),
    options     = require('./config'),
    store       = require('./store')(options.store),
    undefsafe   = require('undefsafe'),
    models      = require('./models').createModels(store),
    routes      = require('./routes'),
    handlers    = require('./handlers'),
    middleware  = require('./middleware'),
    helpers     = require('./helpers'),
    metrics     = require('./metrics'),
    github      = require('./github')(options), // if used, contains github.id
    dropbox     = require('./dropbox')(options),
    _           = require('underscore'),
    crypto      = require('crypto'),
    filteredCookieSession = require('express-cookie-blacklist'),
    sessionVersion = require('./session-version'),
    stripeRoutes  = require('./stripe')(options),
    flattened;

function generateSessionSecret() {
  console.log('Warning: Generating a session key - please see http://jsbin.com/help/session-secret');
  return crypto.createHash('md5').update(Math.random() + '').digest('hex');
}

/**
 * JS Bin configuration
 */

app.store  = store;
app.sessionVersion = sessionVersion;
// Create model singletons
// models.createModels(store);

app.mailer = (function (mail) {
  var mailTransport = null,
      method   = mail && mail.adapter,
      settings = mail && mail[method];

  if (method && options) {
    mailTransport = nodemailer.createTransport(method, settings);
  }

  return new handlers.MailHandler(mailTransport, app.render.bind(app));
})(options.mail);

app.PRODUCTION  = 'production';
app.DEVELOPMENT = 'development';

// Set the NODE_ENV variable as this is used by Express, we want the
// environment to take precedence but allow it to be set using the config file
// too.
if (process.env.NODE_ENV) {
  options.env = process.env.NODE_ENV;
}
process.env.NODE_ENV = options.env;

// Need to set the node environment to run in the same timezone as the database
// This will ideally be UTC in both cases but if not this can be set either
// using the TZ environment variable or the "timezone" option.
// This is because the mysql library coerces MySQL dates (without timezones)
// into JavaScript date objects.
if (!process.env.TZ && options.timezone) {
  process.env.TZ = options.timezone;
}

// Sort out the port.
(function () {
  var port = process.env.PORT;

  // if we're running from the bin/jsbin file, then we modify
  // both the listen port AND the options.url
  if (process.env.JSBIN_PORT) {
    if (options.url.host.indexOf(':') === -1) {
      options.url.host += ':' + process.env.JSBIN_PORT;
    } else {
      options.url.host = options.url.host.replace(/\:\d+$/, function () {
        return ':' + process.env.JSBIN_PORT;
      });
    }
  }

  if (!port) {
    options.url.host.replace(/\:(\d+)$/, function (m, p) {
      if (p.length) {
        port = p;
      }
    });
  }

  if (!port) {
    port = 80;
  }

  // NOTE: this is *only* used for running the server.listen()
  // it's not used in the urls to access assets, etc.
  options.port = port;
})();

// Strip trailing slash from the prefix
options.url.prefix = options.url.prefix.replace(/\/$/, '');

// Apply the keys from the config file. All nested properties are
// space delimited to match the express style.
//
// For example, app.set('url prefix'); //=> '/'
flattened = flatten(options, ' ');

Object.getOwnPropertyNames(flattened).forEach(function (key) {
  app.set(key, flattened[key]);
});

// in live, we run behind a proxy - so this will give us our IPs again:
// http://expressjs.com/guide.html#proxies
if (process.env.NODE_ENV === app.PRODUCTION || process.env.JSBIN_PROXY) {
  app.enable('trust proxy');
}

app.disable('x-powered-by');

app.set('root', path.resolve(path.join(__dirname, '..')));

// ensure jsbin is running from the expected root
process.chdir(app.get('root'));

app.set('version', require('../package').version);

app.set('url prefix', options.url.prefix);
app.set('url ssl', options.url.ssl);
app.set('url full', 'http://' + app.get('url host') + app.get('url prefix'));
app.set('basepath', app.get('url prefix'));
app.set('is_production', app.get('env') === app.PRODUCTION);

if (options.url.static) {
  app.set('static url', 'http://' + app.get('url static'));
} else {
  app.set('static url', app.get('url full'));
}

if (options.url.runner) {
  // strip trailing slash, just in case
  options.url.runner = options.url.runner.replace(/\/$/, '');
  app.set('url runner', options.url.runner);
} else {
  app.set('url runner', app.get('url full'));
}

app.set('views', 'views');
app.set('view engine', 'html');

app.engine('html', hbs.express3({
  extname: '.html',
  // defaultLayout: path.resolve(__dirname + '/../views/layout.html'),
  partialsDir: [
    path.resolve(__dirname + '/../views/partials')
  ]
}));

app.engine('txt', hbs.express3({
  extname: '.txt',
  // defaultLayout: path.resolve(__dirname + '/../views/layout.txt'),
  partialsDir: [
    path.resolve(__dirname + '/../views/partials')
  ]
}));

// Define some global template variables.
var helpers = helpers.createHelpers(app);
app.locals.version = app.get('version');
app.locals.client = app.get('client');
Object.keys(helpers).forEach(function (key) {
  app.locals[key] = helpers[key];
});

// set these as global template variables since they keep getting missed
app.locals.is_production = app.get('is_production');
app.locals.cachebust = app.locals.cacheBust = app.get('is_production') ? '?' + app.get('version') : '';

app.connect = function (callback) {
  app.emit('before:connect', app);

  // Register all the middleware.
  app.configure(function () {
    var mount = app.set('url prefix') || '/',
        logger;

    logger = process.env.JSBIN_LOGGER || app.get('server logger') || 'tiny';

    if (logger !== 'none') {
      app.use(express.logger(logger));
    }

    // favicon
    app.use(express.favicon(path.join(__dirname, '../', 'public','images','favicon.png')));

    app.use(function (req, res, next) {
      // used for timings
      req.start = Date.now();
      next();
    });
    app.emit('before:middleware');

    app.use(middleware.flash());

    // this middleware says:
    // a) if we have a static url in our config, i.e. static.jsbin.com
    // b) only serve static assets IF AND ONLY IF, the full url is requested,
    //    i.e. static.jsbin.com/css/style.css
    // c) otherwise, skip the static handler, and go straight to routes.js
    //
    // If the request hits routes.js and doesn't match a defined route (or doesn't
    // look like a bin url) then it gets 404'd - i.e. you can't request /css/style.css
    // because it'll return a 404.
    app.use(mount, (function () {
      var staticRouter = express.static(path.join(app.get('root'), 'public'));

      return function (req, res, next) {
        // construct the full request url
        var url = req.protocol + '://' + req.get('host') + req.url;

        // *if* the config has a static url path (i.e. different from jsbin.com),
        // i.e. static.jsbin.com
        if (options.url.static) {
          // and the url requested is NOT static, then continue with node
          if (url.indexOf(options.url.static) === -1) {
            return next();
          }
        }
        // otherwise serve it as a static asset...
        return staticRouter(req, res, next);
      };
    })());

    app.use(middleware.limitContentLength({limit: app.get('max-request-size')}));

    app.emit('before:cookies', { app: app });

    app.use(express.cookieParser(app.get('session secret') || generateSessionSecret()));
    app.use(filteredCookieSession(['user.settings', 'passport', 'user.email', 'user.github_id', 'user.github_token', 'user.dropbox_token', 'user.dropbox_id', 'user.flagged', 'user.bincount', 'user.api_key', 'user.key', 'user.id']));
    app.use(express.cookieSession({
      key: 'jsbin',
      cookie: {
        maxAge: 365 * 24 * 60 * 60 * 1000,
        // the domain must contain a dot and should not have a port
        domain: app.get('url host').indexOf('.') === -1 ? undefined : '.' + app.get('url host').replace(/:\d+$/, '')
      }
    }));

    app.emit('before:session', {app: app});

    // memcached sessions
    if (options.session.memcached && options.session.memcached.connection) {
      require('./addons/memcached')(app, options.session.memcached.connection);
    }

    app.emit('after:session', {app: app});

    app.use(function (req, res, next) {
      // Deletes cookies using the old domain (without the leading '.')
      // and we know this because they don't have a version in their cookie.
      if (undefsafe(req, 'session.version') === undefined) {
        var oldSess = _.extend({ version: app.get('version') }, req.session);
        res.clearCookie('jsbin');
        req.session = oldSess;
      }

      next();
    });

    // Passport
    if (options.github && options.github.id) {
      github.initialize(app);
    }
    if (options.dropbox && options.dropbox.id) {
      dropbox.initialize();
    }
    app.use(express.urlencoded());
    app.use(express.json());
    app.use(middleware.csrf({ ignore: ['/', /^\/api\//, /^\/hooks\//, /^\/subscribe\//] }));
    app.use(middleware.api({ app: app }));
    app.use(middleware.subdomain(app));
    app.use(middleware.noslashes());
    app.use(middleware.protectServiceWorker());
    app.use(middleware.ajax());
    // app.use(middleware.cors());
    app.use(middleware.jsonp());
    app.use(middleware.protocolCheck);
    app.use(middleware.decompressBody);
    app.use(middleware.notices());
    app.use(middleware.nofollow({
      allowed: ['/']
    }));

    app.emit('before:router', app);
    app.use(mount, app.router);

    // All routes must be registered after middleware
    // Register stripe webhooks
    if (options.payment && options.payment.stripe && options.store.adapter === 'mysql') {
      stripeRoutes(app);
    }
    // Register all routes
    routes(app);

  });

  app.emit('after', app);

  store.connect(function (err) {
    if (err) {
      metrics.increment('error.store.connect');
      throw err;
    }

    var port = app.set('port');
    app.listen(port);

    if (typeof callback === 'function') {
      callback();
    }
    app.emit('connected');

    process.stdout.write('JS Bin v' + app.get('version') + ' is up and running on port ' + app.get('port') + '. Now point your browser at ' + app.get('url full') + '\n');
  });
};

// Export the application to allow it to be included.
module.exports = { app: app };

// Run a local development server if this file is called directly.
if (require.main === module) {
  app.connect();
}
<|endoftext|>


<commit_before>const CleanWebpackPlugin = require('clean-webpack-plugin');
const kabaBabelPreset = require("kaba-babel-preset");
const {blue, red, yellow} = require("kleur");
const fs = require("fs-extra");
const path = require("path");
const TerserPlugin = require('terser-webpack-plugin');
const typeScriptErrorFormatter = require("@becklyn/typescript-error-formatter");

/**
 * @typedef {{
 *      entries: Object<string, string>,
 *      includePaths: string[],
 *      outputPath: string,
 *      browserList: string[],
 *      cwd: string,
 * }} SassBuildConfig
 *
 * @typedef {{
 *      sass: SassBuildConfig,
 *      js: {
 *          javaScriptDependenciesFileName: string,
 *          webpack: webpack.Configuration,
 *          customTypeScriptConfig: string|null,
 *      },
 *      cwd: string,
 * }} KabaBuildConfig
 */

/**
 * Main Kaba class
 */
class Kaba
{
    /**
     *
     */
    constructor ()
    {
        /**
         * @private
         * @type {string}
         */
        this.cwd = process.cwd();

        /**
         * @private
         * @†ype {string}
         */
        this.libRoot = path.dirname(__dirname);

        /**
         * @private
         * @type {Object<string,string>}
         */
        this.jsEntries = {};

        /**
         * @private
         * @type {Object<string, string>}
         */
        this.sassEntries = {};

        /**
         * @private
         * @type {string}
         */
        this.sassIncludePaths = [];

        /**
         * @private
         * @type {{base : string, css : string, js : string}}
         */
        this.outputPaths = {};

        /**
         * @private
         * @type {string}
         */
        this.publicPath = "";

        /**
         * @private
         * @type {Object<string,string>}
         */
        this.externals = {};

        /**
         * @private
         * @type {string[]}
         */
        this.browserList = [];

        /**
         * @private
         * @type {boolean}
         */
        this.moduleConcatenationEnabled = false;

        /**
         * The webpack plugins
         *
         * @private
         * @type {Array}
         */
        this.plugins = [
            new CleanWebpackPlugin(),
        ];

        /**
         * @private
         * @type {string}
         */
        this.javaScriptDependenciesFileName = "_dependencies";

        /**
         * @private
         * @type {{enabled: boolean, compileJs: boolean}}
         */
        this.typescript = {
            enabled: false,
            compileJs: false,
        };

        /**
         * @private
         * @type {boolean}
         */
        this.splitChunks = true;

        /**
         * @private
         * @type {boolean}
         */
        this.hashFileNames = true;

        /**
         * @private
         * @type {?string}
         */
        this.customTypeScriptConfig = null;

        // set defaults
        this
            .setOutputPath("build")
            .setPublicPath("/assets/")
            .setBrowserList(["last 2 versions", "IE 11"])
        ;
    }

    /**
     * Adds an entry
     *
     * @deprecated
     * @param {Object<string,string>} mapping
     * @return {Kaba}
     */
    addEntries (mapping)
    {
        Object.keys(mapping).forEach(
            name =>
            {
                const source = mapping[name];
                const entry = {
                    [name]: source,
                };

                if (/\.scss$/.test(source))
                {
                    this.addSassEntries(entry);
                }
                else
                {
                    this.addJsEntries(entry);
                }
            }
        );

        return this;
    }

    /**
     * Adds JS entries
     *
     * @param {Object<string,string>} mapping
     * @return {Kaba}
     */
    addJavaScriptEntries (mapping)
    {
        this.addEntriesToList(mapping, this.jsEntries, "js");
        return this;
    }

    /**
     * Adds Sass entries
     *
     * @param {Object<string,string>} mapping
     * @return {Kaba}
     */
    addSassEntries (mapping)
    {
        try
        {
            require("kaba-scss");
        }
        catch (e)
        {
            console.log(`${red("ERROR")} It seems that ${yellow("kaba-scss")} is not installed. Install it with ${blue("npm install -D kaba-scss")}`);
            process.exit(1);
        }

        this.addEntriesToList(mapping, this.sassEntries, "sass");
        return this;
    }

    /**
     * Adds items from a mapping to list
     *
     * @private
     * @param {Object<string,string>} mapping
     * @param {Object<string,string>} list
     * @param {string} type
     */
    addEntriesToList (mapping, list, type)
    {
        Object.keys(mapping).forEach(
            name =>
            {
                let source = mapping[name];

                if (!/^[./]/.test(source))
                {
                    source = `./${source}`;
                }

                if (list[name] !== undefined)
                {
                    throw new Error(`Can't add ${type} entry named ${name}: an entry with this name already exists.`);
                }

                list[name] = source;
            }
        );
    }

    /**
     * Sets the output path
     *
     * @param {string} base
     * @param {string} cssSubDir
     * @param {string} jsSubDir
     * @return {Kaba}
     */
    setOutputPath (base, cssSubDir = "css", jsSubDir = "js")
    {
        this.outputPaths = {
            base: path.join(this.cwd, base),
            css: cssSubDir,
            js: jsSubDir,
        };
        return this;
    }

    /**
     * Sets the relative public path (for automatic imports)
     *
     * @param {string} publicPath relative to the cwd
     * @return {Kaba}
     */
    setPublicPath (publicPath)
    {
        this.publicPath = publicPath;
        return this;
    }

    /**
     *
     * @param {Object<string,string>} externals
     * @return {Kaba}
     */
    setExternals (externals)
    {
        this.externals = externals;
        return this;
    }

    /**
     * Sets the browser list
     *
     * @param {string[]} list
     * @return {Kaba}
     */
    setBrowserList (list)
    {
        this.browserList = list;
        return this;
    }

    /**
     * Disables module concatenation
     *
     * @return {Kaba}
     */
    disableModuleConcatenation ()
    {
        this.moduleConcatenationEnabled = false;
        return this;
    }

    /**
     * Enables module concatenation
     *
     * @return {Kaba}
     */
    enableModuleConcatenation ()
    {
        this.moduleConcatenationEnabled = true;
        return this;
    }

    /**
     * Adds `node_modules` to the include dir of sass.
     *
     * @return {Kaba}
     */
    enableSassNodeModulesIncludePaths ()
    {
        this.sassIncludePaths = [
            path.join(this.cwd, "node_modules"),
        ];
        return this;
    }

    /**
     * Sets the file name of the javascript dependencies file
     *
     * @param {string} name
     * @return {Kaba}
     */
    setJavaScriptDependenciesName (name)
    {
        this.javaScriptDependenciesFileName = name;
        return this;
    }

    /**
     * Enables compilation of JS files via TypeScript
     *
     * @param {boolean} compileJs whether the compile all JS via TypeScript
     * @returns {Kaba}
     */
    enableTypeScript (compileJs = false)
    {
        this.typescript = {
            enabled: true,
            compileJs: compileJs,
        };

        return this;
    }

    /**
     * Disables chunk splitting
     *
     * @returns {Kaba}
     */
    disableChunkSplitting ()
    {
        this.splitChunks = false;
        return this;
    }

    /**
     * Disables chunk hashes in file names
     *
     * @returns {Kaba}
     */
    disableFileNameHashing ()
    {
        this.hashFileNames = false;
        return this;
    }

    /**
     * Returns the kaba config
     *
     * @internal
     * @private
     * @param {CliConfig} cliConfig
     * @return {KabaBuildConfig}
     */
    getBuildConfig (cliConfig)
    {
        return {
            sass: this.buildSassConfig(),
            js: {
                javaScriptDependenciesFileName: this.javaScriptDependenciesFileName,
                webpack: this.buildWebpackConfig(cliConfig),
                customTypeScriptConfig: this.customTypeScriptConfig,
            },
            cwd: this.cwd,
        };
    }

    /**
     * Builds the SCSS config
     *
     * @private
     * @return {SassBuildConfig}
     */
    buildSassConfig ()
    {
        return {
            entries: this.sassEntries,
            includePaths: this.sassIncludePaths,
            outputPath: path.join(this.outputPaths.base, this.outputPaths.css),
            browserList: this.browserList,
            cwd: this.cwd,
        };
    }

    /**
     * Builds the webpack config
     *
     * @private
     * @param {CliConfig} cliConfig
     * @return {webpack.Configuration}
     */
    buildWebpackConfig (cliConfig)
    {
        const babelLoader = {
            loader: "babel-loader?cacheDirectory",
            options: {
                babelrc: false,
                presets: [
                    kabaBabelPreset,
                ],
            },
        };

        /** @type {webpack.Configuration} config */
        const config = {
            // entry
            entry: this.jsEntries,

            // mode
            mode: cliConfig.isDebug() ? "development" : "production",

            // output
            output: {
                path: path.join(this.outputPaths.base, this.outputPaths.js),
                filename: this.hashFileNames ? '[name].[chunkhash].js' : '[name].js',
                publicPath: this.publicPath,
                pathinfo: !cliConfig.isDebug(),
            },

            // module
            module: {
                rules: [
                    // Babel + TS are added below

                    // content files
                    {
                        test: /\.(svg|txt)$/,
                        loader: "raw-loader",
                    },
                ],
            },

            // resolve
            resolve: {
                // TS is potentially added below
                extensions: [".js", ".jsx", ".json"],
            },

            // optimization
            optimization: {
                concatenateModules: this.moduleConcatenationEnabled,
                minimizer: [],
            },

            // performance

            // devtool
            devtool: false,

            // context
            context: this.cwd,

            // target
            target: "web",

            // externals
            externals: this.externals,

            // stats
            stats: {
                // hide children information (like from the ExtractTextPlugin)
                children: false,
            },

            // devServer

            // plugins
            plugins: this.plugins,

            // watch
            watch: cliConfig.isWatch(),

            // node
            // don't automatically polyfill certain node libraries
            // as we don't care about these implementations and they just add weight
            node: false,
        };

        if (this.splitChunks)
        {
            config.optimization.splitChunks = {
                chunks: "all",
                minChunks: 1,
                maxAsyncRequests: 5,
                maxInitialRequests: 3,
                name: true,
                cacheGroups: {
                    default: {
                        minChunks: 2,
                        priority: -20,
                        reuseExistingChunk: true,
                    },
                    vendors: {
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10,
                    },
                },
            };

            config.optimization.runtimeChunk = "single";
        }

        // add typescript support
        if (this.typescript.enabled)
        {
            // add extensions
            config.resolve.extensions.unshift(".tsx", ".ts");

            let customTypeScriptConfig = path.join(this.cwd, "tsconfig.json");

            if (fs.pathExistsSync(customTypeScriptConfig))
            {
                this.customTypeScriptConfig = customTypeScriptConfig;
            }

            // add TypeScript loader
            config.module.rules.unshift({
                test: this.typescript.compileJs ? /\.[jt]sx?$/ : /\.tsx?$/,
                use: [
                    // run TypeScript through Babel, so that ES5 compatibility is ensured
                    babelLoader,
                    {
                        loader: "ts-loader",
                        options: {
                            context: this.cwd,
                            configFile: this.customTypeScriptConfig || path.join(this.libRoot, "tsconfig.json"),
                            errorFormatter: (message, colors) => typeScriptErrorFormatter(message, colors, this.cwd),
                            compilerOptions: {
                                allowJs: this.useTypeScriptForJsFiles,
                                checkJs: this.useTypeScriptForJsFiles,
                            },
                        },
                    },
                ],
            });

            // add Babel, if JS is is not compiled via TS
            if (!this.typescript.compileJs)
            {
                config.module.rules.unshift({
                    test: /\.m?jsx?$/,
                    use: [babelLoader],
                });
            }
        }
        else
        {
            // add Babel
            config.module.rules.unshift({
                test: /\.m?jsx?$/,
                use: [babelLoader],
            });
        }

        if (!cliConfig.isDebug())
        {
            config.optimization.minimizer.push(new TerserPlugin({
                cache: true,
                parallel: true,
                sourceMap: cliConfig.includeSourceMaps(),
                extractComments: true,
                terserOptions: {
                    ecma: 5,
                },
            }));
        }

        if (cliConfig.includeSourceMaps())
        {
            config.devtool = cliConfig.isDebug()
                ? "cheap-module-source-map"
                : "hidden-source-map";
        }

        if (cliConfig.isLint() || cliConfig.isFix())
        {
            config.module.rules.push({
                enforce: "pre",
                test: /\.m?jsx?$/,
                exclude: /node_modules|tests|vendor/,
                loader: "eslint-loader",
                options: {
                    cache: true,
                    configFile: path.join(this.libRoot, ".eslintrc.yml"),
                    fix: cliConfig.isFix(),
                    parser: "babel-eslint",
                },
            });
        }

        if (cliConfig.isBundleAnalyzerEnabled())
        {
            try
            {
                const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
                config.plugins.push(new BundleAnalyzerPlugin());
            }
            catch (e)
            {
                console.log("");

                if (/Cannot find module 'webpack-bundle-analyzer'/.test(e.message))
                {
                    console.log(red("You need to manually install the analyzer plugin:"));
                    console.log(red("    npm i webpack-bundle-analyzer"));
                }
                else
                {
                    console.log(red(e.message));
                }

                process.exit(1);
            }

        }

        return config;
    }
}

module.exports = Kaba;
<commit_msg>Use faster source map type<commit_after>const CleanWebpackPlugin = require('clean-webpack-plugin');
const kabaBabelPreset = require("kaba-babel-preset");
const {blue, red, yellow} = require("kleur");
const fs = require("fs-extra");
const path = require("path");
const TerserPlugin = require('terser-webpack-plugin');
const typeScriptErrorFormatter = require("@becklyn/typescript-error-formatter");

/**
 * @typedef {{
 *      entries: Object<string, string>,
 *      includePaths: string[],
 *      outputPath: string,
 *      browserList: string[],
 *      cwd: string,
 * }} SassBuildConfig
 *
 * @typedef {{
 *      sass: SassBuildConfig,
 *      js: {
 *          javaScriptDependenciesFileName: string,
 *          webpack: webpack.Configuration,
 *          customTypeScriptConfig: string|null,
 *      },
 *      cwd: string,
 * }} KabaBuildConfig
 */

/**
 * Main Kaba class
 */
class Kaba
{
    /**
     *
     */
    constructor ()
    {
        /**
         * @private
         * @type {string}
         */
        this.cwd = process.cwd();

        /**
         * @private
         * @†ype {string}
         */
        this.libRoot = path.dirname(__dirname);

        /**
         * @private
         * @type {Object<string,string>}
         */
        this.jsEntries = {};

        /**
         * @private
         * @type {Object<string, string>}
         */
        this.sassEntries = {};

        /**
         * @private
         * @type {string}
         */
        this.sassIncludePaths = [];

        /**
         * @private
         * @type {{base : string, css : string, js : string}}
         */
        this.outputPaths = {};

        /**
         * @private
         * @type {string}
         */
        this.publicPath = "";

        /**
         * @private
         * @type {Object<string,string>}
         */
        this.externals = {};

        /**
         * @private
         * @type {string[]}
         */
        this.browserList = [];

        /**
         * @private
         * @type {boolean}
         */
        this.moduleConcatenationEnabled = false;

        /**
         * The webpack plugins
         *
         * @private
         * @type {Array}
         */
        this.plugins = [
            new CleanWebpackPlugin(),
        ];

        /**
         * @private
         * @type {string}
         */
        this.javaScriptDependenciesFileName = "_dependencies";

        /**
         * @private
         * @type {{enabled: boolean, compileJs: boolean}}
         */
        this.typescript = {
            enabled: false,
            compileJs: false,
        };

        /**
         * @private
         * @type {boolean}
         */
        this.splitChunks = true;

        /**
         * @private
         * @type {boolean}
         */
        this.hashFileNames = true;

        /**
         * @private
         * @type {?string}
         */
        this.customTypeScriptConfig = null;

        // set defaults
        this
            .setOutputPath("build")
            .setPublicPath("/assets/")
            .setBrowserList(["last 2 versions", "IE 11"])
        ;
    }

    /**
     * Adds an entry
     *
     * @deprecated
     * @param {Object<string,string>} mapping
     * @return {Kaba}
     */
    addEntries (mapping)
    {
        Object.keys(mapping).forEach(
            name =>
            {
                const source = mapping[name];
                const entry = {
                    [name]: source,
                };

                if (/\.scss$/.test(source))
                {
                    this.addSassEntries(entry);
                }
                else
                {
                    this.addJsEntries(entry);
                }
            }
        );

        return this;
    }

    /**
     * Adds JS entries
     *
     * @param {Object<string,string>} mapping
     * @return {Kaba}
     */
    addJavaScriptEntries (mapping)
    {
        this.addEntriesToList(mapping, this.jsEntries, "js");
        return this;
    }

    /**
     * Adds Sass entries
     *
     * @param {Object<string,string>} mapping
     * @return {Kaba}
     */
    addSassEntries (mapping)
    {
        try
        {
            require("kaba-scss");
        }
        catch (e)
        {
            console.log(`${red("ERROR")} It seems that ${yellow("kaba-scss")} is not installed. Install it with ${blue("npm install -D kaba-scss")}`);
            process.exit(1);
        }

        this.addEntriesToList(mapping, this.sassEntries, "sass");
        return this;
    }

    /**
     * Adds items from a mapping to list
     *
     * @private
     * @param {Object<string,string>} mapping
     * @param {Object<string,string>} list
     * @param {string} type
     */
    addEntriesToList (mapping, list, type)
    {
        Object.keys(mapping).forEach(
            name =>
            {
                let source = mapping[name];

                if (!/^[./]/.test(source))
                {
                    source = `./${source}`;
                }

                if (list[name] !== undefined)
                {
                    throw new Error(`Can't add ${type} entry named ${name}: an entry with this name already exists.`);
                }

                list[name] = source;
            }
        );
    }

    /**
     * Sets the output path
     *
     * @param {string} base
     * @param {string} cssSubDir
     * @param {string} jsSubDir
     * @return {Kaba}
     */
    setOutputPath (base, cssSubDir = "css", jsSubDir = "js")
    {
        this.outputPaths = {
            base: path.join(this.cwd, base),
            css: cssSubDir,
            js: jsSubDir,
        };
        return this;
    }

    /**
     * Sets the relative public path (for automatic imports)
     *
     * @param {string} publicPath relative to the cwd
     * @return {Kaba}
     */
    setPublicPath (publicPath)
    {
        this.publicPath = publicPath;
        return this;
    }

    /**
     *
     * @param {Object<string,string>} externals
     * @return {Kaba}
     */
    setExternals (externals)
    {
        this.externals = externals;
        return this;
    }

    /**
     * Sets the browser list
     *
     * @param {string[]} list
     * @return {Kaba}
     */
    setBrowserList (list)
    {
        this.browserList = list;
        return this;
    }

    /**
     * Disables module concatenation
     *
     * @return {Kaba}
     */
    disableModuleConcatenation ()
    {
        this.moduleConcatenationEnabled = false;
        return this;
    }

    /**
     * Enables module concatenation
     *
     * @return {Kaba}
     */
    enableModuleConcatenation ()
    {
        this.moduleConcatenationEnabled = true;
        return this;
    }

    /**
     * Adds `node_modules` to the include dir of sass.
     *
     * @return {Kaba}
     */
    enableSassNodeModulesIncludePaths ()
    {
        this.sassIncludePaths = [
            path.join(this.cwd, "node_modules"),
        ];
        return this;
    }

    /**
     * Sets the file name of the javascript dependencies file
     *
     * @param {string} name
     * @return {Kaba}
     */
    setJavaScriptDependenciesName (name)
    {
        this.javaScriptDependenciesFileName = name;
        return this;
    }

    /**
     * Enables compilation of JS files via TypeScript
     *
     * @param {boolean} compileJs whether the compile all JS via TypeScript
     * @returns {Kaba}
     */
    enableTypeScript (compileJs = false)
    {
        this.typescript = {
            enabled: true,
            compileJs: compileJs,
        };

        return this;
    }

    /**
     * Disables chunk splitting
     *
     * @returns {Kaba}
     */
    disableChunkSplitting ()
    {
        this.splitChunks = false;
        return this;
    }

    /**
     * Disables chunk hashes in file names
     *
     * @returns {Kaba}
     */
    disableFileNameHashing ()
    {
        this.hashFileNames = false;
        return this;
    }

    /**
     * Returns the kaba config
     *
     * @internal
     * @private
     * @param {CliConfig} cliConfig
     * @return {KabaBuildConfig}
     */
    getBuildConfig (cliConfig)
    {
        return {
            sass: this.buildSassConfig(),
            js: {
                javaScriptDependenciesFileName: this.javaScriptDependenciesFileName,
                webpack: this.buildWebpackConfig(cliConfig),
                customTypeScriptConfig: this.customTypeScriptConfig,
            },
            cwd: this.cwd,
        };
    }

    /**
     * Builds the SCSS config
     *
     * @private
     * @return {SassBuildConfig}
     */
    buildSassConfig ()
    {
        return {
            entries: this.sassEntries,
            includePaths: this.sassIncludePaths,
            outputPath: path.join(this.outputPaths.base, this.outputPaths.css),
            browserList: this.browserList,
            cwd: this.cwd,
        };
    }

    /**
     * Builds the webpack config
     *
     * @private
     * @param {CliConfig} cliConfig
     * @return {webpack.Configuration}
     */
    buildWebpackConfig (cliConfig)
    {
        const babelLoader = {
            loader: "babel-loader?cacheDirectory",
            options: {
                babelrc: false,
                presets: [
                    kabaBabelPreset,
                ],
            },
        };

        /** @type {webpack.Configuration} config */
        const config = {
            // entry
            entry: this.jsEntries,

            // mode
            mode: cliConfig.isDebug() ? "development" : "production",

            // output
            output: {
                path: path.join(this.outputPaths.base, this.outputPaths.js),
                filename: this.hashFileNames ? '[name].[chunkhash].js' : '[name].js',
                publicPath: this.publicPath,
                pathinfo: !cliConfig.isDebug(),
            },

            // module
            module: {
                rules: [
                    // Babel + TS are added below

                    // content files
                    {
                        test: /\.(svg|txt)$/,
                        loader: "raw-loader",
                    },
                ],
            },

            // resolve
            resolve: {
                // TS is potentially added below
                extensions: [".js", ".jsx", ".json"],
            },

            // optimization
            optimization: {
                concatenateModules: this.moduleConcatenationEnabled,
                minimizer: [],
            },

            // performance

            // devtool
            devtool: false,

            // context
            context: this.cwd,

            // target
            target: "web",

            // externals
            externals: this.externals,

            // stats
            stats: {
                // hide children information (like from the ExtractTextPlugin)
                children: false,
            },

            // devServer

            // plugins
            plugins: this.plugins,

            // watch
            watch: cliConfig.isWatch(),

            // node
            // don't automatically polyfill certain node libraries
            // as we don't care about these implementations and they just add weight
            node: false,
        };

        if (this.splitChunks)
        {
            config.optimization.splitChunks = {
                chunks: "all",
                minChunks: 1,
                maxAsyncRequests: 5,
                maxInitialRequests: 3,
                name: true,
                cacheGroups: {
                    default: {
                        minChunks: 2,
                        priority: -20,
                        reuseExistingChunk: true,
                    },
                    vendors: {
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10,
                    },
                },
            };

            config.optimization.runtimeChunk = "single";
        }

        // add typescript support
        if (this.typescript.enabled)
        {
            // add extensions
            config.resolve.extensions.unshift(".tsx", ".ts");

            let customTypeScriptConfig = path.join(this.cwd, "tsconfig.json");

            if (fs.pathExistsSync(customTypeScriptConfig))
            {
                this.customTypeScriptConfig = customTypeScriptConfig;
            }

            // add TypeScript loader
            config.module.rules.unshift({
                test: this.typescript.compileJs ? /\.[jt]sx?$/ : /\.tsx?$/,
                use: [
                    // run TypeScript through Babel, so that ES5 compatibility is ensured
                    babelLoader,
                    {
                        loader: "ts-loader",
                        options: {
                            context: this.cwd,
                            configFile: this.customTypeScriptConfig || path.join(this.libRoot, "tsconfig.json"),
                            errorFormatter: (message, colors) => typeScriptErrorFormatter(message, colors, this.cwd),
                            compilerOptions: {
                                allowJs: this.useTypeScriptForJsFiles,
                                checkJs: this.useTypeScriptForJsFiles,
                            },
                        },
                    },
                ],
            });

            // add Babel, if JS is is not compiled via TS
            if (!this.typescript.compileJs)
            {
                config.module.rules.unshift({
                    test: /\.m?jsx?$/,
                    use: [babelLoader],
                });
            }
        }
        else
        {
            // add Babel
            config.module.rules.unshift({
                test: /\.m?jsx?$/,
                use: [babelLoader],
            });
        }

        if (!cliConfig.isDebug())
        {
            config.optimization.minimizer.push(new TerserPlugin({
                cache: true,
                parallel: true,
                sourceMap: cliConfig.includeSourceMaps(),
                extractComments: true,
                terserOptions: {
                    ecma: 5,
                },
            }));
        }

        if (cliConfig.includeSourceMaps())
        {
            config.devtool = cliConfig.isDebug()
                ? "cheap-module-eval-source-map"
                : "hidden-source-map";
        }

        if (cliConfig.isLint() || cliConfig.isFix())
        {
            config.module.rules.push({
                enforce: "pre",
                test: /\.m?jsx?$/,
                exclude: /node_modules|tests|vendor/,
                loader: "eslint-loader",
                options: {
                    cache: true,
                    configFile: path.join(this.libRoot, ".eslintrc.yml"),
                    fix: cliConfig.isFix(),
                    parser: "babel-eslint",
                },
            });
        }

        if (cliConfig.isBundleAnalyzerEnabled())
        {
            try
            {
                const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
                config.plugins.push(new BundleAnalyzerPlugin());
            }
            catch (e)
            {
                console.log("");

                if (/Cannot find module 'webpack-bundle-analyzer'/.test(e.message))
                {
                    console.log(red("You need to manually install the analyzer plugin:"));
                    console.log(red("    npm i webpack-bundle-analyzer"));
                }
                else
                {
                    console.log(red(e.message));
                }

                process.exit(1);
            }

        }

        return config;
    }
}

module.exports = Kaba;
<|endoftext|>


<commit_before>let React = require("react")
let {div} = React.DOM

export default class NestedFieldset extends React.Component {
  static propTypes = {
    form: React.PropTypes.func.isRequired,
    // Provided by the parent Frig Form's HOC props closure
    data: React.PropTypes.object.isRequired,
    theme: React.PropTypes.object.isRequired,
    typeMapping: React.PropTypes.objectOf(React.PropTypes.string),
  }

  state = {
    invalidForms: [],
  }

  componentWillMount() {
    this.props.onComponentMount(this)
  }

  componentWillUnmount() {
    this.props.onComponentUnmount(this)
  }

  componentWillReceiveProps(nextProps) {
    // Truncating the invalid forms list to prevent ghosting of invalid
    // forms that are removed in the props.
    let invalidForms = this.state.invalidForms
    let numberOfForms = this._dataValues(nextProps).length
    invalidForms = invalidForms.slice(0, numberOfForms)
    this.setState({invalidForms})
  }

  validate() {
    return this._everyForm("validate")
  }

  isValid() {
    return this._everyForm("isValid")
  }

  isModified() {
    return this._everyForm("isModified")
  }

  resetModified() {
    this._everyForm("resetModified")
  }

  reset() {
    this._everyForm("reset")
  }

  // Returns true if calling the function returns true for every child form
  _everyForm(fnName) {
    let forms = []
    for (let form of this.refs) forms.push(form)
    return forms.filter((c) => !c[fnName]()).length === 0
  }

  _formProps({data, key}) {
    return Object.assign({}, this.props, {
      key,
      ref: key,
      form: (f) => this.props.form(f, key),
      nestedForm: true,
      data: {
        value: data,
        requestChange: this._onFormRequestChange.bind(this, key),
      },
    })
  }

  _onFormRequestChange(key, formData, valid) {
    let data = this.props.data.value
    // Combine the updated data from the form with the other forms or if this
    // is a single form fieldset overwriting the previous form data.
    if (Array.isArray(data)) {
      data = data.slice()
      data[key] = formData
    }
    else {
      data = formData
    }
    // Combine this valid flag with the other nested form valid flags and relay
    // a valid state to the upstream only if all nested forms are valid
    let invalidForms = this.state.invalidForms
    if (valid) {
      invalidForms[key] = true
    }
    else {
      delete invalidForms[key]
    }
    valid = invalidForms.filter((invalid) => invalid === true).length === 0
    this.setState({invalidForms})
    // Relaying the request change to the upstream data
    this.props.data.requestChange(data, valid)
  }

  _dataValues(nextProps = this.props) {
    let dataValues = nextProps.data.value
    return Array.isArray(dataValues) ? dataValues : [dataValues]
  }

  _renderForm(formProps) {
    let component = require("./form.js")
    return React.createElement(component, formProps)
  }

  render() {
    let i = 0
    let datas = this._dataValues()
    return div({},
      datas.map((data) => this._renderForm(this._formProps({data, key: i++})))
    )
  }

}
<commit_msg>Fixing nested fields for null data and isModified before render<commit_after>let React = require("react")
let {div} = React.DOM

export default class NestedFieldset extends React.Component {
  static propTypes = {
    form: React.PropTypes.func.isRequired,
    // Provided by the parent Frig Form's HOC props closure
    data: React.PropTypes.object.isRequired,
    theme: React.PropTypes.object.isRequired,
    typeMapping: React.PropTypes.objectOf(React.PropTypes.string),
  }

  state = {
    invalidForms: [],
  }

  componentWillMount() {
    this.props.onComponentMount(this)
  }

  componentWillUnmount() {
    this.props.onComponentUnmount(this)
  }

  componentWillReceiveProps(nextProps) {
    // Truncating the invalid forms list to prevent ghosting of invalid
    // forms that are removed in the props.
    let invalidForms = this.state.invalidForms
    let numberOfForms = this._dataValues(nextProps).length
    invalidForms = invalidForms.slice(0, numberOfForms)
    this.setState({invalidForms})
  }

  validate() {
    return this._everyForm("validate")
  }

  isValid() {
    return this._everyForm("isValid")
  }

  isModified() {
    return this._everyForm("isModified")
  }

  resetModified() {
    this._everyForm("resetModified")
  }

  reset() {
    this._everyForm("reset")
  }

  // Returns true if calling the function returns true for every child form
  _everyForm(fnName) {
    let forms = []
    for (let k in this.refs||{}) forms.push(this.refs[k])
    return forms.filter((c) => !c[fnName]()).length === 0
  }

  _formProps({data, key}) {
    return Object.assign({}, this.props, {
      key,
      ref: key,
      form: (f) => this.props.form(f, key),
      nestedForm: true,
      data: {
        value: data,
        requestChange: this._onFormRequestChange.bind(this, key),
      },
    })
  }

  _onFormRequestChange(key, formData, valid) {
    let data = this.props.data.value
    // Combine the updated data from the form with the other forms or if this
    // is a single form fieldset overwriting the previous form data.
    if (Array.isArray(data)) {
      data = data.slice()
      data[key] = formData
    }
    else {
      data = formData
    }
    // Combine this valid flag with the other nested form valid flags and relay
    // a valid state to the upstream only if all nested forms are valid
    let invalidForms = this.state.invalidForms
    if (valid) {
      invalidForms[key] = true
    }
    else {
      delete invalidForms[key]
    }
    valid = invalidForms.filter((invalid) => invalid === true).length === 0
    this.setState({invalidForms})
    // Relaying the request change to the upstream data
    this.props.data.requestChange(data, valid)
  }

  _dataValues(nextProps = this.props) {
    let dataValues = nextProps.data.value || []
    return Array.isArray(dataValues) ? dataValues : [dataValues]
  }

  _renderForm(formProps) {
    let component = require("./form.js")
    return React.createElement(component, formProps)
  }

  render() {
    let i = 0
    let datas = this._dataValues()
    return div({},
      datas.map((data) => this._renderForm(this._formProps({data, key: i++})))
    )
  }

}
<|endoftext|>


<commit_before>/**
 * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of fiware-tidoop (FI-WARE project).
 *
 * fiware-tidoop 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.
 * fiware-tidoop 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 fiware-tidoop. If not, see
 * http://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License please contact with
 * francisco dot romerobueno at telefonica dot com
 */

/**
 * cosmos-gui main app
 *
 * Author: frb
 */

// Module dependencies
var express = require('express');
var https = require('https');
var fs = require('fs');
var boom = require('boom');
var stylus = require('stylus');
var nib = require('nib');
var config = require('../conf/cosmos-gui.json');
var mysqlDriver = require('./mysql_driver.js');
var OAuth2 = require('./oauth2').OAuth2;
var logger = require('./logger.js');
var appUtils = require('./app_utils.js');
var constants = require('constants');

// Global variables
var port = config.gui.port;
var client_id = config.oauth2.client_id;
var client_secret = config.oauth2.client_secret;
var idmURL = config.oauth2.idmURL;
var response_type = config.oauth2.response_type;
var callbackURL = config.oauth2.callbackURL;
var hdfsQuota = config.hdfs.quota;
var hdfsSuperuser = config.hdfs.superuser;
var scPrivKey = config.clusters.storage.private_key;
var scUser = config.clusters.storage.user;
var scEndpoint = config.clusters.storage.endpoint;
var ccPrivKey = config.clusters.computing.private_key;
var ccUser = config.clusters.computing.user;
var ccEndpoint = config.clusters.computing.endpoint;
var httpsOptions = {
    secureOptions: constants.SSL_OP_NO_SSLv3,
    key: fs.readFileSync(config.gui.private_key_file),
    cert: fs.readFileSync(config.gui.certificate_file)
}

// Express configuration
var app = express();

app.set('views', __dirname + '/../views');
app.set('view engine', 'jade');
app.use(express.logger());
app.use(stylus.middleware(
    { src: __dirname + '/../public',
        compile: compile
    }
));
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({secret: "skjghskdjfhbqigohqdiouk"}));
app.configure(function () {
    "use strict";
    app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
    app.use(express.static(__dirname + '/../public'));
});

function compile(str, path) {
    return stylus(str)
        .set('filename', path)
        .use(nib());
}

// Creates oauth library object with the config data
var oa = new OAuth2(client_id,
    client_secret,
    idmURL,
    '/oauth2/authorize',
    '/oauth2/token',
    callbackURL);

// Handles requests to the main page
app.get('/', function (req, res) {
    var access_token = req.session.access_token;

    // Check if the user had a session
    if (access_token) {
        // Get user information given its access token
        oa.get(idmURL + '/user/', access_token, function (error, response) {
            if (error) {
                var boomError = boom.badData('There was some error when getting user information from the IdM', error);
                logger.error('There was some error when getting user information from the IdM', error);
                res.status(boomError.output.statusCode).send(boomError.output.payload.message);
            } else {
                // Get the user's IdM email (username)
                var idm_username = JSON.parse(response).email;
                req.session.idm_username = idm_username;

                // Check if the user, given its IdM username, has a Cosmos account
                mysqlDriver.getUser(idm_username, function(error, result) {
                    if (error) {
                        var boomError = boom.badData('There was some error when getting user information from the ' + 'database', error);
                        logger.error('There was some error when getting user information from the ' + 'database', error);
                        res.status(boomError.output.statusCode).send(boomError.output.payload.message);
                    } else if (result[0]) {
                        req.session.username = result[0].username;

                        if (result[0].password) {
                            res.render('dashboard'); // both old and new Cosmos users with password
                        } else {
                            res.render('new_password'); // old Cosmos users not having a password
                        } // if else
                    } else {
                        res.render('new_account'); // new Cosmos users not having a username
                    } // if else
                });
            } // if else
        });
    } else {
        res.render('login');
    } // if else
});

// Redirection to IDM authentication portal
app.get('/login', function(req, res) {
    var path = oa.getAuthorizeUrl(response_type);
    res.redirect(path);
});

// Handles requests from IDM with the access code
app.get('/auth', function(req, res) {
    // Using the access code goes again to the IDM to obtain the access_token
    oa.getOAuthAccessToken(req.query.code, function (e, results) {
        if ('access_token' in results) {
            // Stores the access_token in a session cookie
            req.session.access_token = results.access_token;
        } // if

        res.redirect('/');
    });
});

app.post('/new_account', function(req, res) {
    var idm_username = req.session.idm_username;

    appUtils.buildUsername(idm_username.split('@')[0], 0, function(username) {
        var password1 = req.body.password1;
        var password2 = req.body.password2;

        if ((password1 === password2) && (username != null)) {
            mysqlDriver.addUser(idm_username, username, password1, hdfsQuota, function(error, result) {
                if (error) {
                    var boomError = boom.badData('There was some error when adding information in the database for user '+ username, error);
                    logger.error('There was some error when adding information in the database for user '+ username);
                    res.status(boomError.output.statusCode).send(boomError.output.payload.message);
                } else {
                    logger.info('Successful information added to the database for user ' + username);

                    if (scEndpoint === ccEndpoint) {
                        // Just one provision step instead of two
                        appUtils.provisionCluster(res, scPrivKey, scUser, scEndpoint, hdfsSuperuser, hdfsQuota, username, password1);
                    } else {
                        // Two different provision steps
                        appUtils.provisionCluster(res, scPrivKey, scUser, scEndpoint, hdfsSuperuser, hdfsQuota, username, password1);
                        appUtils.provisionCluster(res, ccPrivKey, ccUser, ccEndpoint, hdfsSuperuser, hdfsQuota, username, password1);
                    } // if else
                } // if else
            });
        } else {
            res.redirect('/');
        } // if else
    });
});

app.post('/new_password', function(req, res) {
    var idm_username = req.session.idm_username;
    var username = req.username;
    var password1 = req.body.password1;
    var password2 = req.body.password2;

    if (password1 === password2) {
        mysqlDriver.addPassword(idm_username, password1, function(error, result) {
            if (error) {
                var boomError = boom.badData('There was an error while setting up the password for user ' + username, error);
                logger.error('There was an error while setting up the password for user ' + username, error);
                res.status(boomError.output.statusCode).send(boomError.output.payload.message);
            } else {
                res.redirect('/');
            } // if else
        })
    } else {
        res.redirect('/');
    } // if else
});

app.get('/dashboard', function(req, res) {
    res.render('dashboard');
});

app.get('/profile', function(req, res) {
    var idm_username = req.session.idm_username;

    mysqlDriver.getUser(idm_username, function(error, result) {
        if (error) {
            var boomError = boom.badData('There was an error while retrieving profile for user ' + idm_username, error);
            logger.error('There was an error while retrieving profile for user ' + idm_username, error);
            res.status(boomError.output.statusCode).send(boomError.output.payload.message);
        } else {
            res.render('profile', { "results": result });
        } // if else
    })
});

// Handles logout requests to remove access_token from the session cookie
app.get('/logout', function(req, res){
    req.session.access_token = undefined;
    res.redirect('/');
});

// Start the application, listening at the configured port
logger.info("cosmos-gui running at https://localhost:" + port);
https.createServer(httpsOptions, app).listen(port);
<commit_msg>FIX the access_token check<commit_after>/**
 * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of fiware-tidoop (FI-WARE project).
 *
 * fiware-tidoop 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.
 * fiware-tidoop 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 fiware-tidoop. If not, see
 * http://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License please contact with
 * francisco dot romerobueno at telefonica dot com
 */

/**
 * cosmos-gui main app
 *
 * Author: frb
 */

// Module dependencies
var express = require('express');
var https = require('https');
var fs = require('fs');
var boom = require('boom');
var stylus = require('stylus');
var nib = require('nib');
var config = require('../conf/cosmos-gui.json');
var mysqlDriver = require('./mysql_driver.js');
var OAuth2 = require('./oauth2').OAuth2;
var logger = require('./logger.js');
var appUtils = require('./app_utils.js');
var constants = require('constants');

// Global variables
var port = config.gui.port;
var client_id = config.oauth2.client_id;
var client_secret = config.oauth2.client_secret;
var idmURL = config.oauth2.idmURL;
var response_type = config.oauth2.response_type;
var callbackURL = config.oauth2.callbackURL;
var hdfsQuota = config.hdfs.quota;
var hdfsSuperuser = config.hdfs.superuser;
var scPrivKey = config.clusters.storage.private_key;
var scUser = config.clusters.storage.user;
var scEndpoint = config.clusters.storage.endpoint;
var ccPrivKey = config.clusters.computing.private_key;
var ccUser = config.clusters.computing.user;
var ccEndpoint = config.clusters.computing.endpoint;
var httpsOptions = {
    secureOptions: constants.SSL_OP_NO_SSLv3,
    key: fs.readFileSync(config.gui.private_key_file),
    cert: fs.readFileSync(config.gui.certificate_file)
}

// Express configuration
var app = express();

app.set('views', __dirname + '/../views');
app.set('view engine', 'jade');
app.use(express.logger());
app.use(stylus.middleware(
    { src: __dirname + '/../public',
        compile: compile
    }
));
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({secret: "skjghskdjfhbqigohqdiouk"}));
app.configure(function () {
    "use strict";
    app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
    app.use(express.static(__dirname + '/../public'));
});

function compile(str, path) {
    return stylus(str)
        .set('filename', path)
        .use(nib());
}

// Creates oauth library object with the config data
var oa = new OAuth2(client_id,
    client_secret,
    idmURL,
    '/oauth2/authorize',
    '/oauth2/token',
    callbackURL);

// Handles requests to the main page
app.get('/', function (req, res) {
    var access_token = req.session.access_token;

    // Check if the user had a session
    if (access_token) {
        // Get user information given its access token
        oa.get(idmURL + '/user/', access_token, function (error, response) {
            if (error) {
                var boomError = boom.badData('There was some error when getting user information from the IdM', error);
                logger.error('There was some error when getting user information from the IdM', error);
                res.status(boomError.output.statusCode).send(boomError.output.payload.message);
            } else {
                // Get the user's IdM email (username)
                var idm_username = JSON.parse(response).email;
                req.session.idm_username = idm_username;

                // Check if the user, given its IdM username, has a Cosmos account
                mysqlDriver.getUser(idm_username, function(error, result) {
                    if (error) {
                        var boomError = boom.badData('There was some error when getting user information from the ' + 'database', error);
                        logger.error('There was some error when getting user information from the ' + 'database', error);
                        res.status(boomError.output.statusCode).send(boomError.output.payload.message);
                    } else if (result[0]) {
                        req.session.username = result[0].username;

                        if (result[0].password) {
                            res.render('dashboard'); // both old and new Cosmos users with password
                        } else {
                            res.render('new_password'); // old Cosmos users not having a password
                        } // if else
                    } else {
                        res.render('new_account'); // new Cosmos users not having a username
                    } // if else
                });
            } // if else
        });
    } else {
        res.render('login');
    } // if else
});

// Redirection to IDM authentication portal
app.get('/login', function(req, res) {
    var path = oa.getAuthorizeUrl(response_type);
    res.redirect(path);
});

// Handles requests from IDM with the access code
app.get('/auth', function(req, res) {
    // Using the access code goes again to the IDM to obtain the access_token
    oa.getOAuthAccessToken(req.query.code, function (e, results) {
        if (results) {
            if ('access_token' in results) {
                // Stores the access_token in a session cookie
                req.session.access_token = results.access_token;
            } // if
        } // if

        res.redirect('/');
    });
});

app.post('/new_account', function(req, res) {
    var idm_username = req.session.idm_username;

    appUtils.buildUsername(idm_username.split('@')[0], 0, function(username) {
        var password1 = req.body.password1;
        var password2 = req.body.password2;

        if ((password1 === password2) && (username != null)) {
            mysqlDriver.addUser(idm_username, username, password1, hdfsQuota, function(error, result) {
                if (error) {
                    var boomError = boom.badData('There was some error when adding information in the database for user '+ username, error);
                    logger.error('There was some error when adding information in the database for user '+ username);
                    res.status(boomError.output.statusCode).send(boomError.output.payload.message);
                } else {
                    logger.info('Successful information added to the database for user ' + username);

                    if (scEndpoint === ccEndpoint) {
                        // Just one provision step instead of two
                        appUtils.provisionCluster(res, scPrivKey, scUser, scEndpoint, hdfsSuperuser, hdfsQuota, username, password1);
                    } else {
                        // Two different provision steps
                        appUtils.provisionCluster(res, scPrivKey, scUser, scEndpoint, hdfsSuperuser, hdfsQuota, username, password1);
                        appUtils.provisionCluster(res, ccPrivKey, ccUser, ccEndpoint, hdfsSuperuser, hdfsQuota, username, password1);
                    } // if else
                } // if else
            });
        } else {
            res.redirect('/');
        } // if else
    });
});

app.post('/new_password', function(req, res) {
    var idm_username = req.session.idm_username;
    var username = req.username;
    var password1 = req.body.password1;
    var password2 = req.body.password2;

    if (password1 === password2) {
        mysqlDriver.addPassword(idm_username, password1, function(error, result) {
            if (error) {
                var boomError = boom.badData('There was an error while setting up the password for user ' + username, error);
                logger.error('There was an error while setting up the password for user ' + username, error);
                res.status(boomError.output.statusCode).send(boomError.output.payload.message);
            } else {
                res.redirect('/');
            } // if else
        })
    } else {
        res.redirect('/');
    } // if else
});

app.get('/dashboard', function(req, res) {
    res.render('dashboard');
});

app.get('/profile', function(req, res) {
    var idm_username = req.session.idm_username;

    mysqlDriver.getUser(idm_username, function(error, result) {
        if (error) {
            var boomError = boom.badData('There was an error while retrieving profile for user ' + idm_username, error);
            logger.error('There was an error while retrieving profile for user ' + idm_username, error);
            res.status(boomError.output.statusCode).send(boomError.output.payload.message);
        } else {
            res.render('profile', { "results": result });
        } // if else
    })
});

// Handles logout requests to remove access_token from the session cookie
app.get('/logout', function(req, res){
    req.session.access_token = undefined;
    res.redirect('/');
});

// Start the application, listening at the configured port
logger.info("cosmos-gui running at https://localhost:" + port);
https.createServer(httpsOptions, app).listen(port);
<|endoftext|>


<commit_before>/**
 * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dashToCamelCase} from './string';

/**
 * Waits until the child element is constructed. Once the child is found, the
 * callback is executed.
 * @param {!Element} parent
 * @param {function(!Element):boolean} checkFunc
 * @param {function()} callback
 */
export function waitForChild(parent, checkFunc, callback) {
  if (checkFunc(parent)) {
    callback();
    return;
  }
  const win = parent.ownerDocument.defaultView;
  if (win.MutationObserver) {
    const observer = new win.MutationObserver(() => {
      if (checkFunc(parent)) {
        observer.disconnect();
        callback();
      }
    });
    observer.observe(parent, {childList: true});
  } else {
    const interval = win.setInterval(() => {
      if (checkFunc(parent)) {
        win.clearInterval(interval);
        callback();
      }
    }, /* milliseconds */ 5);
  }
}

/**
 * Waits for document's body to be available.
 * @param {!Document} doc
 * @param {function()} callback
 */
export function waitForBody(doc, callback) {
  waitForChild(doc.documentElement, () => !!doc.body, callback);
}

/**
 * Waits for document's body to be available.
 * @param {!Document} doc
 * @return {!Promise}
 */
export function waitForBodyPromise(doc) {
  return new Promise(resolve => {
    waitForBody(doc, resolve);
  });
}

/**
 * Whether the element is currently contained in the DOM. Polyfills
 * `document.contains()` method when necessary. Notice that according to spec
 * `document.contains` is inclusionary.
 * See https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
 * @param {!Document} doc
 * @param {!Element} element
 * @return {boolean}
 */
export function documentContains(doc, element) {
  if (!doc.contains) {
    return documentContainsPolyfillInternal_(doc, element);
  }
  return doc.contains(element);
}

/**
 * Polyfill for `document.contains()` method.
 * See https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
 * @param {!Document} doc
 * @param {!Element} element
 * @return {boolean}
 * @private Visible for testing only.
 */
export function documentContainsPolyfillInternal_(doc, element) {
  // Per spec, "contains" method is inclusionary
  // i.e. `node.contains(node) == true`. However, we still need to test
  // equality to the document itself.
  return element == doc || doc.documentElement.contains(element);
}

/**
 * Removes the element.
 * @param {!Element} element
 */
export function removeElement(element) {
  if (element.parentElement) {
    element.parentElement.removeChild(element);
  }
}

/**
 * Removes all child nodes of the specified element.
 * @param {!Element} parent
 */
export function removeChildren(parent) {
  while (parent.firstChild) {
    parent.removeChild(parent.firstChild);
  }
}

/**
 * Copies all children nodes of element "from" to element "to". Child nodes
 * are deeply cloned. Notice, that this method should be used with care and
 * preferably on smaller subtrees.
 * @param {!Element} from
 * @param {!Element} to
 */
export function copyChildren(from, to) {
  const frag = to.ownerDocument.createDocumentFragment();
  for (let n = from.firstChild; n; n = n.nextSibling) {
    frag.appendChild(n.cloneNode(true));
  }
  to.appendChild(frag);
}

/**
 * Finds the closest element that satisfies the callback from this element
 * up the DOM subtree.
 * @param {!Element} element
 * @param {function(!Element):boolean} callback
 * @return {?Element}
 */
export function closest(element, callback) {
  for (let el = element; el; el = el.parentElement) {
    if (callback(el)) {
      return el;
    }
  }
  return null;
}

/**
 * Finds the closest element with the specified name from this element
 * up the DOM subtree.
 * @param {!Element} element
 * @param {string} tagName
 * @return {?Element}
 */
export function closestByTag(element, tagName) {
  if (element.closest) {
    return element.closest(tagName);
  }
  tagName = tagName.toUpperCase();
  return closest(element, el => {
    return el.tagName == tagName;
  });
}

/**
 * Finds the first descendant element with the specified name.
 * @param {!Element} element
 * @param {string} tagName
 * @return {?Element}
 */
export function elementByTag(element, tagName) {
  const elements = element.getElementsByTagName(tagName);
  return elements[0] || null;
}

/**
 * Finds the first child element that satisfies the callback.
 * @param {!Element} parent
 * @param {function(!Element):boolean} callback
 * @return {?Element}
 */
export function childElement(parent, callback) {
  for (let child = parent.firstElementChild; child;
      child = child.nextElementSibling) {
    if (callback(child)) {
      return child;
    }
  }
  return null;
}

/**
 * Finds all child elements that satisfies the callback.
 * @param {!Element} parent
 * @param {function(!Element):boolean} callback
 * @return {!Array.<!Element>}
 */
export function childElements(parent, callback) {
  const children = [];
  for (let child = parent.firstElementChild; child;
       child = child.nextElementSibling) {
    if (callback(child)) {
      children.push(child);
    }
  }
  return children;
}

/**
 * Finds the last child element that satisfies the callback.
 * @param {!Element} parent
 * @param {function(!Element):boolean} callback
 * @return {?Element}
 */
export function lastChildElement(parent, callback) {
  for (let child = parent.lastElementChild; child;
       child = child.previousElementSibling) {
    if (callback(child)) {
      return child;
    }
  }
  return null;
}

/**
 * Finds all child nodes that satisfies the callback.
 * These nodes can include Text, Comment and other child nodes.
 * @param {!Node} parent
 * @param {function(!Node):boolean} callback
 * @return {!Array<!Node>}
 */
export function childNodes(parent, callback) {
  const nodes = [];
  for (let child = parent.firstChild; child;
       child = child.nextSibling) {
    if (callback(child)) {
      nodes.push(child);
    }
  }
  return nodes;
}

/**
 * @type {boolean|undefined}
 * @visiblefortesting
 */
let scopeSelectorSupported;

/**
 * @param {boolean|undefined} val
 * @visiblefortesting
 */
export function setScopeSelectorSupportedForTesting(val) {
  scopeSelectorSupported = val;
}

/**
 * @param {!Element} parent
 * @return {boolean}
 */
function isScopeSelectorSupported(parent) {
  try {
    parent.ownerDocument.querySelector(':scope');
    return true;
  } catch (e) {
    return false;
  }
}

/**
 * Finds the first child element that has the specified attribute.
 * @param {!Element} parent
 * @param {string} attr
 * @return {?Element}
 */
export function childElementByAttr(parent, attr) {
  if (scopeSelectorSupported == null) {
    scopeSelectorSupported = isScopeSelectorSupported(parent);
  }
  if (scopeSelectorSupported) {
    return parent.querySelector(':scope > [' + attr + ']');
  }
  return childElement(parent, el => {
    return el.hasAttribute(attr);
  });
}

/**
 * Finds the last child element that has the specified attribute.
 * @param {!Element} parent
 * @param {string} attr
 * @return {?Element}
 */
export function lastChildElementByAttr(parent, attr) {
  return lastChildElement(parent, el => {
    return el.hasAttribute(attr);
  });
}

/**
 * Finds all child elements that has the specified attribute.
 * @param {!Element} parent
 * @param {string} attr
 * @return {!Array.<!Element>}
 */
export function childElementsByAttr(parent, attr) {
  if (scopeSelectorSupported == null) {
    scopeSelectorSupported = isScopeSelectorSupported(parent);
  }
  if (scopeSelectorSupported) {
    const nodeList = parent.querySelectorAll(':scope > [' + attr + ']');
    // Convert NodeList into Array.<Element>.
    const children = [];
    for (let i = 0; i < nodeList.length; i++) {
      children.push(nodeList[i]);
    }
    return children;
  }
  return childElements(parent, el => {
    return el.hasAttribute(attr);
  });
}

/**
 * Finds the first child element that has the specified tag name.
 * @param {!Element} parent
 * @param {string} tagName
 * @return {?Element}
 */
export function childElementByTag(parent, tagName) {
  if (scopeSelectorSupported == null) {
    scopeSelectorSupported = isScopeSelectorSupported(parent);
  }
  if (scopeSelectorSupported) {
    return parent.querySelector(':scope > ' + tagName);
  }
  tagName = tagName.toUpperCase();
  return childElement(parent, el => {
    return el.tagName == tagName;
  });
}

/**
 * Returns element data-param- attributes as url parameters key-value pairs.
 * e.g. data-param-some-attr=value -> {someAttr: value}.
 * @param {!Element} element
 * @param {function(string):string} opt_computeParamNameFunc to compute the parameter
 *    name, get passed the camel-case parameter name.
 * @return {!Object<string, string>}
 */
export function getDataParamsFromAttributes(element, opt_computeParamNameFunc) {
  const computeParamNameFunc = opt_computeParamNameFunc || (key => key);
  const attributes = element.attributes;
  const params = Object.create(null);
  for (let i = 0; i < attributes.length; i++) {
    const attr = attributes[i];
    const matches = attr.nodeName.match(/^data-param-(.+)/);
    if (matches) {
      const param = dashToCamelCase(matches[1]);
      params[computeParamNameFunc(param)] = attr.nodeValue;
    }
  }
  return params;
}
<commit_msg>Use Attr#name and Attr#value (#3189)<commit_after>/**
 * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dashToCamelCase} from './string';

/**
 * Waits until the child element is constructed. Once the child is found, the
 * callback is executed.
 * @param {!Element} parent
 * @param {function(!Element):boolean} checkFunc
 * @param {function()} callback
 */
export function waitForChild(parent, checkFunc, callback) {
  if (checkFunc(parent)) {
    callback();
    return;
  }
  const win = parent.ownerDocument.defaultView;
  if (win.MutationObserver) {
    const observer = new win.MutationObserver(() => {
      if (checkFunc(parent)) {
        observer.disconnect();
        callback();
      }
    });
    observer.observe(parent, {childList: true});
  } else {
    const interval = win.setInterval(() => {
      if (checkFunc(parent)) {
        win.clearInterval(interval);
        callback();
      }
    }, /* milliseconds */ 5);
  }
}

/**
 * Waits for document's body to be available.
 * @param {!Document} doc
 * @param {function()} callback
 */
export function waitForBody(doc, callback) {
  waitForChild(doc.documentElement, () => !!doc.body, callback);
}

/**
 * Waits for document's body to be available.
 * @param {!Document} doc
 * @return {!Promise}
 */
export function waitForBodyPromise(doc) {
  return new Promise(resolve => {
    waitForBody(doc, resolve);
  });
}

/**
 * Whether the element is currently contained in the DOM. Polyfills
 * `document.contains()` method when necessary. Notice that according to spec
 * `document.contains` is inclusionary.
 * See https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
 * @param {!Document} doc
 * @param {!Element} element
 * @return {boolean}
 */
export function documentContains(doc, element) {
  if (!doc.contains) {
    return documentContainsPolyfillInternal_(doc, element);
  }
  return doc.contains(element);
}

/**
 * Polyfill for `document.contains()` method.
 * See https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
 * @param {!Document} doc
 * @param {!Element} element
 * @return {boolean}
 * @private Visible for testing only.
 */
export function documentContainsPolyfillInternal_(doc, element) {
  // Per spec, "contains" method is inclusionary
  // i.e. `node.contains(node) == true`. However, we still need to test
  // equality to the document itself.
  return element == doc || doc.documentElement.contains(element);
}

/**
 * Removes the element.
 * @param {!Element} element
 */
export function removeElement(element) {
  if (element.parentElement) {
    element.parentElement.removeChild(element);
  }
}

/**
 * Removes all child nodes of the specified element.
 * @param {!Element} parent
 */
export function removeChildren(parent) {
  while (parent.firstChild) {
    parent.removeChild(parent.firstChild);
  }
}

/**
 * Copies all children nodes of element "from" to element "to". Child nodes
 * are deeply cloned. Notice, that this method should be used with care and
 * preferably on smaller subtrees.
 * @param {!Element} from
 * @param {!Element} to
 */
export function copyChildren(from, to) {
  const frag = to.ownerDocument.createDocumentFragment();
  for (let n = from.firstChild; n; n = n.nextSibling) {
    frag.appendChild(n.cloneNode(true));
  }
  to.appendChild(frag);
}

/**
 * Finds the closest element that satisfies the callback from this element
 * up the DOM subtree.
 * @param {!Element} element
 * @param {function(!Element):boolean} callback
 * @return {?Element}
 */
export function closest(element, callback) {
  for (let el = element; el; el = el.parentElement) {
    if (callback(el)) {
      return el;
    }
  }
  return null;
}

/**
 * Finds the closest element with the specified name from this element
 * up the DOM subtree.
 * @param {!Element} element
 * @param {string} tagName
 * @return {?Element}
 */
export function closestByTag(element, tagName) {
  if (element.closest) {
    return element.closest(tagName);
  }
  tagName = tagName.toUpperCase();
  return closest(element, el => {
    return el.tagName == tagName;
  });
}

/**
 * Finds the first descendant element with the specified name.
 * @param {!Element} element
 * @param {string} tagName
 * @return {?Element}
 */
export function elementByTag(element, tagName) {
  const elements = element.getElementsByTagName(tagName);
  return elements[0] || null;
}

/**
 * Finds the first child element that satisfies the callback.
 * @param {!Element} parent
 * @param {function(!Element):boolean} callback
 * @return {?Element}
 */
export function childElement(parent, callback) {
  for (let child = parent.firstElementChild; child;
      child = child.nextElementSibling) {
    if (callback(child)) {
      return child;
    }
  }
  return null;
}

/**
 * Finds all child elements that satisfies the callback.
 * @param {!Element} parent
 * @param {function(!Element):boolean} callback
 * @return {!Array.<!Element>}
 */
export function childElements(parent, callback) {
  const children = [];
  for (let child = parent.firstElementChild; child;
       child = child.nextElementSibling) {
    if (callback(child)) {
      children.push(child);
    }
  }
  return children;
}

/**
 * Finds the last child element that satisfies the callback.
 * @param {!Element} parent
 * @param {function(!Element):boolean} callback
 * @return {?Element}
 */
export function lastChildElement(parent, callback) {
  for (let child = parent.lastElementChild; child;
       child = child.previousElementSibling) {
    if (callback(child)) {
      return child;
    }
  }
  return null;
}

/**
 * Finds all child nodes that satisfies the callback.
 * These nodes can include Text, Comment and other child nodes.
 * @param {!Node} parent
 * @param {function(!Node):boolean} callback
 * @return {!Array<!Node>}
 */
export function childNodes(parent, callback) {
  const nodes = [];
  for (let child = parent.firstChild; child;
       child = child.nextSibling) {
    if (callback(child)) {
      nodes.push(child);
    }
  }
  return nodes;
}

/**
 * @type {boolean|undefined}
 * @visiblefortesting
 */
let scopeSelectorSupported;

/**
 * @param {boolean|undefined} val
 * @visiblefortesting
 */
export function setScopeSelectorSupportedForTesting(val) {
  scopeSelectorSupported = val;
}

/**
 * @param {!Element} parent
 * @return {boolean}
 */
function isScopeSelectorSupported(parent) {
  try {
    parent.ownerDocument.querySelector(':scope');
    return true;
  } catch (e) {
    return false;
  }
}

/**
 * Finds the first child element that has the specified attribute.
 * @param {!Element} parent
 * @param {string} attr
 * @return {?Element}
 */
export function childElementByAttr(parent, attr) {
  if (scopeSelectorSupported == null) {
    scopeSelectorSupported = isScopeSelectorSupported(parent);
  }
  if (scopeSelectorSupported) {
    return parent.querySelector(':scope > [' + attr + ']');
  }
  return childElement(parent, el => {
    return el.hasAttribute(attr);
  });
}

/**
 * Finds the last child element that has the specified attribute.
 * @param {!Element} parent
 * @param {string} attr
 * @return {?Element}
 */
export function lastChildElementByAttr(parent, attr) {
  return lastChildElement(parent, el => {
    return el.hasAttribute(attr);
  });
}

/**
 * Finds all child elements that has the specified attribute.
 * @param {!Element} parent
 * @param {string} attr
 * @return {!Array.<!Element>}
 */
export function childElementsByAttr(parent, attr) {
  if (scopeSelectorSupported == null) {
    scopeSelectorSupported = isScopeSelectorSupported(parent);
  }
  if (scopeSelectorSupported) {
    const nodeList = parent.querySelectorAll(':scope > [' + attr + ']');
    // Convert NodeList into Array.<Element>.
    const children = [];
    for (let i = 0; i < nodeList.length; i++) {
      children.push(nodeList[i]);
    }
    return children;
  }
  return childElements(parent, el => {
    return el.hasAttribute(attr);
  });
}

/**
 * Finds the first child element that has the specified tag name.
 * @param {!Element} parent
 * @param {string} tagName
 * @return {?Element}
 */
export function childElementByTag(parent, tagName) {
  if (scopeSelectorSupported == null) {
    scopeSelectorSupported = isScopeSelectorSupported(parent);
  }
  if (scopeSelectorSupported) {
    return parent.querySelector(':scope > ' + tagName);
  }
  tagName = tagName.toUpperCase();
  return childElement(parent, el => {
    return el.tagName == tagName;
  });
}

/**
 * Returns element data-param- attributes as url parameters key-value pairs.
 * e.g. data-param-some-attr=value -> {someAttr: value}.
 * @param {!Element} element
 * @param {function(string):string} opt_computeParamNameFunc to compute the parameter
 *    name, get passed the camel-case parameter name.
 * @return {!Object<string, string>}
 */
export function getDataParamsFromAttributes(element, opt_computeParamNameFunc) {
  const computeParamNameFunc = opt_computeParamNameFunc || (key => key);
  const attributes = element.attributes;
  const params = Object.create(null);
  for (let i = 0; i < attributes.length; i++) {
    const attr = attributes[i];
    const matches = attr.name.match(/^data-param-(.+)/);
    if (matches) {
      const param = dashToCamelCase(matches[1]);
      params[computeParamNameFunc(param)] = attr.value;
    }
  }
  return params;
}
<|endoftext|>