<commit_before>import _ from "underscore";

// primary field types used for picking operators, etc
export const NUMBER = "NUMBER";
export const STRING = "STRING";
export const BOOLEAN = "BOOLEAN";
export const DATE_TIME = "DATE_TIME";
export const LOCATION = "LOCATION";
export const COORDINATE = "COORDINATE";

// other types used for various purporses
export const ENTITY = "ENTITY";
export const SUMMABLE = "SUMMABLE";
export const CATEGORY = "CATEGORY";
export const DIMENSION = "DIMENSION";

export const UNKNOWN = "UNKNOWN";

// define various type hierarchies
// NOTE: be sure not to create cycles using the "other" types
const TYPES = {
    [DATE_TIME]: {
        base: ["DateTimeField", "DateField", "TimeField"],
        special: ["timestamp_milliseconds", "timestamp_seconds"]
    },
    [NUMBER]: {
        base: ["IntegerField", "DecimalField", "FloatField", "BigIntegerField"],
        special: ["number"]
    },
    [STRING]: {
        base: ["CharField", "TextField"],
        special: ["name"]
    },
    [BOOLEAN]: {
        base: ["BooleanField"]
    },
    [COORDINATE]: {
        special: ["latitude", "longitude"]
    },
    [LOCATION]: {
        special: ["city", "country", "state", "zip_code"]
    },

    [ENTITY]: {
        special: ["fk", "id", "name"]
    },

    [SUMMABLE]: {
        include: [NUMBER],
        exclude: [ENTITY, LOCATION, DATE_TIME]
    },
    [CATEGORY]: {
        base: ["BooleanField"],
        special: ["category"],
        include: [LOCATION]
    },
    [DIMENSION]: {
        field: ["dimension"],
        include: [DATE_TIME, CATEGORY, ENTITY]
    }
};

export function isFieldType(type, field) {
    let def = TYPES[type];
    // check to see if it belongs to any of the field types:
    for (let prop of ["field", "base", "special"]) {
        if (def[prop] && _.contains(def[prop], field[prop+"_type"])) {
            return true;
        }
    }
    // recursively check to see if it's NOT another field type:
    if (def.exclude) {
        for (let excludeType of def.exclude) {
            if (isFieldType(excludeType, field)) {
                return false;
            }
        }
    }
    // recursively check to see if it's another field type:
    if (def.include) {
        for (let includeType of def.include) {
            if (isFieldType(includeType, field)) {
                return true;
            }
        }
    }
    return false;
}

export function getFieldType(field) {
    // try more specific types first, then more generic types
    for (let type of [DATE_TIME, LOCATION, COORDINATE, NUMBER, STRING, BOOLEAN]) {
        if (isFieldType(type, field)) {
            return type;
        }
    }
}

export const isDate = isFieldType.bind(null, DATE_TIME);
export const isNumeric = isFieldType.bind(null, NUMBER);
export const isBoolean = isFieldType.bind(null, BOOLEAN);
export const isSummable = isFieldType.bind(null, SUMMABLE);
export const isCategory = isFieldType.bind(null, CATEGORY);
export const isDimension = isFieldType.bind(null, DIMENSION);

// operator argument constructors:

function freeformArgument(field, table) {
    return {
        type: "text"
    };
}

function numberArgument(field, table) {
    return {
        type: "number"
    };
}

function comparableArgument(field, table) {
    if (isDate(field)) {
        return {
            type: "date"
        };
    }

    if (isNumeric(field)) {
        return {
            type: "number"
        };
    }

    return {
        type: "text"
    };
}

function equivalentArgument(field, table) {
    if (isBoolean(field)) {
        return {
            type: "select",
            values: [
                { key: true, name: "True" },
                { key: false, name: "False" }
            ]
        };
    }

    if (isCategory(field)) {
        if (field.id in table.field_values && table.field_values[field.id].length > 0) {
            let validValues = table.field_values[field.id];
            // this sort function works for both numbers and strings:
            validValues.sort((a, b) => a === b ? 0 : (a < b ? -1 : 1));
            return {
                type: "select",
                values: validValues
                    .filter(value => value != null)
                    .map(value => ({
                        key: value,
                        name: value
                    }))
            };
        }
    }

    if (isDate(field)) {
        return {
            type: "date"
        };
    }

    if (isNumeric(field)) {
        return {
            type: "number"
        };
    }

    return {
        type: "text"
    };
}

function longitudeFieldSelectArgument(field, table) {
    return {
        type: "select",
        values: table.fields
            .filter(field => field.special_type === "longitude")
            .map(field => ({
                key: field.id,
                name: field.display_name
            }))
    };
}

const OPERATORS = {
    "=": {
        validArgumentsFilters: [equivalentArgument],
        multi: true
    },
    "!=": {
        validArgumentsFilters: [equivalentArgument],
        multi: true
    },
    "IS_NULL": {
        validArgumentsFilters: []
    },
    "NOT_NULL": {
        validArgumentsFilters: []
    },
    "<": {
        validArgumentsFilters: [comparableArgument]
    },
    "<=": {
        validArgumentsFilters: [comparableArgument]
    },
    ">": {
        validArgumentsFilters: [comparableArgument]
    },
    ">=": {
        validArgumentsFilters: [comparableArgument]
    },
    "INSIDE": {
        validArgumentsFilters: [longitudeFieldSelectArgument, numberArgument, numberArgument, numberArgument, numberArgument],
        placeholders: ["Select longitude field", "Enter upper latitude", "Enter left longitude", "Enter lower latitude", "Enter right latitude"]
    },
    "BETWEEN": {
        validArgumentsFilters: [comparableArgument, comparableArgument]
    },
    "STARTS_WITH": {
        validArgumentsFilters: [freeformArgument]
    },
    "ENDS_WITH": {
        validArgumentsFilters: [freeformArgument]
    },
    "CONTAINS": {
        validArgumentsFilters: [freeformArgument]
    }
};

// ordered list of operators and metadata per type
const OPERATORS_BY_TYPE_ORDERED = {
    [NUMBER]: [
        { name: "=",       verboseName: "Equal" },
        { name: "!=",      verboseName: "Not equal" },
        { name: ">",       verboseName: "Greater than" },
        { name: "<",       verboseName: "Less than" },
        { name: "BETWEEN", verboseName: "Between" },
        { name: ">=",      verboseName: "Greater than or equal to", advanced: true },
        { name: "<=",      verboseName: "Less than or equal to", advanced: true },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [STRING]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [DATE_TIME]: [
        { name: "=",       verboseName: "Is" },
        { name: "<",       verboseName: "Before" },
        { name: ">",       verboseName: "After" },
        { name: "BETWEEN", verboseName: "Between" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [LOCATION]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [COORDINATE]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "INSIDE",  verboseName: "Inside" }
    ],
    [BOOLEAN]: [
        { name: "=",       verboseName: "Is", multi: false, defaults: [true] },
        { name: "IS_NULL", verboseName: "Is empty" },
        { name: "NOT_NULL",verboseName: "Not empty" }
    ],
    [UNKNOWN]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ]
};

const MORE_VERBOSE_NAMES = {
    "equal": "is equal to",
    "not equal": "is not equal to",
    "before": "is before",
    "after": "is after",
    "not empty": "is not empty",
    "less than": "is less than",
    "greater than": "is greater than",
    "less than or equal to": "is less than or equal to",
    "greater than or equal to": "is greater than or equal to",
}

function getOperators(field, table) {
    let type = getFieldType(field) || UNKNOWN;
    return OPERATORS_BY_TYPE_ORDERED[type].map(operatorForType => {
        let operator = OPERATORS[operatorForType.name];
        let verboseNameLower = operatorForType.verboseName.toLowerCase();
        return {
            ...operator,
            ...operatorForType,
            moreVerboseName: MORE_VERBOSE_NAMES[verboseNameLower] || verboseNameLower,
            fields: operator.validArgumentsFilters.map(validArgumentsFilter => validArgumentsFilter(field, table))
        };
    });
}

// Breakouts and Aggregation options
function allFields(fields) {
    return fields;
}

function summableFields(fields) {
    return _.filter(fields, isSummable);
}

function dimensionFields(fields) {
    return _.filter(fields, isDimension);
}

var Aggregators = [{
    "name": "Raw data",
    "short": "rows",
    "description": "Just a table with the rows in the answer, no additional operations.",
    "validFieldsFilters": []
}, {
    "name": "Row count",
    "short": "count",
    "description": "Total number of rows in the answer.",
    "validFieldsFilters": []
}, {
    "name": "Sum of ...",
    "short": "sum",
    "description": "Sum of all the values of a column.",
    "validFieldsFilters": [summableFields]
}, {
    "name": "Average of ...",
    "short": "avg",
    "description": "Average of all the values of a column",
    "validFieldsFilters": [summableFields]
}, {
    "name": "Number of distinct values of ...",
    "short": "distinct",
    "description":  "Number of unique values of a column among all the rows in the answer.",
    "validFieldsFilters": [allFields]
}, {
    "name": "Cumulative sum of ...",
    "short": "cum_sum",
    "description": "Additive sum of all the values of a column.\ne.x. total revenue over time.",
    "validFieldsFilters": [summableFields]
}, {
    "name": "Standard deviation of ...",
    "short": "stddev",
    "description": "Number which expresses how much the values of a column vary among all rows in the answer.",
    "validFieldsFilters": [summableFields]
}];

var BreakoutAggregator = {
    "name": "Break out by dimension",
    "short": "breakout",
    "validFieldsFilters": [dimensionFields]
};

function populateFields(aggregator, fields) {
    return {
        'name': aggregator.name,
        'short': aggregator.short,
        'description': aggregator.description || '',
        'advanced': aggregator.advanced || false,
        'fields': _.map(aggregator.validFieldsFilters, function(validFieldsFilterFn) {
            return validFieldsFilterFn(fields);
        }),
        'validFieldsFilters': aggregator.validFieldsFilters
    };
}

function getAggregators(fields) {
    return _.map(Aggregators, function(aggregator) {
        return populateFields(aggregator, fields);
    });
}

function getBreakouts(fields) {
    var result = populateFields(BreakoutAggregator, fields);
    result.fields = result.fields[0];
    result.validFieldsFilter = result.validFieldsFilters[0];
    return result;
}

export function addValidOperatorsToFields(table) {
    for (let field of table.fields) {
        field.valid_operators = getOperators(field, table);
    }
    table.aggregation_options = getAggregators(table.fields);
    table.breakout_options = getBreakouts(table.fields);
    return table;
}

export function hasLatitudeAndLongitudeColumns(columnDefs) {
    let hasLatitude = false;
    let hasLongitude = false;
    for (let col of columnDefs) {
        if (col.special_type === "latitude") {
            hasLatitude = true;
        }
        if (col.special_type === "longitude") {
            hasLongitude = true;
        }
    }
    return hasLatitude && hasLongitude;
}

export function foreignKeyCountsByOriginTable(fks) {
    if (fks === null || !Array.isArray(fks)) {
        return null;
    }

    return fks.map(function(fk) {
        return ('origin' in fk) ? fk.origin.table.id : null;
    }).reduce(function(prev, curr, idx, array) {
        if (curr in prev) {
            prev[curr]++;
        } else {
            prev[curr] = 1;
        }

        return prev;
    }, {});
}
<commit_msg>update our QB operators calculations to check if a given aggregation requires a certain database feature.<commit_after>import _ from "underscore";

