<commit_before>var _ = require('underscore');
var async = require('async');
var string = require('../module/util/string');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var Issue = require('./issue');
var User = require('./user');
var stages = require('./stages');
var ew = require('../module/util/asyncWrap').errorWrap;

var ID_LENGTH = 12;

var createID = string.createRandomCode.bind(string, ID_LENGTH);

var Projects = new Schema({
    id: { type: String, default: createID, required: true },
    name: { type: String, required: true },
    members: [
        {
            user: { type: ObjectId, ref: 'User', required: true },
            created_at: { type: Date, default: Date.now, required: true },
            accessLevel: { type: String, default: 1, required: true },
            wipLimit: { type: Number, default: 4, required: true }
        }
    ],
    issues: [Issue.schema],
    github: {  // option
        userName: { type: String },
        repoName: { type: String },
        url: { type: String },
        sync: { type: Boolean, default: true, required: true }
    },
    create_user: { type: ObjectId, ref: 'User', required: true },
    created_at: { type: Date, default: Date.now, required: true }
});

Projects.static('findPopulated', function (where, o, callback) {
    o = o || {};

    this.find(where)
        .populate('create_user')
        .populate('members.user')
        .exec(ew(callback, function (docs) {
            if (o.one) {
                callback(null, docs.length ? docs[0] : null);
            } else {
                callback(null, docs);
            }
        }));
});

/*** member ***/
// もし存在しないユーザなら作成する
Projects.method('addMember', function (userName, callback) {
    var project = this;

    User.findOrCreate(userName, ew(callback, function (user) {
        if (project.findMemberById(user._id)) {
            callback(new Error('already added member'));
            return;
        }

        project.members.unshift({user: user});

        project.save(ew(callback, function (project) {
            project.populate('members.user', function (err, project) {
                callback(err, project, project.members[0]);
            });
        }));
    }));
});

Projects.method('removeMember', function (userName, callback) {
    var project = this;

    User.findOne({userName: userName}, function (err, user) {
        if (err) { callback(err); return; }
        if (!user) { callback(new Error('undefined user name: ' + userName)); return; }

        project.populate('members.user', function (err, project) {
            if (err) { callback(err); return; }

            var member = _.find(project.members, function (member) {
                return String(member.user._id) === String(user._id);
            });
            if (!member) { callback(new Error('undefined member name: ' + userName)); return; }

            project.members.pull(member);

            project.save(function (err, project) {
                if (err) { callback(err); return; }

                callback(err, project, member);
            });
        });
    });
});

Projects.method('updateMember', function (userName, updateParams, callback) {
    var project = this;

    project.populate('members.user', function (err, project) {
        if (err) { callback(err); return; }

        var member = _.find(project.members, function (member) {
            return String(member.user.userName) === String(userName);
        });
        if (!member) { callback(new Error('undefined member name: ' + userName)); return; }

        _.each(updateParams, function (value, key) {
            member[key] = value;
        });

        project.save(function (err, project) {
            if (err) { callback(err); return; }

            callback(err, project, member);
        });
    });
});

/*** /member ***/

