<commit_before>import crypto from 'crypto';
import path from 'path';

import AppCache from 'parse-server/lib/cache';

const DeployInfoBasePath = "_deployInfo";
const DeployInfoCollectionName = "deployInfo";
const FilesCollectionName = "deployFile";

function computeChecksum(content){
    var algo = 'md5',
        md5sum = crypto.createHash(algo);
    md5sum.update(content, 'binary');
    return md5sum.digest('hex');
}

class ParseCliController {
  constructor(vendorAdapter: any){
    this.vendorAdapter = vendorAdapter;
  }

  getCliLatestVersion(){
    return this.vendorAdapter.getCliLatestVersion();
  }

  isSupported(mode, other, version){
    return new Promise((resolve, reject) => {
      // local commands must be defined
      var localCommands = [
        "default",
        "generate",
        "-h",
        "--help",
        "help",
        "migrate",
        "update",
        "version",
      ],
      commands = [
        "add",
        "configure accountkey",
        "deploy",
        "develop",
        "download",
        "functions",
        "jssdk",
        "list",
        "logs",
        "new",
        "releases",
        "rollback",
        "triggers",
        "cloud",
      ],
      nokCommands = [
        "symbols",
      ],
      tokens = {};
      localCommands.forEach(value => {
        tokens[value] = true;
      });
      commands.forEach(value => {
        tokens[value] = true;
      });

      if (mode == 'parse') {
        var parts = other.split(" ");
        for (var i = 1; i <= parts.length; i++) {
          var token = parts.slice(0, i).join(" ");
          if (tokens[token]) {
            resolve({});
            return;
          }
        }
      }
      reject();
    });
  }

  getAccountKey(email, password){
    return this.vendorAdapter.getAccountKey(email, password);
  }
  getEmail(accountKey){
    return this.vendorAdapter.getEmail(accountKey);
  }

  getJsVersions(appId){
    return this.vendorAdapter.getJsVersions(appId);
  }

  getApps(accountKey) {
    return this.vendorAdapter.getApps(accountKey);
  }

  getApp(accountKey, appId) {
    return this.vendorAdapter.getApp(accountKey, appId);
  }

  createApp(accountKey, appName) {
    return this.vendorAdapter.createApp(accountKey, appName);
  }

  getFile(appId, folder, filename, version, checksum){
    let config = AppCache.get(appId),
        query = { folder, filename, version, checksum };
    return config.databaseController.find(
      this.getCollectionName(appId, FilesCollectionName),
      query)
    .then(objects => {
      if (objects.length == 0) {
        throw new Error("Object " + JSON.stringify(query) + " not found.");
      }
      var object = objects[0];
      return config.filesController.getFileData(config, object.name)
      .then(data => {
        // Data is base64 encoded
        if (object.encoding == 'base64') {
          data = new Buffer(data.toString(), 'base64');
        }
        return data;
      });
    });
  }

  uploadFile(appId, folder, filename, content){

    let config = AppCache.get(appId),
        key = path.join(folder, filename),
        binaryContent = new Buffer(content, 'base64').toString('binary'),
        checksum = computeChecksum(binaryContent),
        filesCollectionName = this.getCollectionName(appId, FilesCollectionName);

    return config.databaseController.find(filesCollectionName, {
        folder: folder,
        filename: filename
      }, {
        sort: {createdAt: 1}
      })
    .then(objects => {
      var nextVersion;

      if (objects.length == 0) {
        nextVersion = 1;
      }
      else {
        var object = objects[0],
            currentVersion = object.version;
        nextVersion = parseInt(currentVersion) + 1;
      }
      return nextVersion.toString();
    })
    .then(version => config.filesController.createFile(
        config,
        path.join(this.getBasePath(appId, DeployInfoBasePath), key),
        content)
      .then(obj => config.databaseController.create(filesCollectionName, {
        version: version,
        checksum: checksum,
        folder: folder,
        filename: filename,
        name: obj.name,
        url: obj.url,
        encoding: 'base64',
      }))
      .then(() => {
        return {
          checksum: checksum,
          version: version
        };
      })
    );
  }

  _findDeployInfo(appId, query, sort) {
    let config = AppCache.get(appId);
    let options = {
      sort: sort,
      limit: 1
    }
    return config.databaseController.find(
      this.getCollectionName(appId, DeployInfoCollectionName),
      query,
      options)
    .then(results => {
      return results.map(deployInfo => {
        if (this.vendorAdapter.filterFiles) {
          this.vendorAdapter.filterFiles(deployInfo);
        }
        return this._unpatchDeployInfo(deployInfo);
      });
    })
    .then(results => {
      return results.length !== 0 ? results[0] : null;
    });
  }

  getDeployInfo(appId){
    return this._findDeployInfo(appId, {}, {deployedAt: -1, createdAt: -1});
  }

  _setDeployInfo(appId, deployInfo){
    let config = AppCache.get(appId);
    let className = this.getCollectionName(appId, DeployInfoCollectionName);
    let query = {releaseId: deployInfo.releaseId};
    let now = new Date().toISOString();
    if (deployInfo.createdAt) {
      return config.databaseController.update(
        className, query, {deployedAt: now});
    }
    else {
      deployInfo.deployedAt = deployInfo.createdAt = now;

      return config.databaseController.create(
        className,
        this._patchDeployInfo(deployInfo));
    }
  }

  // Patch deployInfo to avoid keys invalid chars, like dots in filenames.
  _patchDeployInfo(deployInfo){
    deployInfo.files = [];
    Object.keys(deployInfo.userFiles).forEach(folder => {
      Object.keys(deployInfo.userFiles[folder]).forEach(filename => {
        var version = deployInfo.userFiles[folder][filename],
          checksum = deployInfo.checksums[folder][filename];
        deployInfo.files.push({
          folder: folder,
          filename: filename,
          version: version,
          checksum: checksum
        });
      });
    });
    delete deployInfo.userFiles;
    delete deployInfo.checksums;
    return deployInfo;
  }

  _unpatchDeployInfo(deployInfo){
    deployInfo.userFiles = {};
    deployInfo.checksums = {};

    deployInfo.files.forEach(obj => {
      deployInfo.userFiles[obj.folder] = deployInfo.userFiles[obj.folder] || {};
      deployInfo.userFiles[obj.folder][obj.filename] = obj.version;

      deployInfo.checksums[obj.folder] = deployInfo.checksums[obj.folder] || {};
      deployInfo.checksums[obj.folder][obj.filename] = obj.checksum;
    });
    delete deployInfo.files;
    return deployInfo;
  }

  _collect(appId, deployInfo, folder){
    var promises = Object.keys(deployInfo.userFiles[folder] || {})
    .map(filename => {
      var version = deployInfo.userFiles[folder][filename],
        checksum = deployInfo.checksums[folder][filename];
      return this.getFile(appId, folder, filename, version, checksum)
      .then(data => {
        return this.vendorAdapter.collect(
          appId, deployInfo, folder, filename, data);
      });
    });

    return Promise.all(promises);
  }

  deploy(appId, deployInfo){
    return this._processRollback(appId, deployInfo)
    .then(di => {
      deployInfo = di;
      return this._processFiles(appId, di);
    })
    .then(() => {
      deployInfo._checksum = computeChecksum(JSON.stringify(deployInfo));
      return deployInfo;
    })
    .then(di => this._setReleaseMetadata(appId, di))
    .then(() => this._comparePackageVersion(appId, deployInfo))
    .then((packageInfo) => { deployInfo.packageChanged =
      packageInfo.packageChanged; })
    .then(() => this._collect(appId, deployInfo, 'cloud'))
    .then(() => this._collect(appId, deployInfo, 'public'))
    .then(() => this.vendorAdapter.publish(appId, deployInfo))
    .then(() => {
      delete deployInfo._checksum;
      delete deployInfo.packageChanged;
      return deployInfo;
    })
    .then(() => {
      return this._setDeployInfo(appId, deployInfo);
    })
    .then(() => deployInfo);
  }

  _getNextReleaseId(appId) {
    return this._findDeployInfo(appId, {}, {releaseId: -1})
    .then(deployInfo => {
      let currentReleaseId = deployInfo ? deployInfo.releaseId : 0;
      return parseInt(currentReleaseId) + 1;
    });
  }

  _setReleaseMetadata(appId, deployInfo) {
    if (deployInfo.releaseId) {
      return Promise.resolve(deployInfo);
    }
    return this._getNextReleaseId(appId)
    .then(deployInfoId => {
      deployInfo.releaseId = deployInfoId;
      deployInfo.releaseName = "v" + deployInfoId;
      return deployInfo;
    });
  }

  _processRollback(appId, deployInfo){
    if (deployInfo.releaseName === undefined) {
      // it is not a rollback
      return Promise.resolve(deployInfo);
    }
    else if (deployInfo.releaseName === "") {
      // rollback to deploy before the current one
      return this.getDeployInfo(appId)
      .then(deployInfo => this._findDeployInfo(
        appId,
        {releaseId: {"$lt": deployInfo.releaseId}},
        {releaseId: -1}));
    }
    else {
      return this._findDeployInfo(
        appId,
        {releaseName: deployInfo.releaseName},
        {deployedAt: -1});
    }
  }

  _processFiles(appId, deployInfo){
    if (!deployInfo.files){
      return Promise.resolve(deployInfo);
    }
    if (deployInfo.checksums || deployInfo.userFiles) {
      throw new Error("Files must not be defined with checksums and userFiles.");
    }

    // Prevent deploy request error from dashboard.
    if (deployInfo.checksums === undefined) {
      deployInfo.checksums = {};
    }
    if (deployInfo.userFiles === undefined) {
      deployInfo.userFiles = {};
    }

    var promises = Object.keys(deployInfo.files)
    .map(folder => {
      var folderPromises = Object.keys(deployInfo.files[folder] || {})
      .map(filename => {
        let content = deployInfo.files[folder][filename];

        return this.uploadFile(appId, folder, filename, content)
          .then(obj => {
            deployInfo.checksums[folder] = deployInfo.checksums[folder] || {};
            deployInfo.checksums[folder][filename] = obj.checksum;
            deployInfo.userFiles[folder] = deployInfo.userFiles[folder] || {};
            deployInfo.userFiles[folder][filename] = obj.version;
          });
      });
      return Promise.all(folderPromises);
    });
    return Promise.all(promises).then(() => {
      delete deployInfo.files;
      return deployInfo;
    });
  }

  _comparePackageVersion(appId, deployInfo) {
    let packageInfo = {};
    return this.getDeployInfo(appId)
      .then(deployInfo => this._findDeployInfo(
        appId,
        {releaseId: {"$lt": deployInfo.releaseId}},
        {releaseId: -1}))
      .then(oldDeployInfo => this._processFiles(appId, oldDeployInfo || {}))
      .then(oldDeployInfo => {
        packageInfo.packageChanged =
          !oldDeployInfo.checksums ||
          deployInfo.checksums['cloud'] &&
          oldDeployInfo.checksums['cloud'] &&
          deployInfo.checksums['cloud']['package.json'] !==
            oldDeployInfo.checksums['cloud']['package.json'];
        return packageInfo;
      })
      .then(() => {
        if (this.vendorAdapter.installModules && packageInfo.packageChanged) {
          return this.vendorAdapter.installModules(appId)
            .then(() => packageInfo);
        } else {
          packageInfo.packageChanged = false;
          return packageInfo;
        }
      });
  }

  getCollectionName(appId, collectionName) {
    return this.vendorAdapter.getCollectionName ?
      this.vendorAdapter.getCollectionName(appId, collectionName) :
      collectionName;
  }

  getBasePath(appId, basePath) {
    return this.vendorAdapter.getBasePath ?
      this.vendorAdapter.getBasePath(appId, basePath) :
      basePath;
  }

  getReleases(appId) {
    let config = AppCache.get(appId);
    return config.databaseController.find(
      this.getCollectionName(appId, DeployInfoCollectionName), {}, {
      sort: {createdAt: 1}
    })
    .then(results => {
      return results.map(deployInfo => {
        return this._unpatchDeployInfo(deployInfo);
      });
    })
    .then(results => {
      return results.map(deployInfo => {
        return {
          version: deployInfo.releaseName,
          description: deployInfo.description,
          timestamp: deployInfo.createdAt,
          // parse-cli expects a string for no reason
          userFiles: JSON.stringify(deployInfo.userFiles),
        };
      });
    });
  }
}

export default ParseCliController;
export {
    ParseCliController
};
<commit_msg>Fix Queijo<commit_after>import crypto from 'crypto';
import path from 'path';

import AppCache from 'parse-server/lib/cache';

const DeployInfoBasePath = "_deployInfo";
const DeployInfoCollectionName = "deployInfo";
const FilesCollectionName = "deployFile";

function computeChecksum(content){
    var algo = 'md5',
        md5sum = crypto.createHash(algo);
    md5sum.update(content, 'binary');
    return md5sum.digest('hex');
}

class ParseCliController {
  constructor(vendorAdapter: any){
    this.vendorAdapter = vendorAdapter;
  }

  getCliLatestVersion(){
    return this.vendorAdapter.getCliLatestVersion();
  }

  isSupported(mode, other, version){
    return new Promise((resolve, reject) => {
      // local commands must be defined
      var localCommands = [
        "default",
        "generate",
        "-h",
        "--help",
        "help",
        "migrate",
        "update",
        "version",
      ],
      commands = [
        "add",
        "configure accountkey",
        "deploy",
        "develop",
        "download",
        "functions",
        "jssdk",
        "list",
        "logs",
        "new",
        "releases",
        "rollback",
        "triggers",
        "cloud",
      ],
      nokCommands = [
        "symbols",
      ],
      tokens = {};
      localCommands.forEach(value => {
        tokens[value] = true;
      });
      commands.forEach(value => {
        tokens[value] = true;
      });

      if (mode == 'parse') {
        var parts = other.split(" ");
        for (var i = 1; i <= parts.length; i++) {
          var token = parts.slice(0, i).join(" ");
          if (tokens[token]) {
            resolve({});
            return;
          }
        }
      }
      reject();
    });
  }

  getAccountKey(email, password){
    return this.vendorAdapter.getAccountKey(email, password);
  }
  getEmail(accountKey){
    return this.vendorAdapter.getEmail(accountKey);
  }

  getJsVersions(appId){
    return this.vendorAdapter.getJsVersions(appId);
  }

  getApps(accountKey) {
    return this.vendorAdapter.getApps(accountKey);
  }

  getApp(accountKey, appId) {
    return this.vendorAdapter.getApp(accountKey, appId);
  }

  createApp(accountKey, appName) {
    return this.vendorAdapter.createApp(accountKey, appName);
  }

  getFile(appId, folder, filename, version, checksum){
    let config = AppCache.get(appId),
        query = { folder, filename, version, checksum };
    return config.databaseController.find(
      this.getCollectionName(appId, FilesCollectionName),
      query)
    .then(objects => {
      if (objects.length == 0) {
        throw new Error("Object " + JSON.stringify(query) + " not found.");
      }
      var object = objects[0];
      return config.filesController.getFileData(config, object.name)
      .then(data => {
        // Data is base64 encoded
        if (object.encoding == 'base64') {
          data = new Buffer(data.toString(), 'base64');
        }
        return data;
      });
    });
  }

  uploadFile(appId, folder, filename, content){

    let config = AppCache.get(appId),
        key = path.join(folder, filename),
        binaryContent = new Buffer(content, 'base64').toString('binary'),
        checksum = computeChecksum(binaryContent),
        filesCollectionName = this.getCollectionName(appId, FilesCollectionName);

    return config.databaseController.find(filesCollectionName, {
        folder: folder,
        filename: filename
      }, {
        sort: {createdAt: 1}
      })
    .then(objects => {
      var nextVersion;

      if (objects.length == 0) {
        nextVersion = 1;
      }
      else {
        var object = objects[0],
            currentVersion = object.version;
        nextVersion = parseInt(currentVersion) + 1;
      }
      return nextVersion.toString();
    })
    .then(version => config.filesController.createFile(
        config,
        path.join(this.getBasePath(appId, DeployInfoBasePath), key),
        content)
      .then(obj => config.databaseController.create(filesCollectionName, {
        version: version,
        checksum: checksum,
        folder: folder,
        filename: filename,
        name: obj.name,
        url: obj.url,
        encoding: 'base64',
      }))
      .then(() => {
        return {
          checksum: checksum,
          version: version
        };
      })
    );
  }

  _findDeployInfo(appId, query, sort) {
    let config = AppCache.get(appId);
    let options = {
      sort: sort,
      limit: 1
    }
    return config.databaseController.find(
      this.getCollectionName(appId, DeployInfoCollectionName),
      query,
      options)
    .then(results => {
      return results.map(deployInfo => {
        if (this.vendorAdapter.filterFiles) {
          this.vendorAdapter.filterFiles(deployInfo);
        }
        return this._unpatchDeployInfo(deployInfo);
      });
    })
    .then(results => {
      return results.length !== 0 ? results[0] : null;
    });
  }

  getDeployInfo(appId){
    return this._findDeployInfo(appId, {}, {deployedAt: -1, createdAt: -1});
  }

  _setDeployInfo(appId, deployInfo){
    let config = AppCache.get(appId);
    let className = this.getCollectionName(appId, DeployInfoCollectionName);
    let query = {releaseId: deployInfo.releaseId};
    let now = new Date().toISOString();
    if (deployInfo.createdAt) {
      return config.databaseController.update(
        className, query, {deployedAt: now});
    }
    else {
      deployInfo.deployedAt = deployInfo.createdAt = now;

      return config.databaseController.create(
        className,
        this._patchDeployInfo(deployInfo));
    }
  }

  // Patch deployInfo to avoid keys invalid chars, like dots in filenames.
  _patchDeployInfo(deployInfo){
    deployInfo.files = [];
    Object.keys(deployInfo.userFiles).forEach(folder => {
      Object.keys(deployInfo.userFiles[folder]).forEach(filename => {
        var version = deployInfo.userFiles[folder][filename],
          checksum = deployInfo.checksums[folder][filename];
        deployInfo.files.push({
          folder: folder,
          filename: filename,
          version: version,
          checksum: checksum
        });
      });
    });
    delete deployInfo.userFiles;
    delete deployInfo.checksums;
    return deployInfo;
  }

  _unpatchDeployInfo(deployInfo){
    deployInfo.userFiles = {};
    deployInfo.checksums = {};

    deployInfo.files.forEach(obj => {
      deployInfo.userFiles[obj.folder] = deployInfo.userFiles[obj.folder] || {};
      deployInfo.userFiles[obj.folder][obj.filename] = obj.version;

      deployInfo.checksums[obj.folder] = deployInfo.checksums[obj.folder] || {};
      deployInfo.checksums[obj.folder][obj.filename] = obj.checksum;
    });
    delete deployInfo.files;
    return deployInfo;
  }

  _collect(appId, deployInfo, folder){
    var promises = Object.keys(deployInfo.userFiles[folder] || {})
    .map(filename => {
      var version = deployInfo.userFiles[folder][filename],
        checksum = deployInfo.checksums[folder][filename];
      return this.getFile(appId, folder, filename, version, checksum)
      .then(data => {
        return this.vendorAdapter.collect(
          appId, deployInfo, folder, filename, data);
      });
    });

    return Promise.all(promises);
  }

  deploy(appId, deployInfo){
    return this._processRollback(appId, deployInfo)
    .then(di => {
      deployInfo = di;
      return this._processFiles(appId, di);
    })
    .then(() => {
      deployInfo._checksum = computeChecksum(JSON.stringify(deployInfo));
      return deployInfo;
    })
    .then(di => this._setReleaseMetadata(appId, di))
    .then(() => this._comparePackageVersion(appId, deployInfo))
    .then((packageInfo) => { deployInfo.packageChanged =
      packageInfo.packageChanged; })
    .then(() => this._collect(appId, deployInfo, 'cloud'))
    .then(() => this._collect(appId, deployInfo, 'public'))
    .then(() => this.vendorAdapter.publish(appId, deployInfo))
    .then(() => {
      delete deployInfo._checksum;
      delete deployInfo.packageChanged;
      return deployInfo;
    })
    .then(() => {
      return this._setDeployInfo(appId, deployInfo);
    })
    .then(() => deployInfo);
  }

  _getNextReleaseId(appId) {
    return this._findDeployInfo(appId, {}, {releaseId: -1})
    .then(deployInfo => {
      let currentReleaseId = deployInfo ? deployInfo.releaseId : 0;
      return parseInt(currentReleaseId) + 1;
    });
  }

  _setReleaseMetadata(appId, deployInfo) {
    if (deployInfo.releaseId) {
      return Promise.resolve(deployInfo);
    }
    return this._getNextReleaseId(appId)
    .then(deployInfoId => {
      deployInfo.releaseId = deployInfoId;
      deployInfo.releaseName = "v" + deployInfoId;
      return deployInfo;
    });
  }

  _processRollback(appId, deployInfo){
    if (deployInfo.releaseName === undefined) {
      // it is not a rollback
      return Promise.resolve(deployInfo);
    }
    else if (deployInfo.releaseName === "") {
      // rollback to deploy before the current one
      return this.getDeployInfo(appId)
      .then(deployInfo => this._findDeployInfo(
        appId,
        {releaseId: {"$lt": deployInfo.releaseId}},
        {releaseId: -1}));
    }
    else {
      return this._findDeployInfo(
        appId,
        {releaseName: deployInfo.releaseName},
        {deployedAt: -1});
    }
  }

  _processFiles(appId, deployInfo){
    if (!deployInfo.files){
      return Promise.resolve(deployInfo);
    }
    if (deployInfo.checksums || deployInfo.userFiles) {
      throw new Error("Files must not be defined with checksums and userFiles.");
    }

    // Prevent deploy request error from dashboard.
    if (deployInfo.checksums === undefined) {
      deployInfo.checksums = {};
    }
    if (deployInfo.userFiles === undefined) {
      deployInfo.userFiles = {};
    }

    var promises = Object.keys(deployInfo.files)
    .map(folder => {
      var folderPromises = Object.keys(deployInfo.files[folder] || {})
      .map(filename => {
        let content = deployInfo.files[folder][filename];

        return this.uploadFile(appId, folder, filename, content)
          .then(obj => {
            deployInfo.checksums[folder] = deployInfo.checksums[folder] || {};
            deployInfo.checksums[folder][filename] = obj.checksum;
            deployInfo.userFiles[folder] = deployInfo.userFiles[folder] || {};
            deployInfo.userFiles[folder][filename] = obj.version;
          });
      });
      return Promise.all(folderPromises);
    });
    return Promise.all(promises).then(() => {
      delete deployInfo.files;
      return deployInfo;
    });
  }

  _comparePackageVersion(appId, deployInfo) {
    let packageInfo = {};
    var promise = deployInfo ? Promise.resolve(deployInfo) : this.getDeployInfo(appId);
    return promise
      .then(deployInfo => this._findDeployInfo(
        appId,
        {releaseId: {"$lt": deployInfo.releaseId}},
        {releaseId: -1}))
      .then(oldDeployInfo => this._processFiles(appId, oldDeployInfo || {}))
      .then(oldDeployInfo => {
        packageInfo.packageChanged =
          !oldDeployInfo.checksums ||
          deployInfo.checksums['cloud'] &&
          oldDeployInfo.checksums['cloud'] &&
          deployInfo.checksums['cloud']['package.json'] !==
            oldDeployInfo.checksums['cloud']['package.json'];
        return packageInfo;
      })
      .then(() => {
        if (this.vendorAdapter.installModules && packageInfo.packageChanged) {
          return this.vendorAdapter.installModules(appId)
            .then(() => packageInfo);
        } else {
          packageInfo.packageChanged = false;
          return packageInfo;
        }
      });
  }

  getCollectionName(appId, collectionName) {
    return this.vendorAdapter.getCollectionName ?
      this.vendorAdapter.getCollectionName(appId, collectionName) :
      collectionName;
  }

  getBasePath(appId, basePath) {
    return this.vendorAdapter.getBasePath ?
      this.vendorAdapter.getBasePath(appId, basePath) :
      basePath;
  }

  getReleases(appId) {
    let config = AppCache.get(appId);
    return config.databaseController.find(
      this.getCollectionName(appId, DeployInfoCollectionName), {}, {
      sort: {createdAt: 1}
    })
    .then(results => {
      return results.map(deployInfo => {
        return this._unpatchDeployInfo(deployInfo);
      });
    })
    .then(results => {
      return results.map(deployInfo => {
        return {
          version: deployInfo.releaseName,
          description: deployInfo.description,
          timestamp: deployInfo.createdAt,
          // parse-cli expects a string for no reason
          userFiles: JSON.stringify(deployInfo.userFiles),
        };
      });
    });
  }
}

export default ParseCliController;
export {
    ParseCliController
};
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import autoBind from 'react-auto-bind'

export default class LazyCard extends PureComponent {
  constructor (props, context) {
    super(props, context)
    this.state = {
      image: props.lazyLoad ? props.defaultImage : (props.defaultImage || props.image)
    }

    autoBind(this)
  }

  componentDidMount () {
    if (this.props.autoLoad) this.load()
  }

  load () {
    const {image} = this.props
    if (image && this.state.image !== image) {
      const img = new Image()
      img.src = image
      img.onload = () => {
        this.setState({
          image
        })
      }
    }
  }

  render () {
    const {width, image, attributes, children, title, className} = this.props

    const style = {}

    if (width) style.width = width
    if (this.state.image) style.backgroundImage = `url('${this.state.image}')`

    const mainClass = classNames('rs-img', className, {
      'rs-loaded': this.state.image === image
    })

    return (

{children}

    )
  }
}

LazyCard.propTypes = {
  className: PropTypes.string,

  // additional attributes for the root node
  attributes: PropTypes.object,

  // should the component automatically lazy Load
  autoLoad: PropTypes.bool,

  // width of slide component
  width: PropTypes.number,

  // url of the main image
  image: PropTypes.string,

  // url of placeholder image
  defaultImage: PropTypes.string,

  // title for the image
  // serves similar to alt attribute for image
  // only for the purpose of SEO
  title: PropTypes.string,

  subTitle: PropTypes.string,

  lazyLoad: PropTypes.bool,

  data: PropTypes.object,

  children: PropTypes.oneOfType([
    PropTypes.array, PropTypes.object
  ])
}

LazyCard.defaultProps = {
  title: '',
  attributes: {},
  autoLoad: false,
  lazyLoad: true,
  data: {}
}
<commit_msg>fixed error when onload finish after component unmount; fixed onload not called when is loaded from cache; (#4)<commit_after>import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import autoBind from 'react-auto-bind'

export default class LazyCard extends PureComponent {
  constructor (props, context) {
    super(props, context)
    this.state = {
      image: props.lazyLoad ? props.defaultImage : (props.defaultImage || props.image)
    }

    autoBind(this)
  }

  componentDidMount () {
    if (this.props.autoLoad) this.load()
  }

  componentWillUnmount () {
    if (this.img) {
      this.img.onload = null
      this.img = null
    }
  }

  load () {
    const {image} = this.props
    if (image && this.state.image !== image) {
      const img = this.img = new Image()
      img.onload = () => {
        this.setState({
          image
        })
      }
      img.src = image
      if (img.complete) img.onload()
    }
  }

  render () {
    const {width, image, attributes, children, title, className} = this.props

    const style = {}

    if (width) style.width = width
    if (this.state.image) style.backgroundImage = `url('${this.state.image}')`

    const mainClass = classNames('rs-img', className, {
      'rs-loaded': this.state.image === image
    })

    return (

{children}

    )
  }
}

LazyCard.propTypes = {
  className: PropTypes.string,

  // additional attributes for the root node
  attributes: PropTypes.object,

  // should the component automatically lazy Load
  autoLoad: PropTypes.bool,

  // width of slide component
  width: PropTypes.number,

  // url of the main image
  image: PropTypes.string,

  // url of placeholder image
  defaultImage: PropTypes.string,

  // title for the image
  // serves similar to alt attribute for image
  // only for the purpose of SEO
  title: PropTypes.string,

  subTitle: PropTypes.string,

  lazyLoad: PropTypes.bool,

  data: PropTypes.object,

  children: PropTypes.oneOfType([
    PropTypes.array, PropTypes.object
  ])
}

LazyCard.defaultProps = {
  title: '',
  attributes: {},
  autoLoad: false,
  lazyLoad: true,
  data: {}
}
<|endoftext|>


<commit_before>const { AuthenticationBaseStrategy } = require('@feathersjs/authentication');
const { NotAuthenticated } = require('@feathersjs/errors');

const logger = require('../../../logger');
const {
	verifyToken,
	decryptToken,
	createUserAndAccount,
	findSchool,
	ENTITY_SOURCE, SOURCE_ID_ATTRIBUTE,
} = require('../../sync/strategies/TSP/TSP');
const { SYNCER_TARGET } = require('../../sync/strategies/TSP/TSPSchoolSyncer');

class TSPStrategy extends AuthenticationBaseStrategy {
	get configuration() {
		const authConfig = this.authentication.configuration;
		const config = super.configuration || {};
		return {
			service: authConfig.service,
			entity: authConfig.entity,
			entityId: authConfig.entityId,
			errorMessage: 'Invalid login',
			entityUsernameField: config.usernameField,
			...config,
		};
	}

	verifyConfiguration() {}

	async findUser(app, token) {
		const sourceOptions = {
			[SOURCE_ID_ATTRIBUTE]: token.authUID,
		};
		const [user] = await app.service('users').find({
			query: {
				source: ENTITY_SOURCE,
				sourceOptions,
				$limit: 1,
			},
			paginate: false,
		});
		return user;
	}

	async authenticate(authentication, params) {
		const { ticket } = authentication;
		let decryptedTicket;
		try {
			const verifiedToken = await verifyToken(ticket);
			decryptedTicket = await decryptToken(verifiedToken);
		} catch (err) {
			logger.error('TSP ticket not valid.', err);
			throw new NotAuthenticated('TSP ticket is not valid.');
		}

		const now = Date.now() / 1000;
		if (decryptedTicket.iat > now || now > decryptedTicket.exp) {
			throw new NotAuthenticated('TSP token expired.');
		}

		// translate TSP roles into SC roles
		let roles;
		if (decryptedTicket.ptscListRolle && typeof decryptedTicket.ptscListRolle === 'string') {
			roles = decryptedTicket.ptscListRolle.split(',').map((tspRole) => ({
				schueler: 'student',
				lehrer: 'teacher',
				admin: 'administrator',
			}[tspRole.toLowerCase()])).filter((r) => r);
		}

		const { app } = this;
		let user = await this.findUser(app, decryptedTicket);
		if (!user) {
			// User has been created since the last sync, so we'll create him from the ticket info
			const sourceOptions = {
				[SOURCE_ID_ATTRIBUTE]: decryptedTicket.authUID,
			};
			const school = await findSchool(app, decryptedTicket.ptscSchuleNummer);
			const systemId = school.systems[0];
			user = await createUserAndAccount(
				app,
				{
					namePrefix: decryptedTicket.personTitel,
					firstName: decryptedTicket.personVorname,
					lastName: decryptedTicket.personNachname,
					schoolId: school._id,
					source: ENTITY_SOURCE,
					sourceOptions,
				},
				roles,
				systemId,
			);
		} else if (Array.isArray(roles)) {
			// if we know the user and roles were supplied, we need to reflect role changes
			await app.service('users').patch(user._id, { roles });
		}

		const oneDayInMilliseconds = 864e5;
		const timeOfLastSync = Date.now() - oneDayInMilliseconds;

		// trigger an asynchronous TSP sync to reflect changes to classes
		app.service('sync').find({
			query: {
				target: SYNCER_TARGET,
				config: {
					schoolIdentifier: decryptedTicket.ptscSchuleNummer,
					lastChange: timeOfLastSync,
				},
			},
		});

		const [account] = await app.service('accounts').find({
			query: {
				userId: user._id,
				$limit: 1,
			},
			paginate: false,
		});
		const { entity } = this.configuration;
		return {
			authentication: { strategy: this.name },
			[entity]: account,
			payload: {
				accountId: account._id,
				userId: user._id,
				systemId: account.systemId,
			},
		};
	}
}

module.exports = TSPStrategy;
<commit_msg>Add comment<commit_after>const { AuthenticationBaseStrategy } = require('@feathersjs/authentication');
const { NotAuthenticated } = require('@feathersjs/errors');

const logger = require('../../../logger');
const {
	verifyToken,
	decryptToken,
	createUserAndAccount,
	findSchool,
	ENTITY_SOURCE, SOURCE_ID_ATTRIBUTE,
} = require('../../sync/strategies/TSP/TSP');
const { SYNCER_TARGET } = require('../../sync/strategies/TSP/TSPSchoolSyncer');

class TSPStrategy extends AuthenticationBaseStrategy {
	get configuration() {
		const authConfig = this.authentication.configuration;
		const config = super.configuration || {};
		return {
			service: authConfig.service,
			entity: authConfig.entity,
			entityId: authConfig.entityId,
			errorMessage: 'Invalid login',
			entityUsernameField: config.usernameField,
			...config,
		};
	}

	verifyConfiguration() {}

	async findUser(app, token) {
		const sourceOptions = {
			[SOURCE_ID_ATTRIBUTE]: token.authUID,
		};
		const [user] = await app.service('users').find({
			query: {
				source: ENTITY_SOURCE,
				sourceOptions,
				$limit: 1,
			},
			paginate: false,
		});
		return user;
	}

	async authenticate(authentication, params) {
		const { ticket } = authentication;
		let decryptedTicket;
		try {
			const verifiedToken = await verifyToken(ticket);
			decryptedTicket = await decryptToken(verifiedToken);
		} catch (err) {
			logger.error('TSP ticket not valid.', err);
			throw new NotAuthenticated('TSP ticket is not valid.');
		}

		const now = Date.now() / 1000;
		if (decryptedTicket.iat > now || now > decryptedTicket.exp) {
			throw new NotAuthenticated('TSP token expired.');
		}

		// translate TSP roles into SC roles
		let roles;
		if (decryptedTicket.ptscListRolle && typeof decryptedTicket.ptscListRolle === 'string') {
			roles = decryptedTicket.ptscListRolle.split(',').map((tspRole) => ({
				schueler: 'student',
				lehrer: 'teacher',
				admin: 'administrator',
			}[tspRole.toLowerCase()])).filter((r) => r);
		}

		const { app } = this;
		let user = await this.findUser(app, decryptedTicket);
		if (!user) {
			// User has been created since the last sync, so we'll create him from the ticket info
			const sourceOptions = {
				[SOURCE_ID_ATTRIBUTE]: decryptedTicket.authUID,
			};
			const school = await findSchool(app, decryptedTicket.ptscSchuleNummer);
			const systemId = school.systems[0];
			user = await createUserAndAccount(
				app,
				{
					namePrefix: decryptedTicket.personTitel,
					firstName: decryptedTicket.personVorname,
					lastName: decryptedTicket.personNachname,
					schoolId: school._id,
					source: ENTITY_SOURCE,
					sourceOptions,
				},
				roles,
				systemId,
			);
		} else if (Array.isArray(roles)) {
			// if we know the user and roles were supplied, we need to reflect role changes
			await app.service('users').patch(user._id, { roles });
		}

		const oneDayInMilliseconds = 864e5;
		const timeOfLastSync = Date.now() - oneDayInMilliseconds;

		// trigger an asynchronous TSP sync to reflect changes to classes
		app.service('sync').find({
			query: {
				target: SYNCER_TARGET,
				config: {
					schoolIdentifier: decryptedTicket.ptscSchuleNummer,
					lastChange: timeOfLastSync,
				},
			},
		});

		// find account and generate JWT payload
		const [account] = await app.service('accounts').find({
			query: {
				userId: user._id,
				$limit: 1,
			},
			paginate: false,
		});
		const { entity } = this.configuration;
		return {
			authentication: { strategy: this.name },
			[entity]: account,
			payload: {
				accountId: account._id,
				userId: user._id,
				systemId: account.systemId,
			},
		};
	}
}

module.exports = TSPStrategy;
<|endoftext|>


<commit_before>try {
	w = [
		windows.create(),                     // no tabs, no properties

		windows.create(null),                 // null tabs, no properties
		windows.create(null, null),           // null tabs, null properties
		windows.create(null, undefined),      // null tabs, undefined properties
		windows.create(null, {}),             // null tabs, empty properties

		windows.create(undefined),            // undefined tabs, no properties
		windows.create(undefined, null),      // undefined tabs, null properties
		windows.create(undefined, undefined), // undefined tabs, undefined properties
		windows.create(undefined, {}),        // undefined tabs, empty properties

		windows.create([]),                   // empty tabs, no properties
		windows.create([], null),             // empty tabs, null properties
		windows.create([], undefined),        // empty tabs, undefined properties
		windows.create([], {})                // empty tabs, empty properties
	];
} catch(e) {
	w = [ ]
}

// Global window manager API
test(function() { assert_exists(windows.__proto__, "create", "create method"); }, "Interface check: BrowserWindowManager.create");
test(function() { assert_exists(windows.__proto__, "getAll", "getAll method"); }, "Interface check: BrowserWindowManager.getAll");
test(function() { assert_exists(windows.__proto__, "getLastFocused", "getLastFocused method"); }, "Interface check: BrowserWindowManager.getLastFocused");

// BrowserWindow objects and IDs
test(function() {
	assert_equals(w.length, 13, "Created windows");

	for (var i = 0; i < w.length; i++) {
		assert_true(w[i] !== undefined, "BrowserWindowManager.create() returned an object");
		assert_exists(w[i], "id", "BrowserWindow [" + i + "] has an id")
	}
}, "Creating windows returns BrowserWindow objects");

test(function() {
	var ids = [];
	for (var i = 0; i < w.length; i++) {
		assert_true(ids.indexOf(w[i].id) === -1, "ID for window [" + i + "] is unique");
		ids.push(w[i].id);
	}
}, "Window IDs should be unique");
<commit_msg>added wrapper opera.isReady to test.js<commit_after>opera.isReady(function() {
    try {
        w = [ windows.create(), // no tabs, no properties

        windows.create(null), // null tabs, no properties
        windows.create(null, null), // null tabs, null properties
        windows.create(null, undefined), // null tabs, undefined properties
        windows.create(null, {}), // null tabs, empty properties

        windows.create(undefined), // undefined tabs, no properties
        windows.create(undefined, null), // undefined tabs, null properties
        windows.create(undefined, undefined), // undefined tabs, undefined
        // properties
        windows.create(undefined, {}), // undefined tabs, empty properties

        windows.create([]), // empty tabs, no properties
        windows.create([], null), // empty tabs, null properties
        windows.create([], undefined), // empty tabs, undefined properties
        windows.create([], {}) // empty tabs, empty properties
        ];
    } catch (e) {
        w = []
    }

    // Global window manager API
    test(function() {
        assert_exists(windows.__proto__, "create", "create method");
    }, "Interface check: BrowserWindowManager.create");
    test(function() {
        assert_exists(windows.__proto__, "getAll", "getAll method");
    }, "Interface check: BrowserWindowManager.getAll");
    test(function() {
        assert_exists(windows.__proto__, "getLastFocused", "getLastFocused method");
    }, "Interface check: BrowserWindowManager.getLastFocused");

    // BrowserWindow objects and IDs
    test(function() {
        assert_equals(w.length, 13, "Created windows");

        for ( var i = 0; i < w.length; i++) {
            assert_true(w[i] !== undefined, "BrowserWindowManager.create() returned an object");
            assert_exists(w[i], "id", "BrowserWindow [" + i + "] has an id")
        }
    }, "Creating windows returns BrowserWindow objects");

    test(function() {
        var ids = [];
        for ( var i = 0; i < w.length; i++) {
            assert_true(ids.indexOf(w[i].id) === -1, "ID for window [" + i + "] is unique");
            ids.push(w[i].id);
        }
    }, "Window IDs should be unique");
});
<|endoftext|>


<commit_before>/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF 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 path  = require('path'),
    fs  = require('fs'),
    et = require('elementtree'),
    shell = require('shelljs'),
    ConfigParser = require('./ConfigParser');

var ROOT = path.join(__dirname, '..', '..'),
    accessRules;

module.exports.applyPlatformConfig = function () {
    console.log('Applying Platform Config...');

    var config = new ConfigParser(path.join(ROOT, 'config.xml'));

    accessRules = config.getAccessRules().filter(function(rule) {
        if (rule.indexOf('https://') === 0 || rule == '*') {
            return true;
        } else {
            console.log('Access rules must begin with "https://", the following rule will be ignored: ' + rule);
        }
        return false;
    });

    // Apply appxmanifest changes
    [{ fileName: 'package.windows.appxmanifest',   namespacePrefix: 'm2:' },
     { fileName: 'package.windows80.appxmanifest', namespacePrefix: '' },
     { fileName: 'package.phone.appxmanifest',     namespacePrefix: 'm3:' }].forEach(
        function(manifestFile) {
            updateManifestFile(config, path.join(ROOT, manifestFile.fileName), manifestFile.namespacePrefix);
    });

    // Apply jsproj changes
    ['CordovaApp.Phone.jsproj', 'CordovaApp.Windows.jsproj', 'CordovaApp.Windows80.jsproj'].forEach(
        function(jsprojFile) {
            updatejsprojFile(config, path.join(ROOT, jsprojFile));
    });

    copyImages(config);
};

function updatejsprojFile(config, jsProjFilePath) {
    var defaultLocale = config.defaultLocale() || 'en-US';

    var contents = fs.readFileSync(jsProjFilePath, 'utf-8');
    // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
    if (contents.charCodeAt(0) === 0xFEFF) {
        contents = contents.slice(1);
    }
    var jsProjXML =  new et.ElementTree(et.XML(contents));
    var jsProjDefaultLocale = jsProjXML.find('./PropertyGroup/DefaultLanguage');
    jsProjDefaultLocale.text = defaultLocale;
    fs.writeFileSync(jsProjFilePath, jsProjXML.write({indent: 2}), 'utf-8');
}

function updateManifestFile (config, manifestPath, namespacePrefix) {

    var contents = fs.readFileSync(manifestPath, 'utf-8');
    if(contents) {
        //Windows is the BOM. Skip the Byte Order Mark.
        contents = contents.substring(contents.indexOf('<'));
    }

    var manifest =  new et.ElementTree(et.XML(contents));

    applyCoreProperties(config, manifest, manifestPath, namespacePrefix);
    // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition
    sortCapabilities(manifest);
    applyAccessRules(config, manifest);
    applyBackgroundColor(config, manifest, namespacePrefix);

    //Write out manifest
    fs.writeFileSync(manifestPath, manifest.write({indent: 4}), 'utf-8');
}

function applyCoreProperties(config, manifest, manifestPath, xmlnsPrefix) {
    var version = fixConfigVersion(config.version());
    var name = config.name();
    var pkgName = config.packageName();
    var author = config.author();
    var startPage = config.startPage();

    if (!startPage) {
        // If not specified, set default value
        // http://cordova.apache.org/docs/en/edge/config_ref_index.md.html#The%20config.xml%20File
        startPage = 'index.html';
    }

    var identityNode = manifest.find('.//Identity');
    if(!identityNode) {
        throw new Error('Invalid manifest file (no <Identity> node): ' + manifestPath);
    }
    // Update identity name and version
    if (pkgName) {
        (identityNode.attrib.Name = pkgName);
    }
    if (version) {
        (identityNode.attrib.Version = version);
    }

    // Update name (windows8 has it in the Application[@Id] and Application.VisualElements[@DisplayName])
    var app = manifest.find('.//Application');
    if(!app) {
        throw new Error('Invalid manifest file (no <Application> node): ' + manifestPath);
    }
    if (pkgName) {
        // 64 symbols restriction goes from manifest schema definition
        // http://msdn.microsoft.com/en-us/library/windows/apps/br211415.aspx
        var appId = pkgName.length <= 64 ? pkgName : pkgName.substr(0, 64);
        app.attrib.Id = appId;
    }
    app.attrib.StartPage = 'www/' + startPage;

    var visualElementsName = './/' + xmlnsPrefix + 'VisualElements';
    var visualElems = manifest.find(visualElementsName);

    if(!visualElems) {
        throw new Error('Invalid manifest file (no <' + xmlnsPrefix + 'VisualElements> node): ' + manifestPath);
    }
    if (name) {
        (visualElems.attrib.DisplayName = name);
    }

    // Update properties
    var properties = manifest.find('.//Properties');
    if (properties && properties.find) {
        var displayNameElement = properties.find('.//DisplayName');
        if (displayNameElement && name) {
            displayNameElement.text = name;
        }

        var publisherNameElement = properties.find('.//PublisherDisplayName');
        if (publisherNameElement && author) {
            publisherNameElement.text = author;
        }
    }

    // Supported orientations
    var rotationPreferenceName = xmlnsPrefix + 'Rotation';
    var rotationPreferenceRootName = xmlnsPrefix + 'InitialRotationPreference';

    var orientation = config.getPreference('Orientation');
    var rotationPreferenceRoot;
    if (orientation) {
        rotationPreferenceRoot = manifest.find('.//' + rotationPreferenceRootName);
        if(rotationPreferenceRoot === null) {
            visualElems.append(et.Element(rotationPreferenceRootName));
            rotationPreferenceRoot = manifest.find('.//' + rotationPreferenceRootName);
        }

        rotationPreferenceRoot.clear();

        var applyOrientations = function(orientationsArr) {
            orientationsArr.forEach(function(orientationValue) {
                var el = et.Element(rotationPreferenceName);
                el.attrib.Preference = orientationValue;
                rotationPreferenceRoot.append(el);
            });
        };

        // Updates supported orientations
        //<InitialRotationPreference>
        //    <Rotation Preference = "portrait" | "landscape" | "portraitFlipped" | "landscapeFlipped" /> {1,4}
        //</InitialRotationPreference>
        if(orientation === 'default') {
            // This means landscape and portrait
            applyOrientations(['portrait', 'landscape', 'landscapeFlipped']);
        } else if(orientation === 'portrait') {
            applyOrientations(['portrait']);
        } else if(orientation === 'landscape') {
            applyOrientations(['landscape', 'landscapeFlipped']);
        } else { // Platform-specific setting like "portrait,landscape,portraitFlipped"
            applyOrientations(orientation.split(','));
        }
    } else {
        // Remove InitialRotationPreference root element to revert to defaults
        rotationPreferenceRoot = visualElems.find('.//' + rotationPreferenceRootName);
        if(rotationPreferenceRoot !== null) {
            visualElems.remove(null, rotationPreferenceRoot);
        }
    }
}

// Adjust version number as per CB-5337 Windows8 build fails due to invalid app version
function fixConfigVersion (version) {
    if(version && version.match(/\.\d/g)) {
        var numVersionComponents = version.match(/\.\d/g).length + 1;
        while (numVersionComponents++ < 4) {
            version += '.0';
        }
    }
    return version;
}

function applyAccessRules (config, manifest) {
    // Updates WhiteListing rules
    //<ApplicationContentUriRules>
    //    <Rule Match="https://www.example.com" Type="include"/>
    //</ApplicationContentUriRules>
    var appUriRulesRoot = manifest.find('.//Application'),
        appUriRules = appUriRulesRoot.find('.//ApplicationContentUriRules');

    if (appUriRules !== null) {
        appUriRulesRoot.remove(null, appUriRules);
    }
    // rules are not defined or allow any
    if (accessRules.length === 0 || accessRules.indexOf('*') > -1) {
        return;
    }

    appUriRules = et.Element('ApplicationContentUriRules');
    appUriRulesRoot.append(appUriRules);

    accessRules.forEach(function(rule) {
        var el = et.Element('Rule');
        el.attrib.Match = rule;
        el.attrib.Type = 'include';
        appUriRules.append(el);
    });
}

function sortCapabilities(manifest) {

    // removes namespace prefix (m3:Capability -> Capability)
    // this is required since elementtree returns qualified name with namespace
    function extractLocalName(tag) {
        return tag.split(':').pop(); // takes last part of string after ':'
    }

    var capabilitiesRoot = manifest.find('.//Capabilities'),
        capabilities = capabilitiesRoot._children || [];
    // to sort elements we remove them and then add again in the appropriate order
    capabilities.forEach(function(elem) { // no .clear() method
        capabilitiesRoot.remove(0, elem);
        // CB-7601 we need local name w/o namespace prefix to sort capabilities correctly
        elem.localName = extractLocalName(elem.tag);
    });
    capabilities.sort(function(a, b) {
        return (a.localName > b.localName) ? 1: -1;
    });
    capabilities.forEach(function(elem) {
        capabilitiesRoot.append(elem);
    });
}

function copyImages(config) {
    var platformRoot = ROOT;
    // TODO find the way to detect whether command was triggered by CLI or not
    var appRoot = path.join(platformRoot, '..', '..');

    function copyImage(src, dest) {
        src = path.join(appRoot, src);
        dest = path.join(platformRoot, 'images', dest);
        //console.log('Copying image from ' + src + ' to ' + dest);
        shell.cp('-f', src, dest);
    }

    function copyMrtImage(src, dest) {
        var srcDir = path.dirname(src),
            srcExt = path.extname(src),
            srcFileName = path.basename(src, srcExt);

        var destExt = path.extname(dest),
            destFileName = path.basename(dest, destExt);

        // all MRT images: logo.png, logo.scale-100.png, logo.scale-200.png, etc
        var images = fs.readdirSync(srcDir).filter(function(e) {
            return e.match('^'+srcFileName + '(.scale-[0-9]+)?' + srcExt);
        });
        // warn if no images found
        if (images.length === 0) {
            console.log('No images found for target: ' + destFileName);
            return;
        }
        // copy images with new name but keeping scale suffix
        images.forEach(function(img) {
            var scale = path.extname(path.basename(img, srcExt));
            if (scale === '') {
                scale = '.scale-100';
            }
            copyImage(path.join(srcDir, img), destFileName+scale+destExt);
        });

    }

    // Platform default images
    var platformImages = [
        {dest: 'Square150x150Logo.scale-100.png', width: 150, height: 150},
        {dest: 'Square30x30Logo.scale-100.png', width: 30, height: 30},
        {dest: 'StoreLogo.scale-100.png', width: 50, height: 50},
        {dest: 'SplashScreen.scale-100.png', width: 620, height: 300},
        // scaled images are specified here for backward compatibility only so we can find them by size
        {dest: 'StoreLogo.scale-240.png', width: 120, height: 120},
        {dest: 'Square44x44Logo.scale-240.png', width: 106, height: 106},
        {dest: 'Square71x71Logo.scale-240.png', width: 170, height: 170},
        {dest: 'Square70x70Logo.scale-100.png', width: 70, height: 70},
        {dest: 'Square150x150Logo.scale-240.png', width: 360, height: 360},
        {dest: 'Square310x310Logo.scale-100.png', width: 310, height: 310},
        {dest: 'Wide310x150Logo.scale-100.png', width: 310, height: 150},
        {dest: 'Wide310x150Logo.scale-240.png', width: 744, height: 360},
        {dest: 'SplashScreenPhone.scale-240.png', width: 1152, height: 1920}
    ];

    function findPlatformImage(width, height) {
        if (!width && !height){
            // this could be default image,
            // Windows requires specific image dimension so we can't apply it
            return null;
        }
        for (var idx in platformImages){
            var res = platformImages[idx];
            // If only one of width or height is not specified, use another parameter for comparation
            // If both specified, compare both.
            if ((!width || (width == res.width)) &&
                (!height || (height == res.height))){
                return res;
            }
        }
        return null;
    }

    var images = config.getIcons().concat(config.getSplashScreens());

    images.forEach(function (img) {
        if (img.target) {
            copyMrtImage(img.src, img.target + '.png');
        } else {
            // find target image by size
            var targetImg = findPlatformImage (img.width, img.height);
            if (targetImg) {
                copyImage(img.src, targetImg.dest);
            } else {
                console.log('The following image is skipped due to unsupported size: ' + img.src);
            }
        }
    });
}

function applyBackgroundColor (config, manifest, xmlnsPrefix) {
    var visualElems =null;

    function refineColor(color) {
        // return three-byte hexadecimal number preceded by "#" (required for Windows)
        color = color.replace('0x', '').replace('#', '');
        if (color.length == 3) {
            color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
        }
        // alpha is not supported, so we remove it
        if (color.length == 8) { // AArrggbb
            color = color.slice(2);
        }
        return '#' + color;
    }
    // background color
    var bgColor = config.getPreference('BackgroundColor');
    if (bgColor) {
        var visualElementsName = './/' + xmlnsPrefix + 'VisualElements';
        visualElems = manifest.find(visualElementsName);
        visualElems.attrib.BackgroundColor = refineColor(bgColor);
    }

    // Splash Screen background color
    bgColor = config.getPreference('SplashScreenBackgroundColor');
    if (bgColor) {
        var splashScreenElementsName = './/' + xmlnsPrefix + 'SplashScreen';
        visualElems = manifest.find(splashScreenElementsName);
        visualElems.attrib.BackgroundColor = refineColor(bgColor);
    }
}
<commit_msg>CB-8946: Added the "WindowsToastCapable" preference to indicate that the app can support toasts.  This is to support the Local Notifications plugin.<commit_after>/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF 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 path  = require('path'),
    fs  = require('fs'),
    et = require('elementtree'),
    shell = require('shelljs'),
    ConfigParser = require('./ConfigParser');

var ROOT = path.join(__dirname, '..', '..'),
    accessRules;

module.exports.applyPlatformConfig = function () {
    console.log('Applying Platform Config...');

    var config = new ConfigParser(path.join(ROOT, 'config.xml'));

    accessRules = config.getAccessRules().filter(function(rule) {
        if (rule.indexOf('https://') === 0 || rule == '*') {
            return true;
        } else {
            console.log('Access rules must begin with "https://", the following rule will be ignored: ' + rule);
        }
        return false;
    });

    // Apply appxmanifest changes
    [{ fileName: 'package.windows.appxmanifest',   namespacePrefix: 'm2:' },
     { fileName: 'package.windows80.appxmanifest', namespacePrefix: '' },
     { fileName: 'package.phone.appxmanifest',     namespacePrefix: 'm3:' }].forEach(
        function(manifestFile) {
            updateManifestFile(config, path.join(ROOT, manifestFile.fileName), manifestFile.namespacePrefix);
    });

    // Apply jsproj changes
    ['CordovaApp.Phone.jsproj', 'CordovaApp.Windows.jsproj', 'CordovaApp.Windows80.jsproj'].forEach(
        function(jsprojFile) {
            updatejsprojFile(config, path.join(ROOT, jsprojFile));
    });

    copyImages(config);
};

function updatejsprojFile(config, jsProjFilePath) {
    var defaultLocale = config.defaultLocale() || 'en-US';

    var contents = fs.readFileSync(jsProjFilePath, 'utf-8');
    // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
    if (contents.charCodeAt(0) === 0xFEFF) {
        contents = contents.slice(1);
    }
    var jsProjXML =  new et.ElementTree(et.XML(contents));
    var jsProjDefaultLocale = jsProjXML.find('./PropertyGroup/DefaultLanguage');
    jsProjDefaultLocale.text = defaultLocale;
    fs.writeFileSync(jsProjFilePath, jsProjXML.write({indent: 2}), 'utf-8');
}

function updateManifestFile (config, manifestPath, namespacePrefix) {

    var contents = fs.readFileSync(manifestPath, 'utf-8');
    if(contents) {
        //Windows is the BOM. Skip the Byte Order Mark.
        contents = contents.substring(contents.indexOf('<'));
    }

    var manifest =  new et.ElementTree(et.XML(contents));

    applyCoreProperties(config, manifest, manifestPath, namespacePrefix);
    // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition
    sortCapabilities(manifest);
    applyAccessRules(config, manifest);
    applyBackgroundColor(config, manifest, namespacePrefix);
    applyToastCapability(config, manifest, namespacePrefix);

    //Write out manifest
    fs.writeFileSync(manifestPath, manifest.write({indent: 4}), 'utf-8');
}

function applyCoreProperties(config, manifest, manifestPath, xmlnsPrefix) {
    var version = fixConfigVersion(config.version());
    var name = config.name();
    var pkgName = config.packageName();
    var author = config.author();
    var startPage = config.startPage();

    if (!startPage) {
        // If not specified, set default value
        // http://cordova.apache.org/docs/en/edge/config_ref_index.md.html#The%20config.xml%20File
        startPage = 'index.html';
    }

    var identityNode = manifest.find('.//Identity');
    if(!identityNode) {
        throw new Error('Invalid manifest file (no <Identity> node): ' + manifestPath);
    }
    // Update identity name and version
    if (pkgName) {
        (identityNode.attrib.Name = pkgName);
    }
    if (version) {
        (identityNode.attrib.Version = version);
    }

    // Update name (windows8 has it in the Application[@Id] and Application.VisualElements[@DisplayName])
    var app = manifest.find('.//Application');
    if(!app) {
        throw new Error('Invalid manifest file (no <Application> node): ' + manifestPath);
    }
    if (pkgName) {
        // 64 symbols restriction goes from manifest schema definition
        // http://msdn.microsoft.com/en-us/library/windows/apps/br211415.aspx
        var appId = pkgName.length <= 64 ? pkgName : pkgName.substr(0, 64);
        app.attrib.Id = appId;
    }
    app.attrib.StartPage = 'www/' + startPage;

    var visualElementsName = './/' + xmlnsPrefix + 'VisualElements';
    var visualElems = manifest.find(visualElementsName);

    if(!visualElems) {
        throw new Error('Invalid manifest file (no <' + xmlnsPrefix + 'VisualElements> node): ' + manifestPath);
    }
    if (name) {
        (visualElems.attrib.DisplayName = name);
    }

    // Update properties
    var properties = manifest.find('.//Properties');
    if (properties && properties.find) {
        var displayNameElement = properties.find('.//DisplayName');
        if (displayNameElement && name) {
            displayNameElement.text = name;
        }

        var publisherNameElement = properties.find('.//PublisherDisplayName');
        if (publisherNameElement && author) {
            publisherNameElement.text = author;
        }
    }

    // Supported orientations
    var rotationPreferenceName = xmlnsPrefix + 'Rotation';
    var rotationPreferenceRootName = xmlnsPrefix + 'InitialRotationPreference';

    var orientation = config.getPreference('Orientation');
    var rotationPreferenceRoot;
    if (orientation) {
        rotationPreferenceRoot = manifest.find('.//' + rotationPreferenceRootName);
        if(rotationPreferenceRoot === null) {
            visualElems.append(et.Element(rotationPreferenceRootName));
            rotationPreferenceRoot = manifest.find('.//' + rotationPreferenceRootName);
        }

        rotationPreferenceRoot.clear();

        var applyOrientations = function(orientationsArr) {
            orientationsArr.forEach(function(orientationValue) {
                var el = et.Element(rotationPreferenceName);
                el.attrib.Preference = orientationValue;
                rotationPreferenceRoot.append(el);
            });
        };

        // Updates supported orientations
        //<InitialRotationPreference>
        //    <Rotation Preference = "portrait" | "landscape" | "portraitFlipped" | "landscapeFlipped" /> {1,4}
        //</InitialRotationPreference>
        if(orientation === 'default') {
            // This means landscape and portrait
            applyOrientations(['portrait', 'landscape', 'landscapeFlipped']);
        } else if(orientation === 'portrait') {
            applyOrientations(['portrait']);
        } else if(orientation === 'landscape') {
            applyOrientations(['landscape', 'landscapeFlipped']);
        } else { // Platform-specific setting like "portrait,landscape,portraitFlipped"
            applyOrientations(orientation.split(','));
        }
    } else {
        // Remove InitialRotationPreference root element to revert to defaults
        rotationPreferenceRoot = visualElems.find('.//' + rotationPreferenceRootName);
        if(rotationPreferenceRoot !== null) {
            visualElems.remove(null, rotationPreferenceRoot);
        }
    }
}

// Adjust version number as per CB-5337 Windows8 build fails due to invalid app version
function fixConfigVersion (version) {
    if(version && version.match(/\.\d/g)) {
        var numVersionComponents = version.match(/\.\d/g).length + 1;
        while (numVersionComponents++ < 4) {
            version += '.0';
        }
    }
    return version;
}

function applyAccessRules (config, manifest) {
    // Updates WhiteListing rules
    //<ApplicationContentUriRules>
    //    <Rule Match="https://www.example.com" Type="include"/>
    //</ApplicationContentUriRules>
    var appUriRulesRoot = manifest.find('.//Application'),
        appUriRules = appUriRulesRoot.find('.//ApplicationContentUriRules');

    if (appUriRules !== null) {
        appUriRulesRoot.remove(null, appUriRules);
    }
    // rules are not defined or allow any
    if (accessRules.length === 0 || accessRules.indexOf('*') > -1) {
        return;
    }

    appUriRules = et.Element('ApplicationContentUriRules');
    appUriRulesRoot.append(appUriRules);

    accessRules.forEach(function(rule) {
        var el = et.Element('Rule');
        el.attrib.Match = rule;
        el.attrib.Type = 'include';
        appUriRules.append(el);
    });
}

function sortCapabilities(manifest) {

    // removes namespace prefix (m3:Capability -> Capability)
    // this is required since elementtree returns qualified name with namespace
    function extractLocalName(tag) {
        return tag.split(':').pop(); // takes last part of string after ':'
    }

    var capabilitiesRoot = manifest.find('.//Capabilities'),
        capabilities = capabilitiesRoot._children || [];
    // to sort elements we remove them and then add again in the appropriate order
    capabilities.forEach(function(elem) { // no .clear() method
        capabilitiesRoot.remove(0, elem);
        // CB-7601 we need local name w/o namespace prefix to sort capabilities correctly
        elem.localName = extractLocalName(elem.tag);
    });
    capabilities.sort(function(a, b) {
        return (a.localName > b.localName) ? 1: -1;
    });
    capabilities.forEach(function(elem) {
        capabilitiesRoot.append(elem);
    });
}

function copyImages(config) {
    var platformRoot = ROOT;
    // TODO find the way to detect whether command was triggered by CLI or not
    var appRoot = path.join(platformRoot, '..', '..');

    function copyImage(src, dest) {
        src = path.join(appRoot, src);
        dest = path.join(platformRoot, 'images', dest);
        //console.log('Copying image from ' + src + ' to ' + dest);
        shell.cp('-f', src, dest);
    }

    function copyMrtImage(src, dest) {
        var srcDir = path.dirname(src),
            srcExt = path.extname(src),
            srcFileName = path.basename(src, srcExt);

        var destExt = path.extname(dest),
            destFileName = path.basename(dest, destExt);

        // all MRT images: logo.png, logo.scale-100.png, logo.scale-200.png, etc
        var images = fs.readdirSync(srcDir).filter(function(e) {
            return e.match('^'+srcFileName + '(.scale-[0-9]+)?' + srcExt);
        });
        // warn if no images found
        if (images.length === 0) {
            console.log('No images found for target: ' + destFileName);
            return;
        }
        // copy images with new name but keeping scale suffix
        images.forEach(function(img) {
            var scale = path.extname(path.basename(img, srcExt));
            if (scale === '') {
                scale = '.scale-100';
            }
            copyImage(path.join(srcDir, img), destFileName+scale+destExt);
        });

    }

    // Platform default images
    var platformImages = [
        {dest: 'Square150x150Logo.scale-100.png', width: 150, height: 150},
        {dest: 'Square30x30Logo.scale-100.png', width: 30, height: 30},
        {dest: 'StoreLogo.scale-100.png', width: 50, height: 50},
        {dest: 'SplashScreen.scale-100.png', width: 620, height: 300},
        // scaled images are specified here for backward compatibility only so we can find them by size
        {dest: 'StoreLogo.scale-240.png', width: 120, height: 120},
        {dest: 'Square44x44Logo.scale-240.png', width: 106, height: 106},
        {dest: 'Square71x71Logo.scale-240.png', width: 170, height: 170},
        {dest: 'Square70x70Logo.scale-100.png', width: 70, height: 70},
        {dest: 'Square150x150Logo.scale-240.png', width: 360, height: 360},
        {dest: 'Square310x310Logo.scale-100.png', width: 310, height: 310},
        {dest: 'Wide310x150Logo.scale-100.png', width: 310, height: 150},
        {dest: 'Wide310x150Logo.scale-240.png', width: 744, height: 360},
        {dest: 'SplashScreenPhone.scale-240.png', width: 1152, height: 1920}
    ];

    function findPlatformImage(width, height) {
        if (!width && !height){
            // this could be default image,
            // Windows requires specific image dimension so we can't apply it
            return null;
        }
        for (var idx in platformImages){
            var res = platformImages[idx];
            // If only one of width or height is not specified, use another parameter for comparation
            // If both specified, compare both.
            if ((!width || (width == res.width)) &&
                (!height || (height == res.height))){
                return res;
            }
        }
        return null;
    }

    var images = config.getIcons().concat(config.getSplashScreens());

    images.forEach(function (img) {
        if (img.target) {
            copyMrtImage(img.src, img.target + '.png');
        } else {
            // find target image by size
            var targetImg = findPlatformImage (img.width, img.height);
            if (targetImg) {
                copyImage(img.src, targetImg.dest);
            } else {
                console.log('The following image is skipped due to unsupported size: ' + img.src);
            }
        }
    });
}

function applyBackgroundColor (config, manifest, xmlnsPrefix) {
    var visualElems =null;

    function refineColor(color) {
        // return three-byte hexadecimal number preceded by "#" (required for Windows)
        color = color.replace('0x', '').replace('#', '');
        if (color.length == 3) {
            color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
        }
        // alpha is not supported, so we remove it
        if (color.length == 8) { // AArrggbb
            color = color.slice(2);
        }
        return '#' + color;
    }
    // background color
    var bgColor = config.getPreference('BackgroundColor');
    if (bgColor) {
        var visualElementsName = './/' + xmlnsPrefix + 'VisualElements';
        visualElems = manifest.find(visualElementsName);
        visualElems.attrib.BackgroundColor = refineColor(bgColor);
    }

    // Splash Screen background color
    bgColor = config.getPreference('SplashScreenBackgroundColor');
    if (bgColor) {
        var splashScreenElementsName = './/' + xmlnsPrefix + 'SplashScreen';
        visualElems = manifest.find(splashScreenElementsName);
        visualElems.attrib.BackgroundColor = refineColor(bgColor);
    }
}

/**
 * Applies the ToastCapable attribute to the VisualElements tag
 * @param config {ConfigParser} The configuration reader
 * @param manifest {et.ElementTree} The manifest file
 * @namespacePrefix {String} The XML namespace for the VisualElements tag, in the form 'm2:'
 */
function applyToastCapability(config, manifest, namespacePrefix) {
    var isToastCapable = config.getPreference('WindowsToastCapable');
    isToastCapable = (isToastCapable && isToastCapable.toString().toLowerCase() === 'true');

    var visualElementsName = './/' + namespacePrefix + 'VisualElements';
    var visualElems = manifest.find(visualElementsName);

    if (isToastCapable) {
        visualElems.attrib.ToastCapable = 'true';
    }
    else {
        delete visualElems.attrib.ToastCapable;
    }
}
<|endoftext|>


<commit_before>'use strict';
/* global describe it beforeEach */

/*
 * Unit tests for the content service.
 */

require('./helpers/before');

const chai = require('chai');
const dirtyChai = require('dirty-chai');

chai.use(dirtyChai);
const expect = chai.expect;

const async = require('async');
const targz = require('tar.gz');
const getRawBody = require('raw-body');
const path = require('path');
const request = require('supertest');
const storage = require('../src/storage');
const authHelper = require('./helpers/auth');
const resetHelper = require('./helpers/reset');
const server = require('../src/server');

describe('/content', function () {
  beforeEach(resetHelper);

  describe('#store', function () {
    it('persists new content into Cloud Files', function (done) {
      request(server.create())
        .put('/content/foo%26bar%2F')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "body": "something" }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.getContent('foo&bar/', function (err, uploaded) {
            expect(err).to.be.null();
            expect(uploaded).to.deep.equal({ body: 'something' });

            done();
          });
        });
    });

    it('makes the content available to full-text search', function (done) {
      request(server.create())
        .put('/content/foobar')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "title": "aaa", "body": "bbb ccc ddd" }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('ccc', null, 1, 10, function (err, results) {
            expect(err).to.be.null();
            expect(results.hits.total).to.equal(1);

            var result = results.hits.hits[0];
            expect(result._id).to.equal('foobar');
            expect(result._source.title).to.equal('aaa');

            done();
          });
        });
    });

    it('trims HTML from content before full-text indexing', function (done) {
      request(server.create())
        .put('/content/foobar')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "title": "aaa", "body": "
bbb ccc ddd
" }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('nope', null, 1, 10, function (err, results) {
            expect(err).to.be.null();
            expect(results.hits.total).to.equal(0);

            done();
          });
        });
    });

    it('skips envelopes with unsearchable set to true', function (done) {
      request(server.create())
        .put('/content/foobar')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "title": "aaa", "body": "bbb ccc ddd", "unsearchable": true }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('ccc', null, 1, 10, function (err, results) {
            expect(err).to.be.null();
            expect(results.hits.total).to.equal(0);

            done();
          });
        });
    });

    it('requires authentication', function (done) {
      authHelper.ensureAuthIsRequired(
        request(server.create())
          .put('/content/something')
          .send({
            thing: 'stuff'
          }),
        done);
    });
  });

  describe('#retrieve', function () {
    it('retrieves existing content from Cloud Files', function (done) {
      storage.storeContent('what&huh', { body: 'expected' }, function (err) {
        expect(err).not.to.exist();

        request(server.create())
          .get('/content/what%26huh')
          .expect('Content-Type', 'application/json')
          .expect(200)
          .expect({
            envelope: {
              body: 'expected'
            }
          }, done);
      });
    });
  });

  describe('#delete', function () {
    beforeEach(function (done) {
      storage.storeContent('er&okay', { body: 'expected' }, done);
    });
    beforeEach(function (done) {
      storage.indexContent('er&okay', { body: 'expected' }, done);
    });

    it('requires authentication', function (done) {
      authHelper.ensureAuthIsRequired(
        request(server.create())
          .delete('/content/wat%26nope'),
        done);
    });

    it('deletes content from Cloud Files', function (done) {
      request(server.create())
        .delete('/content/er%26okay')
        .set('Authorization', authHelper.AUTH_USER)
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.getContent('er&okay', function (err, uploaded) {
            expect(err).not.to.be.null();
            expect(err.statusCode).to.equal(404);

            done();
          });
        });
    });

    it('deletes content from Elasticsearch', function (done) {
      request(server.create())
        .delete('/content/er%26okay')
        .set('Authorization', authHelper.AUTH_USER)
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('expected', null, 1, 10, function (err, found) {
            expect(err).to.be.null();
            expect(found.hits.total).to.equal(0);
            expect(found.hits.hits.length).to.equal(0);

            done();
          });
        });
    });
  });

  describe('/bulk', function () {
    it.only('uploads all envelopes from a tarball', function (done) {
      const envelopes = getRawBody(targz().createReadStream(path.join(__dirname, 'fixtures', 'envelopes')));

      async.series([
        (cb) => {
          request(server.create())
            .post('/content/bulk')
            .set('Authorization', authHelper.AUTH_USER)
            .send(envelopes)
            .expect(204)
            .end(cb);
        },
        (cb) => {
          storage.getContent('https://github.com/some/repository/one', (err, c) => {
            if (err) return cb(err);

            expect(c).to.deep.equal({
              title: 'One',
              body: 'Document one'
            });

            cb();
          });
        },
        (cb) => {
          storage.getContent('https://github.com/some/repository/two', (err, c) => {
            if (err) return cb(err);

            expect(c).to.deep.equal({
              title: 'Two',
              body: 'Document two'
            });

            cb();
          });
        }
      ], done);
    });

    it('fails unless .metadata/config.json exists');

    it('deletes all other envelopes that share a groupID');

    it('accepts .metadata/toc.html as a TOC for all envelopes');
  });
});
<commit_msg>/bulkcontent should be its own endpoint.<commit_after>'use strict';
/* global describe it beforeEach */

/*
 * Unit tests for the content service.
 */

require('./helpers/before');

const chai = require('chai');
const dirtyChai = require('dirty-chai');

chai.use(dirtyChai);
const expect = chai.expect;

const async = require('async');
const targz = require('tar.gz');
const getRawBody = require('raw-body');
const path = require('path');
const request = require('supertest');
const storage = require('../src/storage');
const authHelper = require('./helpers/auth');
const resetHelper = require('./helpers/reset');
const server = require('../src/server');

describe('/content', function () {
  beforeEach(resetHelper);

  describe('#store', function () {
    it('persists new content into Cloud Files', function (done) {
      request(server.create())
        .put('/content/foo%26bar%2F')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "body": "something" }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.getContent('foo&bar/', function (err, uploaded) {
            expect(err).to.be.null();
            expect(uploaded).to.deep.equal({ body: 'something' });

            done();
          });
        });
    });

    it('makes the content available to full-text search', function (done) {
      request(server.create())
        .put('/content/foobar')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "title": "aaa", "body": "bbb ccc ddd" }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('ccc', null, 1, 10, function (err, results) {
            expect(err).to.be.null();
            expect(results.hits.total).to.equal(1);

            var result = results.hits.hits[0];
            expect(result._id).to.equal('foobar');
            expect(result._source.title).to.equal('aaa');

            done();
          });
        });
    });

    it('trims HTML from content before full-text indexing', function (done) {
      request(server.create())
        .put('/content/foobar')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "title": "aaa", "body": "
bbb ccc ddd
" }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('nope', null, 1, 10, function (err, results) {
            expect(err).to.be.null();
            expect(results.hits.total).to.equal(0);

            done();
          });
        });
    });

    it('skips envelopes with unsearchable set to true', function (done) {
      request(server.create())
        .put('/content/foobar')
        .set('Authorization', authHelper.AUTH_USER)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send('{ "title": "aaa", "body": "bbb ccc ddd", "unsearchable": true }')
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('ccc', null, 1, 10, function (err, results) {
            expect(err).to.be.null();
            expect(results.hits.total).to.equal(0);

            done();
          });
        });
    });

    it('requires authentication', function (done) {
      authHelper.ensureAuthIsRequired(
        request(server.create())
          .put('/content/something')
          .send({
            thing: 'stuff'
          }),
        done);
    });
  });

  describe('#retrieve', function () {
    it('retrieves existing content from Cloud Files', function (done) {
      storage.storeContent('what&huh', { body: 'expected' }, function (err) {
        expect(err).not.to.exist();

        request(server.create())
          .get('/content/what%26huh')
          .expect('Content-Type', 'application/json')
          .expect(200)
          .expect({
            envelope: {
              body: 'expected'
            }
          }, done);
      });
    });
  });

  describe('#delete', function () {
    beforeEach(function (done) {
      storage.storeContent('er&okay', { body: 'expected' }, done);
    });
    beforeEach(function (done) {
      storage.indexContent('er&okay', { body: 'expected' }, done);
    });

    it('requires authentication', function (done) {
      authHelper.ensureAuthIsRequired(
        request(server.create())
          .delete('/content/wat%26nope'),
        done);
    });

    it('deletes content from Cloud Files', function (done) {
      request(server.create())
        .delete('/content/er%26okay')
        .set('Authorization', authHelper.AUTH_USER)
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.getContent('er&okay', function (err, uploaded) {
            expect(err).not.to.be.null();
            expect(err.statusCode).to.equal(404);

            done();
          });
        });
    });

    it('deletes content from Elasticsearch', function (done) {
      request(server.create())
        .delete('/content/er%26okay')
        .set('Authorization', authHelper.AUTH_USER)
        .expect(204)
        .end(function (err, res) {
          if (err) return done(err);

          storage.queryContent('expected', null, 1, 10, function (err, found) {
            expect(err).to.be.null();
            expect(found.hits.total).to.equal(0);
            expect(found.hits.hits.length).to.equal(0);

            done();
          });
        });
    });
  });
});

describe('/bulkcontent', function () {
  it('uploads all envelopes from a tarball', function (done) {
    const envelopes = getRawBody(targz().createReadStream(path.join(__dirname, 'fixtures', 'envelopes')));

    async.series([
      (cb) => {
        request(server.create())
          .post('/bulkcontent')
          .set('Authorization', authHelper.AUTH_USER)
          .send(envelopes)
          .expect(204)
          .end(cb);
      },
      (cb) => {
        storage.getContent('https://github.com/some/repository/one', (err, c) => {
          if (err) return cb(err);

          expect(c).to.deep.equal({
            title: 'One',
            body: 'Document one'
          });

          cb();
        });
      },
      (cb) => {
        storage.getContent('https://github.com/some/repository/two', (err, c) => {
          if (err) return cb(err);

          expect(c).to.deep.equal({
            title: 'Two',
            body: 'Document two'
          });

          cb();
        });
      }
    ], done);
  });

  it('fails unless .metadata/config.json exists');

  it('deletes all other envelopes that share a groupID');

  it('accepts .metadata/toc.html as a TOC for all envelopes');
});
<|endoftext|>


<commit_before>//extremely simple enum
export class Enum {
    constructor(iterable) {
        iterable.forEach(value => {
            this[value.toUpperCase()] = value;
        });
    }
}

export function addClass(el, className) {
    const classes = className.split(' ');
    classes.filter(className => className.length).forEach(className => {
        if (el.classList) {
            el.classList.add(className);
        } else {
            if (el.className.toString() === '[object SVGAnimatedString]') {
                el.className.baseVal += (el.className.baseVal.length ? " " : "") + className;
            } else {
                el.className += (el.className.length ? " " : "") + className;
            }
        }
    });
}

export function removeClass(el, className) {
    const classes = className.split(' ');
    classes.filter(className => className.length).forEach(className => {
        if (el.classList) {
            el.classList.remove(className);
        } else {
            if (el.className.toString() === '[object SVGAnimatedString]'){
                el.className.baseVal = el.className.baseVal.toString().replace(new RegExp("(^|\s)" + className.split(" ").join("|") + "(\s|$)", "gi"), " ");
            } else {
                el.className = el.className.toString().replace(new RegExp("(^|\s)" + className.split(" ").join("|") + "(\s|$)", "gi"), " ");
            }
        }
    });
}

export function parseStyle(value) {
    value = parseFloat(value);
    return isFinite(value) ? value : 0;
}

export function offset(el) {
    const elBCR = el.getBoundingClientRect();
    return {
        width: Math.round(typeof elBCR.width === 'number' ? elBCR.width : elem.offsetWidth),
        height: Math.round(typeof elBCR.height === 'number' ? elBCR.height : elem.offsetHeight),
        top: Math.round(elBCR.top + (window.pageYOffset || document.documentElement.scrollTop)),
        left: Math.round(elBCR.left + (window.pageXOffset || document.documentElement.scrollLeft)),
        viewportWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
        viewportHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
    };
}

export function viewportOffset(el) {
    var elemBCR = el.getBoundingClientRect(),
        offsetElem = offset(el),
        offsetBCR = {top: 0, left: 0},
        offsetParent = document.documentElement,
        offsetParentBCR = offsetParent.getBoundingClientRect(),
        offsetParentStyle = window.getComputedStyle(offsetParent);

    //raw offset
    offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop;
    offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft;

    //account for window offset
    offsetBCR.top += window.pageYOffset;
    offsetBCR.left += window.pageXOffset;

    //account for padding
    offsetBCR.top += parseStyle(offsetParentStyle.paddingTop);
    offsetBCR.left += parseStyle(offsetParentStyle.paddingLeft);

    return Object.assign(offsetElem, {
        top: Math.round(elemBCR.top - offsetBCR.top),
        left: Math.round(elemBCR.left - offsetBCR.left)
    });
}

export function createElement(html) {
    const container = document.createElement('div');
    container.innerHTML = html.trim();
    return container.firstChild;
}

export function parsePadding(paddingString) {
    const pieces = paddingString.split(' ').filter(p => p.length).map(p => parseInt(p, 10));

    switch (pieces.length) {
    case 0:
        return {
            top: 0,
            right: 0,
            bottom: 0,
            left: 0
        };

    case 1:
        return {
            top: pieces[0],
            right: pieces[0],
            bottom: pieces[0],
            left: pieces[0]
        };

    case 2:
        return {
            top: pieces[0],
            right: pieces[1],
            bottom: pieces[0],
            left: pieces[1]
        };

    case 3:
        return {
            top: pieces[0],
            right: pieces[1],
            bottom: pieces[2],
            left: pieces[1]
        };
    case 4:
        return {
            top: pieces[0],
            right: pieces[1],
            bottom: pieces[2],
            left: pieces[3]
        };

    default:
        throw new Error('Invalid padding for hone scope: ' + paddingString);
    }
}

//stole this from Tether
export const transformKey = (function () {
    if (typeof document === 'undefined') {
        return '';
    }
    var el = document.createElement('div'),
        transforms = ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform'];

    for (let i = 0; i < transforms.length; ++i) {
        let key = transforms[i];
        if (el.style[key] !== undefined) {
            return key;
        }
    }
})();

//adapted from https://github.com/olahol/scrollparent.js
export function scrollParents(node) {
    const regex = /(auto|scroll)/;

    function parents(node, ps) {
        if (node.parentNode === null) { return ps; }

        return parents(node.parentNode, ps.concat([node]));
    }

    function style(node, prop) {
        return getComputedStyle(node, null).getPropertyValue(prop);
    }

    function overflow(node) {
        return style(node, "overflow") + style(node, "overflow-y") + style(node, "overflow-x");
    }

    function scroll(node) {
        return regex.test(overflow(node));
    }

    //start
    if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
        return [];
    }

    const allParents = parents(node.parentNode, []),
        scrollParents = allParents.filter(parent => scroll(parent));

    return [window, ...scrollParents];
}
<commit_msg>linting<commit_after>//extremely simple enum
export class Enum {
    constructor(iterable) {
        iterable.forEach(value => {
            this[value.toUpperCase()] = value;
        });
    }
}

export function addClass(el, className) {
    const classes = className.split(' ');
    classes.filter(className => className.length).forEach(className => {
        if (el.classList) {
            el.classList.add(className);
        } else {
            if (el.className.toString() === '[object SVGAnimatedString]') {
                el.className.baseVal += (el.className.baseVal.length ? ' ' : '') + className;
            } else {
                el.className += (el.className.length ? ' ' : '') + className;
            }
        }
    });
}

export function removeClass(el, className) {
    const classes = className.split(' ');
    classes.filter(className => className.length).forEach(className => {
        if (el.classList) {
            el.classList.remove(className);
        } else {
            if (el.className.toString() === '[object SVGAnimatedString]'){
                el.className.baseVal = el.className.baseVal.toString().replace(new RegExp('(^|\s)' + className.split(' ').join('|') + '(\s|$)', 'gi'), ' ');
            } else {
                el.className = el.className.toString().replace(new RegExp('(^|\s)' + className.split(' ').join('|') + '(\s|$)', 'gi'), ' ');
            }
        }
    });
}

export function parseStyle(value) {
    value = parseFloat(value);
    return isFinite(value) ? value : 0;
}

export function offset(el) {
    const elBCR = el.getBoundingClientRect();
    return {
        width: Math.round(typeof elBCR.width === 'number' ? elBCR.width : elem.offsetWidth),
        height: Math.round(typeof elBCR.height === 'number' ? elBCR.height : elem.offsetHeight),
        top: Math.round(elBCR.top + (window.pageYOffset || document.documentElement.scrollTop)),
        left: Math.round(elBCR.left + (window.pageXOffset || document.documentElement.scrollLeft)),
        viewportWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
        viewportHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
    };
}

export function viewportOffset(el) {
    var elemBCR = el.getBoundingClientRect(),
        offsetElem = offset(el),
        offsetBCR = {top: 0, left: 0},
        offsetParent = document.documentElement,
        offsetParentBCR = offsetParent.getBoundingClientRect(),
        offsetParentStyle = window.getComputedStyle(offsetParent);

    //raw offset
    offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop;
    offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft;

    //account for window offset
    offsetBCR.top += window.pageYOffset;
    offsetBCR.left += window.pageXOffset;

    //account for padding
    offsetBCR.top += parseStyle(offsetParentStyle.paddingTop);
    offsetBCR.left += parseStyle(offsetParentStyle.paddingLeft);

    return Object.assign(offsetElem, {
        top: Math.round(elemBCR.top - offsetBCR.top),
        left: Math.round(elemBCR.left - offsetBCR.left)
    });
}

export function createElement(html) {
    const container = document.createElement('div');
    container.innerHTML = html.trim();
    return container.firstChild;
}

export function parsePadding(paddingString) {
    const pieces = paddingString.split(' ').filter(p => p.length).map(p => parseInt(p, 10));

    switch (pieces.length) {
    case 0:
        return {
            top: 0,
            right: 0,
            bottom: 0,
            left: 0
        };

    case 1:
        return {
            top: pieces[0],
            right: pieces[0],
            bottom: pieces[0],
            left: pieces[0]
        };

    case 2:
        return {
            top: pieces[0],
            right: pieces[1],
            bottom: pieces[0],
            left: pieces[1]
        };

    case 3:
        return {
            top: pieces[0],
            right: pieces[1],
            bottom: pieces[2],
            left: pieces[1]
        };
    case 4:
        return {
            top: pieces[0],
            right: pieces[1],
            bottom: pieces[2],
            left: pieces[3]
        };

    default:
        throw new Error('Invalid padding for hone scope: ' + paddingString);
    }
}

//stole this from Tether
export const transformKey = (function () {
    if (typeof document === 'undefined') {
        return '';
    }
    var el = document.createElement('div'),
        transforms = ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform'];

    for (let i = 0; i < transforms.length; ++i) {
        let key = transforms[i];
        if (el.style[key] !== undefined) {
            return key;
        }
    }
})();

//adapted from https://github.com/olahol/scrollparent.js
export function scrollParents(node) {
    const regex = /(auto|scroll)/;

    function parents(node, ps) {
        if (node.parentNode === null) { return ps; }

        return parents(node.parentNode, ps.concat([node]));
    }

    function style(node, prop) {
        return getComputedStyle(node, null).getPropertyValue(prop);
    }

    function overflow(node) {
        return style(node, 'overflow') + style(node, 'overflow-y') + style(node, 'overflow-x');
    }

    function scroll(node) {
        return regex.test(overflow(node));
    }

    //start
    if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
        return [];
    }

    const allParents = parents(node.parentNode, []),
        scrollParents = allParents.filter(parent => scroll(parent));

    return [window, ...scrollParents];
}
<|endoftext|>