text: const schema = require('../schema').tables;
  const _ = require('lodash');
  const validator = require('validator');
  const moment = require('moment-timezone');
  const assert = require('assert');
  const Promise = require('bluebird');
  const {i18n} = require('../../lib/common');
  const errors = require('@tryghost/errors');
  const settingsCache = require('../../services/settings/cache');
  const urlUtils = require('../../../shared/url-utils');
  
  function assertString(input) {
      assert(typeof input === 'string', 'Validator js validates strings only');
  }
  
  /**
   * Counts repeated characters in a string. When 50% or more characters are the same,
   * we return false and therefore invalidate the string.
   * @param {String} stringToTest The password string to check.
   * @return {Boolean}
   */
  function characterOccurance(stringToTest) {
      const chars = {};
      let allowedOccurancy;
      let valid = true;
  
      stringToTest = _.toString(stringToTest);
      allowedOccurancy = stringToTest.length / 2;
  
      // Loop through string and accumulate character counts
      _.each(stringToTest, function (char) {
          if (!chars[char]) {
              chars[char] = 1;
          } else {
              chars[char] += 1;
          }
      });
  
      // check if any of the accumulated chars exceed the allowed occurancy
      // of 50% of the words' length.
      _.forIn(chars, function (charCount) {
          if (charCount >= allowedOccurancy) {
              valid = false;
          }
      });
  
      return valid;
  }
  
  // extends has been removed in validator >= 5.0.0, need to monkey-patch it back in
  // @TODO: We modify the global validator dependency here! https://github.com/chriso/validator.js/issues/525#issuecomment-213149570
  validator.extend = function (name, fn) {
      validator[name] = function () {
          const args = Array.prototype.slice.call(arguments);
          assertString(args[0]);
          return fn.apply(validator, args);
      };
  };
  
  // Provide a few custom validators
  validator.extend('empty', function empty(str) {
      return _.isEmpty(str);
  });
  
  validator.extend('notContains', function notContains(str, badString) {
      return !_.includes(str, badString);
  });
  
  validator.extend('isTimezone', function isTimezone(str) {
      return moment.tz.zone(str) ? true : false;
  });
  
  validator.extend('isEmptyOrURL', function isEmptyOrURL(str) {
      return (_.isEmpty(str) || validator.isURL(str, {require_protocol: false}));
  });
  
  validator.extend('isSlug', function isSlug(str) {
      return validator.matches(str, /^[a-z0-9\-_]+$/);
  });
  
  /**
   * Validation against simple password rules
   * Returns false when validation fails and true for a valid password
   * @param {String} password The password string to check.
   * @param {String} email The users email address to validate agains password.
   * @param {String} blogTitle Optional blogTitle value, when blog title is not set yet, e. g. in setup process.
   * @return {Object} example for returned validation Object:
   * invalid password: `validationResult: {isValid: false, message: 'Sorry, you cannot use an insecure password.'}`
   * valid password: `validationResult: {isValid: true}`
   */
  function validatePassword(password, email, blogTitle) {
      const validationResult = {isValid: true};
      const disallowedPasswords = ['password', 'ghost', 'passw0rd'];
      let blogUrl = urlUtils.urlFor('home', true);
  
      const badPasswords = [
          '1234567890',
          'qwertyuiop',
          'qwertzuiop',
          'asdfghjkl;',
          'abcdefghij',
          '0987654321',
          '1q2w3e4r5t',
          '12345asdfg'
      ];
  
      blogTitle = blogTitle ? blogTitle : settingsCache.get('title');
      blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
  
      // password must be longer than 10 characters
      if (!validator.isLength(password, 10)) {
          validationResult.isValid = false;
          validationResult.message = i18n.t('errors.models.user.passwordDoesNotComplyLength', {minLength: 10});
  
          return validationResult;
      }
  
      // dissallow password from badPasswords list (e. g. '1234567890')
      _.each(badPasswords, function (badPassword) {
          if (badPassword === password) {
              validationResult.isValid = false;
          }
      });
  
      // password must not match with users' email
      if (email && email.toLowerCase() === password.toLowerCase()) {
          validationResult.isValid = false;
      }
  
      // password must not contain the words 'ghost', 'password', or 'passw0rd'
      _.each(disallowedPasswords, function (disallowedPassword) {
          if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
              validationResult.isValid = false;
          }
      });
  
      // password must not match with blog title
      if (blogTitle && blogTitle.toLowerCase() === password.toLowerCase()) {
          validationResult.isValid = false;
      }
  
      // password must not match with blog URL (without protocol, with or without trailing slash)
      if (blogUrl && (blogUrl.toLowerCase() === password.toLowerCase() || blogUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())) {
          validationResult.isValid = false;
      }
  
      // dissallow passwords where 50% or more of characters are the same
      if (!characterOccurance(password)) {
          validationResult.isValid = false;
      }
  
      // Generic error message for the rules where no dedicated error massage is set
      if (!validationResult.isValid && !validationResult.message) {
          validationResult.message = i18n.t('errors.models.user.passwordDoesNotComplySecurity');
      }
  
      return validationResult;
  }
  
  /**
   * Validate model against schema.
   *
   * ## on model update
   * - only validate changed fields
   * - otherwise we could throw errors which the user is out of control
   * - e.g.
   *   - we add a new field without proper validation, release goes out
   *   - we add proper validation for a single field
   * - if you call `user.save()` the default fallback in bookshelf is `options.method=update`.
   * - we set `options.method` explicit for adding resources (because otherwise bookshelf uses `update`)
   *
   * ## on model add
   * - validate everything to catch required fields
   */
  function validateSchema(tableName, model, options) {
      options = options || {};
  
      const columns = _.keys(schema[tableName]);
      let validationErrors = [];
  
      _.each(columns, function each(columnKey) {
          let message = ''; // KEEP: Validator.js only validates strings.
          const strVal = _.toString(model.get(columnKey));
  
          if (options.method !== 'insert' && !_.has(model.changed, columnKey)) {
              return;
          }
  
          // check nullable
          if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'nullable') &&
              schema[tableName][columnKey].nullable !== true &&
              !Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'defaultTo')
          ) {
              if (validator.empty(strVal)) {
                  message = i18n.t('notices.data.validation.index.valueCannotBeBlank', {
                      tableName: tableName,
                      columnKey: columnKey
                  });
                  validationErrors.push(new errors.ValidationError({
                      message: message,
                      context: tableName + '.' + columnKey
                  }));
              }
          }
  
          // validate boolean columns
          if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')
              && schema[tableName][columnKey].type === 'bool') {
              if (!(validator.isBoolean(strVal) || validator.empty(strVal))) {
                  message = i18n.t('notices.data.validation.index.valueMustBeBoolean', {
                      tableName: tableName,
                      columnKey: columnKey
                  });
                  validationErrors.push(new errors.ValidationError({
                      message: message,
                      context: tableName + '.' + columnKey
                  }));
              }
  
              // CASE: ensure we transform 0|1 to false|true
              if (!validator.empty(strVal)) {
                  model.set(columnKey, !!model.get(columnKey));
              }
          }
  
          // TODO: check if mandatory values should be enforced
          if (model.get(columnKey) !== null && model.get(columnKey) !== undefined) {
              // check length
              if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'maxlength')) {
                  if (!validator.isLength(strVal, 0, schema[tableName][columnKey].maxlength)) {
                      message = i18n.t('notices.data.validation.index.valueExceedsMaxLength',
                          {
                              tableName: tableName,
                              columnKey: columnKey,
                              maxlength: schema[tableName][columnKey].maxlength
                          });
                      validationErrors.push(new errors.ValidationError({
                          message: message,
                          context: tableName + '.' + columnKey
                      }));
                  }
              }
  
              // check validations objects
              if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'validations')) {
                  validationErrors = validationErrors.concat(validate(strVal, columnKey, schema[tableName][columnKey].validations, tableName));
              }
  
              // check type
              if (Object.prototype.hasOwnProperty.call(schema[tableName][columnKey], 'type')) {
                  if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(strVal)) {
                      message = i18n.t('notices.data.validation.index.valueIsNotInteger', {
                          tableName: tableName,
                          columnKey: columnKey
                      });
                      validationErrors.push(new errors.ValidationError({
                          message: message,
                          context: tableName + '.' + columnKey
                      }));
                  }
              }
          }
      });
  
      if (validationErrors.length !== 0) {
          return Promise.reject(validationErrors);
      }
  
      return Promise.resolve();
  }
  
  // Validation for settings
  // settings are checked against the validation objects
  // form default-settings.json
  async function validateSettings(defaultSettings, model) {
      const setting = model.toJSON();
      let validationErrors = [];
      const matchingDefault = defaultSettings[setting.key];
  
      if (matchingDefault && matchingDefault.validations) {
          validationErrors = validationErrors.concat(validate(setting.value, setting.key, matchingDefault.validations, 'settings'));
      }
  
      if (validationErrors.length !== 0) {
          return Promise.reject(validationErrors);
      }
  
      if (setting.key === 'stripe_plans') {
          const plans = JSON.parse(setting.value);
          for (const plan of plans) {
              // We check 100, not 1, because amounts are in fractional units
              if (plan.amount < 100 && plan.name !== 'Complimentary') {
                  throw new errors.ValidationError({
                      message: 'Plans cannot have an amount less than 1'
                  });
              }
          }
      }
  
      return Promise.resolve();
  }
  
  /**
   * Validate keys using the validator module.
   * Each validation's key is a method name and its value is an array of options
   * eg:
   *       validations: { isURL: true, isLength: [20, 40] }
   * will validate that a values's length is a URL between 20 and 40 chars.
   *
   * If you pass a boolean as the value, it will specify the "good" result. By default
   * the "good" result is assumed to be true.
   * eg:
   *       validations: { isNull: false } // means the "good" result would
   *                                      // fail the `isNull` check, so
   *                                      // not null.
   *
   * available validators: https://github.com/chriso/validator.js#validators
   * @param {String} value the value to validate.
   * @param {String} key the db column key of the value to validate.
   * @param {Object} validations the validations object as described above.
   * @param {String} tableName (optional) the db table of the value to validate, used for error message.
   * @return {Array} returns an Array including the found validation errors (empty if none found);
   */
  function validate(value, key, validations, tableName) {
      const validationErrors = [];
      let translation;
      value = _.toString(value);
  
      _.each(validations, function each(validationOptions, validationName) {
          let goodResult = true;
  
          if (_.isBoolean(validationOptions)) {
              goodResult = validationOptions;
              validationOptions = [];
          } else if (!_.isArray(validationOptions)) {
              validationOptions = [validationOptions];
          }
  
          validationOptions.unshift(value);
  
          // equivalent of validator.isSomething(option1, option2)
          if (validator[validationName].apply(validator, validationOptions) !== goodResult) {
              // CASE: You can define specific translations for validators e.g. isLength
              if (i18n.doesTranslationKeyExist('notices.data.validation.index.validationFailedTypes.' + validationName)) {
                  translation = i18n.t('notices.data.validation.index.validationFailedTypes.' + validationName, _.merge({
                      validationName: validationName,
                      key: key,
                      tableName: tableName
                  }, validationOptions[1]));
              } else {
                  translation = i18n.t('notices.data.validation.index.validationFailed', {
                      validationName: validationName,
                      key: key
                  });
              }
  
              validationErrors.push(new errors.ValidationError({
                  message: translation,
                  context: `${tableName}.${key}`
              }));
          }
  
          validationOptions.shift();
      }, this);
  
      return validationErrors;
  }
  
  module.exports = {
      validate,
      validator,
      validatePassword,
      validateSchema,
      validateSettings
  };
  