const { isValidString, getType } = require('./utils.js') const { ERROR } = require('./error') const baseValidator = Object.create(null) baseValidator.username = function (username) { const errCode = ERROR.INVALID_USERNAME if (!isValidString(username)) { return { errCode } } if (/^\d+$/.test(username)) { // 用户名不能为纯数字 return { errCode } }; if (!/^[a-zA-Z0-9_-]+$/.test(username)) { // 用户名仅能使用数字、字母、“_”及“-” return { errCode } } } baseValidator.mobile = function (mobile) { const errCode = ERROR.INVALID_MOBILE if (!isValidString(mobile)) { return { errCode } } if (!/^1\d{10}$/.test(mobile)) { return { errCode } } } baseValidator.email = function (email) { const errCode = ERROR.INVALID_EMAIL if (!isValidString(email)) { return { errCode } } if (!/@/.test(email)) { return { errCode } } } baseValidator.nickname = function (nickname) { const errCode = ERROR.INVALID_NICKNAME if (nickname.indexOf('@') !== -1) { // 昵称不允许含@ return { errCode } }; if (/^\d+$/.test(nickname)) { // 昵称不能为纯数字 return { errCode } }; if (nickname.length > 100) { // 昵称不可超过100字符 return { errCode } } } const baseType = ['string', 'boolean', 'number', 'null'] // undefined不会由客户端提交上来 baseType.forEach((type) => { baseValidator[type] = function (val) { if (getType(val) === type) { return } return { errCode: ERROR.INVALID_PARAM } } }) function tokenize (name) { let i = 0 const result = [] let token = '' while (i < name.length) { const char = name[i] switch (char) { case '|': case '<': case '>': token && result.push(token) result.push(char) token = '' break default: token += char break } i++ if (i === name.length && token) { result.push(token) } } return result } /** * 处理validator名 * @param {string} name */ function parseValidatorName (name) { const tokenList = tokenize(name) let i = 0 let currentToken = tokenList[i] const result = { type: 'root', children: [], parent: null } let lastRealm = result while (currentToken) { switch (currentToken) { case 'array': { const currentRealm = { type: 'array', children: [], parent: lastRealm } lastRealm.children.push(currentRealm) lastRealm = currentRealm break } case '<': if (lastRealm.type !== 'array') { throw new Error('Invalid validator token "<"') } break case '>': if (lastRealm.type !== 'array') { throw new Error('Invalid validator token ">"') } lastRealm = lastRealm.parent break case '|': if (lastRealm.type !== 'array' && lastRealm.type !== 'root') { throw new Error('Invalid validator token "|"') } break default: lastRealm.children.push({ type: currentToken }) break } i++ currentToken = tokenList[i] } return result } function getRuleCategory (rule) { switch (rule.type) { case 'array': return 'array' case 'root': return 'root' default: return 'base' } } function isMatchUnionType (val, rule) { if (!rule.children || rule.children.length === 0) { return true } const children = rule.children for (let i = 0; i < children.length; i++) { const child = children[i] const category = getRuleCategory(child) let pass = false switch (category) { case 'base': pass = isMatchBaseType(val, child) break case 'array': pass = isMatchArrayType(val, child) break default: break } if (pass) { return true } } return false } function isMatchBaseType (val, rule) { if (typeof baseValidator[rule.type] !== 'function') { throw new Error(`invalid schema type: ${rule.type}`) } const validateRes = baseValidator[rule.type](val) if (validateRes && validateRes.errCode) { return false } return true } function isMatchArrayType (arr, rule) { if (getType(arr) !== 'array') { return false } if (rule.children && rule.children.length && arr.some(item => !isMatchUnionType(item, rule))) { return false } return true } // 特殊符号 https://www.ibm.com/support/pages/password-strength-rules ~!@#$%^&*_-+=`|\(){}[]:;"'<>,.?/ // const specialChar = '~!@#$%^&*_-+=`|\(){}[]:;"\'<>,.?/' // const specialCharRegExp = /^[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]$/ // for (let i = 0, arr = specialChar.split(''); i < arr.length; i++) { // const char = arr[i] // if (!specialCharRegExp.test(char)) { // throw new Error('check special character error: ' + char) // } // } // 密码强度表达式 const passwordRules = { // 密码必须包含大小写字母、数字和特殊符号 super: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, // 密码必须包含字母、数字和特殊符号 strong: /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, // 密码必须为字母、数字和特殊符号任意两种的组合 medium: /^(?![0-9]+$)(?![a-zA-Z]+$)(?![~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/, // 密码必须包含字母和数字 weak: /^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{6,16}$/ } function createPasswordVerifier ({ passwordStrength = 'medium' } = {}) { return function (password) { const passwordRegExp = passwordRules[passwordStrength] if (!passwordRegExp) { throw new Error('Invalid password strength config: ' + passwordStrength) } const errCode = ERROR.INVALID_PASSWORD if (!isValidString(password)) { return { errCode } } if (!passwordRegExp.test(password)) { return { errCode: errCode + '-' + passwordStrength } } } } class Validator { constructor ({ passwordStrength = 'medium' } = {}) { this.baseValidator = baseValidator this.customValidator = Object.create(null) this.mixin( 'password', createPasswordVerifier({ passwordStrength }) ) } mixin (type, handler) { this.customValidator[type] = handler } getRealBaseValidator (type) { return this.customValidator[type] || this.baseValidator[type] } get validator () { return new Proxy({}, { get: (_, prop) => { if (typeof prop !== 'string') { return } const realBaseValidator = this.getRealBaseValidator(prop) if (realBaseValidator) { return realBaseValidator } const rule = parseValidatorName(prop) return function (val) { if (!isMatchUnionType(val, rule)) { return { errCode: ERROR.INVALID_PARAM } } } } }) } validate (value = {}, schema = {}) { for (const schemaKey in schema) { let schemaValue = schema[schemaKey] if (getType(schemaValue) === 'string') { schemaValue = { required: true, type: schemaValue } } const { required, type } = schemaValue // value内未传入了schemaKey或对应值为undefined if (value[schemaKey] === undefined) { if (required) { return { errCode: ERROR.PARAM_REQUIRED, errMsgValue: { param: schemaKey }, schemaKey } } else { continue } } const validateMethod = this.validator[type] if (!validateMethod) { throw new Error(`invalid schema type: ${type}`) } const validateRes = validateMethod(value[schemaKey]) if (validateRes) { validateRes.schemaKey = schemaKey return validateRes } } } } function checkClientInfo (clientInfo) { const stringNotRequired = { required: false, type: 'string' } const numberNotRequired = { required: false, type: 'number' } const numberOrStringNotRequired = { required: false, type: 'number|string' } const schema = { uniPlatform: 'string', appId: 'string', deviceId: stringNotRequired, osName: stringNotRequired, locale: stringNotRequired, clientIP: stringNotRequired, appName: stringNotRequired, appVersion: stringNotRequired, appVersionCode: numberOrStringNotRequired, channel: numberOrStringNotRequired, userAgent: stringNotRequired, uniIdToken: stringNotRequired, deviceBrand: stringNotRequired, deviceModel: stringNotRequired, osVersion: stringNotRequired, osLanguage: stringNotRequired, osTheme: stringNotRequired, romName: stringNotRequired, romVersion: stringNotRequired, devicePixelRatio: numberNotRequired, windowWidth: numberNotRequired, windowHeight: numberNotRequired, screenWidth: numberNotRequired, screenHeight: numberNotRequired } const validateRes = new Validator().validate(clientInfo, schema) if (validateRes) { if (validateRes.errCode === ERROR.PARAM_REQUIRED) { console.warn('- 如果使用HBuilderX运行本地云函数/云对象功能时出现此提示,请改为使用客户端调用本地云函数方式调试,或更新HBuilderX到3.4.12及以上版本。\n- 如果是缺少clientInfo.appId,请检查项目manifest.json内是否配置了DCloud AppId') throw new Error(`"clientInfo.${validateRes.schemaKey}" is required.`) } else { throw new Error(`Invalid client info: clienInfo.${validateRes.schemaKey}`) } } } module.exports = { Validator, checkClientInfo }