// primary field types used for picking operators, etc
export const NUMBER = "NUMBER";
export const STRING = "STRING";
export const BOOLEAN = "BOOLEAN";
export const DATE_TIME = "DATE_TIME";
export const LOCATION = "LOCATION";
export const COORDINATE = "COORDINATE";

// other types used for various purporses
export const ENTITY = "ENTITY";
export const SUMMABLE = "SUMMABLE";
export const CATEGORY = "CATEGORY";
export const DIMENSION = "DIMENSION";

export const UNKNOWN = "UNKNOWN";

// define various type hierarchies
// NOTE: be sure not to create cycles using the "other" types
const TYPES = {
    [DATE_TIME]: {
        base: ["DateTimeField", "DateField", "TimeField"],
        special: ["timestamp_milliseconds", "timestamp_seconds"]
    },
    [NUMBER]: {
        base: ["IntegerField", "DecimalField", "FloatField", "BigIntegerField"],
        special: ["number"]
    },
    [STRING]: {
        base: ["CharField", "TextField"],
        special: ["name"]
    },
    [BOOLEAN]: {
        base: ["BooleanField"]
    },
    [COORDINATE]: {
        special: ["latitude", "longitude"]
    },
    [LOCATION]: {
        special: ["city", "country", "state", "zip_code"]
    },

    [ENTITY]: {
        special: ["fk", "id", "name"]
    },

    [SUMMABLE]: {
        include: [NUMBER],
        exclude: [ENTITY, LOCATION, DATE_TIME]
    },
    [CATEGORY]: {
        base: ["BooleanField"],
        special: ["category"],
        include: [LOCATION]
    },
    [DIMENSION]: {
        field: ["dimension"],
        include: [DATE_TIME, CATEGORY, ENTITY]
    }
};

export function isFieldType(type, field) {
    let def = TYPES[type];
    // check to see if it belongs to any of the field types:
    for (let prop of ["field", "base", "special"]) {
        if (def[prop] && _.contains(def[prop], field[prop+"_type"])) {
            return true;
        }
    }
    // recursively check to see if it's NOT another field type:
    if (def.exclude) {
        for (let excludeType of def.exclude) {
            if (isFieldType(excludeType, field)) {
                return false;
            }
        }
    }
    // recursively check to see if it's another field type:
    if (def.include) {
        for (let includeType of def.include) {
            if (isFieldType(includeType, field)) {
                return true;
            }
        }
    }
    return false;
}

export function getFieldType(field) {
    // try more specific types first, then more generic types
    for (let type of [DATE_TIME, LOCATION, COORDINATE, NUMBER, STRING, BOOLEAN]) {
        if (isFieldType(type, field)) {
            return type;
        }
    }
}

export const isDate = isFieldType.bind(null, DATE_TIME);
export const isNumeric = isFieldType.bind(null, NUMBER);
export const isBoolean = isFieldType.bind(null, BOOLEAN);
export const isSummable = isFieldType.bind(null, SUMMABLE);
export const isCategory = isFieldType.bind(null, CATEGORY);
export const isDimension = isFieldType.bind(null, DIMENSION);

// operator argument constructors:

function freeformArgument(field, table) {
    return {
        type: "text"
    };
}

function numberArgument(field, table) {
    return {
        type: "number"
    };
}

function comparableArgument(field, table) {
    if (isDate(field)) {
        return {
            type: "date"
        };
    }

    if (isNumeric(field)) {
        return {
            type: "number"
        };
    }

    return {
        type: "text"
    };
}

function equivalentArgument(field, table) {
    if (isBoolean(field)) {
        return {
            type: "select",
            values: [
                { key: true, name: "True" },
                { key: false, name: "False" }
            ]
        };
    }

    if (isCategory(field)) {
        if (field.id in table.field_values && table.field_values[field.id].length > 0) {
            let validValues = table.field_values[field.id];
            // this sort function works for both numbers and strings:
            validValues.sort((a, b) => a === b ? 0 : (a < b ? -1 : 1));
            return {
                type: "select",
                values: validValues
                    .filter(value => value != null)
                    .map(value => ({
                        key: value,
                        name: value
                    }))
            };
        }
    }

    if (isDate(field)) {
        return {
            type: "date"
        };
    }

    if (isNumeric(field)) {
        return {
            type: "number"
        };
    }

    return {
        type: "text"
    };
}

function longitudeFieldSelectArgument(field, table) {
    return {
        type: "select",
        values: table.fields
            .filter(field => field.special_type === "longitude")
            .map(field => ({
                key: field.id,
                name: field.display_name
            }))
    };
}

const OPERATORS = {
    "=": {
        validArgumentsFilters: [equivalentArgument],
        multi: true
    },
    "!=": {
        validArgumentsFilters: [equivalentArgument],
        multi: true
    },
    "IS_NULL": {
        validArgumentsFilters: []
    },
    "NOT_NULL": {
        validArgumentsFilters: []
    },
    "<": {
        validArgumentsFilters: [comparableArgument]
    },
    "<=": {
        validArgumentsFilters: [comparableArgument]
    },
    ">": {
        validArgumentsFilters: [comparableArgument]
    },
    ">=": {
        validArgumentsFilters: [comparableArgument]
    },
    "INSIDE": {
        validArgumentsFilters: [longitudeFieldSelectArgument, numberArgument, numberArgument, numberArgument, numberArgument],
        placeholders: ["Select longitude field", "Enter upper latitude", "Enter left longitude", "Enter lower latitude", "Enter right latitude"]
    },
    "BETWEEN": {
        validArgumentsFilters: [comparableArgument, comparableArgument]
    },
    "STARTS_WITH": {
        validArgumentsFilters: [freeformArgument]
    },
    "ENDS_WITH": {
        validArgumentsFilters: [freeformArgument]
    },
    "CONTAINS": {
        validArgumentsFilters: [freeformArgument]
    }
};

// ordered list of operators and metadata per type
const OPERATORS_BY_TYPE_ORDERED = {
    [NUMBER]: [
        { name: "=",       verboseName: "Equal" },
        { name: "!=",      verboseName: "Not equal" },
        { name: ">",       verboseName: "Greater than" },
        { name: "<",       verboseName: "Less than" },
        { name: "BETWEEN", verboseName: "Between" },
        { name: ">=",      verboseName: "Greater than or equal to", advanced: true },
        { name: "<=",      verboseName: "Less than or equal to", advanced: true },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [STRING]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [DATE_TIME]: [
        { name: "=",       verboseName: "Is" },
        { name: "<",       verboseName: "Before" },
        { name: ">",       verboseName: "After" },
        { name: "BETWEEN", verboseName: "Between" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [LOCATION]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ],
    [COORDINATE]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "INSIDE",  verboseName: "Inside" }
    ],
    [BOOLEAN]: [
        { name: "=",       verboseName: "Is", multi: false, defaults: [true] },
        { name: "IS_NULL", verboseName: "Is empty" },
        { name: "NOT_NULL",verboseName: "Not empty" }
    ],
    [UNKNOWN]: [
        { name: "=",       verboseName: "Is" },
        { name: "!=",      verboseName: "Is not" },
        { name: "IS_NULL", verboseName: "Is empty", advanced: true },
        { name: "NOT_NULL",verboseName: "Not empty", advanced: true }
    ]
};

const MORE_VERBOSE_NAMES = {
    "equal": "is equal to",
    "not equal": "is not equal to",
    "before": "is before",
    "after": "is after",
    "not empty": "is not empty",
    "less than": "is less than",
    "greater than": "is greater than",
    "less than or equal to": "is less than or equal to",
    "greater than or equal to": "is greater than or equal to",
}

function getOperators(field, table) {
    let type = getFieldType(field) || UNKNOWN;
    return OPERATORS_BY_TYPE_ORDERED[type].map(operatorForType => {
        let operator = OPERATORS[operatorForType.name];
        let verboseNameLower = operatorForType.verboseName.toLowerCase();
        return {
            ...operator,
            ...operatorForType,
            moreVerboseName: MORE_VERBOSE_NAMES[verboseNameLower] || verboseNameLower,
            fields: operator.validArgumentsFilters.map(validArgumentsFilter => validArgumentsFilter(field, table))
        };
    });
}

// Breakouts and Aggregation options
function allFields(fields) {
    return fields;
}

function summableFields(fields) {
    return _.filter(fields, isSummable);
}

function dimensionFields(fields) {
    return _.filter(fields, isDimension);
}

var Aggregators = [{
    "name": "Raw data",
    "short": "rows",
    "description": "Just a table with the rows in the answer, no additional operations.",
    "validFieldsFilters": []
}, {
    "name": "Row count",
    "short": "count",
    "description": "Total number of rows in the answer.",
    "validFieldsFilters": []
}, {
    "name": "Sum of ...",
    "short": "sum",
    "description": "Sum of all the values of a column.",
    "validFieldsFilters": [summableFields]
}, {
    "name": "Average of ...",
    "short": "avg",
    "description": "Average of all the values of a column",
    "validFieldsFilters": [summableFields]
}, {
    "name": "Number of distinct values of ...",
    "short": "distinct",
    "description":  "Number of unique values of a column among all the rows in the answer.",
    "validFieldsFilters": [allFields]
}, {
    "name": "Cumulative sum of ...",
    "short": "cum_sum",
    "description": "Additive sum of all the values of a column.\ne.x. total revenue over time.",
    "validFieldsFilters": [summableFields]
}, {
    "name": "Standard deviation of ...",
    "short": "stddev",
    "description": "Number which expresses how much the values of a column vary among all rows in the answer.",
    "validFieldsFilters": [summableFields],
    "requiredDriverFeature": "standard-deviation-aggregations"
}];

var BreakoutAggregator = {
    "name": "Break out by dimension",
    "short": "breakout",
    "validFieldsFilters": [dimensionFields]
};

function populateFields(aggregator, fields) {
    return {
        'name': aggregator.name,
        'short': aggregator.short,
        'description': aggregator.description || '',
        'advanced': aggregator.advanced || false,
        'fields': _.map(aggregator.validFieldsFilters, function(validFieldsFilterFn) {
            return validFieldsFilterFn(fields);
        }),
        'validFieldsFilters': aggregator.validFieldsFilters
    };
}

function getAggregators(table) {
    const supportedAggregations = Aggregators.filter(function (agg) {
        if (agg.requiredDriverFeature && !_.contains(table.db.features, agg.requiredDriverFeature)) {
            return false;
        } else {
            return true;
        }
    });
    return _.map(supportedAggregations, function(aggregator) {
        return populateFields(aggregator, table.fields);
    });
}

function getBreakouts(fields) {
    var result = populateFields(BreakoutAggregator, fields);
    result.fields = result.fields[0];
    result.validFieldsFilter = result.validFieldsFilters[0];
    return result;
}

export function addValidOperatorsToFields(table) {
    for (let field of table.fields) {
        field.valid_operators = getOperators(field, table);
    }
    table.aggregation_options = getAggregators(table);
    table.breakout_options = getBreakouts(table.fields);
    return table;
}

export function hasLatitudeAndLongitudeColumns(columnDefs) {
    let hasLatitude = false;
    let hasLongitude = false;
    for (let col of columnDefs) {
        if (col.special_type === "latitude") {
            hasLatitude = true;
        }
        if (col.special_type === "longitude") {
            hasLongitude = true;
        }
    }
    return hasLatitude && hasLongitude;
}

export function foreignKeyCountsByOriginTable(fks) {
    if (fks === null || !Array.isArray(fks)) {
        return null;
    }

    return fks.map(function(fk) {
        return ('origin' in fk) ? fk.origin.table.id : null;
    }).reduce(function(prev, curr, idx, array) {
        if (curr in prev) {
            prev[curr]++;
        } else {
            prev[curr] = 1;
        }

        return prev;
    }, {});
}
<|endoftext|>


<commit_before>"use strict";

var assert = require('assert');

var Game = function(dice) {
  if (!dice) {
    dice = [];
    for (var i=0;i<5;++i) {
      dice.push(1 + (Math.random() * 6 | 0));
    }
  }

  return {
    dice: function() {
      return dice;
    },
    score: function(category) {
      return category(dice);
    }
  };
};

var strictAdd = function(x,y) { return x + y; };
var isN = function(x) { return function(z) { return x === z; }; };

var chance = function(nums) {
  return nums.reduce(strictAdd,0);
};

var single = function(n) {
  return function(nums) {
    return nums.filter(isN(n)).reduce(strictAdd,0);
  };
};

var twoPairs = function(nums) {
  var pairs = [];
   for (var i=6;i>=1;i--) {
    var matchingI = nums.filter(isN(i)).length;
     if (matchingI === 4) {
      return 4 * i;
    } else if (matchingI >= 2) {
      pairs.push(2*i);
    }
  }
   if (pairs.length === 2) {
    return pairs[0] + pairs[1];
  } else {
    return 0;
  }
};

var nOfAKind = function(n, nums) {
  for (var i=6;i>=1;i--) {
    if (nums.filter(isN(i)).length >= n) {
       return n * i;
    }
  }
  return 0;
};

var pair = function(nums) { return nOfAKind(2,nums); };

var threeOfAKind = function(nums) { return nOfAKind(3,nums); };

var fourOfAKind = function(nums) { return nOfAKind(4,nums); };

var yahtzee = function(nums) { return nOfAKind(5,nums) ? 50 : 0; };

var matchesGoal = function(nums,goal) {
  var s = nums.sort();
  for (var i=0;i<5;++i) {
    if (s[i] !== goal[i]) {
      return false;
    }
  }
  return true;
};

var smallStraight = function(nums) {
  return matchesGoal(nums,[1,2,3,4,5]) ? 15 : 0;
};

var LargeStraight = function() {};
var FullHouse = function() {};

var categories = [
  chance,
  yahtzee,
  single(1),
  single(2),
  single(3),
  single(4),
  single(5),
  single(6),
  pair,
  twoPairs,
  threeOfAKind,
  fourOfAKind,
  smallStraight,
  new LargeStraight(),
  new FullHouse()
];

describe('yahtzee', function() {
  describe('five dice', function() {
    it('should consist of 5 values between 1 and 6', function() {
      var yahtzee = new Game();
      assert(yahtzee);

      assert.equal(5, yahtzee.dice().length);

      for(var d=0;d<5;d++) {
        assert(yahtzee.dice()[d] > 0 && yahtzee.dice()[d] < 7);
      }
    });
  });

  describe('categories', function() {
    it('should have 15 categories', function() {
      assert.equal(15, categories.length);
    });

    it('always accept chance', function() {
      assert.equal(1 + 2 + 3 + 4 + 5, new Game([1,2,3,4,5]).score(chance));
    });

    it('single works', function() {
      assert.equal(1, new Game([1,2,3,4,5]).score(single(1)));
      assert.equal(0, new Game([1,2,3,4,5]).score(single(6)));
    });

    it('pair', function() {
      assert.equal(12, new Game([6,6,1,2,3]).score(pair));
      assert.equal(0,  new Game([1,2,3,4,5]).score(pair));
    });

    it('two pairs', function() {
      assert.equal(12 + 10, new Game([6,6,5,5,1]).score(twoPairs));
      assert.equal(0, new Game([6,6,5,3,1]).score(twoPairs));
    });

    it('three of a kind', function() {
      assert.equal(9, new Game([3,3,3,3,1,2]).score(threeOfAKind));
      assert.equal(0, new Game([1,2,3,4,5]).score(threeOfAKind));
    });

    it('four of a kind', function() {
      assert.equal(12, new Game([3,3,3,3,0]).score(fourOfAKind));
      assert.equal(0, new Game([3,3,1,3,0]).score(fourOfAKind));
    });

    it('yahtzee', function() {
      assert.equal(50, new Game([1,1,1,1,1]).score(yahtzee));
      assert.equal(0 , new Game([1,2,3,4,5]).score(yahtzee));
    });

    it('small straight', function() {
      assert.equal(15, new Game([1,2,3,4,5]).score(smallStraight));
      assert.equal(0, new Game([1,2,3,4,6]).score(smallStraight));
    });

    it('large straight', function() {
      assert.equal(20, new Game([2,3,4,5,6]).score(largeStraight));
      assert.equal(0, new Game([1,2,3,4,6]).score(largeStraight));
    });
  });
});<commit_msg>passing test<commit_after>"use strict";

var assert = require('assert');

var Game = function(dice) {
  if (!dice) {
    dice = [];
    for (var i=0;i<5;++i) {
      dice.push(1 + (Math.random() * 6 | 0));
    }
  }

  return {
    dice: function() {
      return dice;
    },
    score: function(category) {
      return category(dice);
    }
  };
};

var strictAdd = function(x,y) { return x + y; };
var isN = function(x) { return function(z) { return x === z; }; };

var chance = function(nums) {
  return nums.reduce(strictAdd,0);
};

var single = function(n) {
  return function(nums) {
    return nums.filter(isN(n)).reduce(strictAdd,0);
  };
};

var twoPairs = function(nums) {
  var pairs = [];
   for (var i=6;i>=1;i--) {
    var matchingI = nums.filter(isN(i)).length;
     if (matchingI === 4) {
      return 4 * i;
    } else if (matchingI >= 2) {
      pairs.push(2*i);
    }
  }
   if (pairs.length === 2) {
    return pairs[0] + pairs[1];
  } else {
    return 0;
  }
};

var nOfAKind = function(n, nums) {
  for (var i=6;i>=1;i--) {
    if (nums.filter(isN(i)).length >= n) {
       return n * i;
    }
  }
  return 0;
};

var pair = function(nums) { return nOfAKind(2,nums); };

var threeOfAKind = function(nums) { return nOfAKind(3,nums); };

var fourOfAKind = function(nums) { return nOfAKind(4,nums); };

var yahtzee = function(nums) { return nOfAKind(5,nums) ? 50 : 0; };

var matchesGoal = function(nums,goal) {
  var s = nums.sort();
  for (var i=0;i<5;++i) {
    if (s[i] !== goal[i]) {
      return false;
    }
  }
  return true;
};

var smallStraight = function(nums) {
  return matchesGoal(nums,[1,2,3,4,5]) ? 15 : 0;
};

var largeStraight = function(nums) {
  return matchesGoal(nums,[2,3,4,5,6]) ? 20 : 0;
};

var FullHouse = function() {};

var categories = [
  chance,
  yahtzee,
  single(1),
  single(2),
  single(3),
  single(4),
  single(5),
  single(6),
  pair,
  twoPairs,
  threeOfAKind,
  fourOfAKind,
  smallStraight,
  largeStraight,
  new FullHouse()
];

describe('yahtzee', function() {
  describe('five dice', function() {
    it('should consist of 5 values between 1 and 6', function() {
      var yahtzee = new Game();
      assert(yahtzee);

      assert.equal(5, yahtzee.dice().length);

      for(var d=0;d<5;d++) {
        assert(yahtzee.dice()[d] > 0 && yahtzee.dice()[d] < 7);
      }
    });
  });

  describe('categories', function() {
    it('should have 15 categories', function() {
      assert.equal(15, categories.length);
    });

    it('always accept chance', function() {
      assert.equal(1 + 2 + 3 + 4 + 5, new Game([1,2,3,4,5]).score(chance));
    });

    it('single works', function() {
      assert.equal(1, new Game([1,2,3,4,5]).score(single(1)));
      assert.equal(0, new Game([1,2,3,4,5]).score(single(6)));
    });

    it('pair', function() {
      assert.equal(12, new Game([6,6,1,2,3]).score(pair));
      assert.equal(0,  new Game([1,2,3,4,5]).score(pair));
    });

    it('two pairs', function() {
      assert.equal(12 + 10, new Game([6,6,5,5,1]).score(twoPairs));
      assert.equal(0, new Game([6,6,5,3,1]).score(twoPairs));
    });

    it('three of a kind', function() {
      assert.equal(9, new Game([3,3,3,3,1,2]).score(threeOfAKind));
      assert.equal(0, new Game([1,2,3,4,5]).score(threeOfAKind));
    });

    it('four of a kind', function() {
      assert.equal(12, new Game([3,3,3,3,0]).score(fourOfAKind));
      assert.equal(0, new Game([3,3,1,3,0]).score(fourOfAKind));
    });

    it('yahtzee', function() {
      assert.equal(50, new Game([1,1,1,1,1]).score(yahtzee));
      assert.equal(0 , new Game([1,2,3,4,5]).score(yahtzee));
    });

    it('small straight', function() {
      assert.equal(15, new Game([1,2,3,4,5]).score(smallStraight));
      assert.equal(0, new Game([1,2,3,4,6]).score(smallStraight));
    });

    it('large straight', function() {
      assert.equal(20, new Game([2,3,4,5,6]).score(largeStraight));
      assert.equal(0, new Game([1,2,3,4,6]).score(largeStraight));
    });
  });
});<|endoftext|>


<commit_before>/*
 * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
 * http://paperjs.org/
 *
 * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

module('Style');

test('style defaults', function() {
	var path = new Path();
	equals(function() {
		return path.strokeWidth;
	}, 1);
	equals(function() {
		return path.strokeCap;
	}, 'butt');
	equals(function() {
		return path.strokeJoin;
	}, 'miter');
	equals(function() {
		return path.miterLimit;
	}, 10);
	equals(function() {
		return path.dashOffset;
	}, 0);
	equals(function() {
		return path.dashArray + '';
	}, [] + '');
});

test('currentStyle', function() {
	paper.project.currentStyle.fillColor = 'black';
	var path = new Path();
	compareColors(path.fillColor, 'black', 'path.fillColor');

	// When changing the current style of the project, the style of
	// paths created using project.currentStyle should not change.
	paper.project.currentStyle.fillColor = 'red';
	compareColors(path.fillColor, 'black', 'path.fillColor');
});

test('setting currentStyle to an object', function() {
	paper.project.currentStyle = {
		fillColor: 'red',
		strokeColor: 'green'
	};
	var path = new Path();
	compareColors(path.fillColor, 'red', 'path.fillColor');
	compareColors(path.strokeColor, 'green', 'path.strokeColor');
});

test('setting path styles to an object', function() {
	var path = new Path();
	path.style = {
		fillColor: 'red',
		strokeColor: 'green'
	};
	compareColors(path.fillColor, 'red', 'path.fillColor');
	compareColors(path.strokeColor, 'green', 'path.strokeColor');
});

test('setting group styles to an object', function() {
	var group = new Group();
	var path = new Path();
	group.addChild(path);
	group.style = {
		fillColor: 'red',
		strokeColor: 'green'
	};
	compareColors(path.fillColor, 'red', 'path.fillColor');
	compareColors(path.strokeColor, 'green', 'path.strokeColor');
});

test('getting group styles', function() {
	var group = new Group();
	var path = new Path();
	path.fillColor = 'red';
	group.addChild(path);

	compareColors(group.fillColor, 'red', 'group.fillColor');

	var secondPath = new Path();
	secondPath.fillColor = 'black';
	group.addChild(secondPath);

	// the group now contains two paths with different fillColors and therefore
	// should return undefined:
	equals(function() {
		return group.fillColor;
	}, undefined);

	//If we remove the first path, it should now return 'black':
	group.children[0].remove();
	compareColors(group.fillColor, 'black', 'group.fillColor');
});

test('setting group styles', function() {
	var group = new Group();
	var path = new Path();
	path.fillColor = 'red';
	group.addChild(path);

	var secondPath = new Path();
	secondPath.fillColor = 'blue';
	secondPath.strokeColor = 'red';
	group.addChild(secondPath);

	// Change the fill color of the group:
	group.fillColor = 'black';

	// the paths contained in the group should now both have their fillColor
	// set to black:
	compareColors(path.fillColor, 'black', 'path.fillColor');
	compareColors(secondPath.fillColor, 'black', 'secondPath.fillColor');

	// The second path still has its strokeColor set to red:
	compareColors(secondPath.strokeColor, 'red', 'secondPath.strokeColor');
});

test('setting group styles 2', function() {
	var group = new Group();
	var path = new Path();
	path.fillColor = 'red';
	group.addChild(path);

	compareColors(group.fillColor, 'red', 'group.fillColor');

	var secondPath = new Path();
	secondPath.fillColor = 'blue';
	secondPath.strokeColor = 'red';
	group.addChild(secondPath);

	compareColors(secondPath.fillColor, 'blue', 'secondPath.fillColor');
	compareColors(secondPath.strokeColor, 'red', 'secondPath.strokeColor');

	// By appending a path with a different fillcolor,
	// the group's fillColor should return undefined:
	equals(function() {
		return group.fillColor;
	}, undefined);

	// But, both paths have a red strokeColor, so:
	compareColors(group.strokeColor, 'red', 'group.strokeColor');

	// Change the fill color of the group's style:
	group.style.fillColor = 'black';

	// the paths contained in the group should now both have their fillColor
	// set to black:
	compareColors(path.fillColor, 'black', 'path.fillColor');
	compareColors(secondPath.fillColor, 'black', 'secondPath.fillColor');

	// The second path still has its strokeColor set to red:
	compareColors(secondPath.strokeColor, 'red', 'secondPath.strokeColor');
});<commit_msg>Define failing test for wrongly merged group fillColor.<commit_after>/*
 * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
 * http://paperjs.org/
 *
 * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

module('Style');

test('style defaults', function() {
	var path = new Path();
	equals(function() {
		return path.strokeWidth;
	}, 1);
	equals(function() {
		return path.strokeCap;
	}, 'butt');
	equals(function() {
		return path.strokeJoin;
	}, 'miter');
	equals(function() {
		return path.miterLimit;
	}, 10);
	equals(function() {
		return path.dashOffset;
	}, 0);
	equals(function() {
		return path.dashArray + '';
	}, [] + '');
});

test('currentStyle', function() {
	paper.project.currentStyle.fillColor = 'black';
	var path = new Path();
	compareColors(path.fillColor, 'black', 'path.fillColor');

	// When changing the current style of the project, the style of
	// paths created using project.currentStyle should not change.
	paper.project.currentStyle.fillColor = 'red';
	compareColors(path.fillColor, 'black', 'path.fillColor');
});

test('setting currentStyle to an object', function() {
	paper.project.currentStyle = {
		fillColor: 'red',
		strokeColor: 'green'
	};
	var path = new Path();
	compareColors(path.fillColor, 'red', 'path.fillColor');
	compareColors(path.strokeColor, 'green', 'path.strokeColor');
});

test('setting path styles to an object', function() {
	var path = new Path();
	path.style = {
		fillColor: 'red',
		strokeColor: 'green'
	};
	compareColors(path.fillColor, 'red', 'path.fillColor');
	compareColors(path.strokeColor, 'green', 'path.strokeColor');
});

test('setting group styles to an object', function() {
	var group = new Group();
	var path = new Path();
	group.addChild(path);
	group.style = {
		fillColor: 'red',
		strokeColor: 'green'
	};
	compareColors(path.fillColor, 'red', 'path.fillColor');
	compareColors(path.strokeColor, 'green', 'path.strokeColor');
});

test('getting group styles', function() {
	var group = new Group();
	var path = new Path();
	path.fillColor = 'red';
	group.addChild(path);

	compareColors(group.fillColor, 'red', 'group.fillColor');

	var secondPath = new Path();
	secondPath.fillColor = 'black';
	group.addChild(secondPath);

	// the group now contains two paths with different fillColors and therefore
	// should return undefined:
	equals(function() {
		return group.fillColor;
	}, undefined);

	//If we remove the first path, it should now return 'black':
	group.children[0].remove();
	compareColors(group.fillColor, 'black', 'group.fillColor');
});

test('getting group styles 2', function() {
	var star = new Path.Star({
	   center: [100, 100],
	   points: 6,
	   radius1: 20,
	   radius2: 40,
	   fillColor: 'red'
	});

	var circle = new Path.Circle({
	   center: [100, 100],
	   radius: 25,
	   strokeColor: 'black'
	});
	// Create a group of the two items and clip it:
	var group = new Group(circle, star);

	equals(function() {
		return group.fillColor;
	}, undefined);
});

test('setting group styles', function() {
	var group = new Group();
	var path = new Path();
	path.fillColor = 'red';
	group.addChild(path);

	var secondPath = new Path();
	secondPath.fillColor = 'blue';
	secondPath.strokeColor = 'red';
	group.addChild(secondPath);

	// Change the fill color of the group:
	group.fillColor = 'black';

	// the paths contained in the group should now both have their fillColor
	// set to black:
	compareColors(path.fillColor, 'black', 'path.fillColor');
	compareColors(secondPath.fillColor, 'black', 'secondPath.fillColor');

	// The second path still has its strokeColor set to red:
	compareColors(secondPath.strokeColor, 'red', 'secondPath.strokeColor');
});

test('setting group styles 2', function() {
	var group = new Group();
	var path = new Path();
	path.fillColor = 'red';
	group.addChild(path);

	compareColors(group.fillColor, 'red', 'group.fillColor');

	var secondPath = new Path();
	secondPath.fillColor = 'blue';
	secondPath.strokeColor = 'red';
	group.addChild(secondPath);

	compareColors(secondPath.fillColor, 'blue', 'secondPath.fillColor');
	compareColors(secondPath.strokeColor, 'red', 'secondPath.strokeColor');

	// By appending a path with a different fillcolor,
	// the group's fillColor should return undefined:
	equals(function() {
		return group.fillColor;
	}, undefined);

	// But, both paths have a red strokeColor, so:
	compareColors(group.strokeColor, 'red', 'group.strokeColor');

	// Change the fill color of the group's style:
	group.style.fillColor = 'black';

	// the paths contained in the group should now both have their fillColor
	// set to black:
	compareColors(path.fillColor, 'black', 'path.fillColor');
	compareColors(secondPath.fillColor, 'black', 'secondPath.fillColor');

	// The second path still has its strokeColor set to red:
	compareColors(secondPath.strokeColor, 'red', 'secondPath.strokeColor');
});<|endoftext|>


<commit_before>'use strict';

const fs = require('fs-extra');
const macho = require('macho');
const walk = require('fs-walk');
const rimraf = require('rimraf');
const tools = require('./tools');
const plist = require('simple-plist');
const fatmacho = require('fatmacho');
const EventEmitter = require('events').EventEmitter;

function getResignedFilename (path) {
  if (!path) return null;
  const newPath = path.replace('.ipa', '-resigned.ipa');
  const pos = newPath.lastIndexOf('/');
  if (pos !== -1) return newPath.substring(pos + 1);
  return newPath;
}

function isBinaryEncrypted (path) {
  const data = fs.readFileSync(path);
  try {
    macho.parse(data);
  } catch (e) {
    try {
      fatmacho.parse(data).forEach((bin) => {
        macho.parse(bin.data).cmds.forEach((cmd) => {
          if (cmd.type === 'encryption_info' && cmd.id) {
            return true;
          }
        });
      });
    } catch (e) {
      /* console.error(path, e); */
    }
  }
  return false;
}

function getExecutable (appdir, exename) {
  if (appdir) {
    const plistPath = [ appdir, 'Info.plist' ].join('/');
    const plistData = plist.readFileSync(plistPath);
    const cfBundleExecutable = plistData['CFBundleExecutable'];
    if (cfBundleExecutable) {
      return cfBundleExecutable;
    }
  }
  return exename;
}

function upperDirectory (file) {
  const slash = file.replace(/\/$/, '').lastIndexOf('/');
  if (slash !== -1) {
    return file.substring(0, slash) + '/';
  }
  return file + '/';
}

function isMacho (buffer) {
  const magics = [
    [0xca, 0xfe, 0xba, 0xbe], // fat
    [0xce, 0xfa, 0xed, 0xfe], // 32bit
    [0xcf, 0xfa, 0xed, 0xfe], // 64bit
    [0xfe, 0xed, 0xfa, 0xce]  // big-endian
  ];
  for (let a of magics) {
    if (!buffer.compare(Buffer(a))) {
      return true;
    }
  }
  return false;
}

/*
  TODO: verify is mobileprovision app-id glob string matches the bundleid
  read provision file in raw
  search for application-identifier and <string>...</string>
  check if prefix matches and last dot separated word is an asterisk
  const identifierInProvisioning = 'x'
  Read the one in Info.plist and compare with bundleid
*/
function checkProvision (appdir, file, next) {
  if (file && appdir) {
    const provision = 'embedded.mobileprovision';
    const mobileProvision = [ appdir, provision ].join('/');
    return fs.copy(file, mobileProvision, next);
  }
  next();
}

module.exports = class ApplesignSession {
  constructor (state) {
    this.config = JSON.parse(JSON.stringify(state));
    this.events = new EventEmitter();
    this.events.config = this.config;
  }

  /* Event Wrapper API with cb support */
  emit (ev, msg, cb) {
    function isEnder (ev) {
      return (ev === 'error');
    }
    if (isEnder(ev) && msg && typeof cb === 'function') {
      cb(msg);
    }
    return this.events.emit(ev, msg);
  }

  on (ev, cb) {
    this.events.on(ev, cb);
    return this;
  }

  /* Public API */
  signIPA (cb) {
    this.unzip(this.config.file, this.config.outdir, (error) => {
      if (error) { return this.emit('error', error, cb); }
      this.signAppDirectory(this.config.outdir + '/Payload', (error, res) => {
        if (error) { this.emit('error', error, cb); }
        this.ipafyDirectory((error, res) => {
          if (error) { this.emit('error', error, cb); }
          this.cleanup((ignored_error) => {
            cb(error, res);
          });
        });
      });
    });
    return this;
  }

  signAppDirectory (path, next) {
    if (!path) {
      path = this.config.outdir + '/Payload';
    }
    /* W T F */
    try {
      if (!fs.lstatSync(path).isDirectory()) {
        throw new Error('Invalid IPA');
      }
    } catch (e) {
      return this.cleanup(() => {
        next(e.message);
      });
    }
    this.emit('message', 'Payload found');
    const files = fs.readdirSync(path).filter((x) => {
      return x.indexOf('.app') !== -1;
    });
    if (files.length !== 1) {
      return next('Invalid IPA');
    }
    this.config.appdir = [ path, files[0] ].join('/');
    const binname = getExecutable(this.config.appdir, files[0].replace('.app', ''));
    const binpath = [ this.config.appdir, binname ].join('/');
    if (fs.lstatSync(binpath).isFile()) {
      const isEncrypted = isBinaryEncrypted(binpath);
      if (isEncrypted) {
        return next('ipa is encrypted');
      }
      this.emit('message', 'Main IPA executable is not encrypted');

      const infoPlist = [ this.config.appdir, 'Info.plist' ].join('/');

      this.fixPlist(infoPlist, this.config.bundleid, (err) => {
        if (err) return this.emit('error', err, next);
        checkProvision(this.config.appdir, this.config.mobileprovision, (err) => {
          if (err) return this.emit('error', err, next);
          this.fixEntitlements(binpath, (err) => {
            if (err) return this.emit('error', err, next);
            this.signFile(binpath, (err) => {
              if (err) return this.emit('error', err, next);
              this.signLibraries(this.config.appdir, next);
            });
          });
        });
      });
    } else {
      next('Invalid path');
    }
  }

  fixEntitlements (file, next) {
    if (!this.config.mobileprovision) {
      return next();
    }
    this.emit('message', 'Grabbing entitlements from mobileprovision');
    tools.getEntitlementsFromMobileProvision(this.config.mobileprovision, (error, newEntitlements) => {
      this.emit('message', JSON.stringify(newEntitlements));
      const provision = 'embedded.mobileprovision';
      const pathToProvision = [ this.config.appdir, provision ].join('/');
      this.config.entitlement = pathToProvision;
      plist.writeFileSync(pathToProvision, newEntitlements);
      next(error);
    });
  }

  fixPlist (file, bundleid, next) {
    const appdir = this.config.appdir;
    if (!file || !appdir) {
      return next('Invalid parameters for fixPlist');
    }
    if (bundleid) {
      const pl_path = [ this.config.appdir, file ].join('/');
      const data = plist.readFileSync(pl_path);
      const oldBundleID = data['CFBundleIdentifier'];
      this.emit('message', 'Rebundle ' + pl_path + ' ' + oldBundleID + ' into ' + bundleid);
      data['CFBundleIdentifier'] = bundleid;
      plist.writeFileSync(pl_path, data);
    }
    next();
  }

  signFile (file, next) {
    this.emit('message', 'Sign ' + file);
    tools.codesign(this.config.identity, this.config.entitlement, file, (error, stdout, stderr) => {
      if (error) {
        return this.emit('error', error, next);
      }
      this.emit('message', 'Verify ' + file);
      tools.verifyCodesign(file, (error, stdout, stderr) => {
        next(error, stdout || stderr);
      });
    });
  }

  signLibraries (path, next) {
    let signs = 0;
    let errors = 0;
    let found = false;

    this.emit('message', 'Signing libraries and frameworks');

    const exe = '/' + getExecutable(this.config.appdir);
    walk.walkSync(path, (basedir, filename, stat) => {
      const file = [ basedir, filename ].join('/');
      if (file.endsWith(exe)) {
        found = true;
        return;
      }
      if (!fs.lstatSync(file).isFile()) {
        return;
      }
      try {
        const fd = fs.openSync(file, 'r');
        let buffer = new Buffer(4);
        fs.readSync(fd, buffer, 0, 4);
        if (isMacho(buffer)) {
          found = true;
          signs++;
          this.signFile(file, (err) => {
            signs--;
            if (err) {
              this.emit('error ', err);
              errors++;
            }
            if (signs === 0) {
              if (errors > 0) {
                this.emit('error', 'Warning: Some (' + errors + ') errors happened.');
              } else {
                this.emit('message', 'Everything seems signed now');
              }
              next();
            }
          });
        }
        fs.close(fd);
      } catch (e) {
        console.error(basedir, filename, e);
        next(e);
      }
    });
    if (!found) {
      next('Cannot find any MACH0 binary to sign');
    }
  }

  cleanup (cb) {
    const outdir = this.config.outdir;
    this.emit('message', 'Cleaning up ' + outdir);
    try {
      rimraf(outdir, cb);
    } catch (e) {
      this.emit('error', e);
    }
  }

  ipafyDirectory (next) {
    const ipa_in = this.config.file;
    const ipa_out = upperDirectory(this.config.outdir) + this.config.outfile;
    this.events.emit('message', 'Zipifying into ' + ipa_out + ' ...');
    tools.zip(this.config.outdir, ipa_out, 'Payload', (error) => {
      if (!error && this.config.replaceipa) {
        this.events.emit('message', 'mv into ' + ipa_in);
        return fs.rename(ipa_out, ipa_in, next);
      }
      next(error);
    });
  }

  setFile (name) {
    if (typeof name === 'string') {
      this.config.file = name;
      this.config.outdir = name + '.d';
      if (!this.config.outfile) {
        this.setOutputFile(getResignedFilename(name));
      }
    }
  }

  setOutputFile (name) {
    this.config.outfile = name;
  }

  /* TODO: move to tools.js */
  unzip (file, outdir, cb) {
    if (!file || !outdir) {
      cb(true, 'No output specified');
      return false;
    }
    if (!outdir) {
      cb(true, 'Invalid output directory');
      return false;
    }
    this.events.emit('message', ['rm -rf', outdir].join(' '));
    this.cleanup(() => {
      this.events.emit('message', 'Unzipping ' + file);
      tools.unzip(file, outdir, (error, stdout) => {
        if (error) {
          this.cleanup(() => { cb(error.message); });
        } else {
          cb(undefined, stdout);
        }
      });
    });
  }
};
<commit_msg>Use new Error for consistency<commit_after>'use strict';

const fs = require('fs-extra');
const macho = require('macho');
const walk = require('fs-walk');
const rimraf = require('rimraf');
const tools = require('./tools');
const plist = require('simple-plist');
const fatmacho = require('fatmacho');
const EventEmitter = require('events').EventEmitter;

function getResignedFilename (path) {
  if (!path) return null;
  const newPath = path.replace('.ipa', '-resigned.ipa');
  const pos = newPath.lastIndexOf('/');
  if (pos !== -1) return newPath.substring(pos + 1);
  return newPath;
}

function isBinaryEncrypted (path) {
  const data = fs.readFileSync(path);
  try {
    macho.parse(data);
  } catch (e) {
    try {
      fatmacho.parse(data).forEach((bin) => {
        macho.parse(bin.data).cmds.forEach((cmd) => {
          if (cmd.type === 'encryption_info' && cmd.id) {
            return true;
          }
        });
      });
    } catch (e) {
      /* console.error(path, e); */
    }
  }
  return false;
}

function getExecutable (appdir, exename) {
  if (appdir) {
    const plistPath = [ appdir, 'Info.plist' ].join('/');
    const plistData = plist.readFileSync(plistPath);
    const cfBundleExecutable = plistData['CFBundleExecutable'];
    if (cfBundleExecutable) {
      return cfBundleExecutable;
    }
  }
  return exename;
}

function upperDirectory (file) {
  const slash = file.replace(/\/$/, '').lastIndexOf('/');
  if (slash !== -1) {
    return file.substring(0, slash) + '/';
  }
  return file + '/';
}

function isMacho (buffer) {
  const magics = [
    [0xca, 0xfe, 0xba, 0xbe], // fat
    [0xce, 0xfa, 0xed, 0xfe], // 32bit
    [0xcf, 0xfa, 0xed, 0xfe], // 64bit
    [0xfe, 0xed, 0xfa, 0xce]  // big-endian
  ];
  for (let a of magics) {
    if (!buffer.compare(Buffer(a))) {
      return true;
    }
  }
  return false;
}

/*
  TODO: verify is mobileprovision app-id glob string matches the bundleid
  read provision file in raw
  search for application-identifier and <string>...</string>
  check if prefix matches and last dot separated word is an asterisk
  const identifierInProvisioning = 'x'
  Read the one in Info.plist and compare with bundleid
*/
function checkProvision (appdir, file, next) {
  if (file && appdir) {
    const provision = 'embedded.mobileprovision';
    const mobileProvision = [ appdir, provision ].join('/');
    return fs.copy(file, mobileProvision, next);
  }
  next();
}

module.exports = class ApplesignSession {
  constructor (state) {
    this.config = JSON.parse(JSON.stringify(state));
    this.events = new EventEmitter();
    this.events.config = this.config;
  }

  /* Event Wrapper API with cb support */
  emit (ev, msg, cb) {
    function isEnder (ev) {
      return (ev === 'error');
    }
    if (isEnder(ev) && msg && typeof cb === 'function') {
      cb(msg);
    }
    return this.events.emit(ev, msg);
  }

  on (ev, cb) {
    this.events.on(ev, cb);
    return this;
  }

  /* Public API */
  signIPA (cb) {
    this.unzip(this.config.file, this.config.outdir, (error) => {
      if (error) { return this.emit('error', error, cb); }
      this.signAppDirectory(this.config.outdir + '/Payload', (error, res) => {
        if (error) { this.emit('error', error, cb); }
        this.ipafyDirectory((error, res) => {
          if (error) { this.emit('error', error, cb); }
          this.cleanup((ignored_error) => {
            cb(error, res);
          });
        });
      });
    });
    return this;
  }

  signAppDirectory (path, next) {
    if (!path) {
      path = this.config.outdir + '/Payload';
    }
    /* W T F */
    try {
      if (!fs.lstatSync(path).isDirectory()) {
        throw new Error('Invalid IPA');
      }
    } catch (e) {
      return this.cleanup(() => {
        next(e);
      });
    }
    this.emit('message', 'Payload found');
    const files = fs.readdirSync(path).filter((x) => {
      return x.indexOf('.app') !== -1;
    });
    if (files.length !== 1) {
      return next(new Error('Invalid IPA'));
    }
    this.config.appdir = [ path, files[0] ].join('/');
    const binname = getExecutable(this.config.appdir, files[0].replace('.app', ''));
    const binpath = [ this.config.appdir, binname ].join('/');
    if (fs.lstatSync(binpath).isFile()) {
      const isEncrypted = isBinaryEncrypted(binpath);
      if (isEncrypted) {
        return next(new Error('ipa is encrypted'));
      }
      this.emit('message', 'Main IPA executable is not encrypted');

      const infoPlist = [ this.config.appdir, 'Info.plist' ].join('/');

      this.fixPlist(infoPlist, this.config.bundleid, (err) => {
        if (err) return this.emit('error', err, next);
        checkProvision(this.config.appdir, this.config.mobileprovision, (err) => {
          if (err) return this.emit('error', err, next);
          this.fixEntitlements(binpath, (err) => {
            if (err) return this.emit('error', err, next);
            this.signFile(binpath, (err) => {
              if (err) return this.emit('error', err, next);
              this.signLibraries(this.config.appdir, next);
            });
          });
        });
      });
    } else {
      next(new Error('Invalid path'));
    }
  }

  fixEntitlements (file, next) {
    if (!this.config.mobileprovision) {
      return next();
    }
    this.emit('message', 'Grabbing entitlements from mobileprovision');
    tools.getEntitlementsFromMobileProvision(this.config.mobileprovision, (error, newEntitlements) => {
      this.emit('message', JSON.stringify(newEntitlements));
      const provision = 'embedded.mobileprovision';
      const pathToProvision = [ this.config.appdir, provision ].join('/');
      this.config.entitlement = pathToProvision;
      plist.writeFileSync(pathToProvision, newEntitlements);
      next(error);
    });
  }

  fixPlist (file, bundleid, next) {
    const appdir = this.config.appdir;
    if (!file || !appdir) {
      return next('Invalid parameters for fixPlist');
    }
    if (bundleid) {
      const pl_path = [ this.config.appdir, file ].join('/');
      const data = plist.readFileSync(pl_path);
      const oldBundleID = data['CFBundleIdentifier'];
      this.emit('message', 'Rebundle ' + pl_path + ' ' + oldBundleID + ' into ' + bundleid);
      data['CFBundleIdentifier'] = bundleid;
      plist.writeFileSync(pl_path, data);
    }
    next();
  }

  signFile (file, next) {
    this.emit('message', 'Sign ' + file);
    tools.codesign(this.config.identity, this.config.entitlement, file, (error, stdout, stderr) => {
      if (error) {
        return this.emit('error', error, next);
      }
      this.emit('message', 'Verify ' + file);
      tools.verifyCodesign(file, (error, stdout, stderr) => {
        next(error, stdout || stderr);
      });
    });
  }

  signLibraries (path, next) {
    let signs = 0;
    let errors = 0;
    let found = false;

    this.emit('message', 'Signing libraries and frameworks');

    const exe = '/' + getExecutable(this.config.appdir);
    walk.walkSync(path, (basedir, filename, stat) => {
      const file = [ basedir, filename ].join('/');
      if (file.endsWith(exe)) {
        found = true;
        return;
      }
      if (!fs.lstatSync(file).isFile()) {
        return;
      }
      try {
        const fd = fs.openSync(file, 'r');
        let buffer = new Buffer(4);
        fs.readSync(fd, buffer, 0, 4);
        if (isMacho(buffer)) {
          found = true;
          signs++;
          this.signFile(file, (err) => {
            signs--;
            if (err) {
              this.emit('error ', err);
              errors++;
            }
            if (signs === 0) {
              if (errors > 0) {
                this.emit('error', 'Warning: Some (' + errors + ') errors happened.');
              } else {
                this.emit('message', 'Everything seems signed now');
              }
              next();
            }
          });
        }
        fs.close(fd);
      } catch (e) {
        console.error(basedir, filename, e);
        next(e);
      }
    });
    if (!found) {
      next('Cannot find any MACH0 binary to sign');
    }
  }

  cleanup (cb) {
    const outdir = this.config.outdir;
    this.emit('message', 'Cleaning up ' + outdir);
    try {
      rimraf(outdir, cb);
    } catch (e) {
      this.emit('error', e);
    }
  }

  ipafyDirectory (next) {
    const ipa_in = this.config.file;
    const ipa_out = upperDirectory(this.config.outdir) + this.config.outfile;
    this.events.emit('message', 'Zipifying into ' + ipa_out + ' ...');
    tools.zip(this.config.outdir, ipa_out, 'Payload', (error) => {
      if (!error && this.config.replaceipa) {
        this.events.emit('message', 'mv into ' + ipa_in);
        return fs.rename(ipa_out, ipa_in, next);
      }
      next(error);
    });
  }

  setFile (name) {
    if (typeof name === 'string') {
      this.config.file = name;
      this.config.outdir = name + '.d';
      if (!this.config.outfile) {
        this.setOutputFile(getResignedFilename(name));
      }
    }
  }

  setOutputFile (name) {
    this.config.outfile = name;
  }

  /* TODO: move to tools.js */
  unzip (file, outdir, cb) {
    if (!file || !outdir) {
      cb(new Error('No output specified'));
      return false;
    }
    if (!outdir) {
      cb(new Error('Invalid output directory'));
      return false;
    }
    this.events.emit('message', ['rm -rf', outdir].join(' '));
    this.cleanup(() => {
      this.events.emit('message', 'Unzipping ' + file);
      tools.unzip(file, outdir, (error, stdout) => {
        if (error) {
          this.cleanup(() => { cb(error); });
        } else {
          cb(undefined, stdout);
        }
      });
    });
  }
};
<|endoftext|>


<commit_before>module.exports = function crawler(callback) {
	const request = require('request');
	const cheerio = require('cheerio');

	const URL = 'http://sarc.pucrs.br/Default/';

	var data = {};
	let requestHandler = (err, response, html) => {
		if(err) return callback(err);

		let $ = cheerio.load(html)('#ctl00_cphTitulo_UpdatePanel2 > div > table');

		for (let i = 0, l = $.length; i < l; i++) {
			let content    = cheerio.load($.eq(i).html());
			let time       = content('span').html();
			let table_data = content('.ms-btoolbar').children();
			let details = [];
			for (let j = 0, l = table_data.length; j < l; j+=3) {
				let detail = {
					'resource' : table_data.eq(j).text().trim(),
					'course'   : table_data.eq(j+1).text().trim(),
					'owner'    : table_data.eq(j+2).text().trim()
				};
				details.push(detail);
			}
			time = formatTime(time);
			data[time] = details;
		}
		callback(null, data);
	}
	request(URL, requestHandler);
}

function formatTime(time) {
	var weekday = new Date().getDay() + 1
	return weekday + time
}<commit_msg>Stop adding empty array.<commit_after>module.exports = function crawler(callback) {
	const request = require('request');
	const cheerio = require('cheerio');

	const URL = 'http://sarc.pucrs.br/Default/';

	var data = {};
	let requestHandler = (err, response, html) => {
		if(err) return callback(err);

		let $ = cheerio.load(html)('#ctl00_cphTitulo_UpdatePanel2 > div > table');

		for (let i = 0, l = $.length; i < l; i++) {
			let content    = cheerio.load($.eq(i).html());
			let time       = content('span').html();
			let table_data = content('.ms-btoolbar').children();
			let details = [];
			for (let j = 0, l = table_data.length; j < l; j+=3) {
				let detail = {
					'resource' : table_data.eq(j).text().trim(),
					'course'   : table_data.eq(j+1).text().trim(),
					'owner'    : table_data.eq(j+2).text().trim()
				};
				details.push(detail);
			}
			time = formatTime(time);
			if (details.length > 0) {
				data[time] = details;
			};
		}
		callback(null, data);
	}
	request(URL, requestHandler);
}

function formatTime(time) {
	var weekday = new Date().getDay() + 1
	return weekday + time
}<|endoftext|>


<commit_before>"use strict";

const fs = require("fs");
const os = require("os");
const path = require("path");
const readPkg = require("read-pkg");
const semver = require("semver");

const Package = require("@lerna/package");

const toPackage = ref =>
  ref instanceof Package ? ref : new Package(readPkg.sync(ref, { normalize: false }), ref);

const matchBinaryLinks = () => (pkgRef, raw) => {
  const pkg = toPackage(pkgRef);

  const inputs = Array.isArray(raw) ? raw : [raw];

  const links =
    os.platform() === "win32"
      ? inputs.reduce((acc, input) => [...acc, input, [input, "cmd"].join(".")], [])
      : inputs;

  const expectedName = `expected ${pkg.name}`;
  const expectedAction = `to link to ${links.join(", ")}`;

  let found;

  try {
    found = fs.readdirSync(pkg.binLocation);
  } catch (err) {
    if (links.length === 0 && err.code === "ENOENT") {
      return {
        message: () => `${expectedName} to have binary links`,
        pass: true,
      };
    }

    throw err;
  }

  const missing = links.filter(link => found.indexOf(link) === -1);
  const superfluous = found.filter(link => links.indexOf(link) === -1);

  if (missing.length > 0 || superfluous.length > 0) {
    const message = [
      expectedName,
      expectedAction,
      missing.length > 0 ? `missing: ${missing.join(", ")}` : "",
      superfluous.length > 0 ? `superfluous: ${superfluous.join(", ")}` : "",
    ]
      .filter(Boolean)
      .join("\n");

    return {
      message: () => message,
      pass: false,
    };
  }

  return {
    message: () => `${expectedName} not ${expectedAction}`,
    pass: true,
  };
};

const matchDependency = dependencyType => (manifest, pkg, range, options) => {
  const noDeps = typeof manifest[dependencyType] !== "object";
  const id = [pkg, range].filter(Boolean).join("@");
  const verb = dependencyType === "dependencies" ? "depend" : "dev-depend";
  const exact = options && options.exact;

  const expectedName = `expected ${manifest.name}`;
  const expectedAction = `to ${verb} on ${id}`;
  const expectation = `${expectedName} ${expectedAction}`;
  const json = JSON.stringify(manifest[dependencyType], null, "  ");

  if (noDeps) {
    return {
      message: () => `${expectation} but no .${dependencyType} specified`,
      pass: false,
    };
  }

  const missingDep = !(pkg in manifest[dependencyType]);

  if (missingDep) {
    return {
      message: () => `${expectation} but it is missing from .${dependencyType}\n${json}`,
      pass: false,
    };
  }

  const version = manifest[dependencyType][pkg];
  // we don't care about semver intersection, it's not always a semver range
  const mismatchedDep = range && version !== range;

  if (mismatchedDep) {
    return {
      message: () => `${expectation} but ${version} does not satisfy ${range}\n${json}`,
      pass: false,
    };
  }

  if (exact) {
    if (!semver.valid(version)) {
      return {
        message: () => `${expectation} but ${version} is not an exact version\n${json}`,
        pass: false,
      };
    }

    // semver.eq will throw a TypeError if range is not a valid exact version
    if (!semver.eq(version, range)) {
      return {
        message: () => `${expectation} but ${version} is not ${range}\n${json}`,
        pass: false,
      };
    }
  }

  return {
    message: () => `${expectedName} not ${expectedAction}`,
    pass: true,
  };
};

// eslint-disable-next-line prefer-destructuring
const X_OK = (fs.constants || fs).X_OK;

const matchExecutableFile = () => (pkgRef, raw) => {
  const files = Array.isArray(raw) ? raw : [raw];
  const expectedFiles = `expected ${files.join(", ")}`;
  const expectedAction = "to be executable";
  const expectation = `${expectedFiles} ${expectedAction}`;
  const pkg = toPackage(pkgRef);

  const failed = files.filter(file => {
    try {
      return fs.accessSync(path.join(pkg.location, file), X_OK);
    } catch (_) {
      return true;
    }
  });

  const pass = failed.length === 0;
  const verb = failed.length > 1 ? "were" : "was";

  const message = pass
    ? `${expectedFiles} not ${expectedAction}`
    : `${expectation} while ${failed.join(", ")} ${verb} found to be not executable.`;

  return {
    message: () => message,
    pass,
  };
};

module.exports = {
  toDependOn: matchDependency("dependencies"),
  toDevDependOn: matchDependency("devDependencies"),
  toHaveExecutable: matchExecutableFile(),
  toHaveBinaryLink: matchBinaryLinks(),
};
<commit_msg>chore(pkg-matchers): Normalize backslashes of version comparison<commit_after>"use strict";

const fs = require("fs");
const os = require("os");
const path = require("path");
const readPkg = require("read-pkg");
const semver = require("semver");

const Package = require("@lerna/package");

const toPackage = ref =>
  ref instanceof Package ? ref : new Package(readPkg.sync(ref, { normalize: false }), ref);

const matchBinaryLinks = () => (pkgRef, raw) => {
  const pkg = toPackage(pkgRef);

  const inputs = Array.isArray(raw) ? raw : [raw];

  const links =
    os.platform() === "win32"
      ? inputs.reduce((acc, input) => [...acc, input, [input, "cmd"].join(".")], [])
      : inputs;

  const expectedName = `expected ${pkg.name}`;
  const expectedAction = `to link to ${links.join(", ")}`;

  let found;

  try {
    found = fs.readdirSync(pkg.binLocation);
  } catch (err) {
    if (links.length === 0 && err.code === "ENOENT") {
      return {
        message: () => `${expectedName} to have binary links`,
        pass: true,
      };
    }

    throw err;
  }

  const missing = links.filter(link => found.indexOf(link) === -1);
  const superfluous = found.filter(link => links.indexOf(link) === -1);

  if (missing.length > 0 || superfluous.length > 0) {
    const message = [
      expectedName,
      expectedAction,
      missing.length > 0 ? `missing: ${missing.join(", ")}` : "",
      superfluous.length > 0 ? `superfluous: ${superfluous.join(", ")}` : "",
    ]
      .filter(Boolean)
      .join("\n");

    return {
      message: () => message,
      pass: false,
    };
  }

  return {
    message: () => `${expectedName} not ${expectedAction}`,
    pass: true,
  };
};

const matchDependency = dependencyType => (manifest, pkg, range, options) => {
  const noDeps = typeof manifest[dependencyType] !== "object";
  const id = [pkg, range].filter(Boolean).join("@");
  const verb = dependencyType === "dependencies" ? "depend" : "dev-depend";
  const exact = options && options.exact;

  const expectedName = `expected ${manifest.name}`;
  const expectedAction = `to ${verb} on ${id}`;
  const expectation = `${expectedName} ${expectedAction}`;
  const json = JSON.stringify(manifest[dependencyType], null, "  ");

  if (noDeps) {
    return {
      message: () => `${expectation} but no .${dependencyType} specified`,
      pass: false,
    };
  }

  const missingDep = !(pkg in manifest[dependencyType]);

  if (missingDep) {
    return {
      message: () => `${expectation} but it is missing from .${dependencyType}\n${json}`,
      pass: false,
    };
  }

  // replace backslashes because windows sucks
  const version = manifest[dependencyType][pkg].replace(/[\\]/g, "/");

  // we don't care about semver intersection, it's not always a semver range
  const mismatchedDep = range && version !== range;

  if (mismatchedDep) {
    return {
      message: () => `${expectation} but ${version} does not satisfy ${range}\n${json}`,
      pass: false,
    };
  }

  if (exact) {
    if (!semver.valid(version)) {
      return {
        message: () => `${expectation} but ${version} is not an exact version\n${json}`,
        pass: false,
      };
    }

    // semver.eq will throw a TypeError if range is not a valid exact version
    if (!semver.eq(version, range)) {
      return {
        message: () => `${expectation} but ${version} is not ${range}\n${json}`,
        pass: false,
      };
    }
  }

  return {
    message: () => `${expectedName} not ${expectedAction}`,
    pass: true,
  };
};

// eslint-disable-next-line prefer-destructuring
const X_OK = (fs.constants || fs).X_OK;

const matchExecutableFile = () => (pkgRef, raw) => {
  const files = Array.isArray(raw) ? raw : [raw];
  const expectedFiles = `expected ${files.join(", ")}`;
  const expectedAction = "to be executable";
  const expectation = `${expectedFiles} ${expectedAction}`;
  const pkg = toPackage(pkgRef);

  const failed = files.filter(file => {
    try {
      return fs.accessSync(path.join(pkg.location, file), X_OK);
    } catch (_) {
      return true;
    }
  });

  const pass = failed.length === 0;
  const verb = failed.length > 1 ? "were" : "was";

  const message = pass
    ? `${expectedFiles} not ${expectedAction}`
    : `${expectation} while ${failed.join(", ")} ${verb} found to be not executable.`;

  return {
    message: () => message,
    pass,
  };
};

module.exports = {
  toDependOn: matchDependency("dependencies"),
  toDevDependOn: matchDependency("devDependencies"),
  toHaveExecutable: matchExecutableFile(),
  toHaveBinaryLink: matchBinaryLinks(),
};
<|endoftext|>


<commit_before>'use strict'

const User = require('./user')
const Token = require('./token')
const forgotMail = require('./mail/forgot')
const {del, get, post} = require('koa-route')

// Find User
const findUser = (email) => (
  User.where('trim(lower(email)) = trim(lower(?))', email).find()
)

// Find Token
const findToken = (id) => (
  Token.include('user').where('expires_at >= now()').find(id)
)

module.exports = [

  // Sign In
  post('/session', function *() {
    const {email, password} = this.request.body
    const user = yield findUser(email)

    if (!user) {
      this.status = 422
      this.body = {email: ['Sorry! We don’t recognize that email.']}
      return
    }

    if (yield user.authenticate(password)) {
      this.signIn(user)
      this.body = {}
    } else {
      this.status = 422
      this.body = {password: ['Sorry! That password is incorrect.']}
    }
  }),

  del('/session', function *() {
    this.signOut()
    this.body = {}
  }),

  get('/session/reset/:id', function *(id) {
    const token = yield findToken(id)
    this.react({
      token: token && token.id,
      email: token && token.user.email
    })
  }),

  post('/session/reset/:id', function *(id) {
    const token = yield findToken(id)
    const {password} = this.request.body

    if (!token) {
      this.status = 422
      this.body = {password: ['Sorry! That token is expired.']}
      return
    }

    if (!password) {
      this.status = 422
      this.body = {password: ['You must provide a password.']}
      return
    }

    yield token.user.update(this.permit('password'))
    this.signIn(token.user)
    this.body = {}
  }),

  post('/session/forgot', function *() {
    const {email} = this.request.body
    const user = yield findUser(email)

    if (!user) {
      this.status = 422
      this.body = {email: ['Sorry! We don’t recognize that email.']}
      return
    }

    const expiresAt = new Date()
    expiresAt.setDate(expiresAt.getDate() + 7)

    const token = yield Token.create({expiresAt, userId: user.id})

    yield this.mail(forgotMail, {
      to: [user.email],
      subject: `${process.env.NAME}: Password Reset`,
      url: `http://${this.host}/session/reset/${token.id}`
    })

    this.body = {}
  })

]
<commit_msg>Session routes<commit_after>'use strict'

const User = require('./user')
const Token = require('./token')
const forgotMail = require('./mail/forgot')
const {del, get, post} = require('koa-route')

// Find User
const findUser = (email) => (
  User.where('trim(lower(email)) = trim(lower(?))', email).find()
)

// Find Token
const findToken = (id) => (
  Token.include('user').where('expires_at >= now()').find(id)
)

module.exports = [

  // Sign In
  post('/session', function *() {
    const {email, password} = this.request.body
    const user = yield findUser(email)

    if (!user) {
      this.status = 422
      this.body = {email: ['Sorry! We don’t recognize that email.']}
      return
    }

    if (yield user.authenticate(password)) {
      this.signIn(user)
      this.body = {}
    } else {
      this.status = 422
      this.body = {password: ['Sorry! That password is incorrect.']}
    }
  }),

  del('/session', function *() {
    this.signOut()
    this.body = {}
  }),

  get('/session/signin', function *() {
    this.react()
  }),

  get('/session/forgot', function *() {
    this.react()
  }),

  get('/session/reset/:id', function *(id) {
    const token = yield findToken(id)
    this.react({
      token: token && token.id,
      email: token && token.user.email
    })
  }),

  post('/session/reset/:id', function *(id) {
    const token = yield findToken(id)
    const {password} = this.request.body

    if (!token) {
      this.status = 422
      this.body = {password: ['Sorry! That token is expired.']}
      return
    }

    if (!password) {
      this.status = 422
      this.body = {password: ['You must provide a password.']}
      return
    }

    yield token.user.update(this.permit('password'))
    this.signIn(token.user)
    this.body = {}
  }),

  post('/session/forgot', function *() {
    const {email} = this.request.body
    const user = yield findUser(email)

    if (!user) {
      this.status = 422
      this.body = {email: ['Sorry! We don’t recognize that email.']}
      return
    }

    const expiresAt = new Date()
    expiresAt.setDate(expiresAt.getDate() + 7)

    const token = yield Token.create({expiresAt, userId: user.id})

    yield this.mail(forgotMail, {
      to: [user.email],
      subject: `${process.env.NAME}: Password Reset`,
      url: `http://${this.host}/session/reset/${token.id}`
    })

    this.body = {}
  })

]
<|endoftext|>


<commit_before>const fs = require('fs');
const cp = require('child_process');
const im = require('imagemagick');
const readline = require('readline');
const commandLineArgs = require('command-line-args');

const fullWidth = 1920;
const fullHeight = 1080;

const optionDefinitions = [
  { name: 'delimiter', alias: 'd', type: String },
  { name: 'font', alias: 'f', type: String },
  { name: 'fontfamily', alias: 'a', type: String },
  { name: 'imagefolder', alias: 'i', type: String },
  { name: 'lines', alias: 'l', type: Number },
  { name: 'music', alias: 'm', type: String },
  { name: 'size', alias: 's', type: Number },
  { name: 'text', alias: 't', type: String },
];
const options = commandLineArgs(optionDefinitions);

class TextBlock {
  constructor(text, i, j, font, fontsize) {
    this.text = text;
    this.font = font;
    this.fontsize = fontsize;
    this.i = i;
    this.j = j;
    this.x = 0;
    this.y = 0;
    this.width = 0;
    this.height = 0;
    this.fillSize(text, font, fontsize);
  }

  fillSize(text, font, fontsize) {
    im.convert([
      '-debug', 'annotate',
      'xc:',
      '-font', font,
      '-pointsize', fontsize,
      '-annotate', 0,
      text, 'null:'
    ], function (err, output) {
      if (err) {
        // In this case, stderr should have the output
        console.log('ERR: ' + err + '\n');
      } else if (output) {
        console.log('OUT: ' + output + '\n');
      }
    });
  }
}

var delimiter = options.hasOwnProperty('delimiter') ? options.delimiter : '===';
var font = options.font;
var fontfamily = options.fontfamily;
var nlines = options.hasOwnProperty('lines') ? options.lines : -1;
var music = options.music;
var fontsize = options.hasOwnProperty('size') ? options.size : '48';
var text = options.text;
var imgfolder = options.hasOwnProperty('imagefolder') ? options.imagefolder : '';

if (!options.hasOwnProperty('fontfamily')
 || !options.hasOwnProperty('music')
 || !options.hasOwnProperty('text')) {
  // Can't do anything useful without input
  return;
}

var proc = cp.spawnSync('/usr/bin/soxi', [ '-D', music ]);
var time = parseFloat(proc.stdout.toString().trim(), 10);
var images = [];
var finished = [];
var pages = 1;
var timePerPage = 1;
var lineReader = readline.createInterface({
  terminal: false,
  input: fs.createReadStream(text)
});
var screens = [];
var screenful = [];

lineReader.on('line', function (line) {
  if (line.startsWith(delimiter)) {
    var length = screenful.length;
    var reDelimiter = new RegExp('^' + delimiter, 'i');
    var bgimage = line.replace(reDelimiter, '').trim();
    var imageName = './bg-' + ('0000' + (screens.length + 1)).slice(-5) + '.png';

    images.push(imageName);
    createBackgroundImage(imgfolder, bgimage, screens.length, imageName);

    if (length < nlines) {
      var toAdd = Math.trunc((nlines - length) / 2);
      for (var l = 0; l < toAdd; l++) {
        screenful.unshift('');
      }
    }

    screens.push(screenful);
    screenful = [];
  } else {
    screenful.push(line);
  }
});
lineReader.on('close', function() {
  setTimeout(foregroundImages, 100);
});

function foregroundImages() {
  if (images.length != finished.length) {
    console.log('Waiting for image processing (' + finished.length + ' of ' + images.length + ')...');
    setTimeout(foregroundImages, 100);
  } else {
    pages = screens.length;
    timePerPage = time / pages;

    console.log('Runs for ' + time + 's, or ' + timePerPage + 's per page.');
    setTimeout(videoFromImages, 100);
    for (var page = 0; page < pages; page++) {
      var pageNumber = ('0000' + (page + 1)).slice(-5);
      var imageName = './credit-' + pageNumber + '.png';
      var bgImageName = './bg-' + pageNumber + '.png';
      var blocks = [];
      var lines = screens[page];

      images.push(imageName);
      createPageImage(page, nlines, bgImageName, imageName, font, fontfamily, lines, blocks);
    }
  }
}

function videoFromImages() {
  if (images.length != finished.length) {
    console.log('Waiting for image processing (' + finished.length + ' of ' + images.length + ')...');
    setTimeout(videoFromImages, 100);
  } else {
    var outfile = text + '.mp4';
    fs.stat(outfile, function(err, stat) {
      if(err == null) {
        fs.unlink(outfile);
      }
      cp.spawnSync('/usr/bin/ffmpeg', [
        '-framerate', 1 / timePerPage,
        '-i', 'credit-%05d.png',
        '-i', music,
        '-safe', '0',
        '-strict', '-2',
        '-c:v', 'libx264',
        '-b:a', '4.5M',
        '-b:v', '4.5M',
        '-minrate', '4.5M',
        '-maxrate', '4.5M',
        '-bufsize', '4.5M',
        '-r', 30,
        '-pix_fmt', 'yuv420p',
        '-af', 'aresample=async=1',
        '-max_muxing_queue_size', '9999',
        '-shortest',
        text + '.mp4'
      ]);
      for (var i = 0; i < images.length; i++) {
        fs.unlink(images[i]);
      }
    });
  }
}

function createBackgroundImage(imgfolder, bgimage, number, imageName) {
  if (bgimage !== '') {
    im.convert([
      imgfolder + '/' + bgimage,
      '-background', '#000030',
      '-gravity', 'Center',
      '-extent', fullWidth + 'x' + fullHeight,
      imageName
    ], function (err, output) {
      if (err) {
        console.log(err);
      } else {
        finished.push('bg' + number);
      }
    });
  } else {
    im.convert([
      '-size', fullWidth + 'x' + fullHeight,
      'xc:#000030',
      imageName
    ], function (err, output) {
      if (err) {
        console.log(err);
      } else {
        finished.push('bg' + number);
      }
    });
  }
}

function createPageImage(page, nlines, bgImageName, imageName, font, fontfamily, lines, blocks) {
  var contents = lines.join('\n');

  if (nlines > 0) {

    for (var l = 0; l < lines.length; l++) {
      if (lines[l].startsWith('[')) {
        var imagelist = lines[l].split(' ');
        var cmdConvert = [];

        for (var ii = 0 ; ii < imagelist.length; ii++) {
          cmdConvert.push(imgfolder + '/' + imagelist[ii].slice(1, -1));
        }

        cmdConvert.push('+append');
        cmdConvert.push('line-' + ('00' + l).slice(-2) + '.png');

        im.convert(cmdConvert, function(err, output) {
          if (err) {
            console.log(err);
          }
        });

        lines[l] = '';
      }
    }
  }

  for (var ll = 0; ll < lines.length; ll++) {
    var fields = lines[ll].split('\t');

    for (var ff = 0; ff < fields.length; ff++) {
      blocks.push(new TextBlock(fields[ff], ll, ff, font, fontsize));
    }
  }

  im.convert([
    '-page', '+0+0', bgImageName,
    '-background', 'rgba(0,0,0,0)',
    '-fill', '#F0F0B0',
    '-font', font,
    '-size', fullWidth + 'x' + fullHeight,
    '-pointsize', fontsize,
    '-gravity', 'Center',
    'pango:' + contents + '',
    '-layers', 'flatten',
    imageName
  ], function (err, output) {
    if (err) {
      console.log(err);
    }

    finished.push(page);
  });
}
<commit_msg>Add arrays for embedded images<commit_after>const fs = require('fs');
const cp = require('child_process');
const im = require('imagemagick');
const readline = require('readline');
const commandLineArgs = require('command-line-args');

const fullWidth = 1920;
const fullHeight = 1080;

const optionDefinitions = [
  { name: 'delimiter', alias: 'd', type: String },
  { name: 'font', alias: 'f', type: String },
  { name: 'fontfamily', alias: 'a', type: String },
  { name: 'imagefolder', alias: 'i', type: String },
  { name: 'lines', alias: 'l', type: Number },
  { name: 'music', alias: 'm', type: String },
  { name: 'size', alias: 's', type: Number },
  { name: 'text', alias: 't', type: String },
];
const options = commandLineArgs(optionDefinitions);

class TextBlock {
  constructor(text, i, j, font, fontsize) {
    this.text = text;
    this.font = font;
    this.fontsize = fontsize;
    this.i = i;
    this.j = j;
    this.x = 0;
    this.y = 0;
    this.width = 0;
    this.height = 0;
    this.fillSize(text, font, fontsize);
  }

  fillSize(text, font, fontsize) {
    im.convert([
      '-debug', 'annotate',
      'xc:',
      '-font', font,
      '-pointsize', fontsize,
      '-annotate', 0,
      text, 'null:'
    ], function (err, output) {
      if (err) {
        // In this case, stderr should have the output
        console.log('ERR: ' + err + '\n');
      } else if (output) {
        console.log('OUT: ' + output + '\n');
      }
    });
  }
}

var delimiter = options.hasOwnProperty('delimiter') ? options.delimiter : '===';
var font = options.font;
var fontfamily = options.fontfamily;
var nlines = options.hasOwnProperty('lines') ? options.lines : -1;
var music = options.music;
var fontsize = options.hasOwnProperty('size') ? options.size : '48';
var text = options.text;
var imgfolder = options.hasOwnProperty('imagefolder') ? options.imagefolder : '';

if (!options.hasOwnProperty('fontfamily')
 || !options.hasOwnProperty('music')
 || !options.hasOwnProperty('text')) {
  // Can't do anything useful without input
  return;
}

var proc = cp.spawnSync('/usr/bin/soxi', [ '-D', music ]);
var time = parseFloat(proc.stdout.toString().trim(), 10);
var images = [];
var finished = [];
var internalImages = [];
var internalFinished = [];
var pages = 1;
var timePerPage = 1;
var lineReader = readline.createInterface({
  terminal: false,
  input: fs.createReadStream(text)
});
var screens = [];
var screenful = [];

lineReader.on('line', function (line) {
  if (line.startsWith(delimiter)) {
    var length = screenful.length;
    var reDelimiter = new RegExp('^' + delimiter, 'i');
    var bgimage = line.replace(reDelimiter, '').trim();
    var imageName = './bg-' + ('0000' + (screens.length + 1)).slice(-5) + '.png';

    images.push(imageName);
    createBackgroundImage(imgfolder, bgimage, screens.length, imageName);

    if (length < nlines) {
      var toAdd = Math.trunc((nlines - length) / 2);
      for (var l = 0; l < toAdd; l++) {
        screenful.unshift('');
      }
    }

    screens.push(screenful);
    screenful = [];
  } else {
    screenful.push(line);
  }
});
lineReader.on('close', function() {
  setTimeout(foregroundImages, 100);
});

function foregroundImages() {
  if (images.length != finished.length) {
    console.log('Waiting for image processing (' + finished.length + ' of ' + images.length + ')...');
    setTimeout(foregroundImages, 100);
  } else {
    pages = screens.length;
    timePerPage = time / pages;

    console.log('Runs for ' + time + 's, or ' + timePerPage + 's per page.');
    setTimeout(videoFromImages, 100);
    for (var page = 0; page < pages; page++) {
      var pageNumber = ('0000' + (page + 1)).slice(-5);
      var imageName = './credit-' + pageNumber + '.png';
      var bgImageName = './bg-' + pageNumber + '.png';
      var blocks = [];
      var lines = screens[page];

      images.push(imageName);
      createPageImage(page, nlines, bgImageName, imageName, font, fontfamily, lines, blocks);
    }
  }
}

function videoFromImages() {
  if (images.length != finished.length) {
    console.log('Waiting for image processing (' + finished.length + ' of ' + images.length + ')...');
    setTimeout(videoFromImages, 100);
  } else {
    var outfile = text + '.mp4';
    fs.stat(outfile, function(err, stat) {
      if(err == null) {
        fs.unlink(outfile);
      }
      cp.spawnSync('/usr/bin/ffmpeg', [
        '-framerate', 1 / timePerPage,
        '-i', 'credit-%05d.png',
        '-i', music,
        '-safe', '0',
        '-strict', '-2',
        '-c:v', 'libx264',
        '-b:a', '4.5M',
        '-b:v', '4.5M',
        '-minrate', '4.5M',
        '-maxrate', '4.5M',
        '-bufsize', '4.5M',
        '-r', 30,
        '-pix_fmt', 'yuv420p',
        '-af', 'aresample=async=1',
        '-max_muxing_queue_size', '9999',
        '-shortest',
        text + '.mp4'
      ]);
      for (var i = 0; i < images.length; i++) {
        fs.unlink(images[i]);
      }
    });
  }
}

function createBackgroundImage(imgfolder, bgimage, number, imageName) {
  if (bgimage !== '') {
    im.convert([
      imgfolder + '/' + bgimage,
      '-background', '#000030',
      '-gravity', 'Center',
      '-extent', fullWidth + 'x' + fullHeight,
      imageName
    ], function (err, output) {
      if (err) {
        console.log(err);
      } else {
        finished.push('bg' + number);
      }
    });
  } else {
    im.convert([
      '-size', fullWidth + 'x' + fullHeight,
      'xc:#000030',
      imageName
    ], function (err, output) {
      if (err) {
        console.log(err);
      } else {
        finished.push('bg' + number);
      }
    });
  }
}

function createPageImage(page, nlines, bgImageName, imageName, font, fontfamily, lines, blocks) {
  var contents = lines.join('\n');

  if (nlines > 0) {

    for (var l = 0; l < lines.length; l++) {
      if (lines[l].startsWith('[')) {
        var imagelist = lines[l].split(' ');
        var cmdConvert = [];

        for (var ii = 0 ; ii < imagelist.length; ii++) {
          cmdConvert.push(imgfolder + '/' + imagelist[ii].slice(1, -1));
        }

        cmdConvert.push('+append');
        cmdConvert.push('line-' + ('00' + l).slice(-2) + '.png');

        im.convert(cmdConvert, function(err, output) {
          if (err) {
            console.log(err);
          }
        });

        lines[l] = '';
      }
    }
  }

  for (var ll = 0; ll < lines.length; ll++) {
    var fields = lines[ll].split('\t');

    for (var ff = 0; ff < fields.length; ff++) {
      blocks.push(new TextBlock(fields[ff], ll, ff, font, fontsize));
    }
  }

  im.convert([
    '-page', '+0+0', bgImageName,
    '-background', 'rgba(0,0,0,0)',
    '-fill', '#F0F0B0',
    '-font', font,
    '-size', fullWidth + 'x' + fullHeight,
    '-pointsize', fontsize,
    '-gravity', 'Center',
    'pango:' + contents + '',
    '-layers', 'flatten',
    imageName
  ], function (err, output) {
    if (err) {
      console.log(err);
    }

    finished.push(page);
  });
}
<|endoftext|>