/*** issue ***/
Projects.method('addIssue', function (token, params, callback) {
    var project = this;
    var GitHub = require('./github');
    var issue;

    async.series([
        function (next) {
            // github hook create issue
            if (token && project.github && project.github.sync) {
                (new GitHub(token)).createIssue(project.github.repoName, project.github.userName, params, next);
                issue = null;
            } else {
                // create issue
                async.series([
                    // issue作成
                    function (next) {
                        if (issue) { next(null); return; }
                        Issue.create(params, function (err, doc) {
                            issue = doc;
                            next(err);
                        });
                    },
                    // projectにissue追加
                    function (next) {
                        project.issues.unshift(issue);
                        project.save(function (err, data) {
                            project = data;
                            next(err);
                        });
                    }
                ], next);
            }
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('removeIssue', function (token, issueId, callback) {
    var project = this;
    var GitHub = require('./github');

    // issue取り出し
    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    async.series([
        function (next) {
            // githubのissue削除
            if (token && project.github && project.github.sync) {
                (new GitHub(token)).closeIssue(project.github.repoName, project.github.userName, issue, next);
                issue = null;
            } else {
                // issue削除
                project.issues.pull(issue);
                project.save(function (err, data) {
                    project = data;
                    next(err);
                });
            }
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

/*** /issue ***/

Projects.method('assign', function (token, issueId, userId, callback) {
    var project = this;
    var GitHub = require('./github');
    var username = null;

    // issue取り出し
    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // member取り出し
    var member = _.find(project.members, function (member) {
        return String(member.user) === userId;
    });

    // 更新情報
    var src = {
        assignee: member ? userId : null // 見つからない = アサインを取り消す
    };
    // アサイン直後は必ずTODO
    if (member) {
        src.stage = stages.todo;
    }
    // アサインから外したら、issue, done 以外だったら backlog にする
    if (!member && !src.stage && !_.contains([stages.issue, stages.done], issue.stage)) {
        src.stage = stages.backlog;
    }

    async.series([
        // メンバー名の取得
        function (next) {
            if (!member) { next(null); return; }
            User.findById(userId, function (err, user) {
                username = user.userName;
                next(err);
            });
        },
        function (next) {
            if (token && project.github && project.github.sync) {
                // githubのassign hook
                (new GitHub(token)).assignIssue(project.github.repoName, project.github.userName, issue, username, next);
                issue = null;
            } else {
                // assign
                _.extend(issue, src);
                project.save(function (err, data) {
                    project = data;
                    next(err);
                });
            }
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('updateStage', function (issueId, toStage, callback) {
    var project = this;

    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // TODO: assignee check if next stage is in todo, doing, done
    //if (!issue.assignee) { callback(new Error('issue not assigned: ' + issueId)); return; }

    // TODO: validate stage
    issue.stage = toStage;

    project.save(function (err, project) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('updateIssueDetail', function (token, issueId, title, body, callback) {
    var project = this;
    var GitHub = require('./github');

    // issue取り出し
    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // 更新用オブジェクト
    var src = {};
    if (issue.title !== title) { src.title = title; }
    if (issue.body !== body) { src.body = body; }

    async.series([
        // githubのissueの更新
        function (next) {
            if (token && project.github && project.github.sync) {
                (new GitHub(token)).updateDetailIssue(project.github.repoName, project.github.userName, issue, src, next);
            } else {
                next(null);
            }
        },
        // issue更新
        function (next) {
            _.extend(issue, src);
            project.save(function (err, data) {
                project = data;
                next(err);
            });
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('updateIssuePriority', function (issueId, toPriority, callback) {
    var project = this;

    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    var beforePriority = project.issues.indexOf(issue);
    var nextPriority = toPriority + (beforePriority >= toPriority ? 1 : 0);

    project.issues.splice(beforePriority, 1);
    project.issues.splice(nextPriority, 0, issue);

    project.save(function (err, project) {
        if (err) { callback(err); return; }
        callback(err, project, issue, toPriority);
    });
});

Projects.static('exists', function (where, callback) {
    this.count(where, function (err, count) {
        if (err) { callback(err); return; }
        callback(null, !!count);
    });
});

Projects.method('findIssue', function (predicate, callback) {
    var issue = _.find(this.issues, predicate);
    if (callback) { callback(null, issue); }
    return issue;
});

Projects.method('findIssueById', function (issueId, callback) {
    var issue = _.find(this.issues, function (issue) {
        return String(issue._id) === issueId;
    });
    if (callback) { callback(null, issue, this); }
    return issue;
});

Projects.method('findMemberById', function (userId, callback) {
    var member = _.find(this.members, function (member) {
        return String(member.user) === String(userId);
    });
    if (callback) { callback(null, member); }
    return member;
});

/*** helper ***/

function methodToStatic(methodName/* ,args..., callback */) {
    return function (projectWhere) {
        var callback = arguments[arguments.length - 1];
        var args = Array.prototype.splice.call(arguments, 1);

        this.findOne(projectWhere, function (err, project) {
            if (err) { callback(err); return; }
            if (!project) { callback(new Error('undefined project')); return; }

            project[methodName].apply(project, args);
        });
    };
}

/*** /helper **/

['addMember', 'removeMember', 'updateMember', 'addIssue', 'removeIssue', 'assign', 'updateStage',
    'updateIssueDetail', 'findIssue', 'findIssueById', 'updateIssuePriority'].forEach(function (methodName) {
    Projects.static(methodName, methodToStatic(methodName));
});

if (!mongoose.models.Project) { module.exports = mongoose.model('Project', Projects); }
else { module.exports = mongoose.model('Project'); }<commit_msg>projectModelのリファクタリング<commit_after>var _ = require('underscore');
var async = require('async');
var string = require('../module/util/string');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var Issue = require('./issue');
var User = require('./user');
var stages = require('./stages');
var ew = require('../module/util/asyncWrap').errorWrap;

var ID_LENGTH = 12;

var createID = string.createRandomCode.bind(string, ID_LENGTH);

var Projects = new Schema({
    id: { type: String, default: createID, required: true },
    name: { type: String, required: true },
    members: [
        {
            user: { type: ObjectId, ref: 'User', required: true },
            created_at: { type: Date, default: Date.now, required: true },
            accessLevel: { type: String, default: 1, required: true },
            wipLimit: { type: Number, default: 4, required: true }
        }
    ],
    issues: [Issue.schema],
    github: {  // option
        userName: { type: String },
        repoName: { type: String },
        url: { type: String },
        sync: { type: Boolean, default: true, required: true }
    },
    create_user: { type: ObjectId, ref: 'User', required: true },
    created_at: { type: Date, default: Date.now, required: true }
});

Projects.static('findPopulated', function (where, o, callback) {
    o = o || {};

    this.find(where)
        .populate('create_user')
        .populate('members.user')
        .exec(ew(callback, function (docs) {
            if (o.one) {
                callback(null, docs.length ? docs[0] : null);
            } else {
                callback(null, docs);
            }
        }));
});

/*** member ***/
// もし存在しないユーザなら作成する
Projects.method('addMember', function (userName, callback) {
    var project = this;

    User.findOrCreate(userName, ew(callback, function (user) {
        if (project.findMemberById(user._id)) { return callback(new Error('already added member')); }

        project.members.unshift({user: user});

        project.save(ew(callback, function (project) {
            project.populate('members.user', function (err, project) {
                callback(err, project, project.members[0]);
            });
        }));
    }));
});

// メンバーに対して何か操作する
Projects.method('operationMember', function (userName, predecate, callback) {
    var project = this;

    project.populate('members.user', ew(callback, function (project) {
        var member = project.findMemberByName(userName);
        if (!member) { return callback(new Error('undefined member name: ' + userName)); }

        predecate(project, member);

        project.save(ew(callback, function (project) {
            callback(null, project, member);
        }));
    }));
});

Projects.method('removeMember', function (userName, callback) {
    this.operationMember(userName, function (project, member) {
        project.members.pull(member);
    }, callback);
});

Projects.method('updateMember', function (userName, updateParams, callback) {
    this.operationMember(userName, function (project, member) {
        _.each(updateParams, function (value, key) {
            member[key] = value;
        });
    }, callback);
});

/*** /member ***/

/*** issue ***/
Projects.method('addIssue', function (token, params, callback) {
    var project = this;
    var GitHub = require('./github');
    var issue;

    async.series([
        function (next) {
            // github hook create issue
            if (token && project.github && project.github.sync) {
                (new GitHub(token)).createIssue(project.github.repoName, project.github.userName, params, next);
                issue = null;
            } else {
                // create issue
                async.series([
                    // issue作成
                    function (next) {
                        if (issue) { next(null); return; }
                        Issue.create(params, function (err, doc) {
                            issue = doc;
                            next(err);
                        });
                    },
                    // projectにissue追加
                    function (next) {
                        project.issues.unshift(issue);
                        project.save(function (err, data) {
                            project = data;
                            next(err);
                        });
                    }
                ], next);
            }
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('removeIssue', function (token, issueId, callback) {
    var project = this;
    var GitHub = require('./github');

    // issue取り出し
    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    async.series([
        function (next) {
            // githubのissue削除
            if (token && project.github && project.github.sync) {
                (new GitHub(token)).closeIssue(project.github.repoName, project.github.userName, issue, next);
                issue = null;
            } else {
                // issue削除
                project.issues.pull(issue);
                project.save(function (err, data) {
                    project = data;
                    next(err);
                });
            }
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

/*** /issue ***/

Projects.method('assign', function (token, issueId, userId, callback) {
    var project = this;
    var GitHub = require('./github');
    var username = null;

    // issue取り出し
    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // member取り出し
    var member = _.find(project.members, function (member) {
        return String(member.user) === userId;
    });

    // 更新情報
    var src = {
        assignee: member ? userId : null // 見つからない = アサインを取り消す
    };
    // アサイン直後は必ずTODO
    if (member) {
        src.stage = stages.todo;
    }
    // アサインから外したら、issue, done 以外だったら backlog にする
    if (!member && !src.stage && !_.contains([stages.issue, stages.done], issue.stage)) {
        src.stage = stages.backlog;
    }

    async.series([
        // メンバー名の取得
        function (next) {
            if (!member) { next(null); return; }
            User.findById(userId, function (err, user) {
                username = user.userName;
                next(err);
            });
        },
        function (next) {
            if (token && project.github && project.github.sync) {
                // githubのassign hook
                (new GitHub(token)).assignIssue(project.github.repoName, project.github.userName, issue, username, next);
                issue = null;
            } else {
                // assign
                _.extend(issue, src);
                project.save(function (err, data) {
                    project = data;
                    next(err);
                });
            }
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('updateStage', function (issueId, toStage, callback) {
    var project = this;

    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // TODO: assignee check if next stage is in todo, doing, done
    //if (!issue.assignee) { callback(new Error('issue not assigned: ' + issueId)); return; }

    // TODO: validate stage
    issue.stage = toStage;

    project.save(function (err, project) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('updateIssueDetail', function (token, issueId, title, body, callback) {
    var project = this;
    var GitHub = require('./github');

    // issue取り出し
    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // 更新用オブジェクト
    var src = {};
    if (issue.title !== title) { src.title = title; }
    if (issue.body !== body) { src.body = body; }

    async.series([
        // githubのissueの更新
        function (next) {
            if (token && project.github && project.github.sync) {
                (new GitHub(token)).updateDetailIssue(project.github.repoName, project.github.userName, issue, src, next);
            } else {
                next(null);
            }
        },
        // issue更新
        function (next) {
            _.extend(issue, src);
            project.save(function (err, data) {
                project = data;
                next(err);
            });
        }
    ], function (err) {
        if (err) { callback(err); return; }
        callback(err, project, issue);
    });
});

Projects.method('updateIssuePriority', function (issueId, toPriority, callback) {
    var project = this;

    var issue = _.find(project.issues, function (issue) {
        return String(issue._id) === String(issueId);
    });
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    var beforePriority = project.issues.indexOf(issue);
    var nextPriority = toPriority + (beforePriority >= toPriority ? 1 : 0);

    project.issues.splice(beforePriority, 1);
    project.issues.splice(nextPriority, 0, issue);

    project.save(function (err, project) {
        if (err) { callback(err); return; }
        callback(err, project, issue, toPriority);
    });
});

Projects.static('exists', function (where, callback) {
    this.count(where, function (err, count) {
        if (err) { callback(err); return; }
        callback(null, !!count);
    });
});

Projects.method('findIssue', function (predicate, callback) {
    var issue = _.find(this.issues, predicate);
    if (callback) { callback(null, issue); }
    return issue;
});

Projects.method('findIssueById', function (issueId, callback) {
    return this.findIssue(function (issue) {
        return String(issue._id) === String(issueId);
    }, callback);
});

Projects.method('findMember', function (predicate, callback) {
    var member = _.find(this.members, predicate);
    if (callback) { callback(null, member); }
    return member;
});

Projects.method('findMemberById', function (userId, callback) {
    return this.findMember(function (member) {
        return String(member.user) === String(userId);
    }, callback);
});

Projects.method('findMemberByName', function (userName, callback) {
    return this.findMember(function (member) {
        return String(member.user.userName) === String(userName);
    }, callback);
});

/*** helper ***/

function methodToStatic(methodName/* ,args..., callback */) {
    return function (projectWhere) {
        var callback = arguments[arguments.length - 1];
        var args = Array.prototype.splice.call(arguments, 1);

        this.findOne(projectWhere, function (err, project) {
            if (err) { callback(err); return; }
            if (!project) { callback(new Error('undefined project')); return; }

            project[methodName].apply(project, args);
        });
    };
}

/*** /helper **/

['addMember', 'removeMember', 'updateMember', 'addIssue', 'removeIssue', 'assign', 'updateStage',
    'updateIssueDetail', 'findIssue', 'findIssueById', 'updateIssuePriority'].forEach(function (methodName) {
    Projects.static(methodName, methodToStatic(methodName));
});

if (!mongoose.models.Project) { module.exports = mongoose.model('Project', Projects); }
else { module.exports = mongoose.model('Project'); }<|endoftext|>


<commit_before>'use strict';

let _ = require('lodash');
let Joi = require('joi');
let url = require('url');
let jose = require('node-jose');
let assert = require('assert');
let base64url = require('base64url');
let got = require('got');

let errors = require('../helpers/errors');

const KEY_TYPES = ['RSA', 'EC'];
const RECOGNIZED_METADATA = [
  'application_type',
  'client_id',
  'client_name',
  'client_secret',
  'client_secret_expires_at',
  'client_uri',
  'contacts',
  'default_acr_values',
  'default_max_age',
  'grant_types',
  'id_token_encrypted_response_alg',
  'id_token_encrypted_response_enc',
  'id_token_signed_response_alg',
  'initiate_login_uri',
  'jwks',
  'jwks_uri',
  'logo_uri',
  'policy_uri',
  'post_logout_redirect_uris',
  'redirect_uris',
  'registration_access_token',
  'request_object_encryption_alg',
  'request_object_encryption_enc',
  'request_object_signing_alg',
  'request_uris',
  'require_auth_time',
  'response_types',
  'sector_identifier_uri',
  'subject_type',
  'token_endpoint_auth_method',
  'token_endpoint_auth_signing_alg',
  'tos_uri',
  'userinfo_encrypted_response_alg',
  'userinfo_encrypted_response_enc',
  'userinfo_signed_response_alg',
];

// TODO: validate all

module.exports = function (provider) {

  let webUri = Joi.string().uri({
    scheme: ['http', 'https'],
  });

  let presenceDependant = function (field, value) {
    return function (client) {
      if (_.isUndefined(client[field])) {
        return undefined;
      }

      return value;
    };
  };

  let baseSchema = function () {
    let conf = provider.configuration;
    return Joi.object().keys({
      application_type: Joi.string().valid('web', 'native').default('web'),
      client_id: Joi.required(),
      client_name: Joi.string(),
      client_secret: Joi.required(),
      client_uri: webUri,
      contacts: Joi.array().items(Joi.string().email()),
      default_acr_values: Joi.array().items(Joi.string()),
      default_max_age: Joi.number().integer().positive().strict(),
      grant_types: Joi.array().min(1).items(conf.grantTypesSupported)
        .default(['authorization_code']),
      id_token_signed_response_alg: Joi.string().when('response_types', {
        is: Joi.array().items(Joi.string().regex(/token/).forbidden()),
        then: Joi.string().valid(
          conf.idTokenSigningAlgValuesSupported),
        otherwise: Joi.string().valid(_.without(
          conf.idTokenSigningAlgValuesSupported, 'none')),
      }).default('RS256'),
      initiate_login_uri: webUri,
      redirect_uris: Joi.array().min(1).items(
        Joi.string().regex(/#/).forbidden()).when('application_type', {
          is: 'web',
          then: Joi.array().items(webUri),
          otherwise: Joi.array().items(Joi.string().uri()),
        }).required(),
      jwks_uri: webUri,
      logo_uri: webUri,
      policy_uri: webUri,
      post_logout_redirect_uris: Joi.array().items(webUri).default([]),
      request_object_signing_alg: Joi.string(
        conf.requestObjectSigningAlgValuesSupported),
      require_auth_time: Joi.boolean().default(false),
      response_types: Joi.array().min(1).items(conf.responseTypesSupported)
        .default(['code']),
      sector_identifier_uri: Joi.string().uri({
        scheme: ['https'],
      }),
      subject_type: Joi.string().valid(conf.subjectTypesSupported)
        .default('public'),
      token_endpoint_auth_method: Joi.string().valid(
        conf.tokenEndpointAuthMethodsSupported).default('client_secret_basic'),
      tos_uri: webUri,
      userinfo_signed_response_alg: Joi.string().valid(
        conf.userinfoSigningAlgValuesSupported).default('none'),
      id_token_encrypted_response_alg: Joi.string()
        .valid(conf.idTokenEncryptionAlgValuesSupported).default(undefined),
      id_token_encrypted_response_enc: Joi.string()
        .valid(conf.idTokenEncryptionEncValuesSupported)
          .default(presenceDependant(
            'id_token_encrypted_response_alg', 'A128CBC-HS256'),
            'id_token_encrypted_response_alg dependant default'),
      userinfo_encrypted_response_alg: Joi.string()
        .valid(conf.userinfoEncryptionAlgValuesSupported).default(undefined),
      userinfo_encrypted_response_enc: Joi.string()
        .valid(conf.userinfoEncryptionEncValuesSupported)
          .default(presenceDependant(
            'userinfo_encrypted_response_alg', 'A128CBC-HS256'),
            'userinfo_encrypted_response_alg dependant default'),
    })
    .with('id_token_encrypted_response_enc', 'id_token_encrypted_response_alg')
    .with('userinfo_encrypted_response_enc', 'userinfo_encrypted_response_alg')
    .unknown();
  };

  let schemaValidate = function (metadata) {
    let schema = baseSchema();

    try {
      let signingAlg = metadata.request_object_signing_alg;
      let requireJwks =
        metadata.token_endpoint_auth_method === 'private_key_jwt' ||
        (signingAlg && signingAlg.startsWith('RS')) ||
        (signingAlg && signingAlg.startsWith('ES'));

      if (requireJwks) {
        schema = schema.or('jwks', 'jwks_uri');
      }

      metadata = Joi.attempt(_.chain(metadata).omitBy(_.isNull)
        .pick(RECOGNIZED_METADATA).value(), schema);

      let rts = _.chain(metadata.response_types).map(rt => rt.split(' '))
        .flatten().uniq().value();

      if (_.includes(rts, 'code')) {
        Joi.assert(metadata.grant_types,
          Joi.array().items(
            Joi.string().valid('authorization_code').required()));
      }

      if (_.includes(rts, 'token') || _.includes(rts, 'id_token')) {
        Joi.assert(metadata.grant_types,
          Joi.array().items(
            Joi.string().valid('implicit').required()));
      }

      if (metadata.subject_type === 'pairwise' &&
        !metadata.sector_identifier_uri) {

        let hosts = _.chain(metadata.redirect_uris).map((redirectUri) => {
          return url.parse(redirectUri).host;
        }).uniq().value();

        if (hosts.length === 1) {
          metadata.sector_identifier = hosts[0];
        } else {
          throw new Error('sector_identifier_uri is required when using ' +
            'multiple hosts in your redirect_uris');
        }
      } else if (metadata.sector_identifier_uri) {
        metadata.sector_identifier =
          url.parse(metadata.sector_identifier_uri).host;
      }

      return Promise.resolve(metadata);

    } catch (err) {
      let redirectUriError = err.details && err.details.length === 1 &&
        err.details[0].path.startsWith('redirect_uri');

      let message = _.map(err.details, (member) => {
        return `Validation error '${member.message}' in path '${member.path}'`;
      }).join('. ');

      return Promise.reject(new errors.InvalidClientMetadata(
        message || err.message, redirectUriError ?
          'invalid_redirect_uri' : undefined));
    }
  };

  let sectorValidate = function (metadata) {

    if (metadata.sector_identifier_uri !== undefined) {
      return got(metadata.sector_identifier_uri, {
        headers: {
          'User-Agent': provider.issuer,
        },
        json: true,
        timeout: 1500,
      }).then((response) => {
        try {
          assert(Array.isArray(response.body),
            'sector_identifier_uri must return single JSON array');
          let missing = metadata.redirect_uris.find((uri) => {
            return response.body.indexOf(uri) === -1;
          });
          assert(!missing,
            'all redirect_uris must be included in the sector_identifier_uri');
        } catch (err) {
          throw new errors.InvalidClientMetadata(err.message);
        }

        return metadata;
      }, (error) => {
        throw new errors.InvalidClientMetadata(
          `could not load sector_identifier_uri (${error.message})`);
      });
    } else {
      return metadata;
    }
  };

  let buildClient = function (metadata) {
    let client = new Client();

    Object.defineProperty(client, 'sectorIdentifier', {
      enumerable: false,
      writable: true,
    });

    Object.assign(client, _.mapKeys(metadata, (value, key) => {
      return _.camelCase(key);
    }));

    return client;
  };

  let buildKeyStore = function (client) {
    Object.defineProperty(client, 'keystore', {
      value: jose.JWK.createKeyStore(),
    });
    client.keystore.jwksUri = client.jwksUri;

    client.keystore.refresh = function () {
      if (!this.jwksUri) {
        return Promise.resolve();
      }

      return got(this.jwksUri, {
        headers: {
          'User-Agent': provider.issuer,
        },
        json: true,
        timeout: 1500,
      }).then((response) => {
        if (!Array.isArray(response.body.keys)) {
          throw new Error('invalid jwks_uri response');
        }

        let promises = [];

        response.body.keys.forEach((key) => {
          if (KEY_TYPES.indexOf(key.kty) !== -1 && !this.get(key.kid)) {
            promises.push(this.add(key));
          }
        });

        return Promise.all(promises);
      });
    };

    return client.keystore.add({
      k: base64url(new Buffer(client.clientSecret)),
      kid: 'clientSecret',
      kty: 'oct',
    }).then(() => {
      let promises = [];

      if (client.jwks) {
        client.jwks.keys.forEach((key) => {
          if (KEY_TYPES.indexOf(key.kty) !== -1) {
            promises.push(client.keystore.add(key));
          }
        });
      }

      return Promise.all(promises);
    }).then(() => client);
  };

  let register = function (client) {
    client.constructor.clients = client.constructor.clients || {};
    client.constructor.clients[client.clientId] = client;

    return client;
  };

  class Client {
    // (x) applicationType IN web,native
    // (x) grantTypes IN provider.configuration.grant_types_supported
    // (x) idTokenSignedResponseAlg IN
    //    provider.configuration.idTokenSignedResponseAlgSupported
    // (x) idTokenSignedResponseAlg MAY be 'none' if response_types does not
    //    contain any implicit or hybrid.
    // (x) requireAuthTime IN true/false
    // (x) responseTypes IN provider.configuration.responseTypesSupported
    // (x) tokenEndpointAuthMethod IN
    //    client_secret_basic,client_secret_jwt,
    //    client_secret_post,private_key_jwt
    // (x) userinfoSignedResponseAlg IN
    //    provider.configuration.userinfoSigningAlgValuesSupported
    // (x) redirectUris are uris (http/https), option for strict https only?,
    //    native localhost?
    // (x) redirectUri does not have fragment
    // (x) contacts are emails
    // (x) clientName is String
    // (x) logoUri, clientUri, policyUri, tosUri, jwksUri
    // (x) sectorIdentifierUri
    // (x) subjectType IN pairwise,public and is it supported?
    // (x) defaultMaxAge +number of seconds
    // (x) defaultAcrValues
    // ( ) initiateLoginUri

    responseTypeAllowed(type) {
      return this.responseTypes.indexOf(type) !== -1;
    }

    grantTypeAllowed(type) {
      return this.grantTypes.indexOf(type) !== -1;
    }

    redirectUriAllowed(uri) {
      return this.redirectUris.indexOf(uri) !== -1;
    }

    postLogoutRedirectUriAllowed(uri) {
      return this.postLogoutRedirectUris.indexOf(uri) !== -1;
    }

    static add(metadata) {
      return schemaValidate(metadata)
        .then(sectorValidate)
        .then(buildClient)
        .then(buildKeyStore)
        .then(register);
    }

    static find(id) {
      this.clients = this.clients || {};
      return this.clients[id];
    }

  }

  return Client;
};
<commit_msg>properly validate grant_types<commit_after>'use strict';

let _ = require('lodash');
let Joi = require('joi');
let url = require('url');
let jose = require('node-jose');
let assert = require('assert');
let base64url = require('base64url');
let got = require('got');

let errors = require('../helpers/errors');

const KEY_TYPES = ['RSA', 'EC'];
const RECOGNIZED_METADATA = [
  'application_type',
  'client_id',
  'client_name',
  'client_secret',
  'client_secret_expires_at',
  'client_uri',
  'contacts',
  'default_acr_values',
  'default_max_age',
  'grant_types',
  'id_token_encrypted_response_alg',
  'id_token_encrypted_response_enc',
  'id_token_signed_response_alg',
  'initiate_login_uri',
  'jwks',
  'jwks_uri',
  'logo_uri',
  'policy_uri',
  'post_logout_redirect_uris',
  'redirect_uris',
  'registration_access_token',
  'request_object_encryption_alg',
  'request_object_encryption_enc',
  'request_object_signing_alg',
  'request_uris',
  'require_auth_time',
  'response_types',
  'sector_identifier_uri',
  'subject_type',
  'token_endpoint_auth_method',
  'token_endpoint_auth_signing_alg',
  'tos_uri',
  'userinfo_encrypted_response_alg',
  'userinfo_encrypted_response_enc',
  'userinfo_signed_response_alg',
];

// TODO: validate all

module.exports = function (provider) {

  let webUri = Joi.string().uri({
    scheme: ['http', 'https'],
  });

  let presenceDependant = function (field, value) {
    return function (client) {
      if (_.isUndefined(client[field])) {
        return undefined;
      }

      return value;
    };
  };

  let baseSchema = function () {
    let conf = provider.configuration;
    return Joi.object().keys({
      application_type: Joi.string().valid('web', 'native').default('web'),
      client_id: Joi.required(),
      client_name: Joi.string(),
      client_secret: Joi.required(),
      client_uri: webUri,
      contacts: Joi.array().items(Joi.string().email()),
      default_acr_values: Joi.array().items(Joi.string()),
      default_max_age: Joi.number().integer().positive().strict(),
      grant_types: Joi.array().min(1).items(conf.grantTypesSupported)
        .default(['authorization_code']),
      id_token_signed_response_alg: Joi.string().when('response_types', {
        is: Joi.array().items(Joi.string().regex(/token/).forbidden()),
        then: Joi.string().valid(
          conf.idTokenSigningAlgValuesSupported),
        otherwise: Joi.string().valid(_.without(
          conf.idTokenSigningAlgValuesSupported, 'none')),
      }).default('RS256'),
      initiate_login_uri: webUri,
      redirect_uris: Joi.array().min(1).items(
        Joi.string().regex(/#/).forbidden()).when('application_type', {
          is: 'web',
          then: Joi.array().items(webUri),
          otherwise: Joi.array().items(Joi.string().uri()),
        }).required(),
      jwks_uri: webUri,
      logo_uri: webUri,
      policy_uri: webUri,
      post_logout_redirect_uris: Joi.array().items(webUri).default([]),
      request_object_signing_alg: Joi.string(
        conf.requestObjectSigningAlgValuesSupported),
      require_auth_time: Joi.boolean().default(false),
      response_types: Joi.array().min(1).items(conf.responseTypesSupported)
        .default(['code']),
      sector_identifier_uri: Joi.string().uri({
        scheme: ['https'],
      }),
      subject_type: Joi.string().valid(conf.subjectTypesSupported)
        .default('public'),
      token_endpoint_auth_method: Joi.string().valid(
        conf.tokenEndpointAuthMethodsSupported).default('client_secret_basic'),
      tos_uri: webUri,
      userinfo_signed_response_alg: Joi.string().valid(
        conf.userinfoSigningAlgValuesSupported).default('none'),
      id_token_encrypted_response_alg: Joi.string()
        .valid(conf.idTokenEncryptionAlgValuesSupported).default(undefined),
      id_token_encrypted_response_enc: Joi.string()
        .valid(conf.idTokenEncryptionEncValuesSupported)
          .default(presenceDependant(
            'id_token_encrypted_response_alg', 'A128CBC-HS256'),
            'id_token_encrypted_response_alg dependant default'),
      userinfo_encrypted_response_alg: Joi.string()
        .valid(conf.userinfoEncryptionAlgValuesSupported).default(undefined),
      userinfo_encrypted_response_enc: Joi.string()
        .valid(conf.userinfoEncryptionEncValuesSupported)
          .default(presenceDependant(
            'userinfo_encrypted_response_alg', 'A128CBC-HS256'),
            'userinfo_encrypted_response_alg dependant default'),
    })
    .with('id_token_encrypted_response_enc', 'id_token_encrypted_response_alg')
    .with('userinfo_encrypted_response_enc', 'userinfo_encrypted_response_alg')
    .unknown();
  };

  let schemaValidate = function (metadata) {
    let schema = baseSchema();

    try {
      let signingAlg = metadata.request_object_signing_alg;
      let requireJwks =
        metadata.token_endpoint_auth_method === 'private_key_jwt' ||
        (signingAlg && signingAlg.startsWith('RS')) ||
        (signingAlg && signingAlg.startsWith('ES'));

      if (requireJwks) {
        schema = schema.or('jwks', 'jwks_uri');
      }

      metadata = Joi.attempt(_.chain(metadata).omitBy(_.isNull)
        .pick(RECOGNIZED_METADATA).value(), schema);

      let rts = _.chain(metadata.response_types).map(rt => rt.split(' '))
        .flatten().uniq().value();

      if (_.includes(rts, 'code')) {
        Joi.assert(metadata.grant_types,
          Joi.array().items(
            Joi.string().valid('authorization_code').required(),
            Joi.string()));
      }

      if (_.includes(rts, 'token') || _.includes(rts, 'id_token')) {
        Joi.assert(metadata.grant_types,
          Joi.array().items(
            Joi.string().valid('implicit').required(),
            Joi.string()));
      }

      if (metadata.subject_type === 'pairwise' &&
        !metadata.sector_identifier_uri) {

        let hosts = _.chain(metadata.redirect_uris).map((redirectUri) => {
          return url.parse(redirectUri).host;
        }).uniq().value();

        if (hosts.length === 1) {
          metadata.sector_identifier = hosts[0];
        } else {
          throw new Error('sector_identifier_uri is required when using ' +
            'multiple hosts in your redirect_uris');
        }
      } else if (metadata.sector_identifier_uri) {
        metadata.sector_identifier =
          url.parse(metadata.sector_identifier_uri).host;
      }

      return Promise.resolve(metadata);

    } catch (err) {
      let redirectUriError = err.details && err.details.length === 1 &&
        err.details[0].path.startsWith('redirect_uri');

      let message = _.map(err.details, (member) => {
        return `Validation error '${member.message}' in path '${member.path}'`;
      }).join('. ');

      return Promise.reject(new errors.InvalidClientMetadata(
        message || err.message, redirectUriError ?
          'invalid_redirect_uri' : undefined));
    }
  };

  let sectorValidate = function (metadata) {

    if (metadata.sector_identifier_uri !== undefined) {
      return got(metadata.sector_identifier_uri, {
        headers: {
          'User-Agent': provider.issuer,
        },
        json: true,
        timeout: 1500,
      }).then((response) => {
        try {
          assert(Array.isArray(response.body),
            'sector_identifier_uri must return single JSON array');
          let missing = metadata.redirect_uris.find((uri) => {
            return response.body.indexOf(uri) === -1;
          });
          assert(!missing,
            'all redirect_uris must be included in the sector_identifier_uri');
        } catch (err) {
          throw new errors.InvalidClientMetadata(err.message);
        }

        return metadata;
      }, (error) => {
        throw new errors.InvalidClientMetadata(
          `could not load sector_identifier_uri (${error.message})`);
      });
    } else {
      return metadata;
    }
  };

  let buildClient = function (metadata) {
    let client = new Client();

    Object.defineProperty(client, 'sectorIdentifier', {
      enumerable: false,
      writable: true,
    });

    Object.assign(client, _.mapKeys(metadata, (value, key) => {
      return _.camelCase(key);
    }));

    return client;
  };

  let buildKeyStore = function (client) {
    Object.defineProperty(client, 'keystore', {
      value: jose.JWK.createKeyStore(),
    });
    client.keystore.jwksUri = client.jwksUri;

    client.keystore.refresh = function () {
      if (!this.jwksUri) {
        return Promise.resolve();
      }

      return got(this.jwksUri, {
        headers: {
          'User-Agent': provider.issuer,
        },
        json: true,
        timeout: 1500,
      }).then((response) => {
        if (!Array.isArray(response.body.keys)) {
          throw new Error('invalid jwks_uri response');
        }

        let promises = [];

        response.body.keys.forEach((key) => {
          if (KEY_TYPES.indexOf(key.kty) !== -1 && !this.get(key.kid)) {
            promises.push(this.add(key));
          }
        });

        return Promise.all(promises);
      });
    };

    return client.keystore.add({
      k: base64url(new Buffer(client.clientSecret)),
      kid: 'clientSecret',
      kty: 'oct',
    }).then(() => {
      let promises = [];

      if (client.jwks) {
        client.jwks.keys.forEach((key) => {
          if (KEY_TYPES.indexOf(key.kty) !== -1) {
            promises.push(client.keystore.add(key));
          }
        });
      }

      return Promise.all(promises);
    }).then(() => client);
  };

  let register = function (client) {
    client.constructor.clients = client.constructor.clients || {};
    client.constructor.clients[client.clientId] = client;

    return client;
  };

  class Client {
    // (x) applicationType IN web,native
    // (x) grantTypes IN provider.configuration.grant_types_supported
    // (x) idTokenSignedResponseAlg IN
    //    provider.configuration.idTokenSignedResponseAlgSupported
    // (x) idTokenSignedResponseAlg MAY be 'none' if response_types does not
    //    contain any implicit or hybrid.
    // (x) requireAuthTime IN true/false
    // (x) responseTypes IN provider.configuration.responseTypesSupported
    // (x) tokenEndpointAuthMethod IN
    //    client_secret_basic,client_secret_jwt,
    //    client_secret_post,private_key_jwt
    // (x) userinfoSignedResponseAlg IN
    //    provider.configuration.userinfoSigningAlgValuesSupported
    // (x) redirectUris are uris (http/https), option for strict https only?,
    //    native localhost?
    // (x) redirectUri does not have fragment
    // (x) contacts are emails
    // (x) clientName is String
    // (x) logoUri, clientUri, policyUri, tosUri, jwksUri
    // (x) sectorIdentifierUri
    // (x) subjectType IN pairwise,public and is it supported?
    // (x) defaultMaxAge +number of seconds
    // (x) defaultAcrValues
    // ( ) initiateLoginUri

    responseTypeAllowed(type) {
      return this.responseTypes.indexOf(type) !== -1;
    }

    grantTypeAllowed(type) {
      return this.grantTypes.indexOf(type) !== -1;
    }

    redirectUriAllowed(uri) {
      return this.redirectUris.indexOf(uri) !== -1;
    }

    postLogoutRedirectUriAllowed(uri) {
      return this.postLogoutRedirectUris.indexOf(uri) !== -1;
    }

    static add(metadata) {
      return schemaValidate(metadata)
        .then(sectorValidate)
        .then(buildClient)
        .then(buildKeyStore)
        .then(register);
    }

    static find(id) {
      this.clients = this.clients || {};
      return this.clients[id];
    }

  }

  return Client;
};
<|endoftext|>


<commit_before>// @flow

import escapeStringRegExp from 'escape-string-regexp';
import { meta } from '../../hydration';

import type { HooksTree } from 'src/backend/types';

export function createRegExp(string: string): RegExp {
  // Allow /regex/ syntax with optional last /
  if (string[0] === '/') {
    // Cut off first slash
    string = string.substring(1);
    // Cut off last slash, but only if it's there
    if (string[string.length - 1] === '/') {
      string = string.substring(0, string.length - 1);
    }
    try {
      return new RegExp(string, 'i');
    } catch (err) {
      // Bad regex. Make it not match anything.
      // TODO: maybe warn in console?
      return new RegExp('.^');
    }
  }

  function isLetter(char: string) {
    return char.toLowerCase() !== char.toUpperCase();
  }

  function matchAnyCase(char: string) {
    if (!isLetter(char)) {
      // Don't mess with special characters like [.
      return char;
    }
    return '[' + char.toLowerCase() + char.toUpperCase() + ']';
  }

  // 'item' should match 'Item' and 'ListItem', but not 'InviteMom'.
  // To do this, we'll slice off 'tem' and check first letter separately.
  const escaped = escapeStringRegExp(string);
  const firstChar = escaped[0];
  let restRegex = '';
  // For 'item' input, restRegex becomes '[tT][eE][mM]'
  // We can't simply make it case-insensitive because first letter case matters.
  for (let i = 1; i < escaped.length; i++) {
    restRegex += matchAnyCase(escaped[i]);
  }

  if (!isLetter(firstChar)) {
    // We can't put a non-character like [ in a group
    // so we fall back to the simple case.
    return new RegExp(firstChar + restRegex);
  }

  // Construct a smarter regex.
  return new RegExp(
    // For example:
    // (^[iI]|I)[tT][eE][mM]
    // Matches:
    // 'Item'
    // 'ListItem'
    // but not 'InviteMom'
    '(^' +
      matchAnyCase(firstChar) +
      '|' +
      firstChar.toUpperCase() +
      ')' +
      restRegex
  );
}

export function getMetaValueLabel(data: Object): string | null {
  switch (data[meta.type]) {
    case 'react_element':
      return `<${data[meta.name]} />`;
    case 'function':
      return `${data[meta.name] || 'fn'}()`;
    case 'object':
      return 'Object';
    case 'date':
    case 'symbol':
      return data[meta.name];
    case 'iterator':
      return `${data[meta.name]}(…)`;
    case 'array_buffer':
    case 'data_view':
    case 'array':
    case 'typed_array':
      return `${data[meta.name]}[${data[meta.meta].length}]`;
    default:
      return null;
  }
}

function sanitize(data: Object): void {
  for (const key in data) {
    const value = data[key];

    if (value && value[meta.type]) {
      data[key] = getMetaValueLabel(value);
    } else if (value != null) {
      if (Array.isArray(value)) {
        sanitize(value);
      } else if (typeof value === 'object') {
        sanitize(value);
      }
    }
  }
}

export function serializeDataForCopy(props: Object): string {
  const cloned = Object.assign({}, props);

  sanitize(cloned);

  try {
    return JSON.stringify(cloned, null, 2);
  } catch (error) {
    return '';
  }
}

export function serializeHooksForCopy(hooks: HooksTree | null): string {
  // $FlowFixMe "HooksTree is not an object"
  const cloned = Object.assign([], hooks);

  const queue = [...cloned];

  while (queue.length > 0) {
    const current = queue.pop();

    // These aren't meaningful
    delete current.id;
    delete current.isStateEditable;

    if (current.subHooks.length > 0) {
      queue.push(...current.subHooks);
    }
  }

  sanitize(cloned);

  try {
    return JSON.stringify(cloned, null, 2);
  } catch (error) {
    return '';
  }
}

export function downloadFile(filename: string, text: string): void {
  const element = document.createElement('a');
  element.setAttribute(
    'href',
    'data:text/plain;charset=utf-8,' + encodeURIComponent(text)
  );
  element.setAttribute('download', filename);

  element.style.display = 'none';
  ((document.body: any): HTMLBodyElement).appendChild(element);

  element.click();

  ((document.body: any): HTMLBodyElement).removeChild(element);
}
<commit_msg>Changed export code to (hopefully) support larger exports<commit_after>// @flow

import escapeStringRegExp from 'escape-string-regexp';
import { meta } from '../../hydration';

import type { HooksTree } from 'src/backend/types';

export function createRegExp(string: string): RegExp {
  // Allow /regex/ syntax with optional last /
  if (string[0] === '/') {
    // Cut off first slash
    string = string.substring(1);
    // Cut off last slash, but only if it's there
    if (string[string.length - 1] === '/') {
      string = string.substring(0, string.length - 1);
    }
    try {
      return new RegExp(string, 'i');
    } catch (err) {
      // Bad regex. Make it not match anything.
      // TODO: maybe warn in console?
      return new RegExp('.^');
    }
  }

  function isLetter(char: string) {
    return char.toLowerCase() !== char.toUpperCase();
  }

  function matchAnyCase(char: string) {
    if (!isLetter(char)) {
      // Don't mess with special characters like [.
      return char;
    }
    return '[' + char.toLowerCase() + char.toUpperCase() + ']';
  }

  // 'item' should match 'Item' and 'ListItem', but not 'InviteMom'.
  // To do this, we'll slice off 'tem' and check first letter separately.
  const escaped = escapeStringRegExp(string);
  const firstChar = escaped[0];
  let restRegex = '';
  // For 'item' input, restRegex becomes '[tT][eE][mM]'
  // We can't simply make it case-insensitive because first letter case matters.
  for (let i = 1; i < escaped.length; i++) {
    restRegex += matchAnyCase(escaped[i]);
  }

  if (!isLetter(firstChar)) {
    // We can't put a non-character like [ in a group
    // so we fall back to the simple case.
    return new RegExp(firstChar + restRegex);
  }

  // Construct a smarter regex.
  return new RegExp(
    // For example:
    // (^[iI]|I)[tT][eE][mM]
    // Matches:
    // 'Item'
    // 'ListItem'
    // but not 'InviteMom'
    '(^' +
      matchAnyCase(firstChar) +
      '|' +
      firstChar.toUpperCase() +
      ')' +
      restRegex
  );
}

export function getMetaValueLabel(data: Object): string | null {
  switch (data[meta.type]) {
    case 'react_element':
      return `<${data[meta.name]} />`;
    case 'function':
      return `${data[meta.name] || 'fn'}()`;
    case 'object':
      return 'Object';
    case 'date':
    case 'symbol':
      return data[meta.name];
    case 'iterator':
      return `${data[meta.name]}(…)`;
    case 'array_buffer':
    case 'data_view':
    case 'array':
    case 'typed_array':
      return `${data[meta.name]}[${data[meta.meta].length}]`;
    default:
      return null;
  }
}

function sanitize(data: Object): void {
  for (const key in data) {
    const value = data[key];

    if (value && value[meta.type]) {
      data[key] = getMetaValueLabel(value);
    } else if (value != null) {
      if (Array.isArray(value)) {
        sanitize(value);
      } else if (typeof value === 'object') {
        sanitize(value);
      }
    }
  }
}

export function serializeDataForCopy(props: Object): string {
  const cloned = Object.assign({}, props);

  sanitize(cloned);

  try {
    return JSON.stringify(cloned, null, 2);
  } catch (error) {
    return '';
  }
}

export function serializeHooksForCopy(hooks: HooksTree | null): string {
  // $FlowFixMe "HooksTree is not an object"
  const cloned = Object.assign([], hooks);

  const queue = [...cloned];

  while (queue.length > 0) {
    const current = queue.pop();

    // These aren't meaningful
    delete current.id;
    delete current.isStateEditable;

    if (current.subHooks.length > 0) {
      queue.push(...current.subHooks);
    }
  }

  sanitize(cloned);

  try {
    return JSON.stringify(cloned, null, 2);
  } catch (error) {
    return '';
  }
}

// Keeping this in memory seems to be enough to enable the browser to download larger profiles.
// Without this, we would see a "Download failed: network error" failure.
let downloadUrl = null;

export function downloadFile(filename: string, text: string): void {
  const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });

  if (downloadUrl !== null) {
    URL.revokeObjectURL(downloadUrl);
  }

  downloadUrl = URL.createObjectURL(blob);

  const element = document.createElement('a');
  element.setAttribute('href', downloadUrl);
  element.setAttribute('download', filename);
  element.style.display = 'none';
  ((document.body: any): HTMLBodyElement).appendChild(element);

  element.click();

  ((document.body: any): HTMLBodyElement).removeChild(element);
}
<|endoftext|>


<commit_before>/* global cozy */

import konnectors from './konnectors'
import AUTHORIZED_CATEGORIES from 'config/categories'

// constant
const FETCH_REGISTRY_KONNECTORS = 'FETCH_REGISTRY_KONNECTORS'
const FETCH_REGISTRY_KONNECTORS_ERROR = 'FETCH_REGISTRY_KONNECTORS_ERROR'
const FETCH_REGISTRY_KONNECTORS_SUCCESS = 'FETCH_REGISTRY_KONNECTORS_SUCCESS'

export const OTHERS_CATEGORY = 'others'

// helpers
const getUncategorizedKonnectors = (konnectors, categories) =>
  konnectors.filter(
    konnector =>
      !konnector.categories.length ||
      (konnector.categories &&
        !konnector.categories.every(category => categories.includes(category)))
  )

const sanitizeKonnector = konnector => ({
  categories: [],
  ...konnector
})

// reducers
const reducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_REGISTRY_KONNECTORS:
    case FETCH_REGISTRY_KONNECTORS_SUCCESS:
      return {
        konnectors: konnectors(state.konnectors, action)
      }
    default:
      return state
  }
}

export default reducer

// selectors
export const getRegistryKonnectors = state =>
  Object.values(state.konnectors.data) || []

export const getRegistryKonnectorsByCategory = (state, category) => {
  const stateKonnectors = getRegistryKonnectors(state)
  return category
    ? category === OTHERS_CATEGORY
      ? getUncategorizedKonnectors(stateKonnectors, AUTHORIZED_CATEGORIES)
      : stateKonnectors.filter(konnector =>
          konnector.categories.includes(category)
        )
    : stateKonnectors
}

export const getRegistryKonnectorsByDataType = (state, dataType) =>
  dataType
    ? Object.values(state.konnectors.data).filter(
        konnector =>
          Array.isArray(konnector.dataType) &&
          konnector.dataType.includes(dataType)
      )
    : []

export const getRegistryKonnectorsFromSlugs = (state, slugs = []) => {
  return slugs.reduce((returnedKonnectors, slug) => {
    if (
      state.konnectors &&
      state.konnectors.data &&
      state.konnectors.data[slug]
    ) {
      return returnedKonnectors.concat([state.konnectors.data[slug]])
    }
    return returnedKonnectors
  }, [])
}

export const getRegistryKonnector = (state, slug) => {
  return state.konnectors.data[slug]
}

export const getSlugs = state => {
  return Object.keys(state.konnectors.data)
}

export const isFetchingRegistryKonnector = state => {
  return state.konnectors.fetchStatus === 'loading'
}

// action creators sync

export const initializeRegistry = konnectors => {
  return (dispatch, getState) => {
    dispatch({ type: FETCH_REGISTRY_KONNECTORS })
    return cozy.client
      .fetchJSON('GET', '/settings/context')
      .catch(error => {
        console.warn &&
          console.warn(`Cannot get Cozy context : ${error.message}`)
        return {}
      })
      .then(context => {
        const konnectorsToExclude =
          !!context.attributes && context.attributes.exclude_konnectors
        const filteredKonnectors =
          context.attributes && context.attributes.debug
            ? konnectors
            : konnectors.filter(
                k => k.slug !== 'creditagricole' && k.slug !== 'debug'
              )

        if (konnectorsToExclude && konnectorsToExclude.length) {
          return dispatch({
            type: FETCH_REGISTRY_KONNECTORS_SUCCESS,
            konnectors: filteredKonnectors.filter(
              k => !konnectorsToExclude.includes(k.slug)
            )
          })
        }
        return dispatch({
          type: FETCH_REGISTRY_KONNECTORS_SUCCESS,
          konnectors: filteredKonnectors.map(sanitizeKonnector)
        })
      })
      .catch(() => {
        dispatch({ type: FETCH_REGISTRY_KONNECTORS_ERROR, konnectors: [] })
      })
  }
}
<commit_msg>feat: adds baclays and enedis konnectors to filterd list 📝<commit_after>/* global cozy */

import konnectors from './konnectors'
import AUTHORIZED_CATEGORIES from 'config/categories'

// constant
const FETCH_REGISTRY_KONNECTORS = 'FETCH_REGISTRY_KONNECTORS'
const FETCH_REGISTRY_KONNECTORS_ERROR = 'FETCH_REGISTRY_KONNECTORS_ERROR'
const FETCH_REGISTRY_KONNECTORS_SUCCESS = 'FETCH_REGISTRY_KONNECTORS_SUCCESS'

export const OTHERS_CATEGORY = 'others'

// helpers
const getUncategorizedKonnectors = (konnectors, categories) =>
  konnectors.filter(
    konnector =>
      !konnector.categories.length ||
      (konnector.categories &&
        !konnector.categories.every(category => categories.includes(category)))
  )

const sanitizeKonnector = konnector => ({
  categories: [],
  ...konnector
})

// reducers
const reducer = (state = {}, action) => {
  switch (action.type) {
    case FETCH_REGISTRY_KONNECTORS:
    case FETCH_REGISTRY_KONNECTORS_SUCCESS:
      return {
        konnectors: konnectors(state.konnectors, action)
      }
    default:
      return state
  }
}

export default reducer

// selectors
export const getRegistryKonnectors = state =>
  Object.values(state.konnectors.data) || []

export const getRegistryKonnectorsByCategory = (state, category) => {
  const stateKonnectors = getRegistryKonnectors(state)
  return category
    ? category === OTHERS_CATEGORY
      ? getUncategorizedKonnectors(stateKonnectors, AUTHORIZED_CATEGORIES)
      : stateKonnectors.filter(konnector =>
          konnector.categories.includes(category)
        )
    : stateKonnectors
}

export const getRegistryKonnectorsByDataType = (state, dataType) =>
  dataType
    ? Object.values(state.konnectors.data).filter(
        konnector =>
          Array.isArray(konnector.dataType) &&
          konnector.dataType.includes(dataType)
      )
    : []

export const getRegistryKonnectorsFromSlugs = (state, slugs = []) => {
  return slugs.reduce((returnedKonnectors, slug) => {
    if (
      state.konnectors &&
      state.konnectors.data &&
      state.konnectors.data[slug]
    ) {
      return returnedKonnectors.concat([state.konnectors.data[slug]])
    }
    return returnedKonnectors
  }, [])
}

export const getRegistryKonnector = (state, slug) => {
  return state.konnectors.data[slug]
}

export const getSlugs = state => {
  return Object.keys(state.konnectors.data)
}

export const isFetchingRegistryKonnector = state => {
  return state.konnectors.fetchStatus === 'loading'
}

// action creators sync

export const initializeRegistry = konnectors => {
  return (dispatch, getState) => {
    dispatch({ type: FETCH_REGISTRY_KONNECTORS })
    return cozy.client
      .fetchJSON('GET', '/settings/context')
      .catch(error => {
        console.warn &&
          console.warn(`Cannot get Cozy context : ${error.message}`)
        return {}
      })
      .then(context => {
        const konnectorsToExclude =
          !!context.attributes && context.attributes.exclude_konnectors
        const filteredKonnectors =
          context.attributes && context.attributes.debug
            ? konnectors
            : konnectors.filter(
                k =>
                  ![
                    'barclays136',
                    'creditagricole',
                    'debug',
                    'enedis'
                  ].includes(k.slug)
              )

        if (konnectorsToExclude && konnectorsToExclude.length) {
          return dispatch({
            type: FETCH_REGISTRY_KONNECTORS_SUCCESS,
            konnectors: filteredKonnectors.filter(
              k => !konnectorsToExclude.includes(k.slug)
            )
          })
        }
        return dispatch({
          type: FETCH_REGISTRY_KONNECTORS_SUCCESS,
          konnectors: filteredKonnectors.map(sanitizeKonnector)
        })
      })
      .catch(() => {
        dispatch({ type: FETCH_REGISTRY_KONNECTORS_ERROR, konnectors: [] })
      })
  }
}
<|endoftext|>


<commit_before>[
	'bson/bson', 'bson/collections', 'bson/binary_parser',

	'commands/base_command', 'commands/db_command', 'commands/delete_command',
	'commands/get_more_command', 'commands/insert_command', 'commands/kill_cursor_command',
	'commands/query_command', 'commands/update_command',

	'responses/mongo_reply',

	'admin', 'collection', 'connection', 'cursor', 'db',

	'goog/math/integer', 'goog/math/long',
	'gridfs/chunk', 'gridfs/gridstore'
].forEach(function(path){
	var module = require('./mongodb/' + path);
	for (var i in module)
		exports[i] = module[i];
});<commit_msg>Remove path prefix.<commit_after>[
	'bson/bson', 'bson/collections', 'bson/binary_parser',

	'commands/base_command', 'commands/db_command', 'commands/delete_command',
	'commands/get_more_command', 'commands/insert_command', 'commands/kill_cursor_command',
	'commands/query_command', 'commands/update_command',

	'responses/mongo_reply',

	'admin', 'collection', 'connection', 'cursor', 'db',

	'goog/math/integer', 'goog/math/long',
	'gridfs/chunk', 'gridfs/gridstore'
].forEach(function(path){
	var module = require('./' + path);
	for (var i in module)
		exports[i] = module[i];
});
<|endoftext|>


<commit_before>const createHandler = require('./createHandler')
const readTargetFile = require('./readTargetFile')

module.exports = (name, begin, end = begin) => createHandler(file => {
  file = readTargetFile(file)

  if (file.targetFile.exist) {
    let beginIndex = file.targetFile.content.indexOf(begin)
    beginIndex += begin.length
    const endIndex = file.targetFile.content.indexOf(end, beginIndex)

    if (beginIndex && endIndex) {
      file.view.custom[name] = file.targetFile.content.substring(beginIndex, endIndex)
      console.log(file.view.custom)

    }
  }

  return file
})
<commit_msg>style: useless log<commit_after>const createHandler = require('./createHandler')
const readTargetFile = require('./readTargetFile')

module.exports = (name, begin, end = begin) => createHandler(file => {
  file = readTargetFile(file)

  if (file.targetFile.exist) {
    let beginIndex = file.targetFile.content.indexOf(begin)
    beginIndex += begin.length
    const endIndex = file.targetFile.content.indexOf(end, beginIndex)

    if (beginIndex && endIndex) {
      file.view.custom[name] = file.targetFile.content.substring(beginIndex, endIndex)
    }
  }

  return file
})
<|endoftext|>


<commit_before>var Layout = function(table){
	this.table = table;
	this.mode = null;
};

//initialize layout system
Layout.prototype.initialize = function(layout){

	if(this.modes[layout]){
		this.mode = layout;
	}else{
		console.warn("Layout Error - invalid mode set, defaulting to 'fitData' : " + layout);
		this.mode = 'fitData';
	}

	this.table.element.attr("tabulator-layout", this.mode);
};

Layout.prototype.getMode = function(){
	return this.mode;
};

//trigger table layout
Layout.prototype.layout = function(){
	this.modes[this.mode].call(this, this.table.columnManager.columnsByIndex);
};

//layout render functions
Layout.prototype.modes = {

	//resize columns to fit data the contain
	"fitData": function(columns){
		columns.forEach(function(column){
			column.reinitializeWidth();
		});

		if(this.table.options.responsiveLayout && this.table.extExists("responsiveLayout", true)){
			this.table.extensions.responsiveLayout.update();
		}
	},

	//resize columns to fit data the contain
	"fitDataFill": function(columns){
		columns.forEach(function(column){
			column.reinitializeWidth();
		});

		if(this.table.options.responsiveLayout && this.table.extExists("responsiveLayout", true)){
			this.table.extensions.responsiveLayout.update();
		}
	},

	//resize columns to fit
	"fitColumns": function(columns){
		var self = this;

		var totalWidth = self.table.element.innerWidth(); //table element width
		var fixedWidth = 0; //total width of columns with a defined width
		var flexWidth = 0; //total width available to flexible columns
		var flexColWidth = 0; //desired width of flexible columns
		var flexColumns = []; //array of flexible width columns
		var gapFill=0; //number of pixels to be added to final column to close and half pixel gaps

		//ensure columns resize to take up the correct amount of space
		function scaleColumns(columns, freeSpace, colWidth){

			var oversizeCols = [],
			oversizeSpace = 0,
			remainingSpace = 0,
			nextColWidth = 0,
			gap = 0,
			undersizeCols = [];

			columns.forEach(function(column, i){
				if(column.minWidth >= colWidth){
					oversizeCols.push(column);
				}else{
					undersizeCols.push(column);
				}
			});

			if(oversizeCols.length){
				oversizeCols.forEach(function(column){
					oversizeSpace += column.minWidth;
					column.setWidth(column.minWidth);
				});

				remainingSpace = freeSpace - oversizeSpace;

				nextColWidth = undersizeCols.length ? Math.floor(remainingSpace/undersizeCols.length) : remainingSpace;

				gap = remainingSpace - (nextColWidth * undersizeCols.length);

				gap += scaleColumns(undersizeCols, remainingSpace, nextColWidth);
			}else{
				gap = undersizeCols.length ? freeSpace - (Math.floor(freeSpace/undersizeCols.length) * undersizeCols.length) : freeSpace;

				undersizeCols.forEach(function(column){
					column.setWidth(colWidth);
				});
			}

			return gap;

		}

		if(this.table.options.responsiveLayout && this.table.extExists("responsiveLayout", true)){
			this.table.extensions.responsiveLayout.update();
		}

		//adjust for vertical scrollbar if present
		if(this.table.rowManager.element[0].scrollHeight > this.table.rowManager.element.innerHeight()){
			totalWidth -= this.table.rowManager.element[0].offsetWidth - this.table.rowManager.element[0].clientWidth;
		}

		columns.forEach(function(column){
			var width, minWidth, colWidth;

			if(column.visible){

				width = column.definition.width;
				minWidth =  parseInt(column.minWidth);

				if(width){

					if(typeof(width) == "string"){
						if(width.indexOf("%") > -1){
							colWidth = (totalWidth / 100) * parseInt(width);
						}else{
							colWidth = parseInt(width);
						}
					}else{
						colWidth = width;
					}

					fixedWidth += colWidth > minWidth ? colWidth : minWidth;

				}else{
					flexColumns.push(column);
				}
			}
		});

		//calculate available space
		flexWidth = totalWidth - fixedWidth;

		//calculate correct column size
		flexColWidth = Math.floor(flexWidth / flexColumns.length)

		//generate column widths
		var gapFill = scaleColumns(flexColumns, flexWidth, flexColWidth);

		//increase width of last column to account for rounding errors
		if(flexColumns.length){
			flexColumns[flexColumns.length-1].setWidth(flexColumns[flexColumns.length-1].getWidth() + gapFill);
		}
	},
};

Tabulator.registerExtension("layout", Layout);<commit_msg>remove empty line<commit_after>var Layout = function(table){
	this.table = table;
	this.mode = null;
};

//initialize layout system
Layout.prototype.initialize = function(layout){

	if(this.modes[layout]){
		this.mode = layout;
	}else{
		console.warn("Layout Error - invalid mode set, defaulting to 'fitData' : " + layout);
		this.mode = 'fitData';
	}

	this.table.element.attr("tabulator-layout", this.mode);
};

Layout.prototype.getMode = function(){
	return this.mode;
};

//trigger table layout
Layout.prototype.layout = function(){
	this.modes[this.mode].call(this, this.table.columnManager.columnsByIndex);
};

//layout render functions
Layout.prototype.modes = {

	//resize columns to fit data the contain
	"fitData": function(columns){
		columns.forEach(function(column){
			column.reinitializeWidth();
		});

		if(this.table.options.responsiveLayout && this.table.extExists("responsiveLayout", true)){
			this.table.extensions.responsiveLayout.update();
		}
	},

	//resize columns to fit data the contain
	"fitDataFill": function(columns){
		columns.forEach(function(column){
			column.reinitializeWidth();
		});

		if(this.table.options.responsiveLayout && this.table.extExists("responsiveLayout", true)){
			this.table.extensions.responsiveLayout.update();
		}
	},

	//resize columns to fit
	"fitColumns": function(columns){
		var self = this;

		var totalWidth = self.table.element.innerWidth(); //table element width
		var fixedWidth = 0; //total width of columns with a defined width
		var flexWidth = 0; //total width available to flexible columns
		var flexColWidth = 0; //desired width of flexible columns
		var flexColumns = []; //array of flexible width columns
		var gapFill=0; //number of pixels to be added to final column to close and half pixel gaps

		//ensure columns resize to take up the correct amount of space
		function scaleColumns(columns, freeSpace, colWidth){

			var oversizeCols = [],
			oversizeSpace = 0,
			remainingSpace = 0,
			nextColWidth = 0,
			gap = 0,
			undersizeCols = [];

			columns.forEach(function(column, i){
				if(column.minWidth >= colWidth){
					oversizeCols.push(column);
				}else{
					undersizeCols.push(column);
				}
			});

			if(oversizeCols.length){
				oversizeCols.forEach(function(column){
					oversizeSpace += column.minWidth;
					column.setWidth(column.minWidth);
				});

				remainingSpace = freeSpace - oversizeSpace;

				nextColWidth = undersizeCols.length ? Math.floor(remainingSpace/undersizeCols.length) : remainingSpace;

				gap = remainingSpace - (nextColWidth * undersizeCols.length);

				gap += scaleColumns(undersizeCols, remainingSpace, nextColWidth);
			}else{
				gap = undersizeCols.length ? freeSpace - (Math.floor(freeSpace/undersizeCols.length) * undersizeCols.length) : freeSpace;

				undersizeCols.forEach(function(column){
					column.setWidth(colWidth);
				});
			}

			return gap;
		}

		if(this.table.options.responsiveLayout && this.table.extExists("responsiveLayout", true)){
			this.table.extensions.responsiveLayout.update();
		}

		//adjust for vertical scrollbar if present
		if(this.table.rowManager.element[0].scrollHeight > this.table.rowManager.element.innerHeight()){
			totalWidth -= this.table.rowManager.element[0].offsetWidth - this.table.rowManager.element[0].clientWidth;
		}

		columns.forEach(function(column){
			var width, minWidth, colWidth;

			if(column.visible){

				width = column.definition.width;
				minWidth =  parseInt(column.minWidth);

				if(width){

					if(typeof(width) == "string"){
						if(width.indexOf("%") > -1){
							colWidth = (totalWidth / 100) * parseInt(width);
						}else{
							colWidth = parseInt(width);
						}
					}else{
						colWidth = width;
					}

					fixedWidth += colWidth > minWidth ? colWidth : minWidth;

				}else{
					flexColumns.push(column);
				}
			}
		});

		//calculate available space
		flexWidth = totalWidth - fixedWidth;

		//calculate correct column size
		flexColWidth = Math.floor(flexWidth / flexColumns.length)

		//generate column widths
		var gapFill = scaleColumns(flexColumns, flexWidth, flexColWidth);

		//increase width of last column to account for rounding errors
		if(flexColumns.length){
			flexColumns[flexColumns.length-1].setWidth(flexColumns[flexColumns.length-1].getWidth() + gapFill);
		}
	},
};

Tabulator.registerExtension("layout", Layout);<|endoftext|>


<commit_before>/* See LICENSE file for terms of use */
'use strict';

/* Node module requirement */
let fs = require('fs');
let path = require('path');
let JSON5 = require('json5');
let stringify = require('json-stringify-pretty-compact');

let override = require('../util/override');

/* CONSTANT, Configuration, Global variable */
let mtime = 0;

// Modeled off of (v0.6.18 link; check latest too):
// Origin source: https://github.com/joyent/node/blob/v0.6.18/lib/module.js
//
// given a module name, and a list of paths to test, returns the first
// matching file in the following precedence.
//
// require("a.<ext>")
//   -> a.<ext>
//
// require("a")
//   -> a
//   -> a.<ext>
//   -> a/index.<ext>

function statPath(path) {
  try {
    return fs.statSync(path);
  } catch (ex) {}
  return false;
}

// check if the file exists and is not a directory
function tryFile (requestPath) {
  let stats = statPath(requestPath);
  if (stats && !stats.isDirectory()) {
    return fs.realpathSync(requestPath);
  }
  return false;
}

// given a path check a the file exists with any of the set extensions
function tryExtensions (p, exts) {
  for (let i = 0, EL = exts.length; i < EL; i++) {
    let filename = tryFile(p + exts[i]);

    if (filename) {
      return filename;
    }
  }
  return false;
}

let getArgs = function (argsStr) {
  let result = [];

  let idx;

  // get fname
  let fn = '';
  for (idx = 1; idx < argsStr.length; idx++) {
    let char = argsStr.charAt(idx);
    if (char == '(')
      break;
    fn += char;
  }
  result.push(fn);

  // get args
  let args = '';
  let degree = 0;
  for (++idx; idx < argsStr.length; idx++) {
    let char = argsStr.charAt(idx);

    if (((char == ',') || (char == ')')) && (degree == 0)) {
      result.push(args);
      args = '';
    } else {
      args += char;
    }

    if (char == '(')
      degree++;
    if (char == ')')
      degree--;
  }

  return result;
};

/**
 * Module exports: Declare & Constructor
 */
let Parser = function (config) {
  this._exts = ['', '.cowfig', '.cowfig.json'];

  if (config.srcBase)
    this._srcBase = path.resolve(config.srcBase) + '/';
  else
    this._srcBase = process.cwd() + '/config/source/';

  if (config.templateBase)
    this._templateBase = path.resolve(config.templateBase) + '/';
  else
    this._templateBase = this._srcBase + '/config/source/';

  this._env = config.env || process.env.NODE_ENV;
  this._envPrefix = config.envPrefix || '_';
  this._override = config.override || 'override';

  // supported file formats
  this.fmt = {};
  // cached file content
  this.db = {};
};
module.exports = Parser;

Parser.prototype.register = function (ext, parser) {
  if (!parser)
    return null;
  if ((parser.query) && (typeof(parser.query) === 'function'))
    this.fmt[ext] = parser;
};

/**
 * parse cowfig file
 * - support ENV.cowfig overriding
 * - ENV.cowfig files stores under templateBase/_ENV-NAME/
 *
 * @param  {string} filename
 * @return {object}
 */
Parser.prototype.parseFile = function (filename) {
  let baseData = {}, baseMTime = 0;
  let envData = {}, envMTime = 0, envType;
  let template, result;

  // Load base cowfig template
  let _baseFile = tryExtensions(path.resolve(this._templateBase, filename), this._exts);
  if (_baseFile) {
    try {
      let _content = fs.readFileSync(_baseFile, 'utf8');
      baseData = JSON5.parse(_content);
      baseMTime = Date.parse(fs.statSync(_baseFile).mtime);
    } catch (e) { console.log(e); }
  }

  // Load ENV cowfig template if exist
  let _envFile = tryExtensions(path.resolve(this._templateBase, this._envPrefix + this._env, filename), this._exts);
  if (_envFile) {
    try {
      let _content = fs.readFileSync(_envFile, 'utf8');
      envData = JSON5.parse(_content);
      envMTime = Date.parse(fs.statSync(_envFile).mtime);
    } catch (e) { console.log(e); }
  }

  // Override base template with ENV cowfig template
  envType = envData.__override || this._override;
  delete envData.__override;
  template = override(baseData, envData, envType);
  mtime = (baseMTime > envMTime) ? baseMTime : envMTime;

  result = this.parse(template, {isRoot: true, filename: filename});
  return result;
};

/**
 * Parser.parse
 *
 * @param  {object} template
 * @return {object}
 *
 * TODO:
 * - error handler, logger
 */
Parser.prototype.parse = function (template, root, replacement) {
  let _root, _mtime;
  let result = {}, copy, writer;

  /**
   * Prepare & keep __command info first, once
   */
  if (!root.isRoot) {
    _root = root;
  } else {
    _root = {isRoot: false, macro: {}};

    // macro
    if (template.__macro) {
      for (let i = 0; i < template.__macro.length; i++) {
        _mtime = Date.parse(fs.statSync(this._templateBase + template.__macro[i].src).mtime);
        if (_mtime > mtime)
          mtime = _mtime;
        _root.macro[template.__macro[i].fname] = require(this._templateBase + template.__macro[i].src);
      }
    }

    // alias
    if (template.__alias) {
      for (let key in template.__alias) {
        this.db[key] = this.query(template.__alias[key], _root.macro);
        if (typeof(this.db[key]) === "string")
          delete this.db[key]
        else
          this.db[key].__mtime = mtime;
      }
    }

    // copy
    if (template.__copy) {
      result.__copy = [];

      for (let i = 0; i < template.__copy.length; i++) {
        let fn = template.__copy[i].src;
        let envfn = this._env + '/' + fn;
        let files = [this._srcBase + envfn, this._srcBase + fn];
        for (let j = 0; j < files.length; j++) {
          let filename = tryFile(files[j]);
          if (filename) {
            result.__copy.push({src: filename, dest: template.__copy[i].dest});
            break;
          }
        }
      }
    }

    // writer
    if (template.__writer)
      result.__writer = template.__writer;
  }

  /**
   * Major recursive parser call
   */
  for (let key in template) {
    let _key = key;

    if (replacement !== undefined) {
      _key = _key.replace(/__key__/g, replacement);
    }

    // Process __for__ before others builtin command
    if (key.slice(0, 7) === '__for__') {
      let keys;
      if (key.slice(7, 8) === '@') {
        keys = template[key.slice(8)];
        delete result[key.slice(8)];
        delete template[key.slice(8)];
      } else {
        keys = this.query(key.slice(7), _root.macro);
      }

      let length = keys.length;

      if ((template[key] instanceof Object) && !(template[key] instanceof Array)) {
        for (let i = 0; i < length; i++) {
          result[keys[i]] = this.parse(template[key], _root, keys[i]);
        }
        continue;
      }

      for (let i = 0; i < length; i++) {
        if (typeof template[key] === 'string') {
          result[keys[i]] = this.query(template[key].replace(/__key__/g, keys[i]), _root.macro, keys[i]);
          continue;
        }

        result[keys[i]] = this.query(template[key], _root.macro, keys[i]);
      }
      continue;
    }

    // Skip other builtin command
    if (key.match(/__/))
      continue;

    if (template[key] instanceof Array && template[key][0] instanceof Object && !(template[key][0] instanceof Array)) {
      result[_key] = [];
      let length = template[key].length;
      for (let i = 0; i < length; i++) {
        result[_key][i] = this.parse(template[key][i], _root, replacement);
      }
      continue;
    }

    if ((template[key] instanceof Object) && !(template[key] instanceof Array)) {
      result[_key] = this.parse(template[key], _root, replacement);
      continue;
    }

    if (replacement !== undefined && typeof template[key] === 'string') {
      result[_key] = this.query(template[key].replace(/__key__/g, replacement), _root.macro, replacement);
    }

    result[_key] = this.query(template[key], _root.macro, replacement);
  }

  /**
   * After recursive parsing
   * - execute post process
   * - put back builtin __command info
   */
  if (root.isRoot) {
    // post process before other __command
    if (template.__post) {
      let _fn = root.filename.split('/');
      _fn.pop();
      let _base = _fn.join('/') + '/';
      for (let i = 0; i < template.__post.length; i++) {
        _mtime = Date.parse(fs.statSync(this._templateBase + _base + template.__post[i]).mtime);
        if (_mtime > mtime)
          mtime = _mtime;
        let fn = require(this._templateBase + _base + template.__post[i]);
        result = fn(result);
      }
    }

    result.__mtime = mtime;
    mtime = 0;
  }

  return result;
};

Parser.prototype.parseString = function (template, root, prettyOpt) {
  let result = this.parse(template, root);
  return stringify(result, prettyOpt);
};

Parser.prototype.stringify = function (obj, opt) {
  return stringify(obj, opt);
};

/**
 * Parser.query
 *
 * @param  {string} query
 * @return {object}
 */
Parser.prototype.query = function (queryStr, macro, replacement) {
  let _queryStr = queryStr;
  if (typeof _queryStr === 'string') {
    if (replacement !== undefined) {
      _queryStr = _queryStr.replace(/__key__/g, replacement);
    }

    // $ - on-the-fly macro processing
    if (_queryStr.slice(0, 1) == '$') {
      _queryStr = _queryStr.replace(/\s/g, '');

      let args = getArgs(_queryStr);
      for (let i = 1; i < args.length; i++)
        args[i] = this.query(args[i], macro);
      let fname = args.shift();

      if (macro[fname])
        return macro[fname](args);
      else
        return _queryStr;
    }

    // % - file parser plugin
    else if (_queryStr.slice(0, 1) == '%') {
      _queryStr = _queryStr.replace(/\s/g, '');

      let _tmp = _queryStr.split('?');
      let fn = _tmp[0].slice(1, _tmp[0].length);
      let query = _tmp[1];
      let ext = fn.split('.')[1];

      if (this.fmt[ext]) {
        let result;

        // Load value set
        if (this.db[fn])
          result = this.fmt[ext].query(query, 'cache', this.db[fn]);
        else
          result = this.fmt[ext].query(query, 'file', fn);

        if ((result) && (!result.err)) {
          if (result.cache) {
            this.db[fn] = result.cache;
            if (result.cache.__mtime > mtime)
              mtime = result.__mtime;
          }
          return result.value;
        }
      }
    }
  }

  return _queryStr;
};
<commit_msg>[Parser] add '_' for default data content<commit_after>/* See LICENSE file for terms of use */
'use strict';

/* Node module requirement */
let fs = require('fs');
let path = require('path');
let JSON5 = require('json5');
let stringify = require('json-stringify-pretty-compact');

let override = require('../util/override');

/* CONSTANT, Configuration, Global variable */
let mtime = 0;

// Modeled off of (v0.6.18 link; check latest too):
// Origin source: https://github.com/joyent/node/blob/v0.6.18/lib/module.js
//
// given a module name, and a list of paths to test, returns the first
// matching file in the following precedence.
//
// require("a.<ext>")
//   -> a.<ext>
//
// require("a")
//   -> a
//   -> a.<ext>
//   -> a/index.<ext>

function statPath(path) {
  try {
    return fs.statSync(path);
  } catch (ex) {}
  return false;
}

// check if the file exists and is not a directory
function tryFile (requestPath) {
  let stats = statPath(requestPath);
  if (stats && !stats.isDirectory()) {
    return fs.realpathSync(requestPath);
  }
  return false;
}

// given a path check a the file exists with any of the set extensions
function tryExtensions (p, exts) {
  for (let i = 0, EL = exts.length; i < EL; i++) {
    let filename = tryFile(p + exts[i]);

    if (filename) {
      return filename;
    }
  }
  return false;
}

let getArgs = function (argsStr) {
  let result = [];
  let idx;

  // get fname
  let fn = '';
  for (idx = 1; idx < argsStr.length; idx++) {
    let char = argsStr.charAt(idx);
    if (char == '(')
      break;
    fn += char;
  }
  result.push(fn);

  // get args
  let args = '';
  let degree = 0;
  for (++idx; idx < argsStr.length; idx++) {
    let char = argsStr.charAt(idx);

    if (((char == ',') || (char == ')')) && (degree == 0)) {
      result.push(args);
      args = '';
    } else {
      args += char;
    }

    if (char == '(')
      degree++;
    if (char == ')')
      degree--;
  }

  return result;
};

/**
 * Module exports: Declare & Constructor
 */
let Parser = function (config) {
  this._exts = ['', '.cowfig', '.cowfig.json'];

  if (config.srcBase)
    this._srcBase = path.resolve(config.srcBase) + '/';
  else
    this._srcBase = process.cwd() + '/config/source/';

  if (config.templateBase)
    this._templateBase = path.resolve(config.templateBase) + '/';
  else
    this._templateBase = this._srcBase + '/config/source/';

  this._env = config.env || process.env.NODE_ENV;
  this._envPrefix = config.envPrefix || '_';
  this._override = config.override || 'override';

  // supported file formats
  this.fmt = {};
  // cached file content
  this.db = {};
};
module.exports = Parser;

Parser.prototype.register = function (ext, parser) {
  if (!parser)
    return null;
  if ((parser.query) && (typeof(parser.query) === 'function'))
    this.fmt[ext] = parser;
};

/**
 * parse cowfig file
 * - support ENV.cowfig overriding
 * - ENV.cowfig files stores under templateBase/_ENV-NAME/
 *
 * @param  {string} filename
 * @return {object}
 */
Parser.prototype.parseFile = function (filename) {
  let baseData = {}, baseMTime = 0;
  let envData = {}, envMTime = 0, envType;
  let template, result;

  // Load base cowfig template
  let _baseFile = tryExtensions(path.resolve(this._templateBase, filename), this._exts);
  if (_baseFile) {
    try {
      let _content = fs.readFileSync(_baseFile, 'utf8');
      baseData = JSON5.parse(_content);
      baseMTime = Date.parse(fs.statSync(_baseFile).mtime);
    } catch (e) { console.log(e); }
  }

  // Load ENV cowfig template if exist
  let _envFile = tryExtensions(path.resolve(this._templateBase, this._envPrefix + this._env, filename), this._exts);
  if (_envFile) {
    try {
      let _content = fs.readFileSync(_envFile, 'utf8');
      envData = JSON5.parse(_content);
      envMTime = Date.parse(fs.statSync(_envFile).mtime);
    } catch (e) { console.log(e); }
  }

  // Override base template with ENV cowfig template
  envType = envData.__override || this._override;
  delete envData.__override;
  template = override(baseData, envData, envType);
  mtime = (baseMTime > envMTime) ? baseMTime : envMTime;

  result = this.parse(template, {isRoot: true, filename: filename});
  return result;
};

/**
 * Parser.parse
 *
 * @param  {object} template
 * @return {object}
 *
 * TODO:
 * - error handler, logger
 */
Parser.prototype.parse = function (template, root, replacement) {
  let _root, _mtime;
  let result = {}, copy, writer;

  /**
   * Prepare & keep __command info first, once
   */
  if (!root.isRoot) {
    _root = root;
  } else {
    _root = {isRoot: false, macro: {}};

    // macro
    if (template.__macro) {
      for (let i = 0; i < template.__macro.length; i++) {
        _mtime = Date.parse(fs.statSync(this._templateBase + template.__macro[i].src).mtime);
        if (_mtime > mtime)
          mtime = _mtime;
        _root.macro[template.__macro[i].fname] = require(this._templateBase + template.__macro[i].src);
      }
    }

    // alias
    if (template.__alias) {
      for (let key in template.__alias) {
        this.db[key] = this.query(template.__alias[key], _root.macro);
        if (typeof(this.db[key]) === "string")
          delete this.db[key]
        else
          this.db[key].__mtime = mtime;
      }
    }

    // copy
    if (template.__copy) {
      result.__copy = [];

      for (let i = 0; i < template.__copy.length; i++) {
        let fn = template.__copy[i].src;
        let envfn = this._env + '/' + fn;
        let files = [this._srcBase + envfn, this._srcBase + fn];
        for (let j = 0; j < files.length; j++) {
          let filename = tryFile(files[j]);
          if (filename) {
            result.__copy.push({src: filename, dest: template.__copy[i].dest});
            break;
          }
        }
      }
    }

    // writer
    if (template.__writer)
      result.__writer = template.__writer;
  }

  // template._ for default JSON content
  if (template._) {
    let _result = this.query(template._, _root.macro);
    for (let key in _result)
      result[key] = _result[key];
    delete template._;
  }

  /**
   * Major recursive parser call
   */
  for (let key in template) {
    let _key = key;

    if (replacement !== undefined) {
      _key = _key.replace(/__key__/g, replacement);
    }

    // Process __for__ before others builtin command
    if (key.slice(0, 7) === '__for__') {
      let keys;
      if (key.slice(7, 8) === '@') {
        keys = template[key.slice(8)];
        delete result[key.slice(8)];
        delete template[key.slice(8)];
      } else {
        keys = this.query(key.slice(7), _root.macro);
      }

      let length = keys.length;

      if ((template[key] instanceof Object) && !(template[key] instanceof Array)) {
        for (let i = 0; i < length; i++) {
          result[keys[i]] = this.parse(template[key], _root, keys[i]);
        }
        continue;
      }

      for (let i = 0; i < length; i++) {
        if (typeof template[key] === 'string') {
          result[keys[i]] = this.query(template[key].replace(/__key__/g, keys[i]), _root.macro, keys[i]);
          continue;
        }

        result[keys[i]] = this.query(template[key], _root.macro, keys[i]);
      }
      continue;
    }

    // Skip other builtin command
    if (key.match(/__/))
      continue;

    if (template[key] instanceof Array && template[key][0] instanceof Object && !(template[key][0] instanceof Array)) {
      result[_key] = [];
      let length = template[key].length;
      for (let i = 0; i < length; i++) {
        result[_key][i] = this.parse(template[key][i], _root, replacement);
      }
      continue;
    }

    if ((template[key] instanceof Object) && !(template[key] instanceof Array)) {
      result[_key] = this.parse(template[key], _root, replacement);
      continue;
    }

    if (replacement !== undefined && typeof template[key] === 'string') {
      result[_key] = this.query(template[key].replace(/__key__/g, replacement), _root.macro, replacement);
    }

    result[_key] = this.query(template[key], _root.macro, replacement);
  }

  /**
   * After recursive parsing
   * - execute post process
   * - put back builtin __command info
   */
  if (root.isRoot) {
    // post process before other __command
    if (template.__post) {
      let _fn = root.filename.split('/');
      _fn.pop();
      let _base = _fn.join('/') + '/';
      for (let i = 0; i < template.__post.length; i++) {
        _mtime = Date.parse(fs.statSync(this._templateBase + _base + template.__post[i]).mtime);
        if (_mtime > mtime)
          mtime = _mtime;
        let fn = require(this._templateBase + _base + template.__post[i]);
        result = fn(result);
      }
    }

    result.__mtime = mtime;
    mtime = 0;
  }

  return result;
};

Parser.prototype.parseString = function (template, root, prettyOpt) {
  let result = this.parse(template, root);
  return stringify(result, prettyOpt);
};

Parser.prototype.stringify = function (obj, opt) {
  return stringify(obj, opt);
};

/**
 * Parser.query
 *
 * @param  {string} query
 * @return {object}
 */
Parser.prototype.query = function (queryStr, macro, replacement) {
  let _queryStr = queryStr;
  if (typeof _queryStr === 'string') {
    if (replacement !== undefined) {
      _queryStr = _queryStr.replace(/__key__/g, replacement);
    }

    // $ - on-the-fly macro processing
    if (_queryStr.slice(0, 1) == '$') {
      _queryStr = _queryStr.replace(/\s/g, '');

      let args = getArgs(_queryStr);
      for (let i = 1; i < args.length; i++)
        args[i] = this.query(args[i], macro);
      let fname = args.shift();

      if (macro[fname])
        return macro[fname](args);
      else
        return _queryStr;
    }

    // % - file parser plugin
    else if (_queryStr.slice(0, 1) == '%') {
      _queryStr = _queryStr.replace(/\s/g, '');

      let _tmp = _queryStr.split('?');
      let fn = _tmp[0].slice(1, _tmp[0].length);
      let query = _tmp[1];
      let ext = fn.split('.')[1];

      if (this.fmt[ext]) {
        let result;

        // Load value set
        if (this.db[fn])
          result = this.fmt[ext].query(query, 'cache', this.db[fn]);
        else
          result = this.fmt[ext].query(query, 'file', fn);

        if ((result) && (!result.err)) {
          if (result.cache) {
            this.db[fn] = result.cache;
            if (result.cache.__mtime > mtime)
              mtime = result.__mtime;
          }
          return result.value;
        }
      }
    }
  }

  return _queryStr;
};
<|endoftext|>