<commit_before>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You 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.
 */

var clutch = require('extern/clutch');
var log = require('util/log');
var fs = require('fs');
var sys = require('sys');
var path = require('path');
var config = require('util/config');
var misc = require('util/misc');
var http = require('util/http');
var async = require('extern/async');

/**
 * Given a bundle name and a file name, return a validated and normalized path
 * to a (not necessarily existing) file. Return false on invalid paths.
 *
 * @param {String} bundle The name of the bundle
 * @param {String} file The name of the file
 *
 * @returns The normalized file path, or false on invalid paths
 */
function file_path(bundle, file) {
  var bundleroot = path.join(config.get().data_root, 'bundles');
  var bpath = path.normalize(path.join(bundleroot, bundle));
  var fpath = path.normalize(path.join(bundleroot, bundle, file));
  if ((path.dirname(bpath) === bundleroot) && (path.dirname(fpath) === bpath)) {
    return fpath;
  };
  return false;
}

/**
 * Given a bundle name return a validated and normalized path to the (not
 * necessarily existing) bundle directory. Return false on invalid paths.
 *
 * @param {String} bundle The name of the bundle
 *
 * @returns The normalized bundle path, or false on invalid paths
 */
function bundle_path(bundle) {
  var bundleroot = path.join(config.get().data_root, 'bundles');
  var bpath = path.normalize(path.join(bundleroot, bundle));
  if (path.dirname(bpath) === bundleroot) {
    return bpath;
  };
  return false;
}

/**
 * Get a temporary file name within the same directory as the provided filename
 *
 * @param {String} filename The name of the file to create a temporary name for
 * @returns A path to a temporary file in the same directory as filename.
 */
function tempfileInDirectory(filename) {
  dname = path.dirname(filename);
  randname = ".cast_tmp_" + misc.randstr(8) + path.extname(filename);
  return path.join(dname, randname);
}

/**
 * Stat a path and verify that it exists. Return a 404 if it doesn't. If it
 * does, fire the given callback with the stat object.
 *
 * @param {String} p  The path to verify
 * @param {http.ServerResponse} res The response to potentially send the 404 to
 * @param {Function} cb An optional callback which takes the stat object as
 *                      its only argument.
 */
function path_or_404(p, res, cb) {
  if (!p) {
    return http.return_error(res, 404, "File not found");
  }

  fs.stat(p, function(err, stats) {
    if (err) {
      return http.return_error(res, 404, "File not found");
    }
    return cb(stats);
  });
}

/**
 * Stat a path and verify that it is a file which exists. Return a 404 if it
 * doesn't. If it does, fire the given callback with the stat object.
 *
 * @param {String} p  The path to verify
 * @param {http.ServerResponse} res The response to potentially send the 404 to
 * @param {Function} cb An optional callback which takes the stat object as
 *                      its only argument.
 */
function file_or_404(p, res, cb) {
  path_or_404(p, res, function(stats) {
    if (!stats.isFile()) {
      return http.return_error(res, 404, "File not found");
    }
    return cb(stats);
  });
}

/**
 * Stat a path and verify that it is a directory which exists. Return a 404 if
 * it doesn't. If it does, fire the given callback with the stat object.
 *
 * @param {String} p  The path to verify
 * @param {http.ServerResponse} res The response to potentially send the 404 to
 * @param {Function} cb An optional callback which takes the stat object as
 *                      its only argument.
 */
function directory_or_404(p, res, cb) {
  path_or_404(p, res, function(stats) {
    if (!stats.isDirectory()) {
      return http.return_error(res, 404, "File not found");
    }
    return cb(stats);
  });
}

/**
 * Write a file from an HTTP stream and fire a callback on success or error.
 *
 * @param {String} dest The path to store the file to
 * @param {http.ServerRequest} istream The HTTP stream to read from
 * @param {Function} cb A callback that takes a possible error
 */
function pumpfilein(dest, istream, received, completed, cb) {
  var fstream = fs.createWriteStream(dest, {'flags': 'w', 'encoding': 'binary', 'mode': 0644});

  function close_fstream() {
    try {
      fstream.end();
    }
    catch (err) {
      log.err("Unable to close file: " + err.message);
    }
  }

  fstream.on('error', function(err) {
    log.err("Error on file stream: " + err.message);
    close_fstream();
    return cb(err);
  });

  /* If the request has already been completed, we don't need to actually use
   * the pump. Instead we simply write all of the received buffer chunks to the
   * file stream, and return on success. On error we mooch off of the error
   * listener registered above.
   */
  if (completed) {
    fstream.on('close', function() {
      return cb();
    });

    for (var i = 0; i < received.length; i++) {
      fstream.write(received[i]);
    }

    fstream.end();
    return;
  }

  istream.on('error', function(err) {
    log.err("Error on HTTP stream: " + err.message);
    close_fstream();
    return cb(err);
  });

  istream.on('end', function() {
    /* This is not ideal, but sys.pump and the HTTP request don't interact that
     * well together, so this emits a spurious and technically incorrect
     * 'close' event for when the HTTP request body has ended.
     */
    istream.emit('close');
  });

  for (var i = 0; i < received.length; i++) {
    fstream.write(received[i]);
  }

  sys.pump(istream, fstream, function() {
    cb();
  });
}

/**
 * Write a file from an HTTP stream via a temporary file.
 *
 * @param {String} dest The final path to store the file to
 * @param {http.ServerRequest} istream  The HTTP stream to read from
 * @param {Function} cb A callback which takes a possible error
 */
function pumpfilein_via_tempfile(dest, istream, received, completed, cb) {
  var temp = tempfileInDirectory(dest);
  pumpfilein(temp, istream, received, completed, function(err) {
    if (err) {
      return cb(err);
    }
    fs.rename(temp, dest, cb);
  });
}

/**
 * Write a file to an HTTP stream. Does not write the headers.
 *
 * @param {String} source The path to read the file from
 * @param {http.ServerResponse} ostream The HTTP Stream to write the file to
 * @param {Function} cb A callback which takes a possible error
 */
function pumpfileout(source, ostream, cb) {
  /* TODO: global configuration of the bufferSize setting */
  var fstream = fs.createReadStream(source, {'bufferSize': 1024 * 64});

  function close_both() {
    (function() {
      try {
        ostream.end();
      }
      catch (err) {
        log.err("Unable to close outgoing HTTP stream: " + err.message);
      }
    })();

    (function() {
      try {
        fstream.end();
      }
      catch (err) {
        log.err("Unable to close file: " + err.message);
      }
    })();
  }

  ostream.on('error', function(err) {
    close_both();
    return cb(err);
  });

  fstream.on('error', function(err) {
    close_both();
    return cb(err);
  });

  sys.pump(fstream, ostream, function() {
    cb();
  });
}

/**
 * Receive an uploaded bundle file and store it.
 *
 * @param {http.ServerRequest} req  The HTTP request to read from
 * @param {http.ServerResponse} res The HTTP response to respond on
 * @param {String} bundle The name of the bundle to store
 * @param {String} file   The name of the file to store
 */
function upload(req, res, bundle, file) {
  // We must pause the request until we decide what to do with it
  req.pause();

  // Some data will still arrive, so buffer it
  var received = [];
  var completed = false;

  function on_data(data) {
    received.push(data);
  }
  req.on('data', on_data);

  function on_end() {
    completed = true;
  }
  req.on('end', on_end);

  // Validate the path
  var p = file_path(bundle, file);
  if (!p) {
    return http.return_error(res, "404", "File not found");
  }

  // Make sure the bundle exists
  directory_or_404(path.dirname(p), res, function() {
    // Disable the buffering and resume the request
    req.removeListener('data', on_data);
    req.removeListener('end', on_end);
    req.resume();
    pumpfilein_via_tempfile(p, req, received, completed, function(err) {
      if (err) {
        return http.return_error(res, 500, "Error storing file: " + err.message);
      }
      res.writeHead(204, {});
      res.end();
    });
  });
}

/**
 * Send a requested bundle file
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to write to
 * @param {String} bundle The name of the bundle to read
 * @param {String} file   The name of the file to read
 */
function download(req, res, bundle, file) {
  var fpath = file_path(bundle, file);
  file_or_404(fpath, res, function(stats) {
    res.writeHead(200, {
      'Content-Type': 'application/octet-stream',
      'Content-Length': stats.size
    });
    pumpfileout(fpath, res, function(err) {
      if (err) {
        // The header is already written, there isn't much we can do
        log.err("Error streaming file: " + err);
      }
      // End the response in any case
      res.end();
    });
  });
}

/**
 * Remove a bundle file
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to respond to
 * @param {String} bundle The name of the bundle to delete from
 * @param {String} file   The name of the file to delete
 */
function remove(req, res, bundle, file) {
  var fpath = file_path(bundle, file);
  file_or_404(fpath, res, function() {
    fs.unlink(fpath, function(err) {
      if (err) {
        return http.return_error(res, 500, "Error removing bundle file: " + err.message);
      }
      res.writeHead(204, {});
      res.end();
    });
  });
}

/**
 * List files within a bundle
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to respond to
 * @param {String} bundle The name of the bundle to delete from
 */
function list_files(req, res, bundle) {
  var bpath = bundle_path(bundle);
  if (!bpath) {
    return http.return_error(res, 404, "File not found");
  }
  fs.readdir(bpath, function(err, files) {
    if (err) {
      return http.return_error(res, 404, "File not found");
    }

    var filedata = [];

    // Build a list of files in this directory
    async.forEach(files, function(file, callback) {
      var fpath = path.join(bpath, file);

      fs.stat(fpath, function(err, stats) {
        if (err) {
          log.warning("Error stat-ing file: " + err.message);
        }

        else if (stats.isFile()) {
          // TODO: Do we want anything besides the name of the file?
          filedata.push(file);
        }
        return callback();
      });
    },

    // When all of the files have been checked, return the list
    function(err) {
      return http.return_json(res, 200, filedata);
    });
  });
}

/**
 * List available bundles
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to respond to
 */
function list_bundles(req, res) {
  var p = path.join(config.get().data_root, "bundles");
  fs.readdir(p, function(err, files) {
    if (err) {
      // This should NOT happen in the course of normal operations
      log.err("Error reading bundle directory: " + err.message);
      return http.return_error(res, 500, "Error reading directory");
    }

    var dirs = [];

    // Build a list of directories in this directory
    async.forEach(files, function(file, callback) {
      var fpath = path.join(p, file);

      fs.stat(fpath, function(err, stats) {
        if (err) {
          log.warning("Error stat-ing file: " + err.message);
        }

        else if (stats.isDirectory()) {
          // TODO: Do we want anything besides the name of the bundle?
          dirs.push(file);
        }
        return callback();
      });
    },

    // Return the list of bundles
    function(err) {
      return http.return_json(res, 200, dirs);
    });
  });
}

