<commit_before>// The bulk for this file was taken from genie.js
/* eslint no-underscore-dangle:0 */
const _matchRankMap = {
  equals: 5,
  startsWith: 4,
  wordStartsWith: 3,
  contains: 2,
  acronym: 1,
  matches: 0,
  noMatch: -1
};

azSearchSorter.getMatchingStringIndexes = getMatchingStringIndexes;
azSearchSorter.getAcronymIndexes = getAcronymIndexes;

export default azSearchSorter;

function azSearchSorter(givenString = '', thingsToSearch = [], options = {}) {
  givenString = givenString.toLowerCase();
  return thingsToSearch
    .map(function(obj) { // score items
      const stringToMatch = options.propToMatch ? obj[options.propToMatch] : obj;
      const score = _stringsMatch(stringToMatch, givenString);
      return {obj, score};
    })
    .filter(item => options.keepNonMatching || item.score > -1)
    .sort((itemA, itemB) => itemA.score < itemB.score)
    .map(item => item.obj); // get the value and reverse it (best match first)
}

/**
 * Gives a _matchRankMap score based on
 * how well the two strings match.
 * @param {string} stringToMatch - the string to rank against
 * @param {string} givenString - the given string to rank
 * @returns {number} - the rank
 * @private
 */
function _stringsMatch(stringToMatch, givenString) {
  /* eslint complexity:[2, 8] */
  stringToMatch = ('' + stringToMatch).toLowerCase();

  // too long
  if (givenString.length > stringToMatch.length) {
    return _matchRankMap.noMatch;
  }

  // equals
  if (stringToMatch === givenString) {
    return _matchRankMap.equals;
  }

  // starts with
  if (stringToMatch.indexOf(givenString) === 0) {
    return _matchRankMap.startsWith;
  }

  // word starts with
  if (stringToMatch.indexOf(' ' + givenString) !== -1) {
    return _matchRankMap.wordStartsWith;
  }

  // contains
  if (stringToMatch.indexOf(givenString) !== -1) {
    return _matchRankMap.contains;
  } else if (givenString.length === 1) {
    // If the only character in the given magic word
    //   isn't even contained in the magic word, then
    //   it's definitely not a match.
    return _matchRankMap.noMatch;
  }

  // acronym
  if (_isAcronym(stringToMatch, givenString)) {
    return _matchRankMap.acronym;
  }

  return _stringsByCharOrder(stringToMatch, givenString);
}

function getAcronymIndexes(stringToMatch = '', givenString = '') {
  let matchingIndexes = null;
  stringToMatch = stringToMatch.toLowerCase();
  givenString = givenString.toLowerCase();
  if (_isAcronym(stringToMatch, givenString)) {
    let charNumber = -1;
    matchingIndexes = [];
    givenString.split('').forEach(function(matchChar, index) {
      const isFirst = index === 0;
      const findIndex = isFirst ? matchChar : ' ' + matchChar;
      charNumber = stringToMatch.indexOf(findIndex, charNumber + 1);
      charNumber += isFirst ? 0 : 1;
      matchingIndexes.push(charNumber);
    });
  }
  return matchingIndexes;
}

function _isAcronym(stringToMatch, givenString) {
  return _getAcronym(stringToMatch).indexOf(givenString) !== -1;
}

/**
 * Generates an acronym for a string.
 *
 * @param {string} string - the string to generate the acronym for
 * @returns {string} - the acronym
 * @private
 * @examples
 * _getAcronym('i love candy') // => 'ilc'
 * _getAcronym('water-fall in the spring-time') // => 'wfitst'
 */
function _getAcronym(string) {
  let acronym = '';
  const wordsInString = string.split(' ');
  wordsInString.forEach(function(wordInString) {
    const splitByHyphenWords = wordInString.split('-');
    splitByHyphenWords.forEach(function(splitByHyphenWord) {
      acronym += splitByHyphenWord.substr(0, 1);
    });
  });
  return acronym;
}

/**
 * Returns a _matchRankMap.matches or noMatch score based on whether
 * the characters in the givenString are found in order in the
 * stringToMatch
 * @param {string} stringToMatch - the string to match
 * @param {string} givenString - the string that is given
 * @returns {number} - matches rank or noMatch rank
 * @private
 */
function _stringsByCharOrder(stringToMatch, givenString) {
  const matchingIndexes = getMatchingStringIndexes(stringToMatch, givenString);
  if (!matchingIndexes || !matchingIndexes.length) {
    return _matchRankMap.noMatch;
  } else {
    return _matchRankMap.matches;
  }
}

function getMatchingStringIndexes(stringToMatch, givenString) {
  let charNumber = -1;
  let matchingIndexes = [];
  givenString.split('').every(function(matchChar) {
    charNumber = stringToMatch.indexOf(matchChar, charNumber + 1);
    if (charNumber === -1) {
      matchingIndexes = null;
    } else {
      matchingIndexes.push(charNumber);
      return true;
    }
  });
  return matchingIndexes;
}
<commit_msg>Array.sort must return a trinary value. It must return a 1, 0, or -1. See: [ES6 (http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.sort)] and [ES5(http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.11)]<commit_after>// The bulk for this file was taken from genie.js
/* eslint no-underscore-dangle:0 */
const _matchRankMap = {
  equals: 5,
  startsWith: 4,
  wordStartsWith: 3,
  contains: 2,
  acronym: 1,
  matches: 0,
  noMatch: -1
};

azSearchSorter.getMatchingStringIndexes = getMatchingStringIndexes;
azSearchSorter.getAcronymIndexes = getAcronymIndexes;

export default azSearchSorter;

function azSearchSorter(givenString = '', thingsToSearch = [], options = {}) {
  givenString = givenString.toLowerCase();
  return thingsToSearch
    .map(function(obj) { // score items
      const stringToMatch = options.propToMatch ? obj[options.propToMatch] : obj;
      const score = _stringsMatch(stringToMatch, givenString);
      return {obj, score};
    })
    .filter(item => options.keepNonMatching || item.score > -1)
    .sort((itemA, itemB) => {
      if (itemA.score < itemB.score) {
        return 1;
      } else if (itemA.score > itemB.score) {
        return -1;
      } else {
        return 0;
      }
    })
    .map(item => item.obj); // get the value and reverse it (best match first)
}

/**
 * Gives a _matchRankMap score based on
 * how well the two strings match.
 * @param {string} stringToMatch - the string to rank against
 * @param {string} givenString - the given string to rank
 * @returns {number} - the rank
 * @private
 */
function _stringsMatch(stringToMatch, givenString) {
  /* eslint complexity:[2, 8] */
  stringToMatch = ('' + stringToMatch).toLowerCase();

  // too long
  if (givenString.length > stringToMatch.length) {
    return _matchRankMap.noMatch;
  }

  // equals
  if (stringToMatch === givenString) {
    return _matchRankMap.equals;
  }

  // starts with
  if (stringToMatch.indexOf(givenString) === 0) {
    return _matchRankMap.startsWith;
  }

  // word starts with
  if (stringToMatch.indexOf(' ' + givenString) !== -1) {
    return _matchRankMap.wordStartsWith;
  }

  // contains
  if (stringToMatch.indexOf(givenString) !== -1) {
    return _matchRankMap.contains;
  } else if (givenString.length === 1) {
    // If the only character in the given magic word
    //   isn't even contained in the magic word, then
    //   it's definitely not a match.
    return _matchRankMap.noMatch;
  }

  // acronym
  if (_isAcronym(stringToMatch, givenString)) {
    return _matchRankMap.acronym;
  }

  return _stringsByCharOrder(stringToMatch, givenString);
}

function getAcronymIndexes(stringToMatch = '', givenString = '') {
  let matchingIndexes = null;
  stringToMatch = stringToMatch.toLowerCase();
  givenString = givenString.toLowerCase();
  if (_isAcronym(stringToMatch, givenString)) {
    let charNumber = -1;
    matchingIndexes = [];
    givenString.split('').forEach(function(matchChar, index) {
      const isFirst = index === 0;
      const findIndex = isFirst ? matchChar : ' ' + matchChar;
      charNumber = stringToMatch.indexOf(findIndex, charNumber + 1);
      charNumber += isFirst ? 0 : 1;
      matchingIndexes.push(charNumber);
    });
  }
  return matchingIndexes;
}

function _isAcronym(stringToMatch, givenString) {
  return _getAcronym(stringToMatch).indexOf(givenString) !== -1;
}

/**
 * Generates an acronym for a string.
 *
 * @param {string} string - the string to generate the acronym for
 * @returns {string} - the acronym
 * @private
 * @examples
 * _getAcronym('i love candy') // => 'ilc'
 * _getAcronym('water-fall in the spring-time') // => 'wfitst'
 */
function _getAcronym(string) {
  let acronym = '';
  const wordsInString = string.split(' ');
  wordsInString.forEach(function(wordInString) {
    const splitByHyphenWords = wordInString.split('-');
    splitByHyphenWords.forEach(function(splitByHyphenWord) {
      acronym += splitByHyphenWord.substr(0, 1);
    });
  });
  return acronym;
}

/**
 * Returns a _matchRankMap.matches or noMatch score based on whether
 * the characters in the givenString are found in order in the
 * stringToMatch
 * @param {string} stringToMatch - the string to match
 * @param {string} givenString - the string that is given
 * @returns {number} - matches rank or noMatch rank
 * @private
 */
function _stringsByCharOrder(stringToMatch, givenString) {
  const matchingIndexes = getMatchingStringIndexes(stringToMatch, givenString);
  if (!matchingIndexes || !matchingIndexes.length) {
    return _matchRankMap.noMatch;
  } else {
    return _matchRankMap.matches;
  }
}

function getMatchingStringIndexes(stringToMatch, givenString) {
  let charNumber = -1;
  let matchingIndexes = [];
  givenString.split('').every(function(matchChar) {
    charNumber = stringToMatch.indexOf(matchChar, charNumber + 1);
    if (charNumber === -1) {
      matchingIndexes = null;
    } else {
      matchingIndexes.push(charNumber);
      return true;
    }
  });
  return matchingIndexes;
}
<|endoftext|>


<commit_before>/*
* Simple Pizza Calculator
* ------------------------
* Author: Sandro Speth
* Author: Matthias Hermann
* Author: Heiko Geppert
*/

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const fs = require('fs');
app.use(bodyParser.urlencoded({ extended: true }));
const HashMap = require('hashmap');
const dirname = fs.realpathSync('./');
const timeoutInMS = 28800000;

var teams = new HashMap();
var ingredients = JSON.parse(fs.readFileSync(__dirname + '/data/ingredients.json', 'utf8'));
var team_suggestions = new HashMap();

// build api endpoint for getting js files
fs.readdirSync(dirname + '/src/js').forEach(file => {
    app.get('/js/' + file, function (req, res) {
        res.status(200).sendFile(dirname + '/src/js/' + file);
    });
});

// build api endpoint for getting bootstrap files
fs.readdirSync(dirname + '/node_modules/bootstrap/dist/css').forEach(file => {
    app.get('/css/' + file, function (req, res) {
        res.status(200).sendFile(dirname + '/node_modules/bootstrap/dist/css/' + file);
    });
});

// build api endpoint for getting sweetalert files
fs.readdirSync(dirname + '/node_modules/sweetalert/dist').forEach(file => {
    app.get('/swal/' + file, function (req, res) {
        res.status(200).sendFile(dirname + '/node_modules/sweetalert/dist/' + file);
    })
});

app.get('/', function (req, res) {
    res.status(200).sendFile(__dirname + "/index.htm");
});