exports.urls = clutch.route([
                              ['PUT /(.+)/(.+)$', upload],
                              ['GET /(.+)/(.+)$', download],
                              ['DELETE /(.+)/(.+)$', remove],
                              ['GET /(.+)/$', list_files],
                              ['GET /$', list_bundles]
                              ]);
<commit_msg>Apparently HTTP streams don't emit errors.<commit_after>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You 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.
 */

var clutch = require('extern/clutch');
var log = require('util/log');
var fs = require('fs');
var sys = require('sys');
var path = require('path');
var config = require('util/config');
var misc = require('util/misc');
var http = require('util/http');
var async = require('extern/async');

/**
 * Given a bundle name and a file name, return a validated and normalized path
 * to a (not necessarily existing) file. Return false on invalid paths.
 *
 * @param {String} bundle The name of the bundle
 * @param {String} file The name of the file
 *
 * @returns The normalized file path, or false on invalid paths
 */
function file_path(bundle, file) {
  var bundleroot = path.join(config.get().data_root, 'bundles');
  var bpath = path.normalize(path.join(bundleroot, bundle));
  var fpath = path.normalize(path.join(bundleroot, bundle, file));
  if ((path.dirname(bpath) === bundleroot) && (path.dirname(fpath) === bpath)) {
    return fpath;
  };
  return false;
}

/**
 * Given a bundle name return a validated and normalized path to the (not
 * necessarily existing) bundle directory. Return false on invalid paths.
 *
 * @param {String} bundle The name of the bundle
 *
 * @returns The normalized bundle path, or false on invalid paths
 */
function bundle_path(bundle) {
  var bundleroot = path.join(config.get().data_root, 'bundles');
  var bpath = path.normalize(path.join(bundleroot, bundle));
  if (path.dirname(bpath) === bundleroot) {
    return bpath;
  };
  return false;
}

/**
 * Get a temporary file name within the same directory as the provided filename
 *
 * @param {String} filename The name of the file to create a temporary name for
 * @returns A path to a temporary file in the same directory as filename.
 */
function tempfileInDirectory(filename) {
  dname = path.dirname(filename);
  randname = ".cast_tmp_" + misc.randstr(8) + path.extname(filename);
  return path.join(dname, randname);
}

/**
 * Stat a path and verify that it exists. Return a 404 if it doesn't. If it
 * does, fire the given callback with the stat object.
 *
 * @param {String} p  The path to verify
 * @param {http.ServerResponse} res The response to potentially send the 404 to
 * @param {Function} cb An optional callback which takes the stat object as
 *                      its only argument.
 */
function path_or_404(p, res, cb) {
  if (!p) {
    return http.return_error(res, 404, "File not found");
  }

  fs.stat(p, function(err, stats) {
    if (err) {
      return http.return_error(res, 404, "File not found");
    }
    return cb(stats);
  });
}

/**
 * Stat a path and verify that it is a file which exists. Return a 404 if it
 * doesn't. If it does, fire the given callback with the stat object.
 *
 * @param {String} p  The path to verify
 * @param {http.ServerResponse} res The response to potentially send the 404 to
 * @param {Function} cb An optional callback which takes the stat object as
 *                      its only argument.
 */
function file_or_404(p, res, cb) {
  path_or_404(p, res, function(stats) {
    if (!stats.isFile()) {
      return http.return_error(res, 404, "File not found");
    }
    return cb(stats);
  });
}

/**
 * Stat a path and verify that it is a directory which exists. Return a 404 if
 * it doesn't. If it does, fire the given callback with the stat object.
 *
 * @param {String} p  The path to verify
 * @param {http.ServerResponse} res The response to potentially send the 404 to
 * @param {Function} cb An optional callback which takes the stat object as
 *                      its only argument.
 */
function directory_or_404(p, res, cb) {
  path_or_404(p, res, function(stats) {
    if (!stats.isDirectory()) {
      return http.return_error(res, 404, "File not found");
    }
    return cb(stats);
  });
}

/**
 * Write a file from an HTTP stream and fire a callback on success or error.
 *
 * @param {String} dest The path to store the file to
 * @param {http.ServerRequest} istream The HTTP stream to read from
 * @param {Function} cb A callback that takes a possible error
 */
function pumpfilein(dest, istream, received, completed, cb) {
  var fstream = fs.createWriteStream(dest, {'flags': 'w', 'encoding': 'binary', 'mode': 0644});

  function close_fstream() {
    try {
      fstream.end();
    }
    catch (err) {
      log.err("Unable to close file: " + err.message);
    }
  }

  fstream.on('error', function(err) {
    log.err("Error on file stream: " + err.message);
    close_fstream();
    return cb(err);
  });

  /* If the request has already been completed, we don't need to actually use
   * the pump. Instead we simply write all of the received buffer chunks to the
   * file stream, and return on success. On error we mooch off of the error
   * listener registered above.
   */
  if (completed) {
    fstream.on('close', function() {
      return cb();
    });

    for (var i = 0; i < received.length; i++) {
      fstream.write(received[i]);
    }

    fstream.end();
    return;
  }

  istream.on('end', function() {
    /* This is not ideal, but sys.pump and the HTTP request don't interact that
     * well together, so this emits a spurious and technically incorrect
     * 'close' event for when the HTTP request body has ended.
     */
    istream.emit('close');
  });

  for (var i = 0; i < received.length; i++) {
    fstream.write(received[i]);
  }

  sys.pump(istream, fstream, function() {
    cb();
  });
}

/**
 * Write a file from an HTTP stream via a temporary file.
 *
 * @param {String} dest The final path to store the file to
 * @param {http.ServerRequest} istream  The HTTP stream to read from
 * @param {Function} cb A callback which takes a possible error
 */
function pumpfilein_via_tempfile(dest, istream, received, completed, cb) {
  var temp = tempfileInDirectory(dest);
  pumpfilein(temp, istream, received, completed, function(err) {
    if (err) {
      return cb(err);
    }
    fs.rename(temp, dest, cb);
  });
}

/**
 * Write a file to an HTTP stream. Does not write the headers.
 *
 * @param {String} source The path to read the file from
 * @param {http.ServerResponse} ostream The HTTP Stream to write the file to
 * @param {Function} cb A callback which takes a possible error
 */
function pumpfileout(source, ostream, cb) {
  /* TODO: global configuration of the bufferSize setting */
  var fstream = fs.createReadStream(source, {'bufferSize': 1024 * 64});

  function close_both() {
    (function() {
      try {
        ostream.end();
      }
      catch (err) {
        log.err("Unable to close outgoing HTTP stream: " + err.message);
      }
    })();

    (function() {
      try {
        fstream.end();
      }
      catch (err) {
        log.err("Unable to close file: " + err.message);
      }
    })();
  }

  fstream.on('error', function(err) {
    close_both();
    return cb(err);
  });

  sys.pump(fstream, ostream, function() {
    cb();
  });
}

/**
 * Receive an uploaded bundle file and store it.
 *
 * @param {http.ServerRequest} req  The HTTP request to read from
 * @param {http.ServerResponse} res The HTTP response to respond on
 * @param {String} bundle The name of the bundle to store
 * @param {String} file   The name of the file to store
 */
function upload(req, res, bundle, file) {
  // We must pause the request until we decide what to do with it
  req.pause();

  // Some data will still arrive, so buffer it
  var received = [];
  var completed = false;

  function on_data(data) {
    received.push(data);
  }
  req.on('data', on_data);

  function on_end() {
    completed = true;
  }
  req.on('end', on_end);

  // Validate the path
  var p = file_path(bundle, file);
  if (!p) {
    return http.return_error(res, "404", "File not found");
  }

  // Make sure the bundle exists
  directory_or_404(path.dirname(p), res, function() {
    // Disable the buffering and resume the request
    req.removeListener('data', on_data);
    req.removeListener('end', on_end);
    req.resume();
    pumpfilein_via_tempfile(p, req, received, completed, function(err) {
      if (err) {
        return http.return_error(res, 500, "Error storing file: " + err.message);
      }
      res.writeHead(204, {});
      res.end();
    });
  });
}

/**
 * Send a requested bundle file
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to write to
 * @param {String} bundle The name of the bundle to read
 * @param {String} file   The name of the file to read
 */
function download(req, res, bundle, file) {
  var fpath = file_path(bundle, file);
  file_or_404(fpath, res, function(stats) {
    res.writeHead(200, {
      'Content-Type': 'application/octet-stream',
      'Content-Length': stats.size
    });
    pumpfileout(fpath, res, function(err) {
      if (err) {
        // The header is already written, there isn't much we can do
        log.err("Error streaming file: " + err);
      }
      // End the response in any case
      res.end();
    });
  });
}

/**
 * Remove a bundle file
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to respond to
 * @param {String} bundle The name of the bundle to delete from
 * @param {String} file   The name of the file to delete
 */
function remove(req, res, bundle, file) {
  var fpath = file_path(bundle, file);
  file_or_404(fpath, res, function() {
    fs.unlink(fpath, function(err) {
      if (err) {
        return http.return_error(res, 500, "Error removing bundle file: " + err.message);
      }
      res.writeHead(204, {});
      res.end();
    });
  });
}

/**
 * List files within a bundle
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to respond to
 * @param {String} bundle The name of the bundle to delete from
 */
function list_files(req, res, bundle) {
  var bpath = bundle_path(bundle);
  if (!bpath) {
    return http.return_error(res, 404, "File not found");
  }
  fs.readdir(bpath, function(err, files) {
    if (err) {
      return http.return_error(res, 404, "File not found");
    }

    var filedata = [];

    // Build a list of files in this directory
    async.forEach(files, function(file, callback) {
      var fpath = path.join(bpath, file);

      fs.stat(fpath, function(err, stats) {
        if (err) {
          log.warning("Error stat-ing file: " + err.message);
        }

        else if (stats.isFile()) {
          // TODO: Do we want anything besides the name of the file?
          filedata.push(file);
        }
        return callback();
      });
    },

    // When all of the files have been checked, return the list
    function(err) {
      return http.return_json(res, 200, filedata);
    });
  });
}

/**
 * List available bundles
 *
 * @param {http.ServerRequest} req  The HTTP request to respond to
 * @param {http.ServerResponse} res The HTTP response to respond to
 */
function list_bundles(req, res) {
  var p = path.join(config.get().data_root, "bundles");
  fs.readdir(p, function(err, files) {
    if (err) {
      // This should NOT happen in the course of normal operations
      log.err("Error reading bundle directory: " + err.message);
      return http.return_error(res, 500, "Error reading directory");
    }

    var dirs = [];

    // Build a list of directories in this directory
    async.forEach(files, function(file, callback) {
      var fpath = path.join(p, file);

      fs.stat(fpath, function(err, stats) {
        if (err) {
          log.warning("Error stat-ing file: " + err.message);
        }

        else if (stats.isDirectory()) {
          // TODO: Do we want anything besides the name of the bundle?
          dirs.push(file);
        }
        return callback();
      });
    },

    // Return the list of bundles
    function(err) {
      return http.return_json(res, 200, dirs);
    });
  });
}

exports.urls = clutch.route([
                              ['PUT /(.+)/(.+)$', upload],
                              ['GET /(.+)/(.+)$', download],
                              ['DELETE /(.+)/(.+)$', remove],
                              ['GET /(.+)/$', list_files],
                              ['GET /$', list_bundles]
                              ]);
<|endoftext|>


<commit_before>/**
 * @license Copyright 2016 Google Inc. 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.
 */
'use strict';

const fs = require('fs');
const path = require('path');

/*
 * The relationship between these CLI modules:
 *
 *   index.js     : only calls bin.js's begin()
 *   cli-flags.js : leverages yargs to read argv, outputs LH.CliFlags
 *   bin.js       : CLI args processing. cwd, list/print commands
 *   run.js       : chrome-launcher bits, calling lighthouse-core, output to Printer
 *
 *   index ---->    bin    ---->      run      ----> printer
 *                  ⭏  ⭎               ⭏  ⭎
 *               cli-flags        lh-core/index
 */

const commands = require('./commands/commands.js');
const printer = require('./printer.js');
const getFlags = require('./cli-flags.js').getFlags;
const runLighthouse = require('./run.js').runLighthouse;
const generateConfig = require('../lighthouse-core/index.js').generateConfig;

const log = require('lighthouse-logger');
const pkg = require('../package.json');
const Sentry = require('../lighthouse-core/lib/sentry.js');

const updateNotifier = require('update-notifier');
const askPermission = require('./sentry-prompt.js').askPermission;

/**
 * @return {boolean}
 */
function isDev() {
  return fs.existsSync(path.join(__dirname, '../.git'));
}

/**
 * @return {Promise<LH.RunnerResult|void>}
 */
async function begin() {
  // Tell user if there's a newer version of LH.
  updateNotifier({pkg}).notify();

  const cliFlags = getFlags();

  // Process terminating command
  if (cliFlags.listAllAudits) {
    commands.listAudits();
  }

  // Process terminating command
  if (cliFlags.listTraceCategories) {
    commands.listTraceCategories();
  }

  const url = cliFlags._[0];

  /** @type {LH.Config.Json|undefined} */
  let configJson;
  if (cliFlags.configPath) {
    // Resolve the config file path relative to where cli was called.
    cliFlags.configPath = path.resolve(process.cwd(), cliFlags.configPath);
    configJson = /** @type {LH.Config.Json} */ (require(cliFlags.configPath));
  } else if (cliFlags.preset) {
    if (cliFlags.preset === 'mixed-content') {
      // The mixed-content audits require headless Chrome (https://crbug.com/764505).
      cliFlags.chromeFlags = `${cliFlags.chromeFlags} --headless`;
    }

    configJson = require(`../lighthouse-core/config/${cliFlags.preset}-config.js`);
  }

  // set logging preferences
  cliFlags.logLevel = 'info';
  if (cliFlags.verbose) {
    cliFlags.logLevel = 'verbose';
  } else if (cliFlags.quiet) {
    cliFlags.logLevel = 'silent';
  }
  log.setLevel(cliFlags.logLevel);

  if (
    cliFlags.output.length === 1 &&
    cliFlags.output[0] === printer.OutputMode.json &&
    !cliFlags.outputPath
  ) {
    cliFlags.outputPath = 'stdout';
  }

  if (cliFlags.extraHeaders) {
    // TODO: LH.Flags.extraHeaders is actually a string at this point, but needs to be
    // copied over to LH.Settings.extraHeaders, which is LH.Crdp.Network.Headers. Force
    // the conversion here, but long term either the CLI flag or the setting should have
    // a different name.
    // @ts-ignore
    let extraHeadersStr = /** @type {string} */ (cliFlags.extraHeaders);
    // If not a JSON object, assume it's a path to a JSON file.
    if (extraHeadersStr.substr(0, 1) !== '{') {
      extraHeadersStr = fs.readFileSync(extraHeadersStr, 'utf-8');
    }

    cliFlags.extraHeaders = JSON.parse(extraHeadersStr);
  }

  if (cliFlags.precomputedLanternDataPath) {
    const lanternDataStr = fs.readFileSync(cliFlags.precomputedLanternDataPath, 'utf8');
    /** @type {LH.PrecomputedLanternData} */
    const data = JSON.parse(lanternDataStr);
    if (!data.additionalRttByOrigin || !data.serverResponseTimeByOrigin) {
      throw new Error('Invalid precomputed lantern data file');
    }

    cliFlags.precomputedLanternData = data;
  }

  if (cliFlags.printConfig) {
    const config = generateConfig(configJson, cliFlags);
    process.stdout.write(config.getPrintString());
    return;
  }

  // By default, cliFlags.enableErrorReporting is undefined so the user is
  // prompted. This can be overriden with an explicit flag or by the cached
  // answer returned by askPermission().
  if (typeof cliFlags.enableErrorReporting === 'undefined') {
    cliFlags.enableErrorReporting = await askPermission();
  }
  if (cliFlags.enableErrorReporting) {
    Sentry.init({
      url,
      flags: cliFlags,
      environmentData: {
        name: 'redacted', // prevent sentry from using hostname
        environment: isDev() ? 'development' : 'production',
        release: pkg.version,
        tags: {
          channel: 'cli',
        },
      },
    });
  }

  return runLighthouse(url, cliFlags, configJson);
}

module.exports = {
  begin,
};
<commit_msg>core: add deprecation message for disable-device-emulation flag (#8334)<commit_after>/**
 * @license Copyright 2016 Google Inc. 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.
 */
'use strict';

const fs = require('fs');
const path = require('path');

/*
 * The relationship between these CLI modules:
 *
 *   index.js     : only calls bin.js's begin()
 *   cli-flags.js : leverages yargs to read argv, outputs LH.CliFlags
 *   bin.js       : CLI args processing. cwd, list/print commands
 *   run.js       : chrome-launcher bits, calling lighthouse-core, output to Printer
 *
 *   index ---->    bin    ---->      run      ----> printer
 *                  ⭏  ⭎               ⭏  ⭎
 *               cli-flags        lh-core/index
 */

const commands = require('./commands/commands.js');
const printer = require('./printer.js');
const getFlags = require('./cli-flags.js').getFlags;
const runLighthouse = require('./run.js').runLighthouse;
const generateConfig = require('../lighthouse-core/index.js').generateConfig;

const log = require('lighthouse-logger');
const pkg = require('../package.json');
const Sentry = require('../lighthouse-core/lib/sentry.js');

const updateNotifier = require('update-notifier');
const askPermission = require('./sentry-prompt.js').askPermission;

/**
 * @return {boolean}
 */
function isDev() {
  return fs.existsSync(path.join(__dirname, '../.git'));
}

/**
 * @return {Promise<LH.RunnerResult|void>}
 */
async function begin() {
  // Tell user if there's a newer version of LH.
  updateNotifier({pkg}).notify();

  const cliFlags = getFlags();

  // Process terminating command
  if (cliFlags.listAllAudits) {
    commands.listAudits();
  }

  // Process terminating command
  if (cliFlags.listTraceCategories) {
    commands.listTraceCategories();
  }

  const url = cliFlags._[0];

  /** @type {LH.Config.Json|undefined} */
  let configJson;
  if (cliFlags.configPath) {
    // Resolve the config file path relative to where cli was called.
    cliFlags.configPath = path.resolve(process.cwd(), cliFlags.configPath);
    configJson = /** @type {LH.Config.Json} */ (require(cliFlags.configPath));
  } else if (cliFlags.preset) {
    if (cliFlags.preset === 'mixed-content') {
      // The mixed-content audits require headless Chrome (https://crbug.com/764505).
      cliFlags.chromeFlags = `${cliFlags.chromeFlags} --headless`;
    }

    configJson = require(`../lighthouse-core/config/${cliFlags.preset}-config.js`);
  }

  // set logging preferences
  cliFlags.logLevel = 'info';
  if (cliFlags.verbose) {
    cliFlags.logLevel = 'verbose';
  } else if (cliFlags.quiet) {
    cliFlags.logLevel = 'silent';
  }
  log.setLevel(cliFlags.logLevel);

  if (
    cliFlags.output.length === 1 &&
    cliFlags.output[0] === printer.OutputMode.json &&
    !cliFlags.outputPath
  ) {
    cliFlags.outputPath = 'stdout';
  }

  // @ts-ignore - deprecation message for removed disableDeviceEmulation; can remove warning in v6.
  if (cliFlags.disableDeviceEmulation) {
    log.warn('config', 'The "--disable-device-emulation" has been removed in v5.' +
        ' Please use "--emulated-form-factor=none" instead.');
  }

  if (cliFlags.extraHeaders) {
    // TODO: LH.Flags.extraHeaders is actually a string at this point, but needs to be
    // copied over to LH.Settings.extraHeaders, which is LH.Crdp.Network.Headers. Force
    // the conversion here, but long term either the CLI flag or the setting should have
    // a different name.
    // @ts-ignore
    let extraHeadersStr = /** @type {string} */ (cliFlags.extraHeaders);
    // If not a JSON object, assume it's a path to a JSON file.
    if (extraHeadersStr.substr(0, 1) !== '{') {
      extraHeadersStr = fs.readFileSync(extraHeadersStr, 'utf-8');
    }

    cliFlags.extraHeaders = JSON.parse(extraHeadersStr);
  }

  if (cliFlags.precomputedLanternDataPath) {
    const lanternDataStr = fs.readFileSync(cliFlags.precomputedLanternDataPath, 'utf8');
    /** @type {LH.PrecomputedLanternData} */
    const data = JSON.parse(lanternDataStr);
    if (!data.additionalRttByOrigin || !data.serverResponseTimeByOrigin) {
      throw new Error('Invalid precomputed lantern data file');
    }

    cliFlags.precomputedLanternData = data;
  }

  if (cliFlags.printConfig) {
    const config = generateConfig(configJson, cliFlags);
    process.stdout.write(config.getPrintString());
    return;
  }

  // By default, cliFlags.enableErrorReporting is undefined so the user is
  // prompted. This can be overriden with an explicit flag or by the cached
  // answer returned by askPermission().
  if (typeof cliFlags.enableErrorReporting === 'undefined') {
    cliFlags.enableErrorReporting = await askPermission();
  }
  if (cliFlags.enableErrorReporting) {
    Sentry.init({
      url,
      flags: cliFlags,
      environmentData: {
        name: 'redacted', // prevent sentry from using hostname
        environment: isDev() ? 'development' : 'production',
        release: pkg.version,
        tags: {
          channel: 'cli',
        },
      },
    });
  }

  return runLighthouse(url, cliFlags, configJson);
}

module.exports = {
  begin,
};
<|endoftext|>


<commit_before>'use strict';
var xml2js = require('xml2js');

/**
 * Represents metadata.
 * @constructor
 * @param {!ISeries} series
 * @param {!IChapter} chapter
 */
function Meta(series, chapter) {
  this.genre = series.genres.join(', ');
  this.number = !isNaN(chapter.number) ? chapter.number : undefined;
  this.penciller = series.artists.join(', ');
  this.pages = {page: []};
  this.series = series.title;
  this.summary = series.summary;
  this.title = chapter.title;
  this.volume = !isNaN(chapter.volume) ? chapter.volume : undefined;
  this.writer = series.authors.join(', ');
}

/**
 * Add a page to the metadata.
 * @param {string} key
 * @param {?number} number
 */
Meta.prototype.add = function(key, number) {
  this.pages.page.push({$: {
    key: key,
    image: isNaN(number) ? 0 : number || 0,
    type: Boolean(number) ? undefined : 'FrontCover'
  }});
};

/**
 * Export metadata to xml.
 * @return {string}
 */
Meta.prototype.xml = function() {
  return new xml2js.Builder({
    rootName: 'ComicInfo',
    xmldec: {version: '1.0', encoding: 'utf-8'}
  }).buildObject(map(this));
};

/**
 * Map the source to a duplicate with title case keys.
 * @private
 * @param {(!Array.<!Object>|!Object)} src
 * @return {(!Array.<!Object>|!Object)}
 */
function map(src) {
  var res = Array.isArray(src) ? [] : {};
  Object.keys(src).forEach(function(x) {
    if (typeof src[x] === 'undefined') return;
    var title = x.charAt(0).toUpperCase() + x.substr(1);
    res[title] = typeof src[x] === 'object' ? map(src[x]) : src[x];
  });
  return res;
}

module.exports = Meta;
<commit_msg>Allow meta information to be loaded from XML.<commit_after>'use strict';
var xml2js = require('xml2js');

/**
 * Represents meta information.
 * @constructor
 * @param {!ISeries} series
 * @param {!IChapter} chapter
 */
function Meta(series, chapter) {
  this.genre = series.genres.join(', ');
  this.number = !isNaN(chapter.number) ? chapter.number : undefined;
  this.penciller = series.artists.join(', ');
  this.pages = {page: []};
  this.series = series.title;
  this.summary = series.summary;
  this.title = chapter.title;
  this.volume = !isNaN(chapter.volume) ? chapter.volume : undefined;
  this.writer = series.authors.join(', ');
}

/**
 * Loads meta information from xml.
 * @param {string} xml
 * @param {function(Error, Meta)} done
 */
Meta.load = function(xml, done) {
  xml2js.parseString(xml, {
    explicitArray: false,
    explicitRoot: false
  }, function(err, result) {
    if (err) return done(err);
    var meta = lowerCamelCase(result, Object.create(Meta.prototype));
    meta.genre = defaultTo(meta.genre, '');
    meta.number = defaultTo(meta.number);
    meta.penciller = defaultTo(meta.penciller, '');
    meta.series = defaultTo(meta.series);
    meta.summary = defaultTo(meta.summary);
    meta.title = defaultTo(meta.title);
    meta.volume = defaultTo(meta.volume);
    meta.writer = defaultTo(meta.writer, '');
    done(undefined, meta);
  });
};

/**
 * Add a page to the metadata.
 * @param {string} key
 * @param {?number} number
 */
Meta.prototype.add = function(key, number) {
  this.pages.page.push({$: {
    key: key,
    image: isNaN(number) ? 0 : number || 0,
    type: Boolean(number) ? undefined : 'FrontCover'
  }});
};

/**
 * Export meta information to xml.
 * @return {string}
 */
Meta.prototype.xml = function() {
  return new xml2js.Builder({
    rootName: 'ComicInfo',
    xmldec: {version: '1.0', encoding: 'utf-8'}
  }).buildObject(titleCase(this));
};

/**
 * Defaults a value.
 * @private
 * @param {*} value
 * @param {*=} defaultValue
 * @return {*}
 */
function defaultTo(value, defaultValue) {
  return value || defaultValue;
}

/**
 * Map the source to a duplicate with lower camel case case keys.
 * @private
 * @param {(!Array.<!Object>|!Object)} src
 * @param {!Object=} destination
 * @return {(!Array.<!Object>|!Object)}
 */
function lowerCamelCase(src, destination) {
  var res = destination || (Array.isArray(src) ? [] : {});
  Object.keys(src).forEach(function(x) {
    if (typeof src[x] === 'undefined') return;
    var title = x.charAt(0).toLowerCase() + x.substr(1);
    res[title] = typeof src[x] === 'object' ? lowerCamelCase(src[x]) : src[x];
  });
  return res;
}

/**
 * Map the source to a duplicate with title case keys.
 * @private
 * @param {(!Array.<!Object>|!Object)} src
 * @return {(!Array.<!Object>|!Object)}
 */
function titleCase(src) {
  var res = Array.isArray(src) ? [] : {};
  Object.keys(src).forEach(function(x) {
    if (typeof src[x] === 'undefined') return;
    var title = x.charAt(0).toUpperCase() + x.substr(1);
    res[title] = typeof src[x] === 'object' ? titleCase(src[x]) : src[x];
  });
  return res;
}

module.exports = Meta;
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var log = require('debug')('democracyos:signup');
var utils = require('lib/utils');
var mongoose = require('mongoose');
var api = require('lib/db-api');
var mailer = require('lib/mailer').mandrillMailer;
var jade = require('jade');
var fs = require('fs');
var t = require('t-component');
var config = require('lib/config');
var url = require('url');
var notifier = require('notifier-client')(config.notifications);

/**
 * Citizen Model
 */

var Citizen = mongoose.model('Citizen');

var path = require('path');
var resolve = path.resolve;
var rawJade = fs.readFileSync(resolve(__dirname, '../mail.jade'));
var template = jade.compile(rawJade);

/**
 * Signups a citizen
 *
 * @param {Object} profile object with local signup data
 * @param {Obehect} meta user's ip, user-agent, etc
 * @param {Function} callback Callback accepting `err` and `citizen`
 * @api public
 */

exports.doSignUp = function doSignUp (profile, meta, callback) {
  var citizen = new Citizen(profile);

  log('new citizen [%s] from Local signup [%s]', citizen.id, profile.email);

  citizen.avatar = 'http://gravatar.com/avatar/'.concat(utils.md5(citizen.email)).concat('?d=mm&size=200');
  citizen.firstName = profile.firstName;
  citizen.lastName = profile.lastName;
  citizen.reference = profile.reference;
  if (config('env') == 'development') citizen.emailValidated = true;
  Citizen.register(citizen, profile.password, function(err, citizen) {
    if (err) return callback(err);
    log('Saved citizen [%s]', citizen.id);
    sendValidationEmail(citizen, 'signup', meta, callback);
  });
}

/**
 * Validates user email if a valid token is provided
 *
 * @param {Object} formData contains token
 * @param {Function} callback Callback accepting `err` and `citizen`
 * @api public
 */

exports.emailValidate = function emailValidate (formData, callback) {
  log('email validate requested. token : [%s]', formData.token);
  var tokenId = formData.token;
  api.token.get(tokenId, function (err, token){
    log('Token.findById result err : [%j] token : [%j]', err, token);
    if (err) return callback(err);
    if (!token) {
      return callback(new Error("No token for id " + tokenId));
    }

    log('email validate requested. token : [%s]. token verified', formData.token);
    api.citizen.get(token.user, function (err, citizen){
      if (err) return callback(err);
      log('about email validate. citizen : [%s].', citizen.id);
      citizen.emailValidated = true;
      citizen.save(function (err) {
        if (err) return callback(err);
        log('Saved citizen [%s]', citizen.id);
        token.remove(function (err) {
          if (err) return callback(err);
          log('Token removed [%j]', token);
          return callback(err, citizen);
        });
      });
    });
  });
}

/**
 * Sends a new validation email to a citizen
 *
 * @param {Object} profile object with the email address
 * @param {Obehect} meta user's ip, user-agent, etc
 * @param {Function} callback Callback accepting `err` and `citizen`
 * @api public
 */
exports.resendValidationEmail = function resendValidationEmail (profile, meta, callback) {
  log('Resend validation email to [%s] requested', profile.email);

  api.citizen.getByEmail(profile.email, function(err, citizen) {
    if (err) return callback(err);
    if (!citizen) return callback(new Error(t('No citizen found with that email address')));
    log('Resend validation email to citizen [%j] requested', citizen);
    sendValidationEmail(citizen, 'resend-validation', meta, callback);
  });
}

/**
 * Creates a token and sends a validation email to a citizen
 *
 * @param {Object} citizen to send the email to
 * @param {Obehect} meta user's ip, user-agent, etc
 * @param {Function} callback Callback accepting `err` and `citizen`
 */
function sendValidationEmail(citizen, event, meta, callback) {
  api.token.createEmailValidationToken(citizen, meta, function (err, token) {
    if (err) return callback(err);
    log('email validation token created %j', token);
<<<<<<< HEAD

=======
    // var subject = t('DemocracyOS - Welcome!');
>>>>>>> Make signup email validations rely on notifier-client
    var validateUrl = url.format({
        protocol: config('protocol')
      , hostname: config('host')
      , port: config('publicPort')
      , pathname: '/signup/validate/' + token.id
      , query: (citizen.reference ? { reference: citizen.reference } : null)
    });

<<<<<<< HEAD
    if (notifier.enabled()) {

      // Notifier enabled, relying on it for signup mail
      var payload = {
        user: citizen.email,
        event: event,
        validateUrl: validateUrl
      }

      notifier.notify(event)
        .to(citizen.email)
        .withData( { validateUrl: validateUrl } )
        .send(function (err, data) {
          if (err) {
            log('Error when sending notification for event %s to user %j', event, citizen);
            return callback(err);
          }

          return callback(null, data);
        })
    } else {

      // Notifier disabled, send mail directly
      var subject = t('DemocracyOS - Welcome!');
      var htmlBody = template({
        citizenName: citizen.fullName,
        validateUrl: validateUrl,
        t: t
      });

      mailer.send(citizen, subject, htmlBody, { tags: [token.scope] }, function (err) {
        if (err) return callback(err);
        log('email validation mail sent to %s', citizen.email);
        return callback(err, citizen);
      });
    }
=======
    // var htmlBody = template({
    //   citizenName: citizen.fullName,
    //   validateUrl: validateUrl,
    //   t: t
    // });
    var payload = {
      user: citizen.email,
      event: event,
      validateUrl: validateUrl
    }

    notifier.notify(event)
      .to(citizen.email)
      .withData( { validateUrl: validateUrl } )
      .send(function (err, data) {
        if (err) {
          log('Error when sending notification for event %s to user %j', event, citizen);
          return callback(err);
        }

        return callback(null, data);
      })

    // mailer.send(citizen, subject, htmlBody, { tags: [token.scope] }, function (err) {
    //   if (err) return callback(err);
    //   log('email validation mail sent to %s', citizen.email);
    //   return callback(err, citizen);
    // });
>>>>>>> Make signup email validations rely on notifier-client
  });
}<commit_msg>Bump notifier-client dependency verison<commit_after>/**
 * Module dependencies.
 */

var log = require('debug')('democracyos:signup');
var utils = require('lib/utils');
var mongoose = require('mongoose');
var api = require('lib/db-api');
var mailer = require('lib/mailer').mandrillMailer;
var jade = require('jade');
var fs = require('fs');
var t = require('t-component');
var config = require('lib/config');
var url = require('url');
var notifier = require('notifier-client')(config.notifications);

/**
 * Citizen Model
 */

var Citizen = mongoose.model('Citizen');

var path = require('path');
var resolve = path.resolve;
var rawJade = fs.readFileSync(resolve(__dirname, '../mail.jade'));
var template = jade.compile(rawJade);

/**
 * Signups a citizen
 *
 * @param {Object} profile object with local signup data
 * @param {Obehect} meta user's ip, user-agent, etc
 * @param {Function} callback Callback accepting `err` and `citizen`
 * @api public
 */

exports.doSignUp = function doSignUp (profile, meta, callback) {
  var citizen = new Citizen(profile);

  log('new citizen [%s] from Local signup [%s]', citizen.id, profile.email);

  citizen.avatar = 'http://gravatar.com/avatar/'.concat(utils.md5(citizen.email)).concat('?d=mm&size=200');
  citizen.firstName = profile.firstName;
  citizen.lastName = profile.lastName;
  citizen.reference = profile.reference;
  if (config('env') == 'development') citizen.emailValidated = true;
  Citizen.register(citizen, profile.password, function(err, citizen) {
    if (err) return callback(err);
    log('Saved citizen [%s]', citizen.id);
    sendValidationEmail(citizen, 'signup', meta, callback);
  });
}

/**
 * Validates user email if a valid token is provided
 *
 * @param {Object} formData contains token
 * @param {Function} callback Callback accepting `err` and `citizen`
 * @api public
 */

exports.emailValidate = function emailValidate (formData, callback) {
  log('email validate requested. token : [%s]', formData.token);
  var tokenId = formData.token;
  api.token.get(tokenId, function (err, token){
    log('Token.findById result err : [%j] token : [%j]', err, token);
    if (err) return callback(err);
    if (!token) {
      return callback(new Error("No token for id " + tokenId));
    }

    log('email validate requested. token : [%s]. token verified', formData.token);
    api.citizen.get(token.user, function (err, citizen){
      if (err) return callback(err);
      log('about email validate. citizen : [%s].', citizen.id);
      citizen.emailValidated = true;
      citizen.save(function (err) {
        if (err) return callback(err);
        log('Saved citizen [%s]', citizen.id);
        token.remove(function (err) {
          if (err) return callback(err);
          log('Token removed [%j]', token);
          return callback(err, citizen);
        });
      });
    });
  });
}

/**
 * Sends a new validation email to a citizen
 *
 * @param {Object} profile object with the email address
 * @param {Obehect} meta user's ip, user-agent, etc
 * @param {Function} callback Callback accepting `err` and `citizen`
 * @api public
 */
exports.resendValidationEmail = function resendValidationEmail (profile, meta, callback) {
  log('Resend validation email to [%s] requested', profile.email);

  api.citizen.getByEmail(profile.email, function(err, citizen) {
    if (err) return callback(err);
    if (!citizen) return callback(new Error(t('No citizen found with that email address')));
    log('Resend validation email to citizen [%j] requested', citizen);
    sendValidationEmail(citizen, 'resend-validation', meta, callback);
  });
}

/**
 * Creates a token and sends a validation email to a citizen
 *
 * @param {Object} citizen to send the email to
 * @param {Obehect} meta user's ip, user-agent, etc
 * @param {Function} callback Callback accepting `err` and `citizen`
 */
function sendValidationEmail(citizen, event, meta, callback) {
  api.token.createEmailValidationToken(citizen, meta, function (err, token) {
    if (err) return callback(err);
    log('email validation token created %j', token);

    var validateUrl = url.format({
        protocol: config('protocol')
      , hostname: config('host')
      , port: config('publicPort')
      , pathname: '/signup/validate/' + token.id
      , query: (citizen.reference ? { reference: citizen.reference } : null)
    });

    if (notifier.enabled()) {

      // Notifier enabled, relying on it for signup mail
      var payload = {
        user: citizen.email,
        event: event,
        validateUrl: validateUrl
      }

      notifier.notify(event)
        .to(citizen.email)
        .withData( { validateUrl: validateUrl } )
        .send(function (err, data) {
          if (err) {
            log('Error when sending notification for event %s to user %j', event, citizen);
            return callback(err);
          }

          return callback(null, data);
        })
    } else {

      // Notifier disabled, send mail directly
      var subject = t('DemocracyOS - Welcome!');
      var htmlBody = template({
        citizenName: citizen.fullName,
        validateUrl: validateUrl,
        t: t
      });

      mailer.send(citizen, subject, htmlBody, { tags: [token.scope] }, function (err) {
        if (err) return callback(err);
        log('email validation mail sent to %s', citizen.email);
        return callback(err, citizen);
      });
    }
  });
}<|endoftext|>


<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails oncall+draft_js
 * @format
 */

'use strict';

jest.disableAutomock();

const CompositeDraftDecorator = require('CompositeDraftDecorator');
const ContentBlock = require('ContentBlock');
const ContentState = require('ContentState');
const EditorState = require('EditorState');
const SelectionState = require('SelectionState');

const onBeforeInput = require('editOnBeforeInput');

const DEFAULT_SELECTION = {
  anchorKey: 'a',
  anchorOffset: 0,
  focusKey: 'a',
  focusOffset: 0,
  isBackward: false,
};

const rangedSelection = new SelectionState({
  ...DEFAULT_SELECTION,
  focusOffset: 1,
});

const rangedSelectionBackwards = new SelectionState({
  ...DEFAULT_SELECTION,
  anchorOffset: 1,
  isBackward: true,
});

const getEditorState = (text: string = 'Arsenal') => {
  return EditorState.createWithContent(
    ContentState.createFromBlockArray([
      new ContentBlock({
        key: 'a',
        text,
      }),
    ]),
  );
};

const getInputEvent = data => ({
  data,
  preventDefault: jest.fn(),
});

test('editor is not updated if no character data is provided', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const editor = {
    _latestEditorState: editorState,
    props: {},
    update: jest.fn(),
  };

  onBeforeInput(editor, getInputEvent());

  expect(editor.update).toHaveBeenCalledTimes(0);
});

test('editor is not updated if handled by handleBeforeInput', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const editor = {
    _latestEditorState: editorState,
    props: {
      handleBeforeInput: () => true,
    },
    update: jest.fn(),
  };

  onBeforeInput(editor, getInputEvent('O'));

  expect(editor.update).toHaveBeenCalledTimes(0);
});

test('editor is updated with new text if it does not match current selection', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const editor = {
    _latestEditorState: editorState,
    props: {},
    update: jest.fn(),
  };

  onBeforeInput(editor, getInputEvent('O'));

  expect(editor.update).toHaveBeenCalledTimes(1);

  const newEditorState = editor.update.mock.calls[0][0];
  expect(newEditorState.getCurrentContent()).toMatchSnapshot();
});

test('editor selectionstate is updated if new text matches current selection', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const editor = {
    _latestEditorState: editorState,
    props: {},
    update: jest.fn(),
  };

  onBeforeInput(editor, getInputEvent('A'));

  expect(editor.update).toHaveBeenCalledTimes(1);

  const newEditorState = editor.update.mock.calls[0][0];
  expect(newEditorState.getSelection()).toMatchSnapshot();
});

test('editor selectionstate is updated if new text matches current selection and user selected backwards', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelectionBackwards,
  );

  const editor = {
    _latestEditorState: editorState,
    props: {},
    update: jest.fn(),
  };

  onBeforeInput(editor, getInputEvent('A'));

  expect(editor.update).toHaveBeenCalledTimes(1);

  const newEditorState = editor.update.mock.calls[0][0];
  expect(newEditorState.getSelection()).toMatchSnapshot();
});

const HASHTAG_REGEX = /#[a-z]+/g;
function hashtagStrategy(contentBlock, callback, contentState) {
  findWithRegex(HASHTAG_REGEX, contentBlock, callback);
}

function findWithRegex(regex, contentBlock, callback) {
  const text = contentBlock.getText();
  let matchArr, start;
  while ((matchArr = regex.exec(text)) !== null) {
    start = matchArr.index;
    callback(start, start + matchArr[0].length);
  }
}

function testDecoratorFingerprint(
  text,
  selection,
  charToInsert,
  shouldPrevent,
) {
  const editorState = EditorState.acceptSelection(
    EditorState.set(getEditorState(text), {
      decorator: new CompositeDraftDecorator([
        {
          strategy: hashtagStrategy,
          component: null,
        },
      ]),
    }),
    new SelectionState({
      ...DEFAULT_SELECTION,
      anchorOffset: selection,
      focusOffset: selection,
    }),
  );

  const editor = {
    _latestEditorState: editorState,
    _latestCommittedEditorState: editorState,
    props: {},
    update: jest.fn(),
  };

  const ev = getInputEvent(charToInsert);
  onBeforeInput(editor, ev);

  expect(ev.preventDefault.mock.calls.length).toBe(shouldPrevent ? 1 : 0);
}

test('decorator fingerprint logic bails out of native insertion', () => {
  const oldGetSelection = global.getSelection;
  try {
    global.getSelection = () => ({});

    // Make sure we prevent native insertion in the right cases
    testDecoratorFingerprint('hi #', 4, 'f', true);
    testDecoratorFingerprint('x #foo', 3, '#', true);
    testDecoratorFingerprint('#foobar', 4, ' ', true);

    // but these are OK to let through
    testDecoratorFingerprint('#foo', 4, 'b', false);
    testDecoratorFingerprint('#foo bar #baz', 2, 'o', false);
    testDecoratorFingerprint('#foo bar #baz', 7, 'o', false);
    testDecoratorFingerprint('#foo bar #baz', 12, 'a', false);
  } finally {
    global.getSelection = oldGetSelection;
  }
});
<commit_msg>add flow declaration in editOnBeforeInput-test.js<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails oncall+draft_js
 * @flow
 * @format
 */

'use strict';

jest.disableAutomock();

import type DraftEditor from 'DraftEditor.react';

const CompositeDraftDecorator = require('CompositeDraftDecorator');
const ContentBlock = require('ContentBlock');
const ContentState = require('ContentState');
const EditorState = require('EditorState');
const SelectionState = require('SelectionState');

const onBeforeInput = require('editOnBeforeInput');

const DEFAULT_SELECTION = {
  anchorKey: 'a',
  anchorOffset: 0,
  focusKey: 'a',
  focusOffset: 0,
  isBackward: false,
};

const rangedSelection = new SelectionState({
  ...DEFAULT_SELECTION,
  focusOffset: 1,
});

const rangedSelectionBackwards = new SelectionState({
  ...DEFAULT_SELECTION,
  anchorOffset: 1,
  isBackward: true,
});

const getEditorState = (text: string = 'Arsenal') => {
  return EditorState.createWithContent(
    ContentState.createFromBlockArray([
      new ContentBlock({
        key: 'a',
        text,
      }),
    ]),
  );
};

const getDraftEditor = (obj): DraftEditor => (obj: any);

const getInputEvent = (data): SyntheticInputEvent<> =>
  ({
    data,
    preventDefault: jest.fn(),
  }: any);

test('editor is not updated if no character data is provided', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const editor = getDraftEditor({
    _latestEditorState: editorState,
    props: {},
    update: jest.fn(),
  });

  onBeforeInput(editor, getInputEvent());

  expect(editor.update).toHaveBeenCalledTimes(0);
});

test('editor is not updated if handled by handleBeforeInput', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const editor = getDraftEditor({
    _latestEditorState: editorState,
    props: {
      handleBeforeInput: () => true,
    },
    update: jest.fn(),
  });

  onBeforeInput(editor, getInputEvent('O'));

  expect(editor.update).toHaveBeenCalledTimes(0);
});

test('editor is updated with new text if it does not match current selection', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const update = jest.fn();
  const editor = getDraftEditor({
    _latestEditorState: editorState,
    props: {},
    update,
  });

  onBeforeInput(editor, getInputEvent('O'));

  expect(update).toHaveBeenCalledTimes(1);

  const newEditorState = update.mock.calls[0][0];
  expect(newEditorState.getCurrentContent()).toMatchSnapshot();
});

test('editor selectionstate is updated if new text matches current selection', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelection,
  );

  const update = jest.fn();
  const editor = getDraftEditor({
    _latestEditorState: editorState,
    props: {},
    update,
  });

  onBeforeInput(editor, getInputEvent('A'));

  expect(update).toHaveBeenCalledTimes(1);

  const newEditorState = update.mock.calls[0][0];
  expect(newEditorState.getSelection()).toMatchSnapshot();
});

test('editor selectionstate is updated if new text matches current selection and user selected backwards', () => {
  const editorState = EditorState.acceptSelection(
    getEditorState(),
    rangedSelectionBackwards,
  );

  const update = jest.fn();
  const editor = getDraftEditor({
    _latestEditorState: editorState,
    props: {},
    update,
  });

  onBeforeInput(editor, getInputEvent('A'));

  expect(update).toHaveBeenCalledTimes(1);

  const newEditorState = update.mock.calls[0][0];
  expect(newEditorState.getSelection()).toMatchSnapshot();
});

const HASHTAG_REGEX = /#[a-z]+/g;
function hashtagStrategy(contentBlock, callback, contentState) {
  findWithRegex(HASHTAG_REGEX, contentBlock, callback);
}

function findWithRegex(regex, contentBlock, callback) {
  const text = contentBlock.getText();
  let matchArr = regex.exec(text);
  while (matchArr !== null) {
    callback(matchArr.index, matchArr.index + matchArr[0].length);
    matchArr = regex.exec(text);
  }
}

function testDecoratorFingerprint(
  text,
  selection,
  charToInsert,
  shouldPrevent,
) {
  const editorState = EditorState.acceptSelection(
    EditorState.set(getEditorState(text), {
      decorator: new CompositeDraftDecorator([
        {
          strategy: hashtagStrategy,
          component: null,
        },
      ]),
    }),
    new SelectionState({
      ...DEFAULT_SELECTION,
      anchorOffset: selection,
      focusOffset: selection,
    }),
  );

  const editor = getDraftEditor({
    _latestEditorState: editorState,
    _latestCommittedEditorState: editorState,
    props: {},
    update: jest.fn(),
  });

  const ev = getInputEvent(charToInsert);
  onBeforeInput(editor, ev);

  expect(ev.preventDefault.mock.calls.length).toBe(shouldPrevent ? 1 : 0);
}

test('decorator fingerprint logic bails out of native insertion', () => {
  const oldGetSelection = global.getSelection;
  try {
    global.getSelection = () => ({});

    // Make sure we prevent native insertion in the right cases
    testDecoratorFingerprint('hi #', 4, 'f', true);
    testDecoratorFingerprint('x #foo', 3, '#', true);
    testDecoratorFingerprint('#foobar', 4, ' ', true);

    // but these are OK to let through
    testDecoratorFingerprint('#foo', 4, 'b', false);
    testDecoratorFingerprint('#foo bar #baz', 2, 'o', false);
    testDecoratorFingerprint('#foo bar #baz', 7, 'o', false);
    testDecoratorFingerprint('#foo bar #baz', 12, 'a', false);
  } finally {
    global.getSelection = oldGetSelection;
  }
});
<|endoftext|>


<commit_before>//
// Validation suite for JSON objects against a JSON Schema Draft v4
// schema. Each test returns an array (possibly empty) of errors.
//

'use strict';

var core = require('./core.js')
  , Errors = require('../../errors.js')
  , uri = require('../../uri.js')
  ;

// ******************************************************************
// Categories of properties we can validate.
// ******************************************************************
var PROPS_TO_VALIDATE = {
  general: ['enum', 'allOf', 'anyOf', 'oneOf', 'not'],
  array: ['items', 'additionalItems', 'maxItems', 'minItems', 'uniqueItems'],
  number: ['multipleOf', 'maximum', 'exclusiveMaximum', 'minimum',
      'exclusiveMinimum'],
  object: ['maxProperties', 'minProperties', 'required',
      'additionalProperties', 'properties', 'patternProperties',
      'dependencies'],
  string: ['maxLength', 'minLength', 'pattern', 'format']
};

// ******************************************************************
// Return a set of tests to apply to the instance.
// ******************************************************************
function getApplicableTests(config) {
  var result = [];
  var len, i, key;

  // general tests that apply to all types
  for (i = 0, len = PROPS_TO_VALIDATE.general.length; i !== len; ++i) {
    key = PROPS_TO_VALIDATE.general[i];
    if (Object.prototype.hasOwnProperty.call(config.schema, key)) {
      result.push(key);
    }
  }

  // type-specific tests
  var apparentType = core.apparentType(config.inst);
  if (apparentType === 'integer') { apparentType = 'number'; }
  var props = PROPS_TO_VALIDATE[apparentType] || [];
  for (i = 0, len = props.length; i !== len; ++i)
  {
    key = props[i];
    if (Object.prototype.hasOwnProperty.call(config.schema, key)) {
      result.push(key);
    }
  }

  // for objects: the properties, patternProperties, and
  // additionalProperties validations are inseparable
  if (result.indexOf('properties') !== -1 ||
      result.indexOf('patternProperties') !== -1 ||
      result.indexOf('additionalProperties') !== -1)
  {
    result.push('_propertiesImpl');
  }

  return result;
}

// ******************************************************************
// Run all applicable tests.
//
// config values:
//
//   inst: instance to validate
//   schema: schema to validate against
//   resolutionScope: resolutionScope,
//   instanceContext: current position within the overall instance
//   schemaRegistry: a SchemaRegistry
//   customFormatHandlers: custom handlers for the "format" keyword
//
// ******************************************************************
function run(config)
{
  var errors = [];
  var desc;

  var maxRefDepth = 10;   // avoid infinite loops
  var depth = 0;

  while (depth < maxRefDepth &&
         config.schema &&
         Object.prototype.hasOwnProperty.call(config.schema, '$ref'))
  {
    var ref = uri.resolve(config.resolutionScope,
      decodeURI(config.schema.$ref));
    config.schema = config.schemaRegistry.get(ref);

    if (!config.schema) {
      desc = 'schema not available: ' + ref;
      errors.push(new Errors.ValidationError(undefined, undefined, undefined,
        undefined, undefined, desc));
      return errors;
    }

    config.resolutionScope = ref;
    depth++;

    if (depth >= maxRefDepth) {
      desc = 'maximum nested $ref depth of ' + maxRefDepth + ' exceeded; ' +
        'possible $ref loop';
      errors.push(new Errors.ValidationError(undefined, config.instanceContext,
        '$ref', undefined, undefined, desc));
      return errors;
    }
  }

  // empty schema - bail early
  if (Object.keys(config.schema).length === 0) { return errors; }

  // validate the type: if it isn't valid we can bail early
  errors = errors.concat(require('./keywords/type.js')(config));
  if (errors.length) { return errors; }

  // test all applicable schema properties
  var props = getApplicableTests(config);
  for (var index = 0; index < props.length; ++index) {
    var prop = props[index];
    var fn = require('./keywords/' + prop + '.js');
    errors = errors.concat(fn(config));
  }
  return errors;
}

module.exports = run;
<commit_msg>Caches validator functions in hot path<commit_after>//
// Validation suite for JSON objects against a JSON Schema Draft v4
// schema. Each test returns an array (possibly empty) of errors.
//

'use strict';

var core = require('./core.js')
  , Errors = require('../../errors.js')
  , uri = require('../../uri.js')
  ;

// ******************************************************************
// Categories of properties we can validate.
// ******************************************************************
var PROPS_TO_VALIDATE = {
  general: ['enum', 'allOf', 'anyOf', 'oneOf', 'not'],
  array: ['items', 'additionalItems', 'maxItems', 'minItems', 'uniqueItems'],
  number: ['multipleOf', 'maximum', 'exclusiveMaximum', 'minimum',
      'exclusiveMinimum'],
  object: ['maxProperties', 'minProperties', 'required',
      'additionalProperties', 'properties', 'patternProperties',
      'dependencies'],
  string: ['maxLength', 'minLength', 'pattern', 'format']
};

var VALIDATOR_FUNCTIONS = {
  type: require('./keywords/type.js')
};

// ******************************************************************
// Return a set of tests to apply to the instance.
// ******************************************************************
function getApplicableTests(config) {
  var result = [];
  var len, i, key;

  // general tests that apply to all types
  for (i = 0, len = PROPS_TO_VALIDATE.general.length; i !== len; ++i) {
    key = PROPS_TO_VALIDATE.general[i];
    if (Object.prototype.hasOwnProperty.call(config.schema, key)) {
      result.push(key);
    }
  }

  // type-specific tests
  var apparentType = core.apparentType(config.inst);
  if (apparentType === 'integer') { apparentType = 'number'; }
  var props = PROPS_TO_VALIDATE[apparentType] || [];
  for (i = 0, len = props.length; i !== len; ++i)
  {
    key = props[i];
    if (Object.prototype.hasOwnProperty.call(config.schema, key)) {
      result.push(key);
    }
  }

  // for objects: the properties, patternProperties, and
  // additionalProperties validations are inseparable
  if (result.indexOf('properties') !== -1 ||
      result.indexOf('patternProperties') !== -1 ||
      result.indexOf('additionalProperties') !== -1)
  {
    result.push('_propertiesImpl');
  }

  return result;
}

// ******************************************************************
// Run all applicable tests.
//
// config values:
//
//   inst: instance to validate
//   schema: schema to validate against
//   resolutionScope: resolutionScope,
//   instanceContext: current position within the overall instance
//   schemaRegistry: a SchemaRegistry
//   customFormatHandlers: custom handlers for the "format" keyword
//
// ******************************************************************
function run(config)
{
  var errors = [];
  var desc;

  var maxRefDepth = 10;   // avoid infinite loops
  var depth = 0;

  while (depth < maxRefDepth &&
         config.schema &&
         Object.prototype.hasOwnProperty.call(config.schema, '$ref'))
  {
    var ref = uri.resolve(config.resolutionScope,
      decodeURI(config.schema.$ref));
    config.schema = config.schemaRegistry.get(ref);

    if (!config.schema) {
      desc = 'schema not available: ' + ref;
      errors.push(new Errors.ValidationError(undefined, undefined, undefined,
        undefined, undefined, desc));
      return errors;
    }

    config.resolutionScope = ref;
    depth++;

    if (depth >= maxRefDepth) {
      desc = 'maximum nested $ref depth of ' + maxRefDepth + ' exceeded; ' +
        'possible $ref loop';
      errors.push(new Errors.ValidationError(undefined, config.instanceContext,
        '$ref', undefined, undefined, desc));
      return errors;
    }
  }

  // empty schema - bail early
  if (Object.keys(config.schema).length === 0) { return errors; }

  // validate the type: if it isn't valid we can bail early
  errors = errors.concat(VALIDATOR_FUNCTIONS.type(config));
  if (errors.length) { return errors; }

  // test all applicable schema properties
  var props = getApplicableTests(config);
  for (var index = 0; index < props.length; ++index) {
    var prop = props[index];
    var fn = VALIDATOR_FUNCTIONS[prop];
    if (!fn) {
      fn = VALIDATOR_FUNCTIONS[prop] = require('./keywords/' + prop + '.js');
    }
    errors = errors.concat(fn(config));
  }
  return errors;
}

module.exports = run;
<|endoftext|>


<commit_before>'use strict';

const _          = require('lodash');
const browserify = require('browserify');
const cacheify   = require('cacheify');
const stream     = require('readable-stream');
const path       = require('path');
const babelify   = require('babelify');
const utils      = require('./utils/search_assets');
const no_parse   = require('./browserify_no_parse');
const no_babel   = require('./browserify_no_babelify');

const before = _.template(`
NodecaLoader.registerClientModule('<%= apiPath %>', function (N, exports, module, t) {`);
const after = '});';

module.exports = function (sandbox, callback) {
  let exclude = [];

  _.forEach(sandbox.config.packages, (pkg) => {
    _.forEach(pkg.vendor, (__, name) => {
      exclude.push(name);
    });
  });

  // To check if same module was included in multiple packages
  //
  // widget_path -> [ module_path ]
  //
  sandbox.included_modules = {};

  function browserify_concat_plugin(context, cb) {
    if (!_.trim(context.asset.source, '\n')) {
      cb();
      return;
    }

    let b = browserify({
      prelude: 'NodecaLoader.wrap',
      detectGlobals: false,
      noParse: no_parse
    });

    context.asset.__queue__.forEach(file_path => {
      let s = new stream.Transform();
      s.push(context.bundler.findAsset(file_path).source);
      s.end();

      b.add(s, { basedir: path.dirname(file_path), file: file_path });
    });

    b.transform(cacheify(babelify.configure({ ignore: no_babel }), sandbox.cache_db), { presets: [ 'es2015' ] });

    exclude.forEach(e => b.exclude(e));

    b.on('file', (path) => {
      context.asset.dependOnFile(path);
    });

    b.on('dep', function (row) {
      if (row.entry) {
        _.forEach(row.indexDeps, (index, dep_name) => {
          if (index) {
            let dep_path = utils.resolve_module_path(path.dirname(row.file), dep_name);

            sandbox.included_modules[row.file] = sandbox.included_modules[row.file] || [];
            sandbox.included_modules[row.file].push(dep_path);
          }
        });
      }
    });

    b.bundle((err, out) => {
      if (err) {
        cb(err);
        return;
      }

      context.asset.source = String(out);

      cb();
    });
  }

  _.forEach(sandbox.config.packages, (pkg, pkg_name) => {
    let widget_js = sandbox.bundler.createClass('concat', {
      logicalPath: 'public/package-component-widget-js-' + pkg_name + '.js',
      virtual: true,
      plugins: [ browserify_concat_plugin, 'macros' ]
    });

    _.forEach(pkg.files.widget_js, (file_info) => {
      let asset = sandbox.bundler.createClass('file', {
        logicalPath: file_info.path,
        virtual: true,
        plugins: [ 'load_text', 'auto', 'wrapper' ],
        wrapBefore: before({ apiPath: file_info.api_path }),
        wrapAfter: after
      });

      widget_js.push(asset);
    });

    sandbox.component_client[pkg_name].js.push(widget_js);

    _.forEach(pkg.files.js, (file_info) => {
      sandbox.bundler.createClass('file', {
        logicalPath: file_info.path,
        plugins: [ 'load_text', 'auto', 'macros' ]
      });
    });
  });

  callback();
};
<commit_msg>bundler: fixed babelify options<commit_after>'use strict';

const _          = require('lodash');
const browserify = require('browserify');
const cacheify   = require('cacheify');
const stream     = require('readable-stream');
const path       = require('path');
const babelify   = require('babelify');
const utils      = require('./utils/search_assets');
const no_parse   = require('./browserify_no_parse');
const no_babel   = require('./browserify_no_babelify');

const before = _.template(`
NodecaLoader.registerClientModule('<%= apiPath %>', function (N, exports, module, t) {`);
const after = '});';

module.exports = function (sandbox, callback) {
  let exclude = [];

  _.forEach(sandbox.config.packages, (pkg) => {
    _.forEach(pkg.vendor, (__, name) => {
      exclude.push(name);
    });
  });

  // To check if same module was included in multiple packages
  //
  // widget_path -> [ module_path ]
  //
  sandbox.included_modules = {};

  function browserify_concat_plugin(context, cb) {
    if (!_.trim(context.asset.source, '\n')) {
      cb();
      return;
    }

    let b = browserify({
      prelude: 'NodecaLoader.wrap',
      detectGlobals: false,
      noParse: no_parse
    });

    context.asset.__queue__.forEach(file_path => {
      let s = new stream.Transform();
      s.push(context.bundler.findAsset(file_path).source);
      s.end();

      b.add(s, { basedir: path.dirname(file_path), file: file_path });
    });

    b.transform(cacheify(babelify.configure({
      ignore: no_babel,
      global: true,
      presets: [ 'es2015' ]
    }), sandbox.cache_db));

    exclude.forEach(e => b.exclude(e));

    b.on('file', (path) => {
      context.asset.dependOnFile(path);
    });

    b.on('dep', function (row) {
      if (row.entry) {
        _.forEach(row.indexDeps, (index, dep_name) => {
          if (index) {
            let dep_path = utils.resolve_module_path(path.dirname(row.file), dep_name);

            sandbox.included_modules[row.file] = sandbox.included_modules[row.file] || [];
            sandbox.included_modules[row.file].push(dep_path);
          }
        });
      }
    });

    b.bundle((err, out) => {
      if (err) {
        cb(err);
        return;
      }

      context.asset.source = String(out);

      cb();
    });
  }

  _.forEach(sandbox.config.packages, (pkg, pkg_name) => {
    let widget_js = sandbox.bundler.createClass('concat', {
      logicalPath: 'public/package-component-widget-js-' + pkg_name + '.js',
      virtual: true,
      plugins: [ browserify_concat_plugin, 'macros' ]
    });

    _.forEach(pkg.files.widget_js, (file_info) => {
      let asset = sandbox.bundler.createClass('file', {
        logicalPath: file_info.path,
        virtual: true,
        plugins: [ 'load_text', 'auto', 'wrapper' ],
        wrapBefore: before({ apiPath: file_info.api_path }),
        wrapAfter: after
      });

      widget_js.push(asset);
    });

    sandbox.component_client[pkg_name].js.push(widget_js);

    _.forEach(pkg.files.js, (file_info) => {
      sandbox.bundler.createClass('file', {
        logicalPath: file_info.path,
        plugins: [ 'load_text', 'auto', 'macros' ]
      });
    });
  });

  callback();
};
<|endoftext|>


<commit_before>"use strict";

const Task = require('../Task'),
    del = require('del'),
    path = require('path');

class CleanTask extends Task {

    constructor(buildManager) {
        super(buildManager);
        this.command = "clean";
    }

    action() {
        var directoryToRemove = path.resolve(this._buildManager.getDistDirectory(),
            this._buildManager.isWatching() ? this._buildManager.currentTarget : "");
        return del([directoryToRemove, this._buildManager.getTemporaryDirectory()], {force: true});
    }
}

module.exports = CleanTask;<commit_msg>Clean task available only to frontend<commit_after>"use strict";

const Task = require('../Task'),
    del = require('del'),
    path = require('path'),
    ProjectType = require('../../ProjectType');

class CleanTask extends Task {

    constructor(buildManager) {
        super(buildManager);
        this.command = "clean";
        this.availableTo = [ProjectType.FRONTEND];
    }

    action() {
        var directoryToRemove = path.resolve(this._buildManager.getDistDirectory(),
            this._buildManager.isWatching() ? this._buildManager.currentTarget : "");
        return del([directoryToRemove, this._buildManager.getTemporaryDirectory()], {force: true});
    }
}

module.exports = CleanTask;<|endoftext|>


<commit_before>'use strict';

const TaskClass = require('./-base');

class ReadThermostatTask extends TaskClass {
  run(deviceId) {
    const ui = this.ui;
    const api = this.app.api;
    const table = this.ui.createTable();

    return api.thermostat.read(deviceId).then((thermostat) => {
      table.push(
        { 'ID': thermostat.id },
        { 'Name': thermostat.name },
        { 'State': thermostat.state },
        { 'Mode': thermostat.mode },
        { 'Humidity': thermostat.humidity },
        { 'Target Temperature': thermostat.targetTemperatureF + '°F' },
        { 'Target Temperature Low': thermostat.targetTemperatureLowF + '°F' },
        { 'Target Temperature High': thermostat.targetTemperatureHighF + '°F' }
      );

      ui.writeLine(table.toString());
    });
  }
}

module.exports = ReadThermostatTask;
<commit_msg>Update read-thermostat.js<commit_after>'use strict';

const TaskClass = require('./-base');

class ReadThermostatTask extends TaskClass {
  run(deviceId) {
    const ui = this.ui;
    const api = this.app.api;
    const table = this.ui.createTable();

    return api.thermostat.read(deviceId).then((thermostat) => {
      table.push(
        { 'ID': thermostat.id },
        { 'Name': thermostat.name },
        { 'State': thermostat.state },
        { 'Mode': thermostat.mode },
        { 'Humidity': thermostat.humidity },
        { 'Target Temperature': thermostat.targetTemperatureF + '°F' },
        { 'Target Temperature Low': thermostat.targetTemperatureLowF + '°F' },
        { 'Target Temperature High': thermostat.targetTemperatureHighF + '°F' }
      );

      ui.writeLine(table.toString());

      return thermostat;
    });
  }
}

module.exports = ReadThermostatTask;
<|endoftext|>


<commit_before>/**
 * Binds a CodeMirror widget to a <textarea> element.
 */
angular.module('ui.codemirror', [])
  .constant('uiCodemirrorConfig', {})
  .directive('uiCodemirror', ['uiCodemirrorConfig', function (uiCodemirrorConfig) {
    'use strict';

    return {
      restrict: 'EA',
      require: '?ngModel',
      priority: 1,
      compile: function compile(tElement, tAttrs, transclude) {

        // Require CodeMirror
        if (angular.isUndefined(window.CodeMirror)) {
          throw new Error('ui-codemirror need CodeMirror to work... (o rly?)');
        }

        // Create a codemirror instance with
        // - the function that will to place the editor into the document.
        // - the initial content of the editor.
        //   see http://codemirror.net/doc/manual.html#api_constructor
        var value = tElement.text();
        var codeMirror = new CodeMirror(function (cm_el) {

          angular.forEach(tElement.prop("attributes"), function (a) {
            if (a.name === "ui-codemirror") {
              cm_el.setAttribute("ui-codemirror-opts", a.textContent);
            } else {
              cm_el.setAttribute(a.name, a.textContent);
            }
          });

          // FIX replaceWith throw not parent Error !
          if (tElement.parent().length <= 0) {
            tElement.wrap("<div>");
          }

          tElement.replaceWith(cm_el);
        }, {value: value});

        return  function postLink(scope, iElement, iAttrs, ngModel) {

          var options, opts, onChange;

          options = uiCodemirrorConfig.codemirror || {};
          opts = angular.extend({}, options, scope.$eval(iAttrs.uiCodemirror), scope.$eval(iAttrs.uiCodemirrorOpts));

          function updateOptions(newValues) {
            for (var key in newValues) {
              if (newValues.hasOwnProperty(key)) {
                codeMirror.setOption(key, newValues[key]);
              }
            }
          }

          updateOptions(opts);

          if (angular.isDefined(scope.$eval(iAttrs.uiCodemirror))) {
            scope.$watch(iAttrs.uiCodemirror, updateOptions, true);
          }
          // Specialize change event
          codeMirror.on("change", function (instance, changeObj) {
            var newValue = instance.getValue();
            if (ngModel && newValue !== ngModel.$viewValue) {
              ngModel.$setViewValue(newValue);
            }
            if (!scope.$$phase) {
              scope.$apply();
            }
          });

          if (ngModel) {
            // CodeMirror expects a string, so make sure it gets one.
            // This does not change the model.
            ngModel.$formatters.push(function (value) {
              if (angular.isUndefined(value) || value === null) {
                return '';
              }
              else if (angular.isObject(value) || angular.isArray(value)) {
                throw new Error('ui-codemirror cannot use an object or an array as a model');
              }
              return value;
            });

            // Override the ngModelController $render method, which is what gets called when the model is updated.
            // This takes care of the synchronizing the codeMirror element with the underlying model, in the case that it is changed by something else.
            ngModel.$render = function () {
              //Code mirror expects a string so make sure it gets one
              //Although the formatter have already done this, it can be possible that another formatter returns undefined (for example the required directive)
              var safeViewValue = ngModel.$viewValue || '';
              codeMirror.setValue(safeViewValue);
            };
          }

          // Watch ui-refresh and refresh the directive
          if (iAttrs.uiRefresh) {
            scope.$watch(iAttrs.uiRefresh, function (newVal, oldVal) {
              // Skip the initial watch firing
              if (newVal !== oldVal) {
                  codeMirror.refresh();
              }
            });
          }

          // onLoad callback
          if (angular.isFunction(opts.onLoad)) {
            opts.onLoad(codeMirror);
          }
        };
      }
    };
  }]);
<commit_msg>patcch ui-codemirror to work with ng-repeat, refers to https://github.com/angular-ui/ui-codemirror/issues/24<commit_after>

/**
 * Binds a CodeMirror widget to a <textarea> element.
 */
angular.module('ui.codemirror', [])
  .constant('uiCodemirrorConfig', {})
  .directive('uiCodemirror', ['uiCodemirrorConfig', function (uiCodemirrorConfig) {
    'use strict';
    return {
      restrict: 'EA',
      require: '?ngModel',
      priority: 1,
      compile: function compile(tElement) {

        // Require CodeMirror
        if (angular.isUndefined(window.CodeMirror)) {
          throw new Error('ui-codemirror need CodeMirror to work... (o rly?)');
        }

        // Create a codemirror instance with
        // - the function that will to place the editor into the document.
        // - the initial content of the editor.
        //   see http://codemirror.net/doc/manual.html#api_constructor
        var value = tElement.text();

        return  function postLink(scope, iElement, iAttrs, ngModel) {

            var codeMirror = new window.CodeMirror(function (cm_el) {

                angular.forEach(tElement.prop('attributes'), function (a) {
                    if (a.name === 'ui-codemirror') {
                      cm_el.setAttribute('ui-codemirror-opts', a.textContent);
                    } else {
                      cm_el.setAttribute(a.name, a.textContent);
                    }
                });

              // FIX replaceWith throw not parent Error !
              if (tElement.parent().length <= 0) {
                  iElement.append(cm_el);
              }

//              iElement.append(cm_el);
                iElement.replaceWith(cm_el);
            }, {value: value});

          var options, opts;

          options = uiCodemirrorConfig.codemirror || {};
          opts = angular.extend({}, options, scope.$eval(iAttrs.uiCodemirror), scope.$eval(iAttrs.uiCodemirrorOpts));

          function updateOptions(newValues) {
            for (var key in newValues) {
              if (newValues.hasOwnProperty(key)) {
                codeMirror.setOption(key, newValues[key]);
              }
            }
          }

          updateOptions(opts);

          if (angular.isDefined(scope.$eval(iAttrs.uiCodemirror))) {
            scope.$watch(iAttrs.uiCodemirror, updateOptions, true);
          }
          // Specialize change event
          codeMirror.on('change', function (instance) {
            var newValue = instance.getValue();
            if (ngModel && newValue !== ngModel.$viewValue) {
              ngModel.$setViewValue(newValue);
            }
            if (!scope.$$phase) {
              scope.$apply();
            }
          });

          if (ngModel) {
            // CodeMirror expects a string, so make sure it gets one.
            // This does not change the model.
            ngModel.$formatters.push(function (value) {
              if (angular.isUndefined(value) || value === null) {
                return '';
              }
              else if (angular.isObject(value) || angular.isArray(value)) {
                throw new Error('ui-codemirror cannot use an object or an array as a model');
              }
              return value;
            });

            // Override the ngModelController $render method, which is what gets called when the model is updated.
            // This takes care of the synchronizing the codeMirror element with the underlying model, in the case that it is changed by something else.
            ngModel.$render = function () {
              //Code mirror expects a string so make sure it gets one
              //Although the formatter have already done this, it can be possible that another formatter returns undefined (for example the required directive)
              var safeViewValue = ngModel.$viewValue || '';
              codeMirror.setValue(safeViewValue);
            };
          }

          // Watch ui-refresh and refresh the directive
          if (iAttrs.uiRefresh) {
            scope.$watch(iAttrs.uiRefresh, function (newVal, oldVal) {
              // Skip the initial watch firing
              if (newVal !== oldVal) {
                  codeMirror.refresh();
              }
            });
          }

          // onLoad callback
          if (angular.isFunction(opts.onLoad)) {
            opts.onLoad(codeMirror);
          }
        };
      }
    };
  }]);
<|endoftext|>