app.get('/teams', function (req, res) {
    let teamname = req.query.teamname;
    console.log(teamname);
    if (teamname != undefined && teams.has(teamname)) {
        res.status(200).sendFile(__dirname + "/teamPage.html");
    } else {
        res.redirect(302, '/');
    }

});

app.get('/pizzas/amount', function (req, res) {
    let teamname = req.query.teamname;
    if (teamname != undefined && teams.has(teamname)) {
        let amount = {
            amount: teams.get(teamname).pizza_count
        };
        res.status(200).end(JSON.stringify(amount));
    } else {
        res.redirect(303, '/');
    }
});

app.get('/pizzas/ingredients', function (req, res) {
    if (ingredients.length > 0) {
        res.status(200).end(JSON.stringify(ingredients));
    } else {
        res.redirect(303, '/');
    }
});

app.get('/pizzas/suggestions', function (req, res) {
    let teamname = req.query.teamname;
    if (teamname != undefined && team_suggestions.has(teamname)) {
        let suggestions = team_suggestions.get(teamname);
        res.status(200).end(JSON.stringify(suggestions));
    } else {
        res.redirect(303, '/');
    }
});

app.get('/teams/data', function (req, res) {
    let teamname = req.query.teamname;
    if (teamname != undefined && teams.has(teamname)) {
        res.status(200).end(JSON.stringify(teams.get(teamname)));
    } else {
        res.redirect(303, '/');
    }
});

app.post('/teams/create', function (req, res) {
    let teamname = req.body.teamname;
    if (teamname != undefined && !teams.has(teamname) && !team_suggestions.has(teamname)) {
        console.log('Creating team ' + teamname);
        let data = {
            teamsize: {
                number: 0,
                type: null
            },
            pizza_count: 0
        };
        teams.set(teamname, data);
        team_suggestions.set(teamname, []);
        setTimeout(callback => {
            teams.remove(teamname);
            team_suggestions.remove(teamname);
        }, timeoutInMS);
        res.status(200).sendFile(__dirname + '/js/teamPage.js');
        res.redirect(303, '/teams/?teamname=' + teamname);
    } else {
        console.log('Creating Team: Failure because of undefined teamname!');
        res.status(420).send('' +
            '
Team already exists
' +
            '' +
            '');
    }
});

app.post('/teams/teamsize', function (req, res) {
    let count = parseInt(req.body.count);
    let type = req.body.type;
    let teamname = req.body.teamname;
    if (count != NaN && type != undefined && teamname != undefined) {
        console.log('Update team size for team ' + teamname);
        let pizza_count = computeNumberOfPizzas(count, type);
        let data = {
            teamsize: {
                number: count,
                type: type
            },
            pizza_count: pizza_count
        };
        console.log(data);
        teams.set(teamname, data);
        res.redirect(302, '/teams/?teamname=' + teamname);
        res.status(200).end(JSON.stringify({ teamname: teamname, data: data }));
    } else {
        console.log('Update team size failed');
        res.redirect(303, '/');
    }
});

app.post('/pizzas/suggestions', function (req, res) {
    let teamname = req.body.teamname;
    let ingredients = req.body.ingredients;
    if (teamname != undefined && team_suggestions.has(teamname)) {
        console.log('Add suggestion for team ' + teamname);
        let suggestion = {
            id: team_suggestions.get(teamname).length,
            ingredients: ingredients,
            vote: 0
        };
        team_suggestions.get(teamname).push(suggestion);
        res.end(JSON.stringify(suggestion));
    } else {
        console.log('Failed to add suggestion');
        res.redirect(303, '/');
    }
});

app.post('/pizzas/suggestions/vote', function (req, res) {
    let teamname = req.body.teamname;
    let suggestion_id = parseInt(req.body.id);
    let vote = parseInt(req.body.vote);
    if (teamname != undefined && suggestion_id != NaN && vote != NaN && team_suggestions.has(teamname)) {
        console.log('Update vote vor suggestion');
        vote = Math.sign(vote);
        team_suggestions.get(teamname)[suggestion_id].vote += vote;
        res.sendStatus(200);
    } else {
        console.log('Update voting failed');
        res.redirect(303, '/');
    }
});

function computeNumberOfPizzas(count, type) {
    if (type == 'Personen') {
        let number = count * 4;
        return Math.ceil(number / 16);
    } else if (type == 'Pizzastuecke') {
        return Math.ceil(count / 16);
    } else {
        return 0;
    }
};

function saveToFile(data, filename = "tfmap.log", exitOnWrite = false) {
    let folderName = "results";
    if (!fs.existsSync(folderName)) {
        fs.mkdirSync(folderName);
    }
    fs.writeFileSync('results/' + filename, data);
    console.log("The file was saved!");
    if (exitOnWrite) {
        process.exit(0);
    }
}

var server = app.listen(8081, function () {
    var host = server.address().address;
    var port = server.address().port;
    console.log("REST server listening at http://%s:%s", host, port);
});<commit_msg>Change bootstrap endpoint<commit_after>/*
* Simple Pizza Calculator
* ------------------------
* Author: Sandro Speth
* Author: Matthias Hermann
* Author: Heiko Geppert
*/

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const fs = require('fs');
app.use(bodyParser.urlencoded({ extended: true }));
const HashMap = require('hashmap');
const dirname = fs.realpathSync('./');
const timeoutInMS = 28800000;

var teams = new HashMap();
var ingredients = JSON.parse(fs.readFileSync(__dirname + '/data/ingredients.json', 'utf8'));
var team_suggestions = new HashMap();

// build api endpoint for getting js files
fs.readdirSync(dirname + '/src/js').forEach(file => {
    app.get('/js/' + file, function (req, res) {
        res.status(200).sendFile(dirname + '/src/js/' + file);
    });
});

// build api endpoint for getting bootstrap files
fs.readdirSync(dirname + '/node_modules/bootstrap/dist').forEach(folder => {
    fs.readdirSync(dirname + '/node_modules/bootstrap/dist/' + folder).forEach(file => {
        app.get('/bootstrap/' + folder + '/' + file, function (req, res) {
            res.status(200).sendFile(dirname + '/node_modules/bootstrap/dist/' + folder + '/' + file);
        })
    });
});

// // build api endpoint for getting bootstrap files
// fs.readdirSync(dirname + '/node_modules/bootstrap/dist/css').forEach(file => {
//     app.get('/css/' + file, function (req, res) {
//         res.status(200).sendFile(dirname + '/node_modules/bootstrap/dist/css/' + file);
//     });
// });

// build api endpoint for getting sweetalert files
fs.readdirSync(dirname + '/node_modules/sweetalert/dist').forEach(file => {
    app.get('/swal/' + file, function (req, res) {
        res.status(200).sendFile(dirname + '/node_modules/sweetalert/dist/' + file);
    })
});

app.get('/', function (req, res) {
    res.status(200).sendFile(__dirname + "/index.htm");
});

app.get('/teams', function (req, res) {
    let teamname = req.query.teamname;
    console.log(teamname);
    if (teamname != undefined && teams.has(teamname)) {
        res.status(200).sendFile(__dirname + "/teamPage.html");
    } else {
        res.redirect(302, '/');
    }

});

app.get('/pizzas/amount', function (req, res) {
    let teamname = req.query.teamname;
    if (teamname != undefined && teams.has(teamname)) {
        let amount = {
            amount: teams.get(teamname).pizza_count
        };
        res.status(200).end(JSON.stringify(amount));
    } else {
        res.redirect(303, '/');
    }
});

app.get('/pizzas/ingredients', function (req, res) {
    if (ingredients.length > 0) {
        res.status(200).end(JSON.stringify(ingredients));
    } else {
        res.redirect(303, '/');
    }
});

app.get('/pizzas/suggestions', function (req, res) {
    let teamname = req.query.teamname;
    if (teamname != undefined && team_suggestions.has(teamname)) {
        let suggestions = team_suggestions.get(teamname);
        res.status(200).end(JSON.stringify(suggestions));
    } else {
        res.redirect(303, '/');
    }
});

app.get('/teams/data', function (req, res) {
    let teamname = req.query.teamname;
    if (teamname != undefined && teams.has(teamname)) {
        res.status(200).end(JSON.stringify(teams.get(teamname)));
    } else {
        res.redirect(303, '/');
    }
});

app.post('/teams/create', function (req, res) {
    let teamname = req.body.teamname;
    if (teamname != undefined && !teams.has(teamname) && !team_suggestions.has(teamname)) {
        console.log('Creating team ' + teamname);
        let data = {
            teamsize: {
                number: 0,
                type: null
            },
            pizza_count: 0
        };
        teams.set(teamname, data);
        team_suggestions.set(teamname, []);
        setTimeout(callback => {
            teams.remove(teamname);
            team_suggestions.remove(teamname);
        }, timeoutInMS);
        res.status(200).sendFile(__dirname + '/js/teamPage.js');
        res.redirect(303, '/teams/?teamname=' + teamname);
    } else {
        console.log('Creating Team: Failure because of undefined teamname!');
        res.status(420).send('' +
            '
Team already exists
' +
            '' +
            '');
    }
});

app.post('/teams/teamsize', function (req, res) {
    let count = parseInt(req.body.count);
    let type = req.body.type;
    let teamname = req.body.teamname;
    if (count != NaN && type != undefined && teamname != undefined) {
        console.log('Update team size for team ' + teamname);
        let pizza_count = computeNumberOfPizzas(count, type);
        let data = {
            teamsize: {
                number: count,
                type: type
            },
            pizza_count: pizza_count
        };
        console.log(data);
        teams.set(teamname, data);
        res.redirect(302, '/teams/?teamname=' + teamname);
        res.status(200).end(JSON.stringify({ teamname: teamname, data: data }));
    } else {
        console.log('Update team size failed');
        res.redirect(303, '/');
    }
});

app.post('/pizzas/suggestions', function (req, res) {
    let teamname = req.body.teamname;
    let ingredients = req.body.ingredients;
    if (teamname != undefined && team_suggestions.has(teamname)) {
        console.log('Add suggestion for team ' + teamname);
        let suggestion = {
            id: team_suggestions.get(teamname).length,
            ingredients: ingredients,
            vote: 0
        };
        team_suggestions.get(teamname).push(suggestion);
        res.end(JSON.stringify(suggestion));
    } else {
        console.log('Failed to add suggestion');
        res.redirect(303, '/');
    }
});

app.post('/pizzas/suggestions/vote', function (req, res) {
    let teamname = req.body.teamname;
    let suggestion_id = parseInt(req.body.id);
    let vote = parseInt(req.body.vote);
    if (teamname != undefined && suggestion_id != NaN && vote != NaN && team_suggestions.has(teamname)) {
        console.log('Update vote vor suggestion');
        vote = Math.sign(vote);
        team_suggestions.get(teamname)[suggestion_id].vote += vote;
        res.sendStatus(200);
    } else {
        console.log('Update voting failed');
        res.redirect(303, '/');
    }
});

function computeNumberOfPizzas(count, type) {
    if (type == 'Personen') {
        let number = count * 4;
        return Math.ceil(number / 16);
    } else if (type == 'Pizzastuecke') {
        return Math.ceil(count / 16);
    } else {
        return 0;
    }
};

function saveToFile(data, filename = "tfmap.log", exitOnWrite = false) {
    let folderName = "results";
    if (!fs.existsSync(folderName)) {
        fs.mkdirSync(folderName);
    }
    fs.writeFileSync('results/' + filename, data);
    console.log("The file was saved!");
    if (exitOnWrite) {
        process.exit(0);
    }
}

var server = app.listen(8081, function () {
    var host = server.address().address;
    var port = server.address().port;
    console.log("REST server listening at http://%s:%s", host, port);
});<|endoftext|>


<commit_before>const fs = require('fs')
  ,   os = require('os')
  ,   path = require('path')
  ,   assert = require('assert')
  ,   child_process = require('child_process')
  ,   glob = require('glob')
  ,   express = require('express')
  ,   multer = require('multer')
  ,   bodyParser = require('body-parser')

const resResultProperty = 'dafuq_result'
const resTypeProperty = 'dafuq_type'

/**
 * @typedef DafuqPath
 * @type {object}
 * @property {string} absolute Absolute path pointing to a file
 * @property {string} relative Path to the same file pointed by absolute property,
 *                             but relative to the invoking script
 */

/**
 * Exit code that determines a file response should be used instead of JSON
 * @type {Number}
 */
const EXIT_CODE_FILE = 10

/**
 * Result types when executing a command
 * @type {String}
 */
const RESULT_TYPE_OBJECT = 'object'
const RESULT_TYPE_FILE = 'file'

/**
 * Asserts the provided **absolute** path is a directory
 *
 * @param  {String} path - the path to check if is a directory
 * @return {String} the provided path
 * @throws {AssertionError} If the path is not a directory
 */
function assertIsDirectory(path) {
    let dirStats
    try {
        assert(fs.statSync(path).isDirectory(), `path ${path} is not a directory`)
    } catch(e) {
        const msg = `path ${path} does not exists or doesn't have proper permissions`
        assert(false, msg)
    }
    return path
}

/**
 * Glob pattern that matches all the files that would generate a route
 * @type {String}
 */
const globPattern = '**/+(all|get|post|put|delete|head|options).*'

/**
 * Returns the files at dir matching the given glob pattern
 *
 * @param  {String} dir The directory to search for files
 * @param  {String} pattern A glob pattern that files must match
 * @return {DafuqPath[]} All the files that mattched the pattern at the given dir
 */
function findFilesAt(dir, pattern) {
    return glob.sync(path.join(dir, pattern), { nocase: true })
        .map(p => ({
            absolute: p,
            relative: path.relative(dir, p)
        }))
}

/**
 * Binary mask for files mode. Checks against user execution permision.
 * @type {number}
 */
const MASK_EXEC = parseInt('0100', 8)

/**
 * Returns true if the provided path can be executed, false otherwise
 *
 * @param  {String} path the absolute path to check if is executable
 * @return {Boolean}     true if the path is executable, false otherwise
 */
function isExecutable(path) {
    try {
        const stats = fs.statSync(path)
        return !!(stats.mode & MASK_EXEC)
    } catch(e) { return false }
}

/**
 * Matches and extracts the headers that would be used as commadn line params
 * @type {RegExp}
 */
const HEADER_X_ARG_REGEX = /^x-arg-(.*)$/i

/**
 * Builds the command line arguments string (--option1 value1 --option2) by
 * searching on the necessary request fields.
 *
 * The fields and order or prevailing is as follows:
 *  * Headers: Header fields that match {@link HEADER_X_ARG_REGEX}
 *  * Query Params
 *  * URL params
 *  * Body members
 *  * Multipart uploaded files
 *
 * @param  {Object} req - express request object
 * @return {String} The command options string
 */
function buildCommandFlags(req) {
    let cmdFlags = ''

    // Build headers object for later merge
    const headerFlags = Object.keys(req.headers).reduce((headers, header) => {
        const mtch = HEADER_X_ARG_REGEX.exec(header)
        if (mtch)
            headers[mtch[1]] = req.headers[header]
        return headers
    }, {})

    // Build files
    const uploadFlags = (req.files || []).reduce((files, file) => {
        files[file.fieldname] = file.path
        return files
    }, {})

    const flags = Object.assign({}, headerFlags, req.query, req.params, req.body, uploadFlags)
    Object.keys(flags).forEach(function(flagName) {
        const flagValue = flags[flagName]
        cmdFlags += ` --${flagName}`
        if (flagValue)
            cmdFlags += ` "${flagValue}"`
    })

    return cmdFlags
}

/**
 * Executes the specified command on the OS terminal and returns via cb the
 * response written to the stdout and stderr wrapped arround a JSON object.
 *
 * The result will always contain a field named succes that tells if the
 * command executed properly or not. A command is cosidered successful when
 * its exit code is 0 and no errors are thrown.
 *
 * The output of the command is considered in the following order, the first
 * one to match will be considered as the command output:
 * * The content of strderr if it is not empty
 * * The content of stdout if it is not empty
 * * The error message if the error was reported while using
 *  node's `child_process.exec`
 *
 * Finally, once the output is determined, we try to parse it as json. If
 * successful and the parsed JSON contains the success property the output
 * just generated will be treated as the result of the command execution.
 * If the JSON was correctly parsed but didn't contain any success field, the
 * result will be an object containing `success` and `result`, being result
 * the just parsed JSON.
 * If the output string couldn't be parsed as JSON, the result field will be
 * directly the string.
 *
 * @param  {String}   cmd Command to be executed via `child_process.exec`
 * @param  {Function} cb  Completion callback. The result object and the
 *                        the result type are pased as arguments
 */
function execCommand(command, cb) {
    child_process.exec(command, function(err, stdout, stderr) {
        const code = err && err.code ? err.code : 0
        let result =  stderr || stdout || (err || {}).message
        let type = RESULT_TYPE_OBJECT

        if (result)
            result = result.trim()

        if (code === EXIT_CODE_FILE) {
            type = RESULT_TYPE_FILE
        } else {
            try {
                // Try to parse it as JSON
                const json = JSON.parse(result)
                result = json
            } catch(e) {} // If an error occurs parsing the json leave it as it was

            // If the result doesn't contain the field success, treat its
            // contents as the result part and add the succes field
            if (result.success === undefined) {
                result = {
                    success: code === 0,
                    result: result
                }
            }
        }

        cb(result, type)
    })
}

export default function dafuq(config) {

    // Allow constructor to be only the commands directory
    if (typeof config === 'string')
        config = { path: path }

    // Assign default values
    const opts = Object.assign({
        shebang: '',
        debug: false
    }, config)

    // Options validation

    // Valid string path is mandatory
    if (!opts.path || typeof opts.path !== 'string' || opts.path.length == 0)
        throw new TypeError('path must be a string pointing to the commands directory')

    // If shebang provided, but not valid
    if (opts.shebang && (typeof opts.shebang !== 'string' || opts.shebang.length == 0))
        throw new TypeError('shebang must be a non empty string')

    if (opts.debug !== undefined) {
        if (opts.debug === true)
            opts.debug = console.log
        else if (opts.debug === false)
            opts.debug = (() => {})
        else if (typeof opts.debug !== 'function')
            throw new TypeError('debug must be a boolean or a logging function')
    }

    opts.debug('Building dafuq instance with %j', config)

    // Build an absolute path to the commands directory and assert it is actually a directory
    const commandsPath = path.resolve(path.dirname(module.parent.filename), opts.path)
    assertIsDirectory(commandsPath)

    // Find the files to be mounted as execution points
    let files = findFilesAt(commandsPath, globPattern)

    // If no shebang is specified, the file need to be executable by itself
    if (!opts.shebang) {
        files = files.filter(file => {
            const isExe = isExecutable(file.absolute)
            if (!isExe)
                opts.debug(`${ file.absolute } is not executable, ignoring`)
            return isExe
        })
    }

    const app = express()
    const upload = multer({ dest: os.tmpdir() })

    app.use(bodyParser.urlencoded({ extended: false }))
    app.use(bodyParser.json())

    /**
     * Returns a middleware that once inkoed will execute the specified file and
     * put the result of its execution on response object in the property pointed
     * by moduleName.
     *
     * @param {String} file
     */
    function executionMiddleware(file) {
        return (req, res, next) => {
            // Build the base command
            let cmd = file
            if (opts.shebang)
                cmd = `${ opts.shebang } ${ cmd }`
            /*
             Append all the parameters provided via:
                * query params
                * url params
                * body members
                * x-args headers
            */
            cmd += buildCommandFlags(req)

            opts.debug(`$ ${cmd}`)
            execCommand(cmd, (result, type) => {
                Object.defineProperty(res, resResultProperty, { value: result })
                Object.defineProperty(res, resTypeProperty, { value: type })
                next()
            })
        }
    }

    // Add all the files routes
    files.forEach(file => {
        const filePath = file.relative
        const url = '/' + path.dirname(file.relative)
        const method = path.basename(filePath, path.extname(filePath)).toLowerCase()
        const middleware = executionMiddleware(file.absolute)
        opts.debug(`Adding ${ method } ${ url }`)
        if (method === 'get' || method === 'head' || method === 'options')
            app[method](url, middleware)
        else
            app[method](url, upload.any(), middleware)
    })

    // Fallback behaviour, send the result
    app.all('*', (req, res, next) => {
        if (res[resTypeProperty] === RESULT_TYPE_OBJECT)
            res.type('json').json(res[resResultProperty])
        else if (res[resTypeProperty] === RESULT_TYPE_FILE)
            res.download(res[resResultProperty])
        else
            res.status(404).send()
    })

    return app
}
<commit_msg>Don't output command debug info when running tests<commit_after>const fs = require('fs')
  ,   os = require('os')
  ,   path = require('path')
  ,   assert = require('assert')
  ,   child_process = require('child_process')
  ,   glob = require('glob')
  ,   express = require('express')
  ,   multer = require('multer')
  ,   bodyParser = require('body-parser')

const IS_TEST = process.env['NODE_ENV'] === 'test'

const resResultProperty = 'dafuq_result'
const resTypeProperty = 'dafuq_type'

/**
 * @typedef DafuqPath
 * @type {object}
 * @property {string} absolute Absolute path pointing to a file
 * @property {string} relative Path to the same file pointed by absolute property,
 *                             but relative to the invoking script
 */

/**
 * Exit code that determines a file response should be used instead of JSON
 * @type {Number}
 */
const EXIT_CODE_FILE = 10

/**
 * Result types when executing a command
 * @type {String}
 */
const RESULT_TYPE_OBJECT = 'object'
const RESULT_TYPE_FILE = 'file'

/**
 * Asserts the provided **absolute** path is a directory
 *
 * @param  {String} path - the path to check if is a directory
 * @return {String} the provided path
 * @throws {AssertionError} If the path is not a directory
 */
function assertIsDirectory(path) {
    let dirStats
    try {
        assert(fs.statSync(path).isDirectory(), `path ${path} is not a directory`)
    } catch(e) {
        const msg = `path ${path} does not exists or doesn't have proper permissions`
        assert(false, msg)
    }
    return path
}

/**
 * Glob pattern that matches all the files that would generate a route
 * @type {String}
 */
const globPattern = '**/+(all|get|post|put|delete|head|options).*'

/**
 * Returns the files at dir matching the given glob pattern
 *
 * @param  {String} dir The directory to search for files
 * @param  {String} pattern A glob pattern that files must match
 * @return {DafuqPath[]} All the files that mattched the pattern at the given dir
 */
function findFilesAt(dir, pattern) {
    return glob.sync(path.join(dir, pattern), { nocase: true })
        .map(p => ({
            absolute: p,
            relative: path.relative(dir, p)
        }))
}

/**
 * Binary mask for files mode. Checks against user execution permision.
 * @type {number}
 */
const MASK_EXEC = parseInt('0100', 8)

/**
 * Returns true if the provided path can be executed, false otherwise
 *
 * @param  {String} path the absolute path to check if is executable
 * @return {Boolean}     true if the path is executable, false otherwise
 */
function isExecutable(path) {
    try {
        const stats = fs.statSync(path)
        return !!(stats.mode & MASK_EXEC)
    } catch(e) { return false }
}

/**
 * Matches and extracts the headers that would be used as commadn line params
 * @type {RegExp}
 */
const HEADER_X_ARG_REGEX = /^x-arg-(.*)$/i

/**
 * Builds the command line arguments string (--option1 value1 --option2) by
 * searching on the necessary request fields.
 *
 * The fields and order or prevailing is as follows:
 *  * Headers: Header fields that match {@link HEADER_X_ARG_REGEX}
 *  * Query Params
 *  * URL params
 *  * Body members
 *  * Multipart uploaded files
 *
 * @param  {Object} req - express request object
 * @return {String} The command options string
 */
function buildCommandFlags(req) {
    let cmdFlags = ''

    // Build headers object for later merge
    const headerFlags = Object.keys(req.headers).reduce((headers, header) => {
        const mtch = HEADER_X_ARG_REGEX.exec(header)
        if (mtch)
            headers[mtch[1]] = req.headers[header]
        return headers
    }, {})

    // Build files
    const uploadFlags = (req.files || []).reduce((files, file) => {
        files[file.fieldname] = file.path
        return files
    }, {})

    const flags = Object.assign({}, headerFlags, req.query, req.params, req.body, uploadFlags)
    Object.keys(flags).forEach(function(flagName) {
        const flagValue = flags[flagName]
        cmdFlags += ` --${flagName}`
        if (flagValue)
            cmdFlags += ` "${flagValue}"`
    })

    return cmdFlags
}

/**
 * Executes the specified command on the OS terminal and returns via cb the
 * response written to the stdout and stderr wrapped arround a JSON object.
 *
 * The result will always contain a field named succes that tells if the
 * command executed properly or not. A command is cosidered successful when
 * its exit code is 0 and no errors are thrown.
 *
 * The output of the command is considered in the following order, the first
 * one to match will be considered as the command output:
 * * The content of strderr if it is not empty
 * * The content of stdout if it is not empty
 * * The error message if the error was reported while using
 *  node's `child_process.exec`
 *
 * Finally, once the output is determined, we try to parse it as json. If
 * successful and the parsed JSON contains the success property the output
 * just generated will be treated as the result of the command execution.
 * If the JSON was correctly parsed but didn't contain any success field, the
 * result will be an object containing `success` and `result`, being result
 * the just parsed JSON.
 * If the output string couldn't be parsed as JSON, the result field will be
 * directly the string.
 *
 * @param  {String}   cmd Command to be executed via `child_process.exec`
 * @param  {Function} cb  Completion callback. The result object and the
 *                        the result type are pased as arguments
 */
function execCommand(command, cb) {
    child_process.exec(command, function(err, stdout, stderr) {
        const code = err && err.code ? err.code : 0
        let result =  stderr || stdout || (err || {}).message
        let type = RESULT_TYPE_OBJECT

        if (result)
            result = result.trim()

        if (code === EXIT_CODE_FILE) {
            type = RESULT_TYPE_FILE
        } else {
            try {
                // Try to parse it as JSON
                const json = JSON.parse(result)
                result = json
            } catch(e) {} // If an error occurs parsing the json leave it as it was

            // If the result doesn't contain the field success, treat its
            // contents as the result part and add the succes field
            if (result.success === undefined) {
                result = {
                    success: code === 0,
                    result: result
                }
            }
        }

        cb(result, type)
    })
}

export default function dafuq(config) {

    // Allow constructor to be only the commands directory
    if (typeof config === 'string')
        config = { path: path }

    // Assign default values
    const opts = Object.assign({
        shebang: '',
        debug: false
    }, config)

    // Options validation

    // Valid string path is mandatory
    if (!opts.path || typeof opts.path !== 'string' || opts.path.length == 0)
        throw new TypeError('path must be a string pointing to the commands directory')

    // If shebang provided, but not valid
    if (opts.shebang && (typeof opts.shebang !== 'string' || opts.shebang.length == 0))
        throw new TypeError('shebang must be a non empty string')

    if (opts.debug !== undefined) {
        if (opts.debug === true)
            opts.debug = IS_TEST ? (() => {}) : console.log
        else if (opts.debug === false)
            opts.debug = (() => {})
        else if (typeof opts.debug !== 'function')
            throw new TypeError('debug must be a boolean or a logging function')
    }

    opts.debug('Building dafuq instance with %j', config)

    // Build an absolute path to the commands directory and assert it is actually a directory
    const commandsPath = path.resolve(path.dirname(module.parent.filename), opts.path)
    assertIsDirectory(commandsPath)

    // Find the files to be mounted as execution points
    let files = findFilesAt(commandsPath, globPattern)

    // If no shebang is specified, the file need to be executable by itself
    if (!opts.shebang) {
        files = files.filter(file => {
            const isExe = isExecutable(file.absolute)
            if (!isExe)
                opts.debug(`${ file.absolute } is not executable, ignoring`)
            return isExe
        })
    }

    const app = express()
    const upload = multer({ dest: os.tmpdir() })

    app.use(bodyParser.urlencoded({ extended: false }))
    app.use(bodyParser.json())

    /**
     * Returns a middleware that once inkoed will execute the specified file and
     * put the result of its execution on response object in the property pointed
     * by moduleName.
     *
     * @param {String} file
     */
    function executionMiddleware(file) {
        return (req, res, next) => {
            // Build the base command
            let cmd = file
            if (opts.shebang)
                cmd = `${ opts.shebang } ${ cmd }`
            /*
             Append all the parameters provided via:
                * query params
                * url params
                * body members
                * x-args headers
            */
            cmd += buildCommandFlags(req)

            opts.debug(`$ ${cmd}`)
            execCommand(cmd, (result, type) => {
                Object.defineProperty(res, resResultProperty, { value: result })
                Object.defineProperty(res, resTypeProperty, { value: type })
                next()
            })
        }
    }

    // Add all the files routes
    files.forEach(file => {
        const filePath = file.relative
        const url = '/' + path.dirname(file.relative)
        const method = path.basename(filePath, path.extname(filePath)).toLowerCase()
        const middleware = executionMiddleware(file.absolute)
        opts.debug(`Adding ${ method } ${ url }`)
        if (method === 'get' || method === 'head' || method === 'options')
            app[method](url, middleware)
        else
            app[method](url, upload.any(), middleware)
    })

    // Fallback behaviour, send the result
    app.all('*', (req, res, next) => {
        if (res[resTypeProperty] === RESULT_TYPE_OBJECT)
            res.type('json').json(res[resResultProperty])
        else if (res[resTypeProperty] === RESULT_TYPE_FILE)
            res.download(res[resResultProperty])
        else
            res.status(404).send()
    })

    return app
}
<|endoftext|>


<commit_before>'use strict';

var Hapi = require("hapi");
var Hoek = require('hoek');

var nconf = require("nconf");
nconf.argv().env().file({ file: 'nconf-deve.json' });

var options = {
  host: nconf.get("ABIBAO_WWW_STATIC_EXPOSE_IP"),
  port: nconf.get("ABIBAO_WWW_STATIC_EXPOSE_PORT")
};

var server = new Hapi.Server({
  debug: false,
  connections: {
    routes: {
      cors: true
    }
  }
});

server.connection(options);

server.register(require('inert'), (err) => {
  Hoek.assert(!err, err);
})

server.register(require('vision'), (err) => {
  Hoek.assert(!err, err);
  server.views({
    engines: {
      html: require('handlebars')
    },
    relativeTo: __dirname,
    path: './templates'
    // layoutPath: './templates/layout',
    // helpersPath: './templates/helpers'
  });
})

server.route({
  method: 'GET',
  path: '/',
  handler: {
    view: 'index'
  }
});

server.route({
  method: 'GET',
  path: '/index.html',
  handler: function(request, reply) {
    var data = {
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL')
    }
    reply.view('index', data);
  }
});

server.route({
  method: 'GET',
  path: '/mentions.html',
  handler: {
    view: 'mentions'
  }
});

server.route({
  method: 'GET',
  path: '/faq.html',
  handler: {
    view: 'faq'
  }
});

server.route({
  method: 'GET',
  path: '/cgu.html',
  handler: {
    view: 'cgu'
  }
});

/**
templates for charities
**/

server.route({
  method: 'GET',
  path: '/ellesimaginent',
  handler: function(request, reply) {
    var data = {
      page_title: 'Association / Elle’s Imagine’nt',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Elle’s Imagine’nt',
        name_long: 'Elle’s Imagine’nt',
        hangs: 'Ensemble, aidons les femmes victimes de violences conjugales',
        urn: 'urn:abibao:database:entity:ffd68c859144a29d0893be25caede9d4a2c69b86986b1d38',
        description: 'Elle’s Imagine’nt est composée de psychologues, d’avocats, d’assistantes sociales et de personnes qui viennent d’autres horizons. Nous accompagnons psychologiquement, juridiquement et socialement les femmes victimes de violences conjugales. Nous animons des groupes de parole et proposons du coaching de retour à l’emploi.',
        picture01: './assets/img/logo_associations/asso-elles_imaginent.png',
        picture02: './assets/img/abibao-flyers-ellesimaginent.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/clowns',
  handler: function(request, reply) {
    var data = {
      page_title: 'Association / Les Clowns Z’Hôpitaux',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Les Clowns Z’Hôpitaux',
        name_long: 'Les Clowns Z’Hôpitaux',
        hangs: 'Faîtes entrer les Clowns à l’hôpital',
        urn: 'urn:abibao:database:entity:ffd68c859144fbc90893be25caede9d4a2c69b86986b1d37',
        description: 'Depuis 2004, l’association "Clowns Z’hôpitaux" a pour objectif de créer et d’organiser des interventions de duos de Clowns en milieu hospitalier et dans tous les établissements de soins à destination de celles et ceux qui ont besoin de réconfort à l’hôpital comme en maison de retraite ou en maison d’accueil spécialisé : enfants, personnes âgées, adultes en situation de handicap.',
        picture01: './assets/img/logo_associations/asso-clowns.png',
        picture02: './assets/img/abibao-flyers-clowns.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/casdb',
  handler: function(request, reply) {
    var data = {
      page_title: 'Association / Centre Audiovisuel Simone de Beauvoir',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Le Centre Simone de Beauvoir',
        name_long: 'le Centre Audiovisuel Simone de Beauvoir',
        hangs: 'Préservons la mémoire des femmes !',
        urn: 'urn:abibao:database:entity:ffd68c859145f29f0893be25caede9d4a2c69b86986b1d35',
        description: 'Créé en 1982, le Centre travaille à la préservation de la mémoire des droits, des luttes et des créations des femmes dans la société. Une collection de 1200 films exceptionnels et uniques sur les femmes, l’art, l’histoire, le sport, le travail, le corps… des années 70 à nos jours.',
        picture01: './assets/img/logo_associations/asso-casdb.png',
        picture02: './assets/img/abibao-flyers-casdb.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/schola',
  handler: function(request, reply) {
    var data = {
      page_title: 'Association / Schola Africa',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Schola Africa',
        name_long: 'Schola Africa',
        hangs: 'Une aventure humaine au Burkina',
        urn: 'urn:abibao:database:entity:ffd68c859144a0ce0893be25caede9d4a2c69b86986b1d39',
        description: 'Schola Africa est une association humanitaire étudiante (loi 1901) œuvrant pour la scolarisation et la formation professionnelle en milieu rural au Burkina Faso. Nous menons également des actions de promotion de la culture africaine et de sensibilisation aux problématiques du développement dans la métropole lilloise.',
        picture01: './assets/img/logo_associations/asso-scholaafrica',
        picture02: './assets/img/abibao-flyers-schola.jpg'
      }
    }
    reply.view('association', data);
  }
});

/**
assets
**/

server.route({
  method: 'GET',
  path: '/assets/{path*}',
  handler: {
    directory: {
      path: __dirname + '/templates/assets/'
    }
  }
});

server.start(function(err) {
  if (err) {
    return console.log(err);
  }
  console.log('server running at:', server.info.uri);
});
<commit_msg>Mise à jour static ASSO (part2)<commit_after>'use strict';

var Hapi = require("hapi");
var Hoek = require('hoek');

var nconf = require("nconf");
nconf.argv().env().file({ file: 'nconf-deve.json' });

var options = {
  host: nconf.get("ABIBAO_WWW_STATIC_EXPOSE_IP"),
  port: nconf.get("ABIBAO_WWW_STATIC_EXPOSE_PORT")
};

var server = new Hapi.Server({
  debug: false,
  connections: {
    routes: {
      cors: true
    }
  }
});

server.connection(options);

server.register(require('inert'), (err) => {
  Hoek.assert(!err, err);
})

server.register(require('vision'), (err) => {
  Hoek.assert(!err, err);
  server.views({
    engines: {
      html: require('handlebars')
    },
    relativeTo: __dirname,
    path: './templates'
    // layoutPath: './templates/layout',
    // helpersPath: './templates/helpers'
  });
})

server.route({
  method: 'GET',
  path: '/',
  handler: {
    view: 'index'
  }
});

server.route({
  method: 'GET',
  path: '/index.html',
  handler: function(request, reply) {
    var data = {
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL')
    }
    reply.view('index', data);
  }
});

server.route({
  method: 'GET',
  path: '/mentions.html',
  handler: {
    view: 'mentions'
  }
});

server.route({
  method: 'GET',
  path: '/faq.html',
  handler: {
    view: 'faq'
  }
});

server.route({
  method: 'GET',
  path: '/cgu.html',
  handler: {
    view: 'cgu'
  }
});

/**
templates for charities
**/

server.route({
  method: 'GET',
  path: '/ellesimaginent',
  handler: function(request, reply) {
    var data = {
      page_title: 'Association / Elle’s Imagine’nt',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Elle’s Imagine’nt',
        name_long: 'Elle’s Imagine’nt',
        hangs: 'Ensemble, aidons les femmes victimes de violences conjugales',
        urn: 'urn:abibao:database:entity:ffd68c859144a29d0893be25caede9d4a2c69b86986b1d38',
        description: 'Elle’s Imagine’nt est composée de psychologues, d’avocats, d’assistantes sociales et de personnes qui viennent d’autres horizons. Nous accompagnons psychologiquement, juridiquement et socialement les femmes victimes de violences conjugales. Nous animons des groupes de parole et proposons du coaching de retour à l’emploi.',
        picture01: './assets/img/logo_associations/asso-elles_imaginent.png',
        picture02: './assets/img/abibao-flyers-ellesimaginent.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/clowns',
  handler: function(request, reply) {
    var data = {
      page_title: 'Aidez Les Clowns Z’Hôpitaux en répondant à des sondages',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Les Clowns Z’Hôpitaux',
        name_long: 'Les Clowns Z’Hôpitaux',
        hangs: 'Faîtes entrer les Clowns à l’hôpital',
        urn: 'urn:abibao:database:entity:ffd68c859144fbc90893be25caede9d4a2c69b86986b1d37',
        description: 'Depuis 2004, l’association "Clowns Z’hôpitaux" a pour objectif de créer et d’organiser des interventions de duos de Clowns en milieu hospitalier et dans tous les établissements de soins à destination de celles et ceux qui ont besoin de réconfort à l’hôpital comme en maison de retraite ou en maison d’accueil spécialisé : enfants, personnes âgées, adultes en situation de handicap.',
        picture01: './assets/img/logo_associations/asso-clowns.png',
        picture02: './assets/img/abibao-flyers-clowns.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/casdb',
  handler: function(request, reply) {
    var data = {
      page_title: 'Aidez le Centre Audiovisuel Simone de Beauvoir en répondant à des sondages',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Le Centre Simone de Beauvoir',
        name_long: 'le Centre Audiovisuel Simone de Beauvoir',
        hangs: 'Préservons la mémoire des femmes !',
        urn: 'urn:abibao:database:entity:ffd68c859145f29f0893be25caede9d4a2c69b86986b1d35',
        description: 'Créé en 1982, le Centre travaille à la préservation de la mémoire des droits, des luttes et des créations des femmes dans la société. Une collection de 1200 films exceptionnels et uniques sur les femmes, l’art, l’histoire, le sport, le travail, le corps… des années 70 à nos jours.',
        picture01: './assets/img/logo_associations/asso-casdb.png',
        picture02: './assets/img/abibao-flyers-casdb.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/schola',
  handler: function(request, reply) {
    var data = {
      page_title: 'Aidez l\'association Schola Africa en répondant à des sondages',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Schola Africa',
        name_long: 'Schola Africa',
        hangs: 'Une aventure humaine au Burkina',
        urn: 'urn:abibao:database:entity:ffd68c859144a0ce0893be25caede9d4a2c69b86986b1d39',
        description: 'Schola Africa est une association humanitaire étudiante (loi 1901) œuvrant pour la scolarisation et la formation professionnelle en milieu rural au Burkina Faso. Nous menons également des actions de promotion de la culture africaine et de sensibilisation aux problématiques du développement dans la métropole lilloise.',
        picture01: './assets/img/logo_associations/asso-scholaafrica.png',
        picture02: './assets/img/abibao-flyers-schola.jpg'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/sosh',
  handler: function(request, reply) {
    var data = {
      page_title: 'Aidez l\'association SOS-Homophobie en répondant à des sondages',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'SOS-Homophobie',
        name_long: 'SOS-Homophobie',
        hangs: 'Lutte contre la lesbophobie, la gayphobie, la biphobie et la transphobie',
        urn: 'urn:abibao:database:entity:ffd68c859144a5cc0893be25caede9d4a2c69b86986b1d61',
        description: 'SOS Homophobie, est une association française à but non lucratif, de lutte contre les discriminations et les agressions à caractère homophobe à l\'encontre des lesbiennes, des gays, des bisexuels et des trans créée le 11 avril 1994. Elle se compose essentiellement de membres bénévoles dans toute la France.',
        picture01: './assets/img/logo_associations/asso-soshomophobie.png',
        picture02: './assets/img/logo_associations/asso-soshomophobie.png'
      }
    }
    reply.view('association', data);
  }
});

server.route({
  method: 'GET',
  path: '/tfc',
  handler: function(request, reply) {
    var data = {
      page_title: 'Aidez l\'association Ticket For Change en répondant à des sondages',
      dashboard_url: nconf.get('ABIBAO_WWW_STATIC_DASHBOARD_URL'),
      charity: {
        name: 'Ticket For Change',
        name_long: 'Ticket For Change',
        hangs: 'Changer la société par l\'entrepreneuriat',
        urn: 'urn:abibao:database:entity:ffd68c859144f6c50893be25caede9d4a2c69b86986b1d36',
        description: 'Ticket for Change accompagne des individus qui utilisent l’entrepreneuriat pour répondre à des problèmes sociaux ou environnementaux. Nous les aidons à passer de l’envie à l’idée et de l’idée à l’action afin qu'ils aient le plus d'impact positif. Nous sommes soutenus par des pionniers de l’innovation sociale comme Nicolas Hulot, Pierre Rabhi ou André Dupon. En 2 ans, nous avons contribué à l\'émergence 300 entreprises sociales, dont 40 que nous accompagnons aujourd\'hui.',
        picture01: './assets/img/logo_associations/asso-tfc.png',
        picture02: './assets/img/logo_associations/asso-tfc.png'
      }
    }
    reply.view('association', data);
  }
});

/**
assets
**/

server.route({
  method: 'GET',
  path: '/assets/{path*}',
  handler: {
    directory: {
      path: __dirname + '/templates/assets/'
    }
  }
});

server.start(function(err) {
  if (err) {
    return console.log(err);
  }
  console.log('server running at:', server.info.uri);
});
<|endoftext|>


<commit_before>import {
    defaultsDeep,
    camelCase,
    isRegExp,
    isString,
    isObject,
    isEmpty,
    escapeRegExp,
    mapKeys,
    trim
} from 'lodash';
import {
    sep,
    dirname,
    resolve
} from 'path';
import loaderUtils from 'loader-utils';

/**
 * @private
 * @type {Object}
 */
const DEFAULT_OPTIONS = {
    module: 'translations',
    prefix: '',
    namespaces: sep,
    localeInterpolate: /[a-z]{2}_[A-Z]{2}/,
    defaultLocale: 'en_US'
};

/**
 * @private
 * @type {String}
 */
const CONFIG_KEY = camelCase('angular-translate');

/**
 * @private
 * @type {RegExp}
 */
const MODULE_EXPORTS = /module\.exports\s?=\s?({[\s\S\n\t]+?});?/;

/**
 * @private
 * @type {Object<String,Function>}
 */
const PREFIX_INTERPOLATIONS = {
    /**
     * @private
     * @param {String} context
     * @param {String} resourcePath
     * @returns {String}
     */
    '[dir]': (context, resourcePath) => trim(dirname(resolve(resourcePath).replace(resolve(context), '')), sep)
};

/**
 * @private
 * @param {*} loaderContext
 * @returns {Object}
 */
const getOptions = loaderContext => {
    const options = loaderUtils.getLoaderConfig(loaderContext, CONFIG_KEY);

    defaultsDeep(options, DEFAULT_OPTIONS);

    if (!isRegExp(options.localeInterpolate)) {
        options.localeInterpolate = new RegExp(escapeRegExp(options.localeInterpolate));
    }

    return options;
};

/**
 * @private
 * @param {*} loaderContext
 * @param {Object} options
 * @returns {String}
 */
const extractLocale = (loaderContext, options) => {
    const match = loaderContext.resourcePath.match(options.localeInterpolate);

    return match !== null ? match.toString() : options.defaultLocale;
};

/**
 * @private
 * @param {*} loaderContext
 * @param {*} content
 * @param {Object} options
 * @returns {String}
 */
const interpolatePrefix = (loaderContext, content, options) => {
    let prefix = options.prefix,
        namespaces = options.namespaces;

    if (!isEmpty(prefix)) {
        const context = options.context || loaderContext.options.context || './';

        prefix = loaderUtils.interpolateName(loaderContext, prefix, {
            context: context,
            content: isObject(content) ? JSON.stringify(content) : content,
            regExp: options.regExp
        });

        for (const [key, value] of Object.entries(PREFIX_INTERPOLATIONS)) {
            prefix = prefix.replace(new RegExp(escapeRegExp(key)), value(context, loaderContext.resourcePath));
        }

        prefix = prefix.split(sep).join(namespaces) + namespaces;
    }

    return prefix;
};

/**
 * @private
 * @param {*} loaderContext
 * @param {String} content
 * @param {Object} options
 * @returns {Object}
 */
const extractTranslations = (loaderContext, content, options) => {
    let translations;

    if (Array.isArray(loaderContext.inputValue)) {
        translations = loaderContext.inputValue[0];
    } else if (isString(loaderContext.value)) {
        translations = JSON.parse(loaderContext.value);
    } else if (MODULE_EXPORTS.test(content)) {
        const match = content.match(MODULE_EXPORTS),
            value = match.length >= 1 ? match[1].toString() : '{}';

        translations = JSON.parse(loaderUtils.parseString(value));
    } else {
        translations = content;
    }

    if (!isObject(translations)) {
        translations = {};
    }

    const prefix = interpolatePrefix(loaderContext, translations, options);

    return mapKeys(translations, (value, key) => prefix + key);
};

/**
 * @param {*} content
 * @returns {String}
 */
export default function(content) {
    if (this.cacheable) {
        this.cacheable();
    }

    const options = getOptions(this),
        locale = extractLocale(this, options),
        translations = extractTranslations(this, content, options);

    this.value = translations;

    return `var angular = require("angular");
var translations = ${JSON.stringify(translations, null, '\t')};\n
angular.module("${options.module}", ["pascalprecht.translate"]).config(["$translateProvider", function($translateProvider) {
\t$translateProvider.translations("${locale}", translations);
}]);\n
module.exports = translations;`;
}
<commit_msg>fix: now `namespaces` is os independent<commit_after>import {
    defaultsDeep,
    camelCase,
    isRegExp,
    isString,
    isObject,
    isEmpty,
    escapeRegExp,
    mapKeys,
    trim
} from 'lodash';
import {
    sep,
    dirname,
    resolve
} from 'path';
import loaderUtils from 'loader-utils';

/**
 * @private
 * @type {Object}
 */
const DEFAULT_OPTIONS = {
    module: 'translations',
    prefix: '',
    namespaces: '/',
    localeInterpolate: /[a-z]{2}_[A-Z]{2}/,
    defaultLocale: 'en_US'
};

/**
 * @private
 * @type {String}
 */
const CONFIG_KEY = camelCase('angular-translate');

/**
 * @private
 * @type {RegExp}
 */
const MODULE_EXPORTS = /module\.exports\s?=\s?({[\s\S\n\t]+?});?/;

/**
 * @private
 * @type {Object<String,Function>}
 */
const PREFIX_INTERPOLATIONS = {
    /**
     * @private
     * @param {String} context
     * @param {String} resourcePath
     * @returns {String}
     */
    '[dir]': (context, resourcePath) => trim(dirname(resolve(resourcePath).replace(resolve(context), '')), sep)
};

/**
 * @private
 * @param {*} loaderContext
 * @returns {Object}
 */
const getOptions = loaderContext => {
    const options = loaderUtils.getLoaderConfig(loaderContext, CONFIG_KEY);

    defaultsDeep(options, DEFAULT_OPTIONS);

    if (!isRegExp(options.localeInterpolate)) {
        options.localeInterpolate = new RegExp(escapeRegExp(options.localeInterpolate));
    }

    return options;
};

/**
 * @private
 * @param {*} loaderContext
 * @param {Object} options
 * @returns {String}
 */
const extractLocale = (loaderContext, options) => {
    const match = loaderContext.resourcePath.match(options.localeInterpolate);

    return match !== null ? match.toString() : options.defaultLocale;
};

/**
 * @private
 * @param {*} loaderContext
 * @param {*} content
 * @param {Object} options
 * @returns {String}
 */
const interpolatePrefix = (loaderContext, content, options) => {
    let prefix = options.prefix,
        namespaces = options.namespaces;

    if (!isEmpty(prefix)) {
        const context = options.context || loaderContext.options.context || './';

        prefix = loaderUtils.interpolateName(loaderContext, prefix, {
            context: context,
            content: isObject(content) ? JSON.stringify(content) : content,
            regExp: options.regExp
        });

        for (const [key, value] of Object.entries(PREFIX_INTERPOLATIONS)) {
            prefix = prefix.replace(new RegExp(escapeRegExp(key)), value(context, loaderContext.resourcePath));
        }

        prefix = prefix.split(sep).join(namespaces) + namespaces;
    }

    return prefix;
};

/**
 * @private
 * @param {*} loaderContext
 * @param {String} content
 * @param {Object} options
 * @returns {Object}
 */
const extractTranslations = (loaderContext, content, options) => {
    let translations;

    if (Array.isArray(loaderContext.inputValue)) {
        translations = loaderContext.inputValue[0];
    } else if (isString(loaderContext.value)) {
        translations = JSON.parse(loaderContext.value);
    } else if (MODULE_EXPORTS.test(content)) {
        const match = content.match(MODULE_EXPORTS),
            value = match.length >= 1 ? match[1].toString() : '{}';

        translations = JSON.parse(loaderUtils.parseString(value));
    } else {
        translations = content;
    }

    if (!isObject(translations)) {
        translations = {};
    }

    const prefix = interpolatePrefix(loaderContext, translations, options);

    return mapKeys(translations, (value, key) => prefix + key);
};

/**
 * @param {*} content
 * @returns {String}
 */
export default function(content) {
    if (this.cacheable) {
        this.cacheable();
    }

    const options = getOptions(this),
        locale = extractLocale(this, options),
        translations = extractTranslations(this, content, options);

    this.value = translations;

    return `var angular = require("angular");
var translations = ${JSON.stringify(translations, null, '\t')};\n
angular.module("${options.module}", ["pascalprecht.translate"]).config(["$translateProvider", function($translateProvider) {
\t$translateProvider.translations("${locale}", translations);
}]);\n
module.exports = translations;`;
}
<|endoftext|>


<commit_before>import crypto from 'crypto';
import request from 'request';
import dateformat from 'dateformat';
import defaults from 'lodash/defaults';
import noop from 'lodash/noop';

class Yuntongxun {
  static defaultOptions = {
    urlPrefix: 'https://app.cloopen.com:8883',
    version: '2013-12-26',
    logger: noop
  };

  constructor(options) {
    this.options = defaults({}, options, Yuntongxun.defaultOptions);

    this.rs = request.defaults({
      baseUrl: `${this.options.urlPrefix}/${this.options.version}/Accounts/${this.options.accountSid}`,
      strictSSL: false,
      timeout: this.options.timeout,
      proxy: this.options.proxy,
      headers: {
        accept: 'application/json'
      },
      json: true,
      time: true
    });
  }

  _getSig(timestamp) {
    const md5sum = crypto.createHash('md5');
    md5sum.update(`${this.options.accountSid}${this.options.authToken}${timestamp}`, 'utf8');
    let sig = md5sum.digest('hex');
    sig = sig.toUpperCase();
    if (this.options.debug) {
      this.options.logger(`sig: ${sig}`);
    }

    return sig;
  }

  _getAuthorization(timestamp) {
    const buf = new Buffer(`${this.options.accountSid}:${timestamp}`, 'utf8');
    const authorization = buf.toString('base64');
    if (this.options.debug) {
      this.options.logger(`authorization: ${authorization}`);
    }

    return authorization;
  }

  _request(url, body, method) {
    const timestamp = dateformat(new Date(), 'yyyymmddHHMMss');
    const authorization = this._getAuthorization(timestamp);
    const sig = this._getSig(timestamp);
    if (!method) {
      method = 'POST';
    }
    const opts = {
      headers: {
        authorization
      },
      qs: {
        sig
      },
      method: method
    };
    if (method == "GET") {
      defaults(opts['qs'], body);
    } else {
      opts['body'] = body;
    }

    if (this.options.debug) {
      this.options.logger(`opts: ${JSON.stringify(opts)}`);
    }

    return new Promise((resolve, reject) => {
      this.rs(url, opts, (err, message, body) => {
        if (err) {
          this.options.logger(`Request failed. err[${err}]`);
          reject(err);
          return;
        }
        if (!body) {
          this.options.logger(`Request err. statusCode[${message.statusCode}] statusMessage[${message.statusMessage}]`);
          reject('null response body');
          return;
        }
        resolve(body);
      }).on('complete', (response) => {
        this.options.logger(`Request complete. elapsedTime[${response.elapsedTime}]`);
      });
    });
  }

  voiceVerify(to, verifyCode, displayNum = '', playTimes = 3) {
    if (!to || !verifyCode) {
      return Promise.reject('mobile or token empty');
    }

    return this._request('/Calls/VoiceVerify', {
      appId: this.options.appId,
      verifyCode,
      to,
      displayNum,
      playTimes
    }).then((body) => {
      if (body.statusCode !== '000000') {
        this.options.logger(`Request voiceVerify err. body[${JSON.stringify(body)}]`);
        throw new Error('response invalid');
      }
      const callSid = body.VoiceVerify && body.VoiceVerify.callSid;
      this.options.logger(`Request voiceVerify succ. callSid[${callSid}]`);
      return callSid;
    });
  }

  callResult(callsid) {
    if (!callsid) {
      return Promise.reject('callsid empty');
    }
    return this._request('/CallResult', {
      callsid: callsid,
    }, 'GET').then((body) => {
      return body;
    });
  }

  templateSms(to, templateId, datas = []) {
    if (!to || !templateId) {
      return Promise.reject('mobile or templateId empty');
    }
    return this._request('/SMS/TemplateSMS', {
      appId: this.options.appId,
      to,
      templateId,
      datas
    }).then((body) => {
      if (body.statusCode !== '000000') {
        this.options.logger(`Request templateSms err. body[${JSON.stringify(body)}]`);
        throw new Error('response invalid');
      }
      const smsMessageSid = body.TemplateSMS && body.TemplateSMS.smsMessageSid;
      this.options.logger(`Request templateSms succ. smsMessageSid[${smsMessageSid}]`);
      return smsMessageSid;
    });
  }
}

export default Yuntongxun;
<commit_msg>修改eslint<commit_after>import crypto from 'crypto';
import request from 'request';
import dateformat from 'dateformat';
import defaults from 'lodash/defaults';
import noop from 'lodash/noop';

class Yuntongxun {
  static defaultOptions = {
    urlPrefix: 'https://app.cloopen.com:8883',
    version: '2013-12-26',
    logger: noop
  };

  constructor(options) {
    this.options = defaults({}, options, Yuntongxun.defaultOptions);

    this.rs = request.defaults({
      baseUrl: `${this.options.urlPrefix}/${this.options.version}/Accounts/${this.options.accountSid}`,
      strictSSL: false,
      timeout: this.options.timeout,
      proxy: this.options.proxy,
      headers: {
        accept: 'application/json'
      },
      json: true,
      time: true
    });
  }

  _getSig(timestamp) {
    const md5sum = crypto.createHash('md5');
    md5sum.update(`${this.options.accountSid}${this.options.authToken}${timestamp}`, 'utf8');
    let sig = md5sum.digest('hex');
    sig = sig.toUpperCase();
    if (this.options.debug) {
      this.options.logger(`sig: ${sig}`);
    }

    return sig;
  }

  _getAuthorization(timestamp) {
    const buf = new Buffer(`${this.options.accountSid}:${timestamp}`, 'utf8');
    const authorization = buf.toString('base64');
    if (this.options.debug) {
      this.options.logger(`authorization: ${authorization}`);
    }

    return authorization;
  }

  _request(url, body, method) {
    const timestamp = dateformat(new Date(), 'yyyymmddHHMMss');
    const authorization = this._getAuthorization(timestamp);
    const sig = this._getSig(timestamp);
    if (!method) {
      method = 'POST';
    }
    const opts = {
      headers: {
        authorization
      },
      qs: {
        sig
      },
      method
    };
    if (method === 'GET') {
      defaults(opts.qs, body);
    } else {
      opts.body = body;
    }

    if (this.options.debug) {
      this.options.logger(`opts: ${JSON.stringify(opts)}`);
    }

    return new Promise((resolve, reject) => {
      this.rs(url, opts, (err, message, body) => {
        if (err) {
          this.options.logger(`Request failed. err[${err}]`);
          reject(err);
          return;
        }
        if (!body) {
          this.options.logger(`Request err. statusCode[${message.statusCode}] statusMessage[${message.statusMessage}]`);
          reject('null response body');
          return;
        }
        resolve(body);
      }).on('complete', (response) => {
        this.options.logger(`Request complete. elapsedTime[${response.elapsedTime}]`);
      });
    });
  }

  voiceVerify(to, verifyCode, displayNum = '', playTimes = 3) {
    if (!to || !verifyCode) {
      return Promise.reject('mobile or token empty');
    }

    return this._request('/Calls/VoiceVerify', {
      appId: this.options.appId,
      verifyCode,
      to,
      displayNum,
      playTimes
    }).then((body) => {
      if (body.statusCode !== '000000') {
        this.options.logger(`Request voiceVerify err. body[${JSON.stringify(body)}]`);
        throw new Error('response invalid');
      }
      const callSid = body.VoiceVerify && body.VoiceVerify.callSid;
      this.options.logger(`Request voiceVerify succ. callSid[${callSid}]`);
      return callSid;
    });
  }

  callResult(callsid) {
    if (!callsid) {
      return Promise.reject('callsid empty');
    }
    return this._request('/CallResult', {
      callsid
    }, 'GET').then((body) => {
      return body;
    });
  }

  templateSms(to, templateId, datas = []) {
    if (!to || !templateId) {
      return Promise.reject('mobile or templateId empty');
    }
    return this._request('/SMS/TemplateSMS', {
      appId: this.options.appId,
      to,
      templateId,
      datas
    }).then((body) => {
      if (body.statusCode !== '000000') {
        this.options.logger(`Request templateSms err. body[${JSON.stringify(body)}]`);
        throw new Error('response invalid');
      }
      const smsMessageSid = body.TemplateSMS && body.TemplateSMS.smsMessageSid;
      this.options.logger(`Request templateSms succ. smsMessageSid[${smsMessageSid}]`);
      return smsMessageSid;
    });
  }
}

export default Yuntongxun;
<|endoftext|>


<commit_before>const React = require('react');
const arrayFindIndex = require('array-find-index');
const classNames = require('classnames');

const log = console.log.bind(console);
const logError = console.error ? console.error.bind(console) : log;
const logWarning = console.warn ? console.warn.bind(console) : log;

/* converts given number of seconds to standard time display format
 * http://goo.gl/kEvnKn
 */
function convertToTime (number) {
  const mins = Math.floor(number / 60);
  const secs = (number % 60).toFixed();
  return `${ mins < 10 ? '0' : '' }${ mins }:${ secs < 10 ? '0' : '' }${ secs }`;
}

/*
 * AudioPlayer
 *
 * Accepts 'playlist' prop of the form:
 *
 * [{ "url": "./path/to/file.mp3",
 *    "displayText": "ArtistA - Track 1" },
 *  { "url": "https://domain.com/track2.ogg",
 *    "displayText": "ArtistB - Track 2" }]
 *
 * Accepts 'autoplay' prop (true/[false]).
 *
 * Accepts 'autoplayDelayInSeconds' prop (default 0).
 *
 * Accepts 'gapLengthInSeconds' prop (default 0).
 * Specifies gap at end of one track before next
 * track begins (ignored for manual skip).
 *
 * Accepts 'hideBackSkip' prop (default false,
 * hides back skip button if true).
 *
 * Accepts 'stayOnBackSkipThreshold' prop, default 5,
 * is number of seconds to progress until pressing back skip
 * restarts the current track.
 *
 * Accepts 'placeAtTop' prop, default false, if true,
 * player is placed at top of screen instead of bottom.
 *
 */
class AudioPlayer extends React.Component {

  constructor (props) {
    super(props);

    this.playlist = props.playlist;

    /* true if the user is currently dragging the mouse
     * to seek a new track position
     */
    this.seekInProgress = false;
    // index matching requested track (whether track has loaded or not)
    this.currentTrackIndex = 0;

    this.state = {
      /* activeTrackIndex will change to match
       * this.currentTrackIndex once metadata has loaded
       */
      activeTrackIndex: -1,
      // indicates whether audio player should be paused
      paused: true,
      /* elapsed time for current track, in seconds -
       * DISPLAY ONLY! the actual elapsed time may
       * not match up if we're currently seeking, since
       * the new time is visually previewed before the
       * audio seeks.
       */
      displayedTime: 0
    };

    // html audio element used for playback
    this.audio = null;
    this.audioProgressContainer = null;
    /* bounding rectangle used for calculating seek
     * position from mouse/touch coordinates
     */
    this.audioProgressBoundingRect = null;

    // EventListeners to create on mount and remove on unmount
    this.seekReleaseListener = null;
    this.resizeListener = null;
  }

  componentDidMount () {
    require('./index.scss');

    const seekReleaseListener = this.seekReleaseListener = this.seek.bind(this);
    window.addEventListener('mouseup', seekReleaseListener);
    document.addEventListener('touchend', seekReleaseListener);
    const resizeListener = this.resizeListener = this.fetchAudioProgressBoundingRect.bind(this);
    window.addEventListener('resize', resizeListener);
    resizeListener();

    const audio = this.audio = document.createElement('audio');
    audio.preload = 'metadata';
    audio.addEventListener('ended', () => {
      const gapLengthInSeconds = this.props.gapLengthInSeconds || 0;
      setTimeout(this.skipToNextTrack.bind(this), gapLengthInSeconds * 1000);
    });
    audio.addEventListener('timeupdate', this.handleTimeUpdate.bind(this));
    audio.addEventListener('loadedmetadata', () => {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    });
    audio.addEventListener('play', () => {
      this.setState({
        paused: false
      });
    });
    audio.addEventListener('stalled', this.togglePause.bind(this, true));
    if (this.playlist && this.playlist.length) {
      this.updateSource();
      if (this.props.autoplay) {
        const delay = this.props.autoplayDelayInSeconds || 0;
        setTimeout(this.togglePause.bind(this, false), delay * 1000);
      }
    }
  }

  componentWillUnmount () {
    // remove event listeners bound outside the scope of our component
    window.removeEventListener('mouseup', this.seekReleaseListener);
    document.removeEventListener('touchend', this.seekReleaseListener);
    window.removeEventListener('resize', this.resizeListener);

    /* pause the audio element before dereferencing it
     * (we can't know when garbage collection will run)
     */
    this.audio.pause();
    this.audio = null;
  }

  componentWillReceiveProps (nextProps) {
    const currentTrackUrl = (
      (this.playlist || [])[this.currentTrackIndex] || {}
    ).url;
    this.playlist = nextProps.playlist;
    this.currentTrackIndex = arrayFindIndex((this.playlist || []), track => {
      return track.url && currentTrackUrl === track.url;
    });
    /* if the track we're already playing is in the new playlist, update the
     * activeTrackIndex and we're done.
     */
    if (this.currentTrackIndex !== -1) {
      return this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    }
    /* otherwise, if our audio element is available, pause and queue up
     * the new first track.
     */
    if (this.audio) {
      this.skipToNextTrack(false);
    }
  }

  togglePause (value) {
    if (!this.audio) {
      return;
    }
    const pause = typeof value === 'boolean' ? value : !this.state.paused;
    if (pause) {
      this.audio.pause();
      return this.setState({
        paused: true
      });
    }
    if (!this.playlist || !this.playlist.length) {
      return;
    }
    try {
      this.audio.play();
    } catch (error) {
      logError(error);
      const warningMessage =
        'Audio playback failed at ' +
        new Date().toLocaleTimeString() +
        '! (Perhaps autoplay is disabled in this browser.)';
      logWarning(warningMessage);
    }
  }

  skipToNextTrack (shouldPlay) {
    if (!this.audio) {
      return;
    }
    this.audio.pause();
    if (!this.playlist || !this.playlist.length) {
      return;
    }
    let i = this.currentTrackIndex + 1;
    if (i >= this.playlist.length) {
      i = 0;
    }
    this.currentTrackIndex = i;
    this.setState({
      activeTrackIndex: -1,
      displayedTime: 0
    }, () => {
      this.updateSource();
      const shouldPause = typeof shouldPlay === 'boolean' ? !shouldPlay : false;
      this.togglePause(shouldPause);
    });
  }

  backSkip () {
    if (!this.playlist || !this.playlist.length) {
      return;
    }
    const audio = this.audio;
    let stayOnBackSkipThreshold = this.props.stayOnBackSkipThreshold;
    if (isNaN(stayOnBackSkipThreshold)) {
      stayOnBackSkipThreshold = 5;
    }
    if (audio.currentTime >= stayOnBackSkipThreshold) {
      return audio.currentTime = 0;
    }
    let i = this.currentTrackIndex - 1;
    if (i < 0) {
      i = this.playlist.length - 1;
    }
    this.currentTrackIndex = i - 1;
    this.skipToNextTrack();
  }

  updateSource () {
    this.audio.src = this.playlist[this.currentTrackIndex].url;
  }

  fetchAudioProgressBoundingRect () {
    this.audioProgressBoundingRect = this.audioProgressContainer.getBoundingClientRect();
  }

  handleTimeUpdate () {
    if (!this.seekInProgress && this.audio) {
      this.setState({
        displayedTime: this.audio.currentTime
      });
    }
  }

  adjustDisplayedTime (event) {
    if (!this.playlist || !this.playlist.length) {
      return;
    }
    // make sure we don't select stuff in the background while seeking
    if (event.type === 'mousedown' || event.type === 'touchstart') {
      this.seekInProgress = true;
      document.body.classList.add('noselect');
    } else if (!this.seekInProgress) {
      return;
    }
    /* we don't want mouse handlers to receive the event
     * after touch handlers if we're seeking.
     */
    event.preventDefault();
    const boundingRect = this.audioProgressBoundingRect;
    const isTouch = event.type.slice(0, 5) === 'touch';
    const pageX = isTouch ? event.targetTouches.item(0).pageX : event.pageX;
    const position = pageX - boundingRect.left - document.body.scrollLeft;
    const containerWidth = boundingRect.width;
    const progressPercentage = position / containerWidth;
    this.setState({
      displayedTime: progressPercentage * this.audio.duration
    });
  }

  seek (event) {
    /* this function is activated when the user lets
     * go of the mouse, so if .noselect was applied
     * to the document body, get rid of it.
     */
    document.body.classList.remove('noselect');
    if (!this.seekInProgress) {
      return;
    }
    /* we don't want mouse handlers to receive the event
     * after touch handlers if we're seeking.
     */
    event.preventDefault();
    this.seekInProgress = false;
    const displayedTime = this.state.displayedTime;
    if (isNaN(displayedTime)) {
      return;
    }
    this.audio.currentTime = displayedTime;
  }

  render () {
    const activeIndex = this.state.activeTrackIndex;
    const displayText = activeIndex < 0 ? null : this.playlist[activeIndex].displayText;

    const displayedTime = this.state.displayedTime;
    const duration = this.audio && this.audio.duration || 0;

    const elapsedTime = convertToTime(displayedTime);
    const fullTime = convertToTime(duration);
    const timeRatio = `${ elapsedTime } / ${ fullTime }`;

    const progressBarWidth = `${ (displayedTime / duration) * 100 }%`;

    return (

 this.audioProgressContainer = ref }
             onMouseDown={ this.adjustDisplayedTime.bind(this) }
             onMouseMove={ this.adjustDisplayedTime.bind(this) }
             onTouchStart={ this.adjustDisplayedTime.bind(this) }
             onTouchMove={ this.adjustDisplayedTime.bind(this) }>

                { displayText }

              { timeRatio }

    );
  }

}

AudioPlayer.propTypes = {
  playlist: React.PropTypes.array,
  autoplay: React.PropTypes.bool,
  autoplayDelayInSeconds: React.PropTypes.number,
  gapLengthInSeconds: React.PropTypes.number,
  hideBackSkip: React.PropTypes.bool,
  stayOnBackSkipThreshold: React.PropTypes.number,
  placeAtTop: React.PropTypes.bool
};

module.exports = AudioPlayer;
<commit_msg>AudioPlayer now supports null playlist (displays message asking user to load a playlist).<commit_after>const React = require('react');
const arrayFindIndex = require('array-find-index');
const classNames = require('classnames');

const log = console.log.bind(console);
const logError = console.error ? console.error.bind(console) : log;
const logWarning = console.warn ? console.warn.bind(console) : log;

/* converts given number of seconds to standard time display format
 * http://goo.gl/kEvnKn
 */
function convertToTime (number) {
  const mins = Math.floor(number / 60);
  const secs = (number % 60).toFixed();
  return `${ mins < 10 ? '0' : '' }${ mins }:${ secs < 10 ? '0' : '' }${ secs }`;
}

/*
 * AudioPlayer
 *
 * Accepts 'playlist' prop of the form:
 *
 * [{ "url": "./path/to/file.mp3",
 *    "displayText": "ArtistA - Track 1" },
 *  { "url": "https://domain.com/track2.ogg",
 *    "displayText": "ArtistB - Track 2" }]
 *
 * Accepts 'autoplay' prop (true/[false]).
 *
 * Accepts 'autoplayDelayInSeconds' prop (default 0).
 *
 * Accepts 'gapLengthInSeconds' prop (default 0).
 * Specifies gap at end of one track before next
 * track begins (ignored for manual skip).
 *
 * Accepts 'hideBackSkip' prop (default false,
 * hides back skip button if true).
 *
 * Accepts 'stayOnBackSkipThreshold' prop, default 5,
 * is number of seconds to progress until pressing back skip
 * restarts the current track.
 *
 * Accepts 'placeAtTop' prop, default false, if true,
 * player is placed at top of screen instead of bottom.
 *
 */
class AudioPlayer extends React.Component {

  constructor (props) {
    super(props);

    /* true if the user is currently dragging the mouse
     * to seek a new track position
     */
    this.seekInProgress = false;
    // index matching requested track (whether track has loaded or not)
    this.currentTrackIndex = 0;

    this.defaultState = {
      /* activeTrackIndex will change to match
       * this.currentTrackIndex once metadata has loaded
       */
      activeTrackIndex: -1,
      // indicates whether audio player should be paused
      paused: true,
      /* elapsed time for current track, in seconds -
       * DISPLAY ONLY! the actual elapsed time may
       * not match up if we're currently seeking, since
       * the new time is visually previewed before the
       * audio seeks.
       */
      displayedTime: 0
    };

    this.state = this.defaultState;

    // html audio element used for playback
    this.audio = null;
    this.audioProgressContainer = null;
    /* bounding rectangle used for calculating seek
     * position from mouse/touch coordinates
     */
    this.audioProgressBoundingRect = null;

    // EventListeners to create on mount and remove on unmount
    this.seekReleaseListener = null;
    this.resizeListener = null;
  }

  componentDidMount () {
    require('./index.scss');

    const seekReleaseListener = this.seekReleaseListener = this.seek.bind(this);
    window.addEventListener('mouseup', seekReleaseListener);
    document.addEventListener('touchend', seekReleaseListener);
    const resizeListener = this.resizeListener = this.fetchAudioProgressBoundingRect.bind(this);
    window.addEventListener('resize', resizeListener);
    resizeListener();

    const audio = this.audio = document.createElement('audio');
    audio.preload = 'metadata';
    audio.addEventListener('ended', () => {
      const gapLengthInSeconds = this.props.gapLengthInSeconds || 0;
      setTimeout(this.skipToNextTrack.bind(this), gapLengthInSeconds * 1000);
    });
    audio.addEventListener('timeupdate', this.handleTimeUpdate.bind(this));
    audio.addEventListener('loadedmetadata', () => {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    });
    audio.addEventListener('play', () => {
      this.setState({
        paused: false
      });
    });
    audio.addEventListener('stalled', this.togglePause.bind(this, true));
    if (this.props.playlist && this.props.playlist.length) {
      this.updateSource();
      if (this.props.autoplay) {
        const delay = this.props.autoplayDelayInSeconds || 0;
        setTimeout(this.togglePause.bind(this, false), delay * 1000);
      }
    }
  }

  componentWillUnmount () {
    // remove event listeners bound outside the scope of our component
    window.removeEventListener('mouseup', this.seekReleaseListener);
    document.removeEventListener('touchend', this.seekReleaseListener);
    window.removeEventListener('resize', this.resizeListener);

    /* pause the audio element before dereferencing it
     * (we can't know when garbage collection will run)
     */
    this.audio.pause();
    this.audio = null;
  }

  componentWillReceiveProps (nextProps) {
    const newPlaylist = nextProps.playlist;
    if (!newPlaylist || !newPlaylist.length) {
      if (this.audio) {
        this.audio.src = '';
      }
      this.currentTrackIndex = 0;
      return this.setState(this.defaultState);
    }

    const oldPlaylist = this.props.playlist;

    const currentTrackUrl = ((oldPlaylist || [])[this.currentTrackIndex] || {}).url;
    this.currentTrackIndex = arrayFindIndex(newPlaylist, track => {
      return track.url && currentTrackUrl === track.url;
    });
    /* if the track we're already playing is in the new playlist, update the
     * activeTrackIndex.
     */
    if (this.currentTrackIndex !== -1) {
      this.setState({
        activeTrackIndex: this.currentTrackIndex
      });
    }
  }

  componentDidUpdate () {
    /* if we loaded a new playlist and reset the current track marker, we
     * should load up the first one.
     */
    if (this.audio && this.currentTrackIndex === -1) {
      this.skipToNextTrack(false);
    }
  }

  togglePause (value) {
    if (!this.audio) {
      return;
    }
    const pause = typeof value === 'boolean' ? value : !this.state.paused;
    if (pause) {
      this.audio.pause();
      return this.setState({
        paused: true
      });
    }
    if (!this.props.playlist || !this.props.playlist.length) {
      return;
    }
    try {
      this.audio.play();
    } catch (error) {
      logError(error);
      const warningMessage =
        'Audio playback failed at ' +
        new Date().toLocaleTimeString() +
        '! (Perhaps autoplay is disabled in this browser.)';
      logWarning(warningMessage);
    }
  }

  skipToNextTrack (shouldPlay) {
    if (!this.audio) {
      return;
    }
    this.audio.pause();
    if (!this.props.playlist || !this.props.playlist.length) {
      return;
    }
    let i = this.currentTrackIndex + 1;
    if (i >= this.props.playlist.length) {
      i = 0;
    }
    this.currentTrackIndex = i;
    this.setState({
      activeTrackIndex: -1,
      displayedTime: 0
    }, () => {
      this.updateSource();
      const shouldPause = typeof shouldPlay === 'boolean' ? !shouldPlay : false;
      this.togglePause(shouldPause);
    });
  }

  backSkip () {
    if (!this.props.playlist || !this.props.playlist.length) {
      return;
    }
    const audio = this.audio;
    let stayOnBackSkipThreshold = this.props.stayOnBackSkipThreshold;
    if (isNaN(stayOnBackSkipThreshold)) {
      stayOnBackSkipThreshold = 5;
    }
    if (audio.currentTime >= stayOnBackSkipThreshold) {
      return audio.currentTime = 0;
    }
    let i = this.currentTrackIndex - 1;
    if (i < 0) {
      i = this.props.playlist.length - 1;
    }
    this.currentTrackIndex = i - 1;
    this.skipToNextTrack();
  }

  updateSource () {
    this.audio.src = this.props.playlist[this.currentTrackIndex].url;
  }

  fetchAudioProgressBoundingRect () {
    this.audioProgressBoundingRect = this.audioProgressContainer.getBoundingClientRect();
  }

  handleTimeUpdate () {
    if (!this.seekInProgress && this.audio) {
      this.setState({
        displayedTime: this.audio.currentTime
      });
    }
  }

  adjustDisplayedTime (event) {
    if (!this.props.playlist || !this.props.playlist.length) {
      return;
    }
    // make sure we don't select stuff in the background while seeking
    if (event.type === 'mousedown' || event.type === 'touchstart') {
      this.seekInProgress = true;
      document.body.classList.add('noselect');
    } else if (!this.seekInProgress) {
      return;
    }
    /* we don't want mouse handlers to receive the event
     * after touch handlers if we're seeking.
     */
    event.preventDefault();
    const boundingRect = this.audioProgressBoundingRect;
    const isTouch = event.type.slice(0, 5) === 'touch';
    const pageX = isTouch ? event.targetTouches.item(0).pageX : event.pageX;
    const position = pageX - boundingRect.left - document.body.scrollLeft;
    const containerWidth = boundingRect.width;
    const progressPercentage = position / containerWidth;
    this.setState({
      displayedTime: progressPercentage * this.audio.duration
    });
  }

  seek (event) {
    /* this function is activated when the user lets
     * go of the mouse, so if .noselect was applied
     * to the document body, get rid of it.
     */
    document.body.classList.remove('noselect');
    if (!this.seekInProgress) {
      return;
    }
    /* we don't want mouse handlers to receive the event
     * after touch handlers if we're seeking.
     */
    event.preventDefault();
    this.seekInProgress = false;
    const displayedTime = this.state.displayedTime;
    if (isNaN(displayedTime)) {
      return;
    }
    this.audio.currentTime = displayedTime;
  }

  render () {
    const activeIndex = this.state.activeTrackIndex;
    const displayText = this.props.playlist ? (
      activeIndex < 0 ? null : this.props.playlist[activeIndex].displayText
    ) : 'Please load a playlist';

    const displayedTime = this.state.displayedTime;
    const duration = this.audio && this.audio.duration || 0;

    const elapsedTime = convertToTime(displayedTime);
    const fullTime = convertToTime(duration);
    const timeRatio = `${ elapsedTime } / ${ fullTime }`;

    const progressBarWidth = `${ (displayedTime / duration) * 100 }%`;

    return (

 this.audioProgressContainer = ref }
             onMouseDown={ this.adjustDisplayedTime.bind(this) }
             onMouseMove={ this.adjustDisplayedTime.bind(this) }
             onTouchStart={ this.adjustDisplayedTime.bind(this) }
             onTouchMove={ this.adjustDisplayedTime.bind(this) }>

                { displayText }

              { timeRatio }

    );
  }

}

AudioPlayer.propTypes = {
  playlist: React.PropTypes.array,
  autoplay: React.PropTypes.bool,
  autoplayDelayInSeconds: React.PropTypes.number,
  gapLengthInSeconds: React.PropTypes.number,
  hideBackSkip: React.PropTypes.bool,
  stayOnBackSkipThreshold: React.PropTypes.number,
  placeAtTop: React.PropTypes.bool
};

module.exports = AudioPlayer;
<|endoftext|>