| @@ -0,0 +1,8 @@ | |||
| # add git-ignore syntax here of things you don't want copied into docker image | |||
| .git | |||
| .svn | |||
| .wappler | |||
| *Dockerfile* | |||
| *docker-compose* | |||
| node_modules | |||
| @@ -0,0 +1,4 @@ | |||
| node_modules | |||
| .svn | |||
| .env | |||
| **/.DS_Store | |||
| @@ -0,0 +1 @@ | |||
| scripts-prepend-node-path=true | |||
| @@ -0,0 +1,38 @@ | |||
| { | |||
| "projectName": "RPM_Access", | |||
| "styleFile": "/css/style.css", | |||
| "assetsFolder": "/assets", | |||
| "designFramework": "bootstrap5", | |||
| "frameworks": [ | |||
| { | |||
| "name": "fontawesome_5", | |||
| "type": "cdn" | |||
| }, | |||
| { | |||
| "name": "bootstrap5", | |||
| "type": "local" | |||
| }, | |||
| { | |||
| "name": "appConnect", | |||
| "type": "local" | |||
| } | |||
| ], | |||
| "hostingType": "wappler", | |||
| "projectServerModel": "node", | |||
| "runtime": "capacitor", | |||
| "webRootFolder": "/public", | |||
| "useRouting": true, | |||
| "addBase": true, | |||
| "routingHandler": "node", | |||
| "projectLinksType": "site", | |||
| "targets": [ | |||
| { | |||
| "name": "Development", | |||
| "remoteURL": "http://localhost:3000/", | |||
| "access": "local", | |||
| "targetType": "wappler" | |||
| } | |||
| ], | |||
| "activeTarget": "Development", | |||
| "projectType": "web" | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| { | |||
| "routes": [ | |||
| { | |||
| "path": "/", | |||
| "page": "index", | |||
| "routeType": "page", | |||
| "layout": "main" | |||
| } | |||
| ] | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "exec": {} | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "exec": {} | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| const server = require('./lib/server'); | |||
| server.start(); | |||
| @@ -0,0 +1,72 @@ | |||
| const AuthProvider = require('./provider'); | |||
| class DatabaseProvider extends AuthProvider { | |||
| constructor(app, opts, name) { | |||
| super(app, opts, name); | |||
| this.users = opts.users; | |||
| this.perms = opts.permissions; | |||
| this.db = app.getDbConnection(opts.connection); | |||
| } | |||
| async validate(username, password, passwordVerify) { | |||
| if (!username) return false; | |||
| if (!password) password = ''; | |||
| let results = await this.db | |||
| .select(this.users.identity, this.users.username, this.users.password) | |||
| .from(this.users.table) | |||
| .where(this.users.username, username); | |||
| for (let result of results) { | |||
| if (result[this.users.username] == username) { | |||
| if (passwordVerify && result[this.users.password].startsWith('$')) { | |||
| const argon2 = require('argon2'); | |||
| const valid = await argon2.verify(result[this.users.password], password); | |||
| return valid ? result[this.users.identity] : false; | |||
| } else if (result[this.users.password] == password) { | |||
| return result[this.users.identity]; | |||
| } | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| async permissions(identity, permissions) { | |||
| for (let permission of permissions) { | |||
| if (!this.perms[permission]) return false; | |||
| let perm = this.perms[permission]; | |||
| let table = perm.table || this.users.table; | |||
| let ident = perm.identity || this.users.identity; | |||
| let results = await this.db | |||
| .select(ident) | |||
| .from(table) | |||
| .where(ident, identity) | |||
| .where(function() { | |||
| for (let condition of perm.conditions) { | |||
| if (condition.operator == 'in') { | |||
| this.whereIn(condition.column, condition.value); | |||
| } else if (condition.operator == 'not in') { | |||
| this.whereNotIn(condition.column, condition.value); | |||
| } else if (condition.operator == 'is null') { | |||
| this.whereNull(condition.column); | |||
| } else if (condition.operator == 'is not null') { | |||
| this.whereNotNull(condition.column); | |||
| } else { | |||
| this.where(condition.column, condition.operator, condition.value); | |||
| } | |||
| } | |||
| }); | |||
| if (!results.length) return false; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| module.exports = DatabaseProvider; | |||
| @@ -0,0 +1,149 @@ | |||
| const config = require('../setup/config'); | |||
| const crypto = require('crypto'); | |||
| const debug = require('debug')('server-connect:auth'); | |||
| class AuthProvider { | |||
| constructor(app, opts, name) { | |||
| this.app = app; | |||
| this.name = name; | |||
| this.identity = this.app.getSession(this.name + 'Id') || false; | |||
| this.secret = opts.secret || config.secret; | |||
| this.basicAuth = opts.basicAuth; | |||
| this.basicRealm = opts.basicRealm; | |||
| this.passwordVerify = opts.passwordVerify || false; | |||
| this.cookieOpts = { | |||
| domain: opts.domain || undefined, | |||
| httpOnly: true, | |||
| maxAge: (opts.expires || 30) * 24 * 60 * 60 * 1000, // from days to ms | |||
| path: opts.path || '/', | |||
| secure: !!opts.secure, | |||
| sameSite: opts.sameSite || 'Strict', | |||
| signed: true | |||
| }; | |||
| } | |||
| async autoLogin() { | |||
| if (this.basicAuth) { | |||
| const auth = require('../core/basicauth')(this.app.req); | |||
| debug(`Basic auth credentials received: %o`, auth); | |||
| if (auth) await this.login(auth.username, auth.password, false, true); | |||
| } | |||
| const cookie = this.app.getCookie(this.name + '.auth', true); | |||
| if (cookie) { | |||
| const auth = this.decrypt(cookie); | |||
| debug(`Login with cookie: %o`, auth); | |||
| if (auth) await this.login(auth.username, auth.password, true, true); | |||
| } else { | |||
| debug(`No login cookie found`); | |||
| } | |||
| } | |||
| async login(username, password, remember, autoLogin) { | |||
| const identity = await this.validate(username, password, this.passwordVerify); | |||
| if (!identity) { | |||
| await this.logout(); | |||
| if (!autoLogin) { | |||
| this.unauthorized(); | |||
| return false; | |||
| } | |||
| } else { | |||
| this.app.setSession(this.name + 'Id', identity); | |||
| this.app.set('identity', identity); | |||
| if (remember) { | |||
| debug('setCookie', identity, username, password); | |||
| this.app.setCookie(this.name + '.auth', this.encrypt({ username, password }), this.cookieOpts); | |||
| } | |||
| this.identity = identity; | |||
| } | |||
| return identity; | |||
| } | |||
| async logout() { | |||
| this.app.removeSession(this.name + 'Id'); | |||
| this.app.removeCookie(this.name + '.auth', this.cookieOpts); | |||
| this.app.remove('identity'); | |||
| this.identity = false; | |||
| } | |||
| async restrict(opts) { | |||
| if (this.identity === false) { | |||
| if (opts.loginUrl) { | |||
| if (this.app.req.fragment) { | |||
| this.app.res.status(222).send(opts.loginUrl); | |||
| } else { | |||
| this.app.res.redirect(opts.loginUrl); | |||
| } | |||
| } else { | |||
| this.unauthorized(); | |||
| } | |||
| return; | |||
| } | |||
| if (opts.permissions) { | |||
| const allowed = await this.permissions(this.identity, opts.permissions); | |||
| if (!allowed) { | |||
| if (opts.forbiddenUrl) { | |||
| if (this.app.req.fragment) { | |||
| this.app.res.status(222).send(opts.forbiddenUrl); | |||
| } else { | |||
| this.app.res.redirect(opts.forbiddenUrl); | |||
| } | |||
| } else { | |||
| this.forbidden(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| encrypt(data) { | |||
| const iv = crypto.randomBytes(16); | |||
| const key = crypto.scryptSync(this.secret, iv, 32); | |||
| const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); | |||
| const encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64'); | |||
| return iv.toString('base64') + '.' + encrypted + cipher.final('base64'); | |||
| } | |||
| decrypt(data) { | |||
| // try/catch to prevent errors on currupt cookies | |||
| try { | |||
| const iv = Buffer.from(data.split('.')[0], 'base64'); | |||
| const key = crypto.scryptSync(this.secret, iv, 32); | |||
| const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); | |||
| const decrypted = decipher.update(data.split('.')[1], 'base64', 'utf8'); | |||
| return JSON.parse(decrypted + decipher.final('utf8')); | |||
| } catch (err) { | |||
| return undefined; | |||
| } | |||
| } | |||
| unauthorized() { | |||
| if (this.basicAuth) { | |||
| this.app.res.set('WWW-Authenticate', `Basic Realm="${this.basicRealm}"`); | |||
| } | |||
| this.app.res.sendStatus(401); | |||
| } | |||
| forbidden() { | |||
| this.app.res.sendStatus(403); | |||
| } | |||
| async validate(username, password) { | |||
| throw new Error('auth.validate needs to be extended.'); | |||
| } | |||
| async permissions(identity, permissions) { | |||
| throw new Error('auth.permissions needs to be extended.'); | |||
| } | |||
| } | |||
| module.exports = AuthProvider; | |||
| @@ -0,0 +1,26 @@ | |||
| const AuthProvider = require('./provider'); | |||
| const debug = require('debug')('server-connect:auth'); | |||
| class SingleProvider extends AuthProvider { | |||
| constructor(app, opts, name) { | |||
| super(app, opts, name); | |||
| this.username = opts.username; | |||
| this.password = opts.password; | |||
| } | |||
| validate(username, password) { | |||
| if (username == this.username && password == this.password) { | |||
| return username; | |||
| } | |||
| return false; | |||
| } | |||
| permissions() { | |||
| return true; | |||
| } | |||
| } | |||
| module.exports = SingleProvider; | |||
| @@ -0,0 +1,31 @@ | |||
| const AuthProvider = require('./provider'); | |||
| class StaticProvider extends AuthProvider { | |||
| constructor(app, opts, name) { | |||
| super(app, opts, name); | |||
| this.users = opts.users; | |||
| this.perms = opts.perms; | |||
| } | |||
| validate(username, password) { | |||
| if (this.users[username] == password) { | |||
| return username; | |||
| } | |||
| return false; | |||
| } | |||
| permissions(username, permissions) { | |||
| for (let permission of permissions) { | |||
| if (!this.perms[permission] || !this.perms[permission].includes(username)) { | |||
| return false; | |||
| } | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| module.exports = StaticProvider; | |||
| @@ -0,0 +1,713 @@ | |||
| const fs = require('fs-extra'); | |||
| const Scope = require('./scope'); | |||
| const Parser = require('./parser'); | |||
| const db = require('./db'); | |||
| const validator = require('../validator'); | |||
| const config = require('../setup/config'); | |||
| const debug = require('debug')('server-connect:app'); | |||
| const { clone, formatDate, parseDate } = require('../core/util'); | |||
| const { toSystemPath } = require('../core/path'); | |||
| const os = require('os'); | |||
| if (!global.db) { | |||
| global.db = {}; | |||
| } | |||
| function App(req = {}, res = {}) { | |||
| this.error = false; | |||
| this.data = {}; | |||
| this.meta = {}; | |||
| this.settings = {}; | |||
| this.modules = {}; | |||
| this.io = global.io; | |||
| this.req = req; | |||
| this.res = res; | |||
| this.global = new Scope(); | |||
| this.scope = this.global; | |||
| this.mail = {}; | |||
| this.auth = {}; | |||
| this.oauth = {}; | |||
| this.db = {}; | |||
| this.s3 = {}; | |||
| this.jwt = {}; | |||
| if (config.globals.data) { | |||
| this.set(config.globals.data); | |||
| } | |||
| this.set({ | |||
| $_ERROR: null, | |||
| //$_SERVER: process.env, | |||
| $_ENV: process.env, | |||
| $_GET: req.query, | |||
| $_POST: req.method == 'POST' ? req.body : {}, | |||
| $_PARAM: req.params, | |||
| $_HEADER: req.headers, | |||
| $_COOKIE: req.cookies, | |||
| $_SESSION: req.session | |||
| }); | |||
| let urlParts = req.originalUrl ? req.originalUrl.split('?') : ['', '']; | |||
| let $server = { | |||
| CONTENT_TYPE: req.headers && req.headers['content-type'], | |||
| HTTPS: req.protocol == 'https', | |||
| PATH_INFO: req.path, | |||
| QUERY_STRING: urlParts[1], | |||
| REMOTE_ADDR: req.ip, | |||
| REQUEST_PROTOCOL: req.protocol, | |||
| REQUEST_METHOD: req.method, | |||
| SERVER_NAME: req.hostname, | |||
| BASE_URL: req.headers && req.protocol + '://' + req.headers['host'], | |||
| URL: urlParts[0], | |||
| HOSTNAME: os.hostname() | |||
| }; | |||
| if (req.headers) { | |||
| for (let header in req.headers) { | |||
| $server['HTTP_' + header.toUpperCase().replace(/-/, '_')] = req.headers[header]; | |||
| } | |||
| } | |||
| // Try to keep same as ASP/PHP | |||
| this.set('$_SERVER', $server); | |||
| } | |||
| App.prototype = { | |||
| set: function(key, value) { | |||
| this.global.set(key, value); | |||
| }, | |||
| get: function(key, def) { | |||
| let value = this.global.get(key); | |||
| return value !== undefined ? value : def; | |||
| }, | |||
| remove: function(key) { | |||
| this.global.remove(key); | |||
| }, | |||
| setSession: function(key, value) { | |||
| this.req.session[key] = value; | |||
| }, | |||
| getSession: function(key) { | |||
| return this.req.session[key]; | |||
| }, | |||
| removeSession: function(key) { | |||
| delete this.req.session[key]; | |||
| }, | |||
| setCookie: function(name, value, opts) { | |||
| if (!this.res.headersSent) { | |||
| if (this.res.cookie) { | |||
| this.res.cookie(name, value, opts); | |||
| } | |||
| } else { | |||
| debug(`Trying to set ${name} cookie while headers were already sent.`); | |||
| } | |||
| }, | |||
| getCookie: function(name, signed) { | |||
| return signed ? this.req.signedCookies[name] : this.req.cookies[name]; | |||
| }, | |||
| removeCookie: function(name, opts) { | |||
| if (this.res.clearCookie) { | |||
| // copy all options except expires and maxAge | |||
| const clearOpts = Object.assign({}, opts); | |||
| delete clearOpts.expires; | |||
| delete clearOpts.maxAge; | |||
| this.res.clearCookie(name, clearOpts); | |||
| } | |||
| }, | |||
| setMailer: function(name, options) { | |||
| let setup = {}; | |||
| options = this.parse(options); | |||
| switch (options.server) { | |||
| case 'mail': | |||
| setup.sendmail = true; | |||
| break; | |||
| case 'ses': | |||
| const aws = require('@aws-sdk/client-ses'); | |||
| const ses = new AWS.SES({ | |||
| endpoint: options.endpoint, | |||
| credentials: { | |||
| accessKeyId: options.accessKeyId, | |||
| secretAccessKey: options.secretAccessKey | |||
| } | |||
| }); | |||
| setup.SES = { ses, aws }; | |||
| break; | |||
| default: | |||
| // https://nodemailer.com/smtp/ | |||
| setup.host = options.host || 'localhost'; | |||
| setup.port = options.port || 25; | |||
| setup.secure = options.useSSL || false; | |||
| setup.auth = { | |||
| user: options.username, | |||
| pass: options.password | |||
| }; | |||
| setup.tls = { | |||
| rejectUnauthorized: false | |||
| }; | |||
| } | |||
| return this.mail[name] = setup; | |||
| }, | |||
| getMailer: function(name) { | |||
| if (this.mail[name]) { | |||
| return this.mail[name]; | |||
| } | |||
| if (config.mail[name]) { | |||
| return this.setMailer(name, config.mail[name]); | |||
| } | |||
| if (fs.existsSync(`app/modules/Mailer/${name}.json`)) { | |||
| let action = fs.readJSONSync(`app/modules/Mailer/${name}.json`); | |||
| return this.setMailer(name, action.options); | |||
| } | |||
| throw new Error(`Couldn't find mailer "${name}".`); | |||
| }, | |||
| setAuthProvider: async function(name, options) { | |||
| const Provider = require('../auth/' + options.provider.toLowerCase()); | |||
| const provider = new Provider(this, options, name); | |||
| if (!provider.identity) await provider.autoLogin(); | |||
| this.set('identity', provider.identity); | |||
| return this.auth[name] = provider; | |||
| }, | |||
| getAuthProvider: async function(name) { | |||
| if (this.auth[name]) { | |||
| return this.auth[name]; | |||
| } | |||
| if (config.auth[name]) { | |||
| return await this.setAuthProvider(name, config.auth[name]); | |||
| } | |||
| if (fs.existsSync(`app/modules/SecurityProviders/${name}.json`)) { | |||
| let action = fs.readJSONSync(`app/modules/SecurityProviders/${name}.json`); | |||
| return await this.setAuthProvider(name, action.options); | |||
| } | |||
| throw new Error(`Couldn't find security provider "${name}".`); | |||
| }, | |||
| setOAuthProvider: async function(name, options) { | |||
| const OAuth2 = require('../oauth'); | |||
| const services = require('../oauth/services'); | |||
| options = this.parse(options); | |||
| let service = this.parseOptional(options.service, 'string', null); | |||
| let opts = service ? services[service] : {}; | |||
| opts.client_id = this.parseOptional(options.client_id, 'string', null); | |||
| opts.client_secret = this.parseOptional(options.client_secret, 'string', null); | |||
| opts.token_endpoint = opts.token_endpoint || this.parseRequired(options.token_endpoint, 'string', 'oauth.provider: token_endpoint is required.'); | |||
| opts.auth_endpoint = opts.auth_endpoint || this.parseOptional(options.auth_endpoint, 'string', ''); | |||
| opts.scope_separator = opts.scope_separator || this.parseOptional(options.scope_separator, 'string', ' '); | |||
| opts.access_token = this.parseOptional(options.access_token, 'string', null); | |||
| opts.refresh_token = this.parseOptional(options.refresh_token, 'string', null); | |||
| opts.jwt_bearer = this.parseOptional(options.jwt_bearer, 'string', false); | |||
| opts.client_credentials = this.parseOptional(options.client_credentials, 'boolean', false); | |||
| opts.params = Object.assign({}, opts.params, this.parseOptional(options.params, 'object', {})); | |||
| this.oauth[name] = new OAuth2(this, this.parse(options), name); | |||
| await this.oauth[name].init(); | |||
| return this.oauth[name]; | |||
| }, | |||
| getOAuthProvider: async function(name) { | |||
| if (this.oauth[name]) { | |||
| return this.oauth[name]; | |||
| } | |||
| if (config.oauth[name]) { | |||
| return await this.setOAuthProvider(name, config.oauth[name]); | |||
| } | |||
| if (fs.existsSync(`app/modules/oauth/${name}.json`)) { | |||
| let action = fs.readJSONSync(`app/modules/oauth/${name}.json`); | |||
| return await this.setOAuthProvider(name, action.options); | |||
| } | |||
| throw new Error(`Couldn't find oauth provider "${name}".`); | |||
| }, | |||
| setDbConnection: function(name, options) { | |||
| if (global.db[name]) { | |||
| return global.db[name]; | |||
| } | |||
| options = this.parse(options); | |||
| switch (options.client) { | |||
| case 'couchdb': | |||
| const { host, port, user, password, database } = options.connection; | |||
| const nano = require('nano'); | |||
| global.db[name] = nano(`http://${user ? user + (password ? ':' + password : '') + '@' : ''}${host}${port ? ':' + port : ''}/${database}`); | |||
| global.db[name].client = options.client; | |||
| return global.db[name]; | |||
| case 'sqlite3': | |||
| if (options.connection.filename) { | |||
| options.connection.filename = toSystemPath(options.connection.filename); | |||
| } | |||
| break; | |||
| case 'mysql': | |||
| case 'mysql2': | |||
| options.connection = { | |||
| supportBigNumber: true, | |||
| dateStrings: !!options.tz, | |||
| ...options.connection | |||
| }; | |||
| break; | |||
| case 'mssql': | |||
| if (options.tz) { | |||
| // use local timezone to have same behavior as the other drivers | |||
| // to prevent problems node should have same timezone as database | |||
| options.connection.options = { useUTC: false, ...options.connection.options }; | |||
| } | |||
| break; | |||
| case 'postgres': | |||
| if (options.tz) { | |||
| const types = require('pg').types; | |||
| const parseFn = val => val; | |||
| types.setTypeParser(types.builtins.TIME, parseFn); | |||
| types.setTypeParser(types.builtins.TIMETZ, parseFn); | |||
| types.setTypeParser(types.builtins.TIMESTAMP, parseFn); | |||
| types.setTypeParser(types.builtins.TIMESTAMPTZ, parseFn); | |||
| } | |||
| break; | |||
| } | |||
| if (options.connection && options.connection.ssl) { | |||
| if (options.connection.ssl.key) { | |||
| options.connection.ssl.key = fs.readFileSync(toSystemPath(options.connection.ssl.key)); | |||
| } | |||
| if (options.connection.ssl.ca) { | |||
| options.connection.ssl.ca = fs.readFileSync(toSystemPath(options.connection.ssl.ca)); | |||
| } | |||
| if (options.connection.ssl.cert) { | |||
| options.connection.ssl.cert = fs.readFileSync(toSystemPath(options.connection.ssl.cert)); | |||
| } | |||
| } | |||
| options.useNullAsDefault = true; | |||
| const formatRecord = (record, meta) => { | |||
| for (column in record) { | |||
| if (record[column] != null) { | |||
| if (meta.has(column)) { | |||
| const info = meta.get(column); | |||
| if (['json', 'object', 'array'].includes(info.type)) { | |||
| if (typeof record[column] == 'string') { | |||
| try { | |||
| // column of type json returned as string, need parse | |||
| record[column] = JSON.parse(record[column]); | |||
| } catch (err) { | |||
| console.warn(err); | |||
| } | |||
| } | |||
| } | |||
| if (info.type == 'date') { | |||
| if (typeof record[column] == 'string' && record[column].startsWith('0000-00-00')) { | |||
| record[column] = undefined; | |||
| } else { | |||
| record[column] = formatDate(record[column], 'yyyy-MM-dd'); | |||
| } | |||
| } | |||
| if (info.type == 'time') { | |||
| if (options.tz == 'local') { | |||
| record[column] = formatDate(record[column], 'HH:mm:ss.v'); | |||
| } else if (options.tz == 'utc') { | |||
| record[column] = parseDate(parseDate(formatDate('now', 'yyyy-MM-dd ') + formatDate(record[column], 'HH:mm:ss.v'))).toISOString().slice(11); | |||
| } | |||
| } | |||
| if (['datetime', 'timestamp'].includes(info.type) || Object.prototype.toString.call(record[column]) == '[object Date]') { | |||
| if (typeof record[column] == 'string' && record[column].startsWith('0000-00-00')) { | |||
| record[column] = undefined; | |||
| } else if (options.tz == 'local') { | |||
| record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v'); | |||
| } else if (options.tz == 'utc') { | |||
| record[column] = parseDate(record[column]).toISOString(); | |||
| } | |||
| } | |||
| } else { | |||
| // try to detect datetime | |||
| if (Object.prototype.toString.call(record[column]) == '[object Date]') { | |||
| if (options.tz == 'local') { | |||
| record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v'); | |||
| } else if (options.tz == 'utc') { | |||
| record[column] = parseDate(record[column]).toISOString(); | |||
| } | |||
| } else if (typeof record[column] == 'string' && /\d{4}-\d{2}-\d{2}([T\s]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}(:\d{2})?)?)?/.test(record[column])) { | |||
| if (record[column].startsWith('0000-00-00')) { | |||
| record[column] = undefined; | |||
| } else if (options.tz == 'local') { | |||
| record[column] = formatDate(record[column], 'yyyy-MM-dd HH:mm:ss.v'); | |||
| } else if (options.tz == 'utc') { | |||
| record[column] = parseDate(record[column]).toISOString(); | |||
| } | |||
| } | |||
| } | |||
| if (record[column] == undefined) { | |||
| record[column] = null; | |||
| } else if (record[column].toJSON) { | |||
| record[column] = record[column].toJSON(); | |||
| } | |||
| } | |||
| } | |||
| return record; | |||
| }; | |||
| options.postProcessResponse = function(result, queryContext) { | |||
| const meta = new Map(); | |||
| if (Array.isArray(queryContext)) { | |||
| for (let item of queryContext) { | |||
| if (item.name && item.type) { | |||
| meta.set(item.name, item); | |||
| } | |||
| } | |||
| } | |||
| if (Array.isArray(result)) { | |||
| return result.map(record => formatRecord(record, meta)); | |||
| } else { | |||
| return formatRecord(result, meta); | |||
| } | |||
| }; | |||
| global.db[name] = db(options); | |||
| //this.res.on('finish', () => global.db[name].destroy()); | |||
| return global.db[name]; | |||
| }, | |||
| getDbConnection: function(name) { | |||
| if (this.__trx) return this.__trx; | |||
| if (global.db[name]) { | |||
| return global.db[name]; | |||
| } | |||
| if (config.db[name]) { | |||
| return this.setDbConnection(name, config.db[name]); | |||
| } | |||
| if (fs.existsSync(`app/modules/connections/${name}.json`)) { | |||
| let action = fs.readJSONSync(`app/modules/connections/${name}.json`); | |||
| return this.setDbConnection(name, action.options); | |||
| } | |||
| throw new Error(`Couldn't find database connection "${name}".`); | |||
| }, | |||
| setS3Provider: function(name, options) { | |||
| options = this.parse(options); | |||
| const { S3 } = require('@aws-sdk/client-s3'); | |||
| const endpoint = this.parseRequired(options.endpoint, 'string', 's3.provider: endpoint is required.'); | |||
| const accessKeyId = this.parseRequired(options.accessKeyId, 'string', 's3.provider: accessKeyId is required.'); | |||
| const secretAccessKey = this.parseRequired(options.secretAccessKey, 'string', 's3.provider: secretAccessKey is required.'); | |||
| let region = options.region || 'us-east-1'; | |||
| let pos = endpoint.indexOf('.amazonaws'); | |||
| if (pos > 3) region = endpoint.substr(3, pos - 3); | |||
| let forcePathStyle = options.forcePathStyle || false; | |||
| this.s3[name] = new S3({ endpoint: 'https://' + endpoint, credentials: { accessKeyId, secretAccessKey }, region, signatureVersion: 'v4', forcePathStyle }); | |||
| return this.s3[name]; | |||
| }, | |||
| getS3Provider: function(name) { | |||
| if (this.s3[name]) { | |||
| return this.s3[name]; | |||
| } | |||
| if (config.s3[name]) { | |||
| return this.setS3Provider(name, config.s3[name]); | |||
| } | |||
| if (fs.existsSync(`app/modules/s3/${name}.json`)) { | |||
| let action = fs.readJSONSync(`app/modules/s3/${name}.json`); | |||
| return this.setS3Provider(name, action.options); | |||
| } | |||
| throw new Error(`Couldn't find S3 provider "${name}".`); | |||
| }, | |||
| setJSONWebToken: function(name, options) { | |||
| const jwt = require('jsonwebtoken'); | |||
| options = this.parse(options); | |||
| let opts = {}; | |||
| if (options.alg) opts.algorithm = options.alg; | |||
| if (options.iss) opts.issuer = options.iss; | |||
| if (options.sub) opts.subject = options.sub; | |||
| if (options.aud) opts.audience = options.aud; | |||
| if (options.jti) opts.jwtid = options.jti; | |||
| if (options.expiresIn) opts.expiresIn = options.expiresIn | |||
| return this.jwt[name] = jwt.sign({ | |||
| ...options.claims | |||
| }, options.key, { | |||
| expiresIn: 3600, // use as default (required by google) | |||
| ...opts | |||
| }); | |||
| }, | |||
| getJSONWebToken: function(name) { | |||
| if (config.jwt[name]) { | |||
| return this.setJSONWebToken(name, config.jwt[name]); | |||
| } | |||
| if (fs.existsSync(`app/modules/jwt/${name}.json`)) { | |||
| let action = fs.readJSONSync(`app/modules/jwt/${name}.json`); | |||
| return this.setJSONWebToken(name, action.options); | |||
| } | |||
| throw new Error(`Couldn't find JSON Web Token "${name}".`); | |||
| }, | |||
| define: async function(cfg, internal) { | |||
| if (cfg.settings) { | |||
| this.settings = clone(cfg.settings); | |||
| } | |||
| if (cfg.vars) { | |||
| this.set(clone(cfg.vars)); | |||
| } | |||
| if (cfg.meta) { | |||
| this.meta = clone(cfg.meta); | |||
| await validator.init(this, this.meta); | |||
| } | |||
| if (fs.existsSync('app/modules/global.json')) { | |||
| await this.exec(await fs.readJSON('app/modules/global.json'), true); | |||
| } | |||
| /* | |||
| debug('body: %o', this.req.body); | |||
| debug('query: %o', this.req.query); | |||
| debug('params: %o', this.req.params); | |||
| debug('headers: %o', this.req.headers); | |||
| debug('cookies: %o', this.req.cookies); | |||
| debug('session: %o', this.req.session); | |||
| */ | |||
| await this.exec(cfg.exec || cfg, internal); | |||
| }, | |||
| sub: async function(actions, scope) { | |||
| const subApp = new App(this.req, this.res); | |||
| subApp.global = this.global; | |||
| subApp.scope = this.scope.create(scope); | |||
| await subApp.exec(actions, true); | |||
| return subApp.data; | |||
| }, | |||
| exec: async function(actions, internal) { | |||
| if (actions.exec) { | |||
| return this.exec(actions.exec, internal); | |||
| } | |||
| actions = clone(actions); | |||
| await this._exec(actions.steps || actions); | |||
| if (this.error !== false) { | |||
| if (actions.catch) { | |||
| this.scope.set('$_ERROR', this.error.message); | |||
| this.error = false; | |||
| await this._exec(actions.catch, true); | |||
| } else { | |||
| throw this.error; | |||
| } | |||
| } | |||
| if (!internal && !this.res.headersSent && !this.noOutput) { | |||
| this.res.json(this.data); | |||
| } | |||
| }, | |||
| _exec: async function(steps, ignoreAbort) { | |||
| if (this.res.headersSent) return; | |||
| if (typeof steps == 'string') { | |||
| return this.exec(await fs.readJSON(`app/modules/${steps}.json`), true); | |||
| } | |||
| if (this.res.headersSent) { | |||
| // do not execute other steps after headers has been sent | |||
| return; | |||
| } | |||
| if (Array.isArray(steps)) { | |||
| for (let step of steps) { | |||
| await this._exec(step); | |||
| if (this.error) return; | |||
| } | |||
| return; | |||
| } | |||
| if (steps.disabled) { | |||
| return; | |||
| } | |||
| if (steps.action) { | |||
| try { | |||
| let module; | |||
| if (fs.existsSync(`extensions/server_connect/modules/${steps.module}.js`)) { | |||
| module = require(`../../extensions/server_connect/modules/${steps.module}`); | |||
| } else if (fs.existsSync(`lib/modules/${steps.module}.js`)) { | |||
| module = require(`../modules/${steps.module}`); | |||
| } else { | |||
| throw new Error(`Module ${steps.module} doesn't exist`); | |||
| } | |||
| if (typeof module[steps.action] != 'function') { | |||
| throw new Error(`Action ${steps.action} doesn't exist in ${steps.module || 'core'}`); | |||
| } | |||
| debug(`Executing action step ${steps.action}`); | |||
| debug(`options: %O`, steps.options); | |||
| if (!ignoreAbort && config.abortOnDisconnect && this.req.isDisconnected) { | |||
| throw new Error('Aborted'); | |||
| } | |||
| const data = await module[steps.action].call(this, clone(steps.options), steps.name, steps.meta); | |||
| if (data instanceof Error) { | |||
| throw data; | |||
| } | |||
| if (steps.name) { | |||
| this.scope.set(steps.name, data); | |||
| if (steps.output) { | |||
| this.data[steps.name] = data; | |||
| } | |||
| } | |||
| } catch (e) { | |||
| this.error = e; | |||
| return; | |||
| } | |||
| } | |||
| }, | |||
| parse: function(value, scope) { | |||
| return Parser.parseValue(value, scope || this.scope); | |||
| }, | |||
| parseRequired: function(value, type, err) { | |||
| if (value === undefined) { | |||
| throw new Error(err); | |||
| } | |||
| let val = Parser.parseValue(value, this.scope); | |||
| if (type == '*') { | |||
| if (val === undefined) { | |||
| throw new Error(err); | |||
| } | |||
| } else if (type == 'boolean') { | |||
| val = !!val; | |||
| } else if (typeof val != type) { | |||
| throw new Error(err); | |||
| } | |||
| return val; | |||
| }, | |||
| parseOptional: function(value, type, def) { | |||
| if (value === undefined) return def; | |||
| let val = Parser.parseValue(value, this.scope); | |||
| if (type == '*') { | |||
| if (val === undefined) val = def; | |||
| } else if (type == 'boolean') { | |||
| if (val === undefined) { | |||
| val = def; | |||
| } else { | |||
| val = !!val; | |||
| } | |||
| } else if (typeof val != type) { | |||
| val = def; | |||
| } | |||
| return val; | |||
| }, | |||
| parseSQL: function(sql) { | |||
| if (!sql) return null; | |||
| ['values', 'orders'].forEach((prop) => { | |||
| if (Array.isArray(sql[prop])) { | |||
| sql[prop] = sql[prop].filter((value) => { | |||
| if (!value.condition) return true; | |||
| return !!Parser.parseValue(value.condition, this.scope); | |||
| }); | |||
| } | |||
| }); | |||
| if (sql.wheres && sql.wheres.rules) { | |||
| if (sql.wheres.conditional && !Parser.parseValue(sql.wheres.conditional, this.scope)) { | |||
| delete sql.wheres; | |||
| } else { | |||
| sql.wheres.rules = sql.wheres.rules.filter(function filterConditional(rule) { | |||
| if (!rule.rules) return true; | |||
| if (rule.conditional && !Parser.parseValue(rule.conditional, this.scope)) return false; | |||
| rule.rules = rule.rules.filter(filterConditional, this); | |||
| return rule.rules.length; | |||
| }, this); | |||
| if (!sql.wheres.rules.length) { | |||
| delete sql.wheres; | |||
| } | |||
| } | |||
| } | |||
| if (sql.sub) { | |||
| for (const name in sql.sub) { | |||
| sql.sub[name] = this.parseSQL(sql.sub[name]); | |||
| } | |||
| } | |||
| return Parser.parseValue(sql, this.scope); | |||
| }, | |||
| }; | |||
| module.exports = App; | |||
| @@ -0,0 +1,97 @@ | |||
| function defered() { | |||
| let resolve, reject; | |||
| let promise = new Promise((_resolve, _reject) => { | |||
| resolve = _resolve; | |||
| reject = _reject; | |||
| }); | |||
| return { | |||
| promise, | |||
| resolve, | |||
| reject | |||
| }; | |||
| } | |||
| function limit(concurrency) { | |||
| let running = 0; | |||
| let queue = []; | |||
| function init() { | |||
| if (running < concurrency) { | |||
| running++; | |||
| return Promise.resolve(); | |||
| } | |||
| let defer = defered(); | |||
| queue.push(defer); | |||
| return defer.promise; | |||
| } | |||
| function complete(a) { | |||
| let next = queue.shift(); | |||
| if (next) { | |||
| next.resolve(); | |||
| } else { | |||
| running--; | |||
| } | |||
| return a; | |||
| } | |||
| return function(fn) { | |||
| return function() { | |||
| let args = Array.prototype.slice.apply(arguments); | |||
| return init().then(() => { | |||
| return fn.apply(null, args); | |||
| }).finally(complete); | |||
| } | |||
| } | |||
| } | |||
| module.exports = { | |||
| map: function(arr, fn, concurrency) { | |||
| arr = Array.isArray(arr) ? arr : [arr]; | |||
| if (concurrency) { | |||
| const limiter = limit(concurrency) | |||
| return Promise.all(arr.map(limiter(fn))); | |||
| } | |||
| return Promise.all(arr.map(fn)); | |||
| }, | |||
| mapSeries: function(arr, fn) { | |||
| arr = Array.isArray(arr) ? arr : [arr]; | |||
| return arr.reduce((promise, curr, index, arr) => { | |||
| return promise.then(prev => { | |||
| return fn(curr, index, arr).then(val => { | |||
| prev.push(val); | |||
| return prev; | |||
| }); | |||
| }) | |||
| }, Promise.resolve([])); | |||
| }, | |||
| reduce: function(arr, fn, start) { | |||
| arr = Array.isArray(arr) ? arr : [arr]; | |||
| if (!arr.length) { | |||
| return Promise.resolve(start); | |||
| } | |||
| return arr.reduce((promise, curr, index, arr) => { | |||
| return promise.then(prev => { | |||
| if (prev === undefined && arr.length === 1) { | |||
| return curr; | |||
| } | |||
| return fn(prev, curr, index, arr); | |||
| }); | |||
| }, Promise.resolve(start)); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,43 @@ | |||
| // Code taken from https://github.com/jshttp/basic-auth/blob/master/index.js | |||
| // Copyright(c) 2013 TJ Holowaychuk | |||
| // Copyright(c) 2014 Jonathan Ong | |||
| // Copyright(c) 2015-2016 Douglas Christopher Wilson | |||
| module.exports = auth; | |||
| module.exports.parse = parse; | |||
| const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/; | |||
| const USER_PASS_REGEXP = /^([^:]*):(.*)$/; | |||
| function auth(req) { | |||
| if (!req) throw new Error('argument req is required.'); | |||
| if (typeof req != 'object') throw new Error('argument req needs to be an object.'); | |||
| return parse(getAuthorization(req)); | |||
| } | |||
| function decodeBase64(str) { | |||
| return Buffer.from(str, 'base64').toString(); | |||
| } | |||
| function getAuthorization(req) { | |||
| if (!req.headers || typeof req.headers != 'object') { | |||
| throw new Error('argument req needs to have headers.'); | |||
| } | |||
| return req.headers.authorization; | |||
| } | |||
| function parse(str) { | |||
| if (typeof str != 'string') { | |||
| return undefined; | |||
| } | |||
| const match = CREDENTIALS_REGEXP.exec(str); | |||
| if (!match) return undefined; | |||
| const userPass = USER_PASS_REGEXP.exec(decodeBase64(match[1])); | |||
| if (!userPass) return undefined; | |||
| return { username: userPass[1], password: userPass[2] }; | |||
| } | |||
| @@ -0,0 +1,210 @@ | |||
| const knex = require('knex'); | |||
| knex.QueryBuilder.extend('whereGroup', function(condition, rules) { | |||
| condition = condition.toLowerCase(); | |||
| // TODO: Escape likes | |||
| for (let rule of rules) { | |||
| if (!rule.condition) { | |||
| let where = condition == 'or' ? 'orWhere' : 'where'; | |||
| let column = rule.data ? rule.data.column : rule.column; | |||
| if (rule.data && rule.data.table) { | |||
| if (rule.data.schema) { | |||
| column = rule.data.schema + '.' + rule.data.table + '.' + column; | |||
| } else { | |||
| column = rule.data.table + '.' + column; | |||
| } | |||
| } else if (rule.table) { | |||
| if (rule.schema) { | |||
| column = rule.schema + '.' + rule.table + '.' + column; | |||
| } else { | |||
| column = rule.table + '.' + column; | |||
| } | |||
| } | |||
| if (typeof rule.value == 'undefined') { | |||
| rule.value = null; | |||
| } | |||
| if (rule.operator == 'between') { | |||
| this[where + 'Between'](column, rule.value); | |||
| } else if (rule.operator == 'not_between') { | |||
| this[where + 'NotBetween'](column, rule.value); | |||
| } else if (rule.operator == 'is_null') { | |||
| this[where + 'Null'](column); | |||
| } else if (rule.operator == 'is_not_null') { | |||
| this[where + 'NotNull'](column); | |||
| } else if (rule.operator == 'in') { | |||
| this[where + 'In'](column, rule.value); | |||
| } else if (rule.operator == 'not_in') { | |||
| this[where + 'NotIn'](column, rule.value); | |||
| } else if (rule.operator == 'begins_with') { | |||
| this[where](column, 'like', rule.value + '%'); | |||
| } else if (rule.operator == 'not_begins_with') { | |||
| this[where + 'Not'](column, 'like', rule.value + '%'); | |||
| } else if (rule.operator == 'ends_with') { | |||
| this[where](column, 'like', '%' + rule.value); | |||
| } else if (rule.operator == 'not_ends_with') { | |||
| this[where + 'Not'](column, 'like', '%' + rule.value); | |||
| } else if (rule.operator == 'contains') { | |||
| this[where](column, 'like', '%' + rule.value + '%'); | |||
| } else if (rule.operator == 'not_contains') { | |||
| this[where + 'Not'](column, 'like', '%' + rule.value + '%'); | |||
| } else { | |||
| this[where](column, rule.operation, rule.value); | |||
| } | |||
| } else { | |||
| this[condition + 'Where'](function() { | |||
| this.whereGroup(rule.condition, rule.rules); | |||
| }); | |||
| } | |||
| } | |||
| return this; | |||
| }); | |||
| knex.QueryBuilder.extend('fromJSON', function(ast, meta) { | |||
| if (ast.type == 'count') { | |||
| return this.count('* as Total').from(function() { | |||
| this.fromJSON(Object.assign({}, ast, { | |||
| type: 'select', | |||
| offset: null, | |||
| limit: null, | |||
| orders: null | |||
| })).as('t1'); | |||
| }).first(); | |||
| } else if (ast.type == 'insert' || ast.type == 'update') { | |||
| let values = {}; | |||
| for (let val of ast.values) { | |||
| if (val.type == 'json') { | |||
| // json support | |||
| values[val.column] = JSON.stringify(val.value); | |||
| } else { | |||
| values[val.column] = val.value; | |||
| } | |||
| } | |||
| this[ast.type](values); | |||
| } else { | |||
| this[ast.type](); | |||
| } | |||
| if (ast.returning) { | |||
| // Adding the option includeTriggerModifications allows you | |||
| // to run statements on tables that contain triggers. Only affects MSSQL. | |||
| this.returning(ast.returning, { includeTriggerModifications: true }); | |||
| } | |||
| if (ast.table) { | |||
| let table = ast.table.name || ast.table; | |||
| if (ast.table.schema) { | |||
| table = ast.table.schema + '.' + table; | |||
| } | |||
| if (ast.table.alias) { | |||
| table += ' as ' + ast.table.alias; | |||
| } | |||
| this.from(table); | |||
| } | |||
| if ((ast.type == 'select' || ast.type == 'first') && ast.joins && ast.joins.length) { | |||
| for (let join of ast.joins) { | |||
| let table = join.table; | |||
| if (join.schema) { | |||
| table = join.schema + '.' + table; | |||
| } | |||
| if (join.alias) { | |||
| table += ' as ' + join.alias; | |||
| } | |||
| this[(join.type || 'inner').toLowerCase() + 'Join'](table, function() { | |||
| for (let clause of join.clauses.rules) { | |||
| this.on( | |||
| (clause.schema ? clause.schema + '.' : '') + clause.table + '.' + clause.column, | |||
| clause.operation, | |||
| (clause.value.schema ? clause.value.schema + '.' : '') + clause.value.table + '.' + clause.value.column | |||
| ); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| if ((ast.type == 'select' || ast.type == 'first') && ast.columns) { | |||
| for (let col of ast.columns) { | |||
| let column = col.column || col; | |||
| if (ast.joins && ast.joins.length && col.table) { | |||
| column = col.table + '.' + column; | |||
| if (col.schema) { | |||
| column = col.schema + '.' + column; | |||
| } | |||
| } | |||
| if (col.alias) { | |||
| column += ' as ' + col.alias; | |||
| } | |||
| this[col.aggregate ? col.aggregate.toLowerCase() : 'column'](column); | |||
| } | |||
| } | |||
| if ((ast.type == 'select' || ast.type == 'first') && ast.groupBy && ast.groupBy.length) { | |||
| for (let col of ast.groupBy) { | |||
| if (ast.joins && ast.joins.length && col.table) { | |||
| if (col.schema) { | |||
| this.groupBy(col.schema + '.' + col.table + '.' + col.column); | |||
| } else { | |||
| this.groupBy(col.table + '.' + col.column); | |||
| } | |||
| } else { | |||
| this.groupBy(col.column || col); | |||
| } | |||
| } | |||
| } | |||
| if (ast.wheres && ast.wheres.condition) { | |||
| this.whereGroup(ast.wheres.condition, ast.wheres.rules); | |||
| } | |||
| if ((ast.type == 'select' || ast.type == 'first') && ast.orders && ast.orders.length) { | |||
| for (let order of ast.orders) { | |||
| if (ast.joins && ast.joins.length && order.table) { | |||
| if (order.schema) { | |||
| this.orderBy(order.schema + '.' + order.table + '.' + order.column, order.direction); | |||
| } else { | |||
| this.orderBy(order.table + '.' + order.column, order.direction); | |||
| } | |||
| } else { | |||
| this.orderBy(order.column, order.direction); | |||
| } | |||
| } | |||
| } | |||
| if (ast.distinct) { | |||
| this.distinct(); | |||
| } | |||
| if (ast.type == 'select' && ast.limit) { | |||
| this.limit(ast.limit); | |||
| } | |||
| if (ast.type == 'select' && ast.offset) { | |||
| this.offset(ast.offset); | |||
| } | |||
| if (meta) { | |||
| this.queryContext(meta); | |||
| } | |||
| return this; | |||
| }); | |||
| module.exports = knex; | |||
| @@ -0,0 +1,96 @@ | |||
| const diacriticsMap = [ | |||
| {'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g}, | |||
| {'base':'AA','letters':/[\uA732]/g}, | |||
| {'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g}, | |||
| {'base':'AO','letters':/[\uA734]/g}, | |||
| {'base':'AU','letters':/[\uA736]/g}, | |||
| {'base':'AV','letters':/[\uA738\uA73A]/g}, | |||
| {'base':'AY','letters':/[\uA73C]/g}, | |||
| {'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g}, | |||
| {'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g}, | |||
| {'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g}, | |||
| {'base':'DZ','letters':/[\u01F1\u01C4]/g}, | |||
| {'base':'Dz','letters':/[\u01F2\u01C5]/g}, | |||
| {'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g}, | |||
| {'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g}, | |||
| {'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g}, | |||
| {'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g}, | |||
| {'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g}, | |||
| {'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g}, | |||
| {'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g}, | |||
| {'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g}, | |||
| {'base':'LJ','letters':/[\u01C7]/g}, | |||
| {'base':'Lj','letters':/[\u01C8]/g}, | |||
| {'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g}, | |||
| {'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g}, | |||
| {'base':'NJ','letters':/[\u01CA]/g}, | |||
| {'base':'Nj','letters':/[\u01CB]/g}, | |||
| {'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g}, | |||
| {'base':'OI','letters':/[\u01A2]/g}, | |||
| {'base':'OO','letters':/[\uA74E]/g}, | |||
| {'base':'OU','letters':/[\u0222]/g}, | |||
| {'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g}, | |||
| {'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g}, | |||
| {'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g}, | |||
| {'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g}, | |||
| {'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g}, | |||
| {'base':'TZ','letters':/[\uA728]/g}, | |||
| {'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g}, | |||
| {'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g}, | |||
| {'base':'VY','letters':/[\uA760]/g}, | |||
| {'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g}, | |||
| {'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g}, | |||
| {'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g}, | |||
| {'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g}, | |||
| {'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g}, | |||
| {'base':'aa','letters':/[\uA733]/g}, | |||
| {'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g}, | |||
| {'base':'ao','letters':/[\uA735]/g}, | |||
| {'base':'au','letters':/[\uA737]/g}, | |||
| {'base':'av','letters':/[\uA739\uA73B]/g}, | |||
| {'base':'ay','letters':/[\uA73D]/g}, | |||
| {'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g}, | |||
| {'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g}, | |||
| {'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g}, | |||
| {'base':'dz','letters':/[\u01F3\u01C6]/g}, | |||
| {'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g}, | |||
| {'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g}, | |||
| {'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g}, | |||
| {'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g}, | |||
| {'base':'hv','letters':/[\u0195]/g}, | |||
| {'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g}, | |||
| {'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g}, | |||
| {'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g}, | |||
| {'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g}, | |||
| {'base':'lj','letters':/[\u01C9]/g}, | |||
| {'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g}, | |||
| {'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g}, | |||
| {'base':'nj','letters':/[\u01CC]/g}, | |||
| {'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g}, | |||
| {'base':'oi','letters':/[\u01A3]/g}, | |||
| {'base':'ou','letters':/[\u0223]/g}, | |||
| {'base':'oo','letters':/[\uA74F]/g}, | |||
| {'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g}, | |||
| {'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g}, | |||
| {'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g}, | |||
| {'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g}, | |||
| {'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g}, | |||
| {'base':'tz','letters':/[\uA729]/g}, | |||
| {'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g}, | |||
| {'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g}, | |||
| {'base':'vy','letters':/[\uA761]/g}, | |||
| {'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g}, | |||
| {'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g}, | |||
| {'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g}, | |||
| {'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g} | |||
| ]; | |||
| module.exports = { | |||
| replace: function(str) { | |||
| for (let change of diacriticsMap) { | |||
| str = str.replace(change.letters, change.base); | |||
| } | |||
| return str; | |||
| } | |||
| }; | |||
| @@ -0,0 +1,92 @@ | |||
| module.exports = function(session) { | |||
| const Store = session.Store | |||
| const defer = (cb, ...args) => { | |||
| if (typeof cb != 'function') return | |||
| setImmediate(cb, ...args) | |||
| } | |||
| class ExtendedMap extends Map { | |||
| set(key, value, maxAge) { | |||
| const cached = super.get(key) | |||
| if (cached && cached.timer) clearTimeout(cached.timer) | |||
| const timer = maxAge ? setTimeout(super.delete.bind(this, key), maxAge) : null | |||
| return super.set(key, {timer, value}) | |||
| } | |||
| get(key) { | |||
| const cached = super.get(key) | |||
| return cached && cached.value | |||
| } | |||
| delete(key) { | |||
| const cached = super.get(key) | |||
| if (cached && cached.timer) clearTimeout(cached.timer) | |||
| return super.delete(key) | |||
| } | |||
| } | |||
| class MemoryStore extends Store { | |||
| constructor(options = {}) { | |||
| super(options) | |||
| this.sessions = new ExtendedMap() | |||
| this.ttl = options.ttl | |||
| } | |||
| get(sid, callback) { | |||
| let session = this.sessions.get(sid) | |||
| defer(callback, null, session) | |||
| } | |||
| set(sid, session, callback) { | |||
| let ttl = this._getTTL(session) | |||
| if (ttl > 0) { | |||
| this.sessions.set(sid, session, ttl) | |||
| } else { | |||
| this.sessions.delete(sid) | |||
| } | |||
| defer(callback, null) | |||
| } | |||
| touch(sid, session, callback) { | |||
| let ttl = this._getTTL(session) | |||
| let stored = this.sessions.get(sid) | |||
| stored.cookie = session.cookie | |||
| this.sessions.set(sid, stored, ttl) | |||
| defer(callback, null) | |||
| } | |||
| destroy(sid, callback) { | |||
| this.sessions.delete(sid) | |||
| defer(callback, null) | |||
| } | |||
| clear(callback) { | |||
| this.sessions.clear() | |||
| defer(callback, null) | |||
| } | |||
| length(callback) { | |||
| let len = this.sessions.size | |||
| defer(callback, null, len) | |||
| } | |||
| ids(callback) { | |||
| let keys = Array.from(this.sessions.keys()) | |||
| defer(callback, null, keys) | |||
| } | |||
| all(callback) { | |||
| let sessions = Array.from(this.sessions.values()) | |||
| defer(callback, null, sessions) | |||
| } | |||
| _getTTL(session) { | |||
| if (typeof this.ttl == 'number') return this.ttl | |||
| let maxAge = (session && session.cookie) ? session.cookie.maxAge : null | |||
| return (typeof maxAge == 'number') ? maxAge : 86400000 | |||
| } | |||
| } | |||
| return MemoryStore | |||
| } | |||
| @@ -0,0 +1,151 @@ | |||
| const config = require('../setup/config'); | |||
| const debug = require('debug')('server-connect:router'); | |||
| const { clone } = require('./util'); | |||
| const App = require('./app'); | |||
| module.exports = { | |||
| cache: function(options) { | |||
| return async function(req, res, next) { | |||
| if (!options.ttl || !global.redisClient) { | |||
| // no caching | |||
| return next(); | |||
| } | |||
| let key = 'erc:' + (req.originalUrl || req.url); | |||
| let nocache = false; | |||
| let nostore = false; | |||
| if (req.fragment) { | |||
| key += '-fragment'; | |||
| } | |||
| if (req.query.nocache) { | |||
| nocache = true; | |||
| } | |||
| try { | |||
| const cacheControl = req.get('Cache-Control'); | |||
| if (cacheControl) { | |||
| if (cacheControl.includes('no-cache')) nocache = true; | |||
| if (cacheControl.includes('no-store')) nostore = true; | |||
| } | |||
| } catch (err) { | |||
| // Ignore but log errors | |||
| console.error(err); | |||
| } | |||
| if (!nocache) { | |||
| try { | |||
| const cache = await global.redisClient.hGetAll(key); | |||
| if (cache.body) { | |||
| res.type(cache.type || 'text/html'); | |||
| res.send(cache.body); | |||
| return; | |||
| } | |||
| } catch (err) { | |||
| // Ignore but log errors | |||
| console.error(err); | |||
| } | |||
| } | |||
| if (!nostore) { | |||
| // wrap res.send | |||
| const send = res.send.bind(res); | |||
| res.send = function (body) { | |||
| const ret = send(body); | |||
| if (this.statusCode !== 200 || typeof body !== 'string') { | |||
| // do not cache when not status 200 or body is not a string | |||
| return ret; | |||
| } | |||
| global.redisClient.hSet(key, { | |||
| body: body, | |||
| type: this.get('Content-Type') || 'text/html' | |||
| }).then(() => { | |||
| return global.redisClient.expire(key, +options.ttl); | |||
| }).catch(err => { | |||
| // Ignore but log errors | |||
| console.error(err); | |||
| }); | |||
| return ret; | |||
| }; | |||
| } | |||
| return next(); | |||
| }; | |||
| }, | |||
| serverConnect: function(json) { | |||
| return async function(req, res, next) { | |||
| const app = new App(req, res); | |||
| debug(`Serving serverConnect ${req.path}`); | |||
| return Promise.resolve(app.define(json)).catch(next); | |||
| }; | |||
| }, | |||
| templateView: function(layout, page, data, exec) { | |||
| return async function(req, res, next) { | |||
| const app = new App(req, res); | |||
| debug(`Serving templateView ${req.path}`); | |||
| const routeData = clone(data); | |||
| const methods = { | |||
| _: (expr, data = {}) => { | |||
| return app.parse(`{{${expr}}}`, app.scope.create(data)); | |||
| }, | |||
| _exec: async (name, steps, data = {}) => { | |||
| let context = {}; | |||
| app.scope = app.scope.create(data, context); | |||
| await app.exec(steps, true); | |||
| app.scope = app.scope.parent; | |||
| app.set(name, context); | |||
| return context; | |||
| }, | |||
| _repeat: (expr, cb) => { | |||
| let data = app.parse(`{{${expr}}}`); | |||
| if (Array.isArray(data)) return data.forEach(cb); | |||
| return ''; | |||
| }, | |||
| _route: req.route.path | |||
| }; | |||
| let template = page; | |||
| if (layout && !req.fragment) { | |||
| routeData.content = '/' + page; | |||
| template = 'layouts/' + layout; | |||
| } | |||
| if (exec) { | |||
| return Promise.resolve(app.define(exec, true)).then(() => { | |||
| if (!res.headersSent) { | |||
| app.set(app.parse(routeData)); | |||
| debug(`Render template ${template}`); | |||
| debug(`Template data: %O`, Object.assign({}, app.global.data, app.data)); | |||
| res.render(template, Object.assign({}, app.global.data, app.data, methods), async (err, html) => { | |||
| // callback is needed when using ejs with aync, html is a promise | |||
| if (err) return next(err); | |||
| html.then(html => res.send(html)).catch(err => next(err)); | |||
| }); | |||
| } | |||
| }).catch(next) | |||
| } else { | |||
| app.set(app.parse(routeData)); | |||
| debug(`Render template ${template}`); | |||
| debug(`Template data: %O`, app.global.data); | |||
| res.render(template, Object.assign({}, app.global.data, methods), async (err, html) => { | |||
| // callback is needed when using ejs with aync, html is a promise | |||
| if (err) return next(err); | |||
| html.then(html => res.send(html)).catch(err => next(err)); | |||
| }); | |||
| } | |||
| }; | |||
| } | |||
| }; | |||
| @@ -0,0 +1,752 @@ | |||
| const fs = require('fs-extra'); | |||
| const path = require('path'); | |||
| const { randomUUID } = require('crypto'); | |||
| const { v4: uuidv4 } = require('uuid'); | |||
| const NOOP = function() {}; | |||
| const OPERATORS = { | |||
| '{' : 'L_CURLY', | |||
| '}' : 'R_CURLY', | |||
| '(' : 'L_PAREN', | |||
| ')' : 'R_PAREN', | |||
| '[' : 'L_BRACKET', | |||
| ']' : 'R_BRACKET', | |||
| '.' : 'PERIOD', | |||
| ',' : 'COMMA', | |||
| ':' : 'COLON', | |||
| '?' : 'QUESTION', | |||
| // Arithmetic operators | |||
| '+' : 'ADDICTIVE', | |||
| '-' : 'ADDICTIVE', | |||
| '*' : 'MULTIPLICATIVE', | |||
| '/' : 'MULTIPLICATIVE', | |||
| '%' : 'MULTIPLICATIVE', | |||
| // Comparison operators | |||
| '===': 'EQUALITY', | |||
| '!==': 'EQUALITY', | |||
| '==' : 'EQUALITY', | |||
| '!=' : 'EQUALITY', | |||
| '<' : 'RELATIONAL', | |||
| '>' : 'RELATIONAL', | |||
| '<=' : 'RELATIONAL', | |||
| '>=' : 'RELATIONAL', | |||
| 'in' : 'RELATIONAL', | |||
| // Logical operators | |||
| '&&' : 'LOGICAL_AND', | |||
| '||' : 'LOGICAL_OR', | |||
| '!' : 'LOGICAL_NOT', | |||
| // Bitwise operators | |||
| '&' : 'BITWISE_AND', | |||
| '|' : 'BITWISE_OR', | |||
| '^' : 'BITWISE_XOR', | |||
| '~' : 'BITWISE_NOT', | |||
| '<<' : 'BITWISE_SHIFT', | |||
| '>>' : 'BITWISE_SHIFT', | |||
| '>>>': 'BITWISE_SHIFT' | |||
| }; | |||
| const EXPRESSIONS = { | |||
| 'in' : function(a, b) { return a() in b(); }, | |||
| '?' : function(a, b, c) { return a() ? b() : c(); }, | |||
| '+' : function(a, b) { a = a(); b = b(); return a == null ? b : b == null ? a : a + b; }, | |||
| '-' : function(a, b) { return a() - b(); }, | |||
| '*' : function(a, b) { return a() * b(); }, | |||
| '/' : function(a, b) { return a() / b(); }, | |||
| '%' : function(a, b) { return a() % b(); }, | |||
| '===': function(a, b) { return a() === b(); }, | |||
| '!==': function(a, b) { return a() !== b(); }, | |||
| '==' : function(a, b) { return a() == b(); }, | |||
| '!=' : function(a, b) { return a() != b(); }, | |||
| '<' : function(a, b) { return a() < b(); }, | |||
| '>' : function(a, b) { return a() > b(); }, | |||
| '<=' : function(a, b) { return a() <= b(); }, | |||
| '>=' : function(a, b) { return a() >= b(); }, | |||
| '&&' : function(a, b) { return a() && b(); }, | |||
| '||' : function(a, b) { return a() || b(); }, | |||
| '&' : function(a, b) { return a() & b(); }, | |||
| '|' : function(a, b) { return a() | b(); }, | |||
| '^' : function(a, b) { return a() ^ b(); }, | |||
| '<<' : function(a, b) { return a() << b(); }, | |||
| '>>' : function(a, b) { return a() >> b(); }, | |||
| '>>>': function(a, b) { return a() >>> b(); }, | |||
| '~' : function(a) { return ~a(); }, | |||
| '!' : function(a) { return !a(); } | |||
| }; | |||
| const ESCAPE = { | |||
| 'n': '\n', | |||
| 'f': '\f', | |||
| 'r': '\r', | |||
| 't': '\t', | |||
| 'v': '\v', | |||
| "'": "'", | |||
| '"': '"', | |||
| '`': '`' | |||
| }; | |||
| const formatters = require('../formatters'); | |||
| // User formatters | |||
| if (fs.existsSync('extensions/server_connect/formatters')) { | |||
| const files = fs.readdirSync('extensions/server_connect/formatters'); | |||
| for (let file of files) { | |||
| if (path.extname(file) == '.js') { | |||
| Object.assign(formatters, require(`../../extensions/server_connect/formatters/${file}`)); | |||
| } | |||
| } | |||
| } | |||
| function lexer(expr) { | |||
| let tokens = [], | |||
| token, | |||
| name, | |||
| start, | |||
| index = 0, | |||
| op = true, | |||
| ch, | |||
| ch2, | |||
| ch3; | |||
| while (index < expr.length) { | |||
| start = index; | |||
| ch = read(); | |||
| if (isQuote(ch) && op) { | |||
| name = 'STRING'; | |||
| token = readString(ch); | |||
| op = false; | |||
| } else if ((isDigid(ch) || (is('.') && peek() && isDigid(peek()))) && op) { | |||
| name = 'NUMBER'; | |||
| token = readNumber(); | |||
| op = false; | |||
| } else if (isAlpha(ch) && op) { | |||
| name = 'IDENT'; | |||
| token = readIdent(); | |||
| if (is('(')) { | |||
| name = 'METHOD'; | |||
| } | |||
| op = false; | |||
| } else if (is('/') && op && (token == '(' || token == ',' || token == '?' || token == ':') && testRegexp()) { | |||
| name = 'REGEXP'; | |||
| token = readRegexp(); | |||
| op = false; | |||
| } else if (isWhitespace(ch)) { | |||
| index++; | |||
| continue; | |||
| } else if ((ch3 = read(3)) && OPERATORS[ch3]) { | |||
| name = OPERATORS[ch3]; | |||
| token = ch3; | |||
| op = true; | |||
| index += 3; | |||
| } else if ((ch2 = read(2)) && OPERATORS[ch2]) { | |||
| name = OPERATORS[ch2]; | |||
| token = ch2; | |||
| op = true; | |||
| index += 2; | |||
| } else if (OPERATORS[ch]) { | |||
| name = OPERATORS[ch]; | |||
| token = ch; | |||
| op = true; | |||
| index++; | |||
| } else { | |||
| throw new Error('Lexer Error: Unexpected token "' + ch + '" at column ' + index + ' in expression {{' + expr + '}}'); | |||
| } | |||
| tokens.push({ | |||
| name: name, | |||
| index: start, | |||
| value: token | |||
| }); | |||
| } | |||
| return tokens; | |||
| function read(n) { | |||
| if (!n) n = 1; | |||
| return expr.substr(index, n); | |||
| } | |||
| function peek(n) { | |||
| n = n || 1; | |||
| return index + n < expr.length ? expr[index + n] : false; | |||
| } | |||
| function is(chars) { | |||
| return chars.indexOf(ch) != -1; | |||
| } | |||
| function isQuote(ch) { | |||
| return ch == '"' || ch == "'"; | |||
| } | |||
| function isDigid(ch) { | |||
| return ch >= '0' && ch <= '9'; | |||
| } | |||
| function isAlpha(ch) { | |||
| return (ch >= 'a' && ch <= 'z') || | |||
| (ch >= 'A' && ch <= 'Z') || | |||
| ch == '_' || ch == '$'; | |||
| } | |||
| function isAlphaNum(ch) { | |||
| return isAlpha(ch) || isDigid(ch); | |||
| } | |||
| function isWhitespace(ch) { | |||
| return ch == ' ' || ch == '\r' || ch == '\t' || ch == '\n' || ch == '\v' || ch == '\u00A0'; | |||
| } | |||
| function isExpOperator(ch) { | |||
| return ch == '-' || ch == '+' || isDigid(ch); | |||
| } | |||
| function readString(quote) { | |||
| let str = '', esc = false; | |||
| index++; | |||
| while (index < expr.length) { | |||
| ch = read(); | |||
| if (esc) { | |||
| if (ch == 'u') { | |||
| // unicode escape | |||
| index++; | |||
| let hex = read(4); | |||
| if (!hex.match(/[\da-f]{4}/i)) { | |||
| throw new Error('Lexer Error: Invalid unicode escape at column ' + index + ' in expression {{' + expr + '}}'); | |||
| } | |||
| str += String.fromCharCode(parseInt(hex, 16)); | |||
| index += 3; | |||
| } else { | |||
| str += ESCAPE[ch] ? ESCAPE[ch] : ch; | |||
| } | |||
| esc = false; | |||
| } else if (ch == '\\') { | |||
| // escape character | |||
| esc = true; | |||
| } else if (ch == quote) { | |||
| // end of string | |||
| index ++; | |||
| return str; | |||
| } else { | |||
| str += ch; | |||
| } | |||
| index++; | |||
| } | |||
| throw new Error('Lexer Error: Unterminated string at column ' + index + ' in expression {{' + expr + '}}'); | |||
| } | |||
| function readNumber() { | |||
| let num = '', exp = false; | |||
| while (index < expr.length) { | |||
| ch = read(); | |||
| if (isDigid(ch) || (is('.') && peek() && isDigid(peek()))) { | |||
| num += ch; | |||
| } else { | |||
| let next = peek(); | |||
| if (is('eE') && isExpOperator(next)) { | |||
| num += 'e'; | |||
| exp = true; | |||
| } else if (isExpOperator(ch) && next && isDigid(next) && exp) { | |||
| num += ch; | |||
| exp = false; | |||
| } else if (isExpOperator(ch) && (!next || !isDigid(next)) && exp) { | |||
| throw new Error('Lexer Error: Invalid exponent at column ' + index + ' in expression {{' + expr + '}}'); | |||
| } else { | |||
| break; | |||
| } | |||
| } | |||
| index++; | |||
| } | |||
| return +num; | |||
| } | |||
| function readIdent() { | |||
| let ident = ''; | |||
| while (index < expr.length) { | |||
| ch = read(); | |||
| if (isAlphaNum(ch)) { | |||
| ident += ch; | |||
| } else { | |||
| break; | |||
| } | |||
| index++; | |||
| } | |||
| return ident; | |||
| } | |||
| function readRegexp() { | |||
| let re = '', mod = '', esc = false; | |||
| index ++; | |||
| while (index < expr.length) { | |||
| ch = read(); | |||
| if (esc) { | |||
| esc = false; | |||
| } else if (ch == '\\') { | |||
| esc = true; | |||
| } else if (ch == '/') { | |||
| index++; | |||
| while ('ign'.indexOf(ch = read()) != -1) { | |||
| mod += ch; | |||
| index++; | |||
| } | |||
| return re + '%%%' + mod; | |||
| } | |||
| re += ch; | |||
| index++; | |||
| } | |||
| throw new Error('Lexer Error: Unterminated regexp at column ' + index + ' in expression {{' + expr + '}}'); | |||
| } | |||
| function testRegexp() { | |||
| var idx = index, ok = true; | |||
| try { | |||
| readRegexp(); | |||
| } catch (e) { | |||
| ok = false; | |||
| } | |||
| // reset our index and ch | |||
| index = idx; | |||
| ch = '/'; | |||
| return ok; | |||
| } | |||
| } | |||
| function parser(expr, scope) { | |||
| let tokens = lexer(expr), | |||
| context = undefined, | |||
| RESERVED = { | |||
| 'PI' : function() { return Math.PI; }, | |||
| 'UUID' : function() { return randomUUID ? randomUUID() : uuidv4(); }, | |||
| 'NOW' : function() { return date(); }, | |||
| 'NOW_UTC' : function() { return utc_date(); }, | |||
| 'TIMESTAMP': function() { return timestamp(); }, | |||
| '$this' : function() { return scope.data; }, | |||
| '$global' : function() { return globalScope.data; }, | |||
| '$parent' : function() { return scope.parent && scope.parent.data; }, | |||
| 'null' : function() { return null; }, | |||
| 'true' : function() { return true; }, | |||
| 'false' : function() { return false; }, | |||
| '_' : function() { return { __dmxScope__: true } } | |||
| }; | |||
| return start()(); | |||
| function pad(s, n) { | |||
| return ('000' + s).substr(-n); | |||
| } | |||
| function date(dt) { | |||
| dt = dt || new Date(); | |||
| return pad(dt.getFullYear(), 4) + '-' + pad(dt.getMonth() + 1, 2) + '-' + pad(dt.getDate(), 2) + ' ' + | |||
| pad(dt.getHours(), 2) + ':' + pad(dt.getMinutes(), 2) + ':' + pad(dt.getSeconds(), 2); | |||
| } | |||
| function utc_date(dt) { | |||
| dt = dt || new Date(); | |||
| return pad(dt.getUTCFullYear(), 4) + '-' + pad(dt.getUTCMonth() + 1, 2) + '-' + pad(dt.getUTCDate(), 2) + 'T' + | |||
| pad(dt.getUTCHours(), 2) + ':' + pad(dt.getUTCMinutes(), 2) + ':' + pad(dt.getUTCSeconds(), 2) + 'Z'; | |||
| } | |||
| function timestamp(dt) { | |||
| dt = dt || new Date(); | |||
| return ~~(dt / 1000); | |||
| } | |||
| function read() { | |||
| if (tokens.length === 0) { | |||
| throw new Error('Parser Error: Unexpected end of expression {{' + expr + '}}'); | |||
| } | |||
| return tokens[0]; | |||
| } | |||
| function peek(e) { | |||
| if (tokens.length > 0) { | |||
| let token = tokens[0]; | |||
| if (!e || token.name == e) { | |||
| return token; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| function expect(e) { | |||
| let token = peek(e); | |||
| if (token) { | |||
| tokens.shift(); | |||
| return token; | |||
| } | |||
| return false; | |||
| } | |||
| function consume(e) { | |||
| if (!expect(e)) { | |||
| throw new Error('Parser Error: Unexpected token, expecting ' + e + ' in expression {{' + expr + '}}'); | |||
| } | |||
| } | |||
| function fn(expr) { | |||
| let args = [].slice.call(arguments, 1); | |||
| return function() { | |||
| if (EXPRESSIONS[expr]) { | |||
| return EXPRESSIONS[expr].apply(context, args); | |||
| } else { | |||
| return expr; | |||
| } | |||
| } | |||
| } | |||
| function start() { | |||
| return conditional(); | |||
| } | |||
| function conditional() { | |||
| let left = logicalOr(), middle, token; | |||
| if ((token = expect('QUESTION'))) { | |||
| middle = conditional(); | |||
| if ((token = expect('COLON'))) { | |||
| return fn('?', left, middle, conditional()); | |||
| } else { | |||
| throw new Error('Parse Error: Expecting : in expression {{' + expr + '}}'); | |||
| } | |||
| } else { | |||
| return left; | |||
| } | |||
| } | |||
| function logicalOr() { | |||
| let left = logicalAnd(), token; | |||
| while (true) { | |||
| if ((token = expect('LOGICAL_OR'))) { | |||
| left = fn(token.value, left, logicalAnd()); | |||
| } else { | |||
| return left; | |||
| } | |||
| } | |||
| } | |||
| function logicalAnd() { | |||
| let left = bitwiseOr(), token; | |||
| if ((token = expect('LOGICAL_AND'))) { | |||
| left = fn(token.value, left, logicalAnd()); | |||
| } | |||
| return left; | |||
| } | |||
| function bitwiseOr() { | |||
| let left = bitwiseXor(), token; | |||
| if ((token = expect('BITWISE_OR'))) { | |||
| left = fn(token.value, left, bitwiseXor()); | |||
| } | |||
| return left; | |||
| } | |||
| function bitwiseXor() { | |||
| let left = bitwiseAnd(), token; | |||
| if ((token = expect('BITWISE_XOR'))) { | |||
| left = fn(token.value, left, bitwiseAnd()); | |||
| } | |||
| return left; | |||
| } | |||
| function bitwiseAnd() { | |||
| let left = equality(), token; | |||
| if ((token = expect('BITWISE_AND'))) { | |||
| left = fn(token.value, left, bitwiseAnd()); | |||
| } | |||
| return left; | |||
| } | |||
| function equality() { | |||
| let left = relational(), token; | |||
| if ((token = expect('EQUALITY'))) { | |||
| left = fn(token.value, left, equality()); | |||
| } | |||
| return left; | |||
| } | |||
| function relational() { | |||
| let left = bitwiseShift(), token; | |||
| if ((token = expect('RELATIONAL'))) { | |||
| left = fn(token.value, left, relational()); | |||
| } | |||
| return left; | |||
| } | |||
| function bitwiseShift() { | |||
| let left = addictive(), token; | |||
| if ((token = expect('BITWISE_SHIFT'))) { | |||
| left = fn(token.value, left, addictive()); | |||
| } | |||
| return left; | |||
| } | |||
| function addictive() { | |||
| let left = multiplicative(), token; | |||
| while ((token = expect('ADDICTIVE'))) { | |||
| left = fn(token.value, left, multiplicative()); | |||
| } | |||
| return left; | |||
| } | |||
| function multiplicative() { | |||
| let left = unary(), token; | |||
| while ((token = expect('MULTIPLICATIVE'))) { | |||
| left = fn(token.value, left, unary()); | |||
| } | |||
| return left; | |||
| } | |||
| function unary() { | |||
| let token; | |||
| if ((token = expect('ADDICTIVE'))) { | |||
| if (token.value == '+') { | |||
| return primary(); | |||
| } else { | |||
| return fn(token.value, function() { return 0; }, unary()); | |||
| } | |||
| } else if ((token = expect('LOGICAL_NOT'))) { | |||
| return fn(token.value, unary()); | |||
| } | |||
| return primary(); | |||
| } | |||
| function primary() { | |||
| let value, next; | |||
| if (expect('L_PAREN')) { | |||
| value = start(); | |||
| consume('R_PAREN'); | |||
| } else if (expect('L_CURLY')) { | |||
| let obj = {}; | |||
| if (read().name != 'R_CURLY') { | |||
| do { | |||
| let key = expect().value; | |||
| consume('COLON'); | |||
| obj[key] = start()(); | |||
| } while (expect('COMMA')); | |||
| } | |||
| value = fn(obj); | |||
| consume('R_CURLY'); | |||
| } else if (expect('L_BRACKET')) { | |||
| let arr = []; | |||
| if (read().name != 'R_BRACKET') { | |||
| do { | |||
| arr.push(start()()); | |||
| } while (expect('COMMA')); | |||
| } | |||
| value = fn(arr); | |||
| consume('R_BRACKET'); | |||
| } else if (expect('PERIOD')) { | |||
| value = peek() ? objectMember(fn(scope.data)) : fn(scope.data); | |||
| } else { | |||
| let token = expect(); | |||
| if (token === false) { | |||
| throw new Error('Parser Error: Not a primary expression {{' + expr + '}}'); | |||
| } | |||
| if (token.name == 'IDENT') { | |||
| value = RESERVED.hasOwnProperty(token.value) | |||
| ? RESERVED[token.value] | |||
| : function() { return scope.get(token.value) }; | |||
| } else if (token.name == 'METHOD') { | |||
| if (!formatters[token.value]) { | |||
| throw new Error('Parser Error: Formatter "' + token.value + '" does not exist, expression {{' + expression + '}}'); | |||
| } | |||
| value = fn(formatters[token.value]); | |||
| } else if (token.name == 'REGEXP') { | |||
| value = function() { | |||
| let re = token.value.split('%%%'); | |||
| return new RegExp(re[0], re[1]); | |||
| }; | |||
| } else { | |||
| value = function() { return token.value }; | |||
| } | |||
| } | |||
| while ((next = expect('L_PAREN') || expect('L_BRACKET') || expect('PERIOD'))) { | |||
| if (next.value == '(') { | |||
| value = functionCall(value, context); | |||
| } else if (next.value == '[') { | |||
| value = objectIndex(value); | |||
| } else if (next.value == '.') { | |||
| context = value; | |||
| value = objectMember(value); | |||
| } else { | |||
| throw new Error('Parser Error: Parse error in expression {{' + expr + '}}'); | |||
| } | |||
| } | |||
| context = undefined; | |||
| return value; | |||
| } | |||
| function functionCall(func, ctx) { | |||
| let argsFn = []; | |||
| if (read().name != 'R_PAREN') { | |||
| do { | |||
| argsFn.push(start()); | |||
| } while (expect('COMMA')); | |||
| } | |||
| consume('R_PAREN'); | |||
| return function() { | |||
| let args = []; | |||
| if (ctx) args.push(ctx()); | |||
| for (let argFn of argsFn) { | |||
| args.push(argFn()); | |||
| } | |||
| let fnPtr = func() || NOOP; | |||
| return fnPtr.apply(null, args); | |||
| } | |||
| } | |||
| function objectIndex(obj) { | |||
| let indexFn = start(); | |||
| consume('R_BRACKET'); | |||
| return function() { | |||
| let o = obj(), | |||
| i = indexFn(); | |||
| if (typeof o != 'object') return undefined; | |||
| if (o.__dmxScope__) { | |||
| return scope.get(i); | |||
| } | |||
| return o[i]; | |||
| } | |||
| } | |||
| function objectMember(obj) { | |||
| let token = expect(); | |||
| return function() { | |||
| let o = obj(); | |||
| if (token.name == 'METHOD') { | |||
| if (!formatters[token.value]) { | |||
| throw new Error('Parser Error: Formatter "' + token.value + '" does not exist, expression {{' + expr + '}}'); | |||
| } | |||
| return formatters[token.value]; | |||
| } | |||
| if (o && o.__dmxScope) { | |||
| return scope.get(token.value); | |||
| } | |||
| return o && o[token.value]; | |||
| } | |||
| } | |||
| } | |||
| function parseValue(value, scope) { | |||
| if (value == null) return value; | |||
| value = value.valueOf(); | |||
| if (typeof value == 'object') { | |||
| for (let key in value) { | |||
| value[key] = parseValue(value[key], scope); | |||
| } | |||
| } | |||
| if (typeof value == 'string') { | |||
| if (value.substr(0, 2) == '{{' && value.substr(-2) == '}}') { | |||
| let expr = value.replace(/^\{\{|\}\}$/g, ''); | |||
| if (expr.indexOf('{{') == -1) { | |||
| return parser(expr, scope); | |||
| } | |||
| } | |||
| return parseTemplate(value, scope); | |||
| } | |||
| return value; | |||
| } | |||
| function parseTemplate(template, scope) { | |||
| return template.replace(/\{\{(.*?)\}\}/g, function(a, m) { | |||
| var value = parser(m, scope); | |||
| return value != null ? String(value) : ''; | |||
| }); | |||
| } | |||
| exports.lexer = lexer; | |||
| exports.parse = parser; | |||
| exports.parseValue = parseValue; | |||
| exports.parseTemplate = parseTemplate; | |||
| @@ -0,0 +1,97 @@ | |||
| const { existsSync: exists } = require('fs'); | |||
| const { dirname, basename, extname, join, resolve, relative, posix } = require('path'); | |||
| const { v4: uuidv4 } = require('uuid'); | |||
| const debug = require('debug')('server-connect:path'); | |||
| module.exports = { | |||
| getFilesArray: function(paths) { | |||
| let files = []; | |||
| if (!Array.isArray(paths)) { | |||
| paths = [paths]; | |||
| } | |||
| for (let path of paths) { | |||
| if (Array.isArray(path)) { | |||
| files = files.concat(module.exports.getFilesArray(path)); | |||
| } else if (path && path.path) { | |||
| files.push(module.exports.toSystemPath(path.path)); | |||
| } else if (path) { | |||
| files.push(module.exports.toSystemPath(path)); | |||
| } | |||
| } | |||
| return files; | |||
| }, | |||
| toSystemPath: function(path) { | |||
| if (path[0] != '/' || path.includes('../')) { | |||
| throw new Error(`path.toSystemPath: Invalid path "${path}".`); | |||
| } | |||
| return resolve('.' + path); | |||
| }, | |||
| toAppPath: function(path) { | |||
| let root = resolve('.'); | |||
| let rel = relative(root, path).replace(/\\/g, '/'); | |||
| debug('toAppPath: %O', { root, path, rel }); | |||
| if (rel.includes('../')) { | |||
| throw new Error(`path.toAppPath: Invalid path "${rel}".`); | |||
| } | |||
| return '/' + rel; | |||
| }, | |||
| toSiteUrl: function(path) { | |||
| let root = resolve('public'); | |||
| let rel = relative(root, path).replace(/\\/g, '/'); | |||
| debug('toSiteUrl: %O', { root, path, rel }); | |||
| if (rel.includes('../')) { | |||
| return ''; | |||
| } | |||
| return '/' + rel; | |||
| }, | |||
| getUniqFile: function(path) { | |||
| let n = 1; | |||
| while (exists(path)) { | |||
| path = path.replace(/(_(\d+))?(\.\w+)$/, (a, b, c, d) => '_' + (n++) + (d || a)); | |||
| if (n > 999) throw new Error(`path.getUniqFile: Couldn't create a unique filename for ${path}`); | |||
| } | |||
| return path; | |||
| }, | |||
| parseTemplate: function(path, template) { | |||
| let n = 1, dir = dirname(path), file = template.replace(/\{([^\}]+)\}/g, (a, b) => { | |||
| switch (b) { | |||
| case 'name': return basename(path, extname(path)); | |||
| case 'ext' : return extname(path); | |||
| case 'guid': return uuidv4(); | |||
| } | |||
| return a; | |||
| }); | |||
| if (file.includes('{_n}')) { | |||
| template = file; | |||
| file = template.replace('{_n}', ''); | |||
| while (exists(join(dir, file))) { | |||
| file = template.replace('{_n}', n++); | |||
| if (n > 999) throw new Error(`path.parseTemplate: Couldn't create a unique filename for ${path}`); | |||
| } | |||
| } | |||
| return join(dir, file); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,63 @@ | |||
| function Scope(data, parent, context) { | |||
| if (typeof data != 'object') { | |||
| data = { $value: data }; | |||
| } | |||
| this.data = data; | |||
| this.parent = parent; | |||
| this.context = context; | |||
| }; | |||
| Scope.prototype = { | |||
| create: function(data, context) { | |||
| return new Scope(data, this, context); | |||
| }, | |||
| get: function(name) { | |||
| if (this.data[name] !== undefined) { | |||
| return this.data[name]; | |||
| } | |||
| if (this.parent) { | |||
| return this.parent.get(name); | |||
| } | |||
| return undefined; | |||
| }, | |||
| set: function(name, value) { | |||
| if (typeof name == 'object') { | |||
| for (let prop in name) { | |||
| this.set(prop, name[prop]); | |||
| } | |||
| } else { | |||
| this.data[name] = value; | |||
| if (this.context) { | |||
| this.context[name] = value; | |||
| } | |||
| } | |||
| }, | |||
| has: function(name) { | |||
| if (this.data[name] !== undefined) { | |||
| return true; | |||
| } | |||
| if (this.parent) { | |||
| return this.parent.has(name); | |||
| } | |||
| return false; | |||
| }, | |||
| remove: function(name) { | |||
| delete this.data[name]; | |||
| if (this.context) { | |||
| delete this.context[name]; | |||
| } | |||
| }, | |||
| } | |||
| module.exports = Scope; | |||
| @@ -0,0 +1,185 @@ | |||
| const reGrouping = /(\d+)(\d{3})/; | |||
| function clone(o) { | |||
| if (o == null) { | |||
| return o; | |||
| } | |||
| if (Array.isArray(o)) { | |||
| return o.map(clone); | |||
| } | |||
| if (o instanceof Date) { | |||
| // we do not clone date objects but convert them to an iso string | |||
| return o.toISOString(); | |||
| } | |||
| if (typeof o == 'object') { | |||
| let oo = {}; | |||
| for (let key in o) { | |||
| oo[key] = clone(o[key]); | |||
| } | |||
| return oo; | |||
| } | |||
| return o; | |||
| } | |||
| function mergeDeep(target, source) { | |||
| if (isObject(target) && isObject(source)) { | |||
| for (const key in source) { | |||
| if (isObject(source[key])) { | |||
| if (!target[key]) { | |||
| Object.assign(target, { [key]: {} }); | |||
| } | |||
| mergeDeep(target[key], source[key]); | |||
| } else { | |||
| Object.assign(target, { [key]: source[key] }); | |||
| } | |||
| } | |||
| } | |||
| return target; | |||
| } | |||
| function isObject(item) { | |||
| return (item && typeof item === 'object' && !Array.isArray(item) && item !== null); | |||
| } | |||
| module.exports = { | |||
| clone, | |||
| mergeDeep, | |||
| escapeRegExp: function(val) { | |||
| // https://github.com/benjamingr/RegExp.escape | |||
| return val.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); | |||
| }, | |||
| keysToLowerCase: function(o) { | |||
| return Object.keys(o).reduce((c, k) => (c[k.toLowerCase()] = o[k], c), {}); | |||
| }, | |||
| padNumber: function(num, digids, trim) { | |||
| let sign = ''; | |||
| if (num < 0) { | |||
| sign = '-'; | |||
| num = -num; | |||
| } | |||
| num = String(num); | |||
| while (num.length < digids) { | |||
| num = '0' + num; | |||
| } | |||
| if (trim) { | |||
| num = num.substr(num.length - digids); | |||
| } | |||
| return sign + num; | |||
| }, | |||
| formatNumber: function(num, decimals, decimalSeparator, groupingSeparator) { | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return ''; | |||
| decimalSeparator = typeof decimalSeparator == 'string' ? decimalSeparator : '.'; | |||
| groupingSeparator = typeof groupingSeparator == 'string' ? groupingSeparator : ''; | |||
| precision = typeof precision == 'number' ? Math.abs(precision) : null; | |||
| let minus = num < 0; | |||
| let parts = (decimals == null ? String(Math.abs(num)) : Math.abs(num).toFixed(decimals)).split('.'); | |||
| let wholePart = parts[0]; | |||
| let decimalPart = parts.length > 1 ? decimalSeparator + parts[1] : ''; | |||
| if (groupingSeparator) { | |||
| while (reGrouping.test(wholePart)) { | |||
| wholePart = wholePart.replace(reGrouping, '$1' + groupingSeparator + '$2'); | |||
| } | |||
| } | |||
| return (minus ? '-' : '') + wholePart + decimalPart; | |||
| }, | |||
| parseDate: function(obj) { | |||
| if (Object.prototype.toString.call(obj) == '[object Date]') return new Date(obj); | |||
| let date = new Date(obj); | |||
| if (typeof obj == 'number') { | |||
| date = new Date(obj * 1000); | |||
| } | |||
| if (typeof obj == 'string') { | |||
| if (obj == 'now') { | |||
| date = new Date(); | |||
| } else if (/^\d{4}-\d{2}-\d{2}$/.test(obj)) { | |||
| let parts = obj.split('-'); | |||
| date = new Date(parts[0], parts[1] - 1, parts[2]); | |||
| } else if (/^\d{2}:\d{2}:\d{2}$/.test(obj)) { | |||
| let parts = obj.split(':'); | |||
| date = new Date(); | |||
| date.setHours(parts[0]); | |||
| date.setMinutes(parts[1]); | |||
| date.setSeconds(parts[2]); | |||
| date.setMilliseconds(0); | |||
| } | |||
| } | |||
| if (date.toString() == 'Invalid Date') { | |||
| return undefined; | |||
| } | |||
| return date; | |||
| }, | |||
| formatDate: function(date, format, utc, locale) { | |||
| date = module.exports.parseDate(date); | |||
| if (date == null) return date; | |||
| locale = typeof locale == 'string' ? locale : 'en-US'; | |||
| const lang = require('../locale/' + locale); | |||
| const pad = (v, n) => `0000${v}`.substr(-n); | |||
| let y = utc ? date.getUTCFullYear() : date.getFullYear(), | |||
| n = utc ? date.getUTCMonth() : date.getMonth(), | |||
| d = utc ? date.getUTCDate() : date.getDate(), | |||
| w = utc ? date.getUTCDay() : date.getDay(), | |||
| h = utc ? date.getUTCHours() : date.getHours(), | |||
| m = utc ? date.getUTCMinutes() : date.getMinutes(), | |||
| s = utc ? date.getUTCSeconds() : date.getSeconds(), | |||
| v = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); | |||
| return format.replace(/([yMdHhmsaAv])(\1+)?/g, part => { | |||
| switch (part) { | |||
| case 'yyyy': return pad(y, 4); | |||
| case 'yy': return pad(y, 2); | |||
| case 'y': return y; | |||
| case 'MMMM': return lang.months[n]; | |||
| case 'MMM': return lang.monthsShort[n]; | |||
| case 'MM': return pad(n + 1, 2); | |||
| case 'M': return n + 1; | |||
| case 'dddd': return lang.days[w]; | |||
| case 'ddd': return lang.daysShort[w]; | |||
| case 'dd': return pad(d, 2); | |||
| case 'd': return d; | |||
| case 'HH': return pad(h, 2); | |||
| case 'H': return h; | |||
| case 'hh': return pad((h % 12) || 12, 2); | |||
| case 'h': return (h % 12) || 12; | |||
| case 'mm': return pad(m, 2); | |||
| case 'm': return m; | |||
| case 'ss': return pad(s, 2); | |||
| case 's': return s; | |||
| case 'a': return h < 12 ? 'am' : 'pm'; | |||
| case 'A': return h < 12 ? 'AM' : 'PM'; | |||
| case 'v': return pad(v, 3); | |||
| } | |||
| }); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,24 @@ | |||
| // Helper methods for webhooks | |||
| const fs = require('fs-extra'); | |||
| const App = require('./app'); | |||
| exports.createHandler = function(name, fn = () => 'handler') { | |||
| return async (req, res, next) => { | |||
| const action = await fn(req, res, next); | |||
| if (typeof action == 'string') { | |||
| const path = `app/webhooks/${name}/${action}.json`; | |||
| if (fs.existsSync(path)) { | |||
| const app = new App(req, res); | |||
| let json = await fs.readJSON(path); | |||
| return Promise.resolve(app.define(json)).catch(next); | |||
| } else { | |||
| res.json({error: `No action found for ${action}.`}); | |||
| // do not return 404 else stripe will retry | |||
| //next(); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| @@ -0,0 +1,169 @@ | |||
| module.exports = { | |||
| join: function(arr, separator, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.map(item => prop ? item[prop] : item).join(separator); | |||
| }, | |||
| first: function(arr) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.length ? arr[0] : null; | |||
| }, | |||
| top: function(arr, count) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.slice(0, count); | |||
| }, | |||
| last: function(arr, count) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| if (count) return arr.slice(-count); | |||
| return arr[arr.length - 1]; | |||
| }, | |||
| where: function(arr, prop, operator, value) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.filter(item => { | |||
| let val = item[prop]; | |||
| switch (operator) { | |||
| case 'startsWith': return String(val).startsWith(value); | |||
| case 'endsWith': return String(val).endsWith(value); | |||
| case 'contains': return String(val).includes(value); | |||
| case '===': return val === value; | |||
| case '!==': return val !== value; | |||
| case '==': return val == value; | |||
| case '!=': return val != value; | |||
| case '<=': return val <= value; | |||
| case '>=': return val >= value; | |||
| case '<': return val < value; | |||
| case '>': return val > value; | |||
| }; | |||
| return true; | |||
| }); | |||
| }, | |||
| unique: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| if (prop) arr = arr.map(item => item[prop]); | |||
| let lookup = []; | |||
| return arr.filter(item => { | |||
| if (lookup.includes(item)) return false; | |||
| lookup.push(item); | |||
| return true; | |||
| }); | |||
| }, | |||
| groupBy: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.reduce((groups, item) => { | |||
| let group = String(item[prop]); | |||
| if (!groups[group]) groups[group] = []; | |||
| groups[group].push(item); | |||
| return groups; | |||
| }, {}); | |||
| }, | |||
| sort: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.slice(0).sort((a, b) => { | |||
| a = prop ? a && a[prop] : a; | |||
| b = prop ? b && b[prop] : b; | |||
| if (typeof a == 'string' && typeof b == 'string') { | |||
| return a.localeCompare(b); | |||
| } | |||
| return a < b ? -1 : a > b ? 1 : 0; | |||
| }); | |||
| }, | |||
| randomize: function(arr) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| arr = arr.slice(0); | |||
| let len = arr.length; | |||
| if (len) { | |||
| while (--len) { | |||
| let rnd = Math.floor(Math.random() * (len + 1)); | |||
| let val = arr[len]; | |||
| arr[len] = arr[rnd]; | |||
| arr[rnd] = val; | |||
| } | |||
| } | |||
| return arr; | |||
| }, | |||
| reverse: function(arr) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.slice(0).reverse(); | |||
| }, | |||
| count: function(arr) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.length; | |||
| }, | |||
| min: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.reduce((min, num) => { | |||
| num = prop ? num[prop] : num; | |||
| if (num == null) return min; | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return min; | |||
| return min == null || num < min ? num : min; | |||
| }, null); | |||
| }, | |||
| max: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.reduce((max, num) => { | |||
| num = prop ? num[prop] : num; | |||
| if (num == null) return max; | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return max; | |||
| return max == null || num > max ? num : max; | |||
| }, null); | |||
| }, | |||
| sum: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.reduce((sum, num) => { | |||
| num = prop ? num[prop] : num; | |||
| if (num == null) return sum; | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return sum; | |||
| return sum + num; | |||
| }, 0); | |||
| }, | |||
| avg: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| let cnt = 0; | |||
| return arr.reduce((avg, num) => { | |||
| num = prop ? num[prop] : num; | |||
| if (num == null) return avg; | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return avg; | |||
| cnt++; | |||
| return avg + num; | |||
| }, 0) / cnt; | |||
| }, | |||
| flatten: function(arr, prop) { | |||
| if (!Array.isArray(arr)) return arr; | |||
| return arr.map(item => item[prop]); | |||
| }, | |||
| keys: function(obj) { | |||
| if (typeof obj != 'object') return obj; | |||
| return Object.keys(obj); | |||
| }, | |||
| values: function(obj) { | |||
| if (typeof obj != 'object') return obj; | |||
| return Object.values(obj); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,30 @@ | |||
| module.exports = { | |||
| startsWith: function(val, str) { | |||
| if (val == null) return false; | |||
| return String(val).startsWith(str); | |||
| }, | |||
| endsWith: function(val, str) { | |||
| if (val == null) return false; | |||
| return String(val).endsWith(str); | |||
| }, | |||
| contains: function(val, str) { | |||
| if (val == null) return false; | |||
| return String(val).includes(str); | |||
| }, | |||
| between: function(val, min, max) { | |||
| return val >= min && val <= max; | |||
| }, | |||
| inRange: function(val, min, max) { | |||
| val = Number(val); | |||
| min = Number(min); | |||
| max = Number(max); | |||
| return val >= min && val <= max; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,27 @@ | |||
| module.exports = { | |||
| default: function(val, def) { | |||
| return val ? val : def; | |||
| }, | |||
| then: function(val, trueVal, falseVal) { | |||
| return val ? trueVal : falseVal; | |||
| }, | |||
| toNumber: function(val) { | |||
| return Number(val); | |||
| }, | |||
| toString: function(val) { | |||
| return String(val); | |||
| }, | |||
| toJSON: function(val) { | |||
| return JSON.stringify(val); | |||
| }, | |||
| parseJSON: function(val) { | |||
| return JSON.parse(val); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,99 @@ | |||
| const { createHash, createHmac, createCipheriv, createDecipheriv, randomBytes, scryptSync, randomUUID } = require('crypto'); | |||
| const { v4: uuidv4 } = require('uuid'); | |||
| module.exports = { | |||
| randomUUID: function() { | |||
| return randomUUID ? randomUUID() : uuidv4(); | |||
| }, | |||
| md5: function(data, salt, enc) { | |||
| return hash('md5', data, salt, enc) | |||
| }, | |||
| sha1: function(data, salt, enc) { | |||
| return hash('sha1', data, salt, enc) | |||
| }, | |||
| sha256: function(data, salt, enc) { | |||
| return hash('sha256', data, salt, enc) | |||
| }, | |||
| sha512: function(data, salt, enc) { | |||
| return hash('sha512', data, salt, enc) | |||
| }, | |||
| hash: function(data, alg, enc) { | |||
| return hash(alg, data, '', enc); | |||
| }, | |||
| hmac: function(data, alg, secret, enc) { | |||
| return hmac(alg, data, secret, enc); | |||
| }, | |||
| transform: function(data, from, to) { | |||
| return transform(data, from, to); | |||
| }, | |||
| encodeBase64: function(data, enc) { | |||
| enc = typeof enc == 'string' ? enc : 'utf8'; | |||
| return transform(data, enc, 'base64'); | |||
| }, | |||
| decodeBase64: function(data, enc) { | |||
| enc = typeof enc == 'string' ? enc : 'utf8'; | |||
| return transform(data, 'base64', enc); | |||
| }, | |||
| encodeBase64Url: function(data, enc) { | |||
| enc = typeof enc == 'string' ? enc : 'utf8'; | |||
| return transform(data, enc, 'base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); | |||
| }, | |||
| decodeBase64Url: function(data, enc) { | |||
| enc = typeof enc == 'string' ? enc : 'utf8'; | |||
| return transform(data.replace(/-/g, '+').replace(/_/g, '/'), 'base64', enc); | |||
| }, | |||
| encrypt: function(data, password, enc) { | |||
| const config = require('../setup/config'); | |||
| password = typeof password == 'string' ? password : config.secret; | |||
| enc = typeof enc == 'string' ? enc : 'base64'; | |||
| const iv = randomBytes(16); | |||
| const key = scryptSync(password, iv, 32); | |||
| const cipher = createCipheriv('aes-256-cbc', key, iv); | |||
| const encrypted = cipher.update(data, 'utf8', enc); | |||
| return iv.toString(enc) + '.' + encrypted + cipher.final(enc); | |||
| }, | |||
| decrypt: function(data, password, enc) { | |||
| const config = require('../setup/config'); | |||
| password = typeof password == 'string' ? password : config.secret; | |||
| enc = typeof enc == 'string' ? enc : 'base64'; | |||
| const iv = Buffer.from(data.split('.')[0], enc); | |||
| const key = scryptSync(password, iv, 32); | |||
| const decipher = createDecipheriv('aes-256-cbc', key, iv); | |||
| const decrypted = decipher.update(data.split('.')[1], enc, 'utf8'); | |||
| return decrypted + decipher.final('utf8'); | |||
| }, | |||
| }; | |||
| function hash(alg, data, salt, enc) { | |||
| if (data == null) return data; | |||
| salt = typeof salt == 'string' ? salt : ''; | |||
| enc = typeof enc == 'string' ? enc : 'hex'; | |||
| return createHash(alg).update(data + salt).digest(enc); | |||
| } | |||
| function hmac(alg, data, secret, enc) { | |||
| if (data == null) return data; | |||
| secret = typeof secret == 'string' ? secret : ''; | |||
| enc = typeof enc == 'string' ? enc : 'hex'; | |||
| return createHmac(alg, secret).update(data).digest(enc); | |||
| } | |||
| function transform(data, from, to) { | |||
| if (data == null) return data; | |||
| return Buffer.from(String(data), from).toString(to); | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| const { padNumber, parseDate, formatDate } = require('../core/util'); | |||
| module.exports = { | |||
| formatDate: function(date, format, utc) { | |||
| date = parseDate(date); | |||
| if (date == null) return date; | |||
| return formatDate(date, format, utc); | |||
| }, | |||
| dateAdd: function(date, interval, num) { | |||
| date = parseDate(date); | |||
| if (date == null) return date; | |||
| switch (interval) { | |||
| case 'years': date.setFullYear(date.getFullYear() + num); break; | |||
| case 'months': date.setMonth(date.getMonth() + num); break; | |||
| case 'weeks': date.setDate(date.getDate() + (num * 7)); break; | |||
| case 'days': date.setDate(date.getDate() + num); break; | |||
| case 'hours': date.setHours(date.getHours() + num); break; | |||
| case 'minutes': date.setMinutes(date.getMinutes() + num); break; | |||
| case 'seconds': date.setSeconds(date.getSeconds() + num); break; | |||
| } | |||
| return date.toISOString(); | |||
| }, | |||
| dateDiff: function(date, interval, date2) { | |||
| date = parseDate(date); | |||
| date2 = parseDate(date2); | |||
| let diff = Math.abs(date2 - date); | |||
| if (isNaN(diff) || date == null || date2 == null) return undefined; | |||
| let s = 1000, m = s * 60, h = m * 60, d = h * 24, w = d * 7; | |||
| switch (interval) { | |||
| case 'years': return Math.abs(date2.getFullYear() - date.getFullYear()); | |||
| case 'months': return Math.abs((date2.getFullYear() * 12 + date2.getMonth()) - (date.getFullYear() * 12 + date.getMonth())); | |||
| case 'weeks': return Math.floor(diff / w); | |||
| case 'days': return Math.floor(diff / d); | |||
| case 'hours': return Math.floor(diff/ h); | |||
| case 'minutes': return Math.floor(diff / m); | |||
| case 'seconds': return Math.floor(diff / s); | |||
| case 'hours:minutes': | |||
| h = Math.floor(diff / h); | |||
| m = Math.floor(diff / m); | |||
| return padNumber(h, 2) + ':' + padNumber(m - (h * 60), 2); | |||
| case 'minutes:seconds': | |||
| m = Math.floor(diff / m); | |||
| s = Math.floor(diff / s); | |||
| return padNumber(m, 2) + ':' + padNumber(s - (m * 60), 2); | |||
| case 'hours:minutes:seconds': | |||
| h = Math.floor(diff / h); | |||
| m = Math.floor(diff / m); | |||
| s = Math.floor(diff / s); | |||
| return padNumber(h, 2) + ':' + padNumber(m - (h * 60), 2) + ':' + padNumber(s - m * 60, 2); | |||
| } | |||
| return undefined; | |||
| }, | |||
| toTimestamp: function(date) { | |||
| date = parseDate(date); | |||
| if (date == null) return date; | |||
| return Math.floor(date.getTime() / 1000); | |||
| }, | |||
| toLocalTime: function(date) { | |||
| date = parseDate(date); | |||
| if (date == null) return date; | |||
| return formatDate(date, 'yyyy-MM-dd HH:mm:ss.v'); | |||
| }, | |||
| toUTCTime: function(date) { | |||
| date = parseDate(date); | |||
| if (date == null) return date; | |||
| return date.toISOString(); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,17 @@ | |||
| const collections = require('./collections'); | |||
| const conditional = require('./conditional'); | |||
| const core = require('./core'); | |||
| const crypto = require('./crypto'); | |||
| const date = require('./date'); | |||
| const number = require('./number'); | |||
| const string = require('./string'); | |||
| module.exports = { | |||
| ...collections, | |||
| ...conditional, | |||
| ...core, | |||
| ...crypto, | |||
| ...date, | |||
| ...number, | |||
| ...string | |||
| }; | |||
| @@ -0,0 +1,73 @@ | |||
| const { padNumber, formatNumber } = require('../core/util'); | |||
| module.exports = { | |||
| floor: function(num) { | |||
| return Math.floor(Number(num)); | |||
| }, | |||
| ceil: function(num) { | |||
| return Math.ceil(Number(num)); | |||
| }, | |||
| round: function(num) { | |||
| return Math.round(Number(num)); | |||
| }, | |||
| abs: function(num) { | |||
| return Math.abs(Number(num)); | |||
| }, | |||
| pow: function(num, exp) { | |||
| return Math.pow(num, exp); | |||
| }, | |||
| padNumber: function(num, digids) { | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return 'NaN'; | |||
| return padNumber(num, digids); | |||
| }, | |||
| formatNumber: function(num, decimals, decimalSeparator, groupingSeparator) { | |||
| return formatNumber(num, decimals, decimalSeparator, groupingSeparator); | |||
| }, | |||
| hex: function(num) { | |||
| return parseInt(String(num), 16) || NaN; | |||
| }, | |||
| currency: function(num, unit, decimalSeparator, groupingSeparator, decimals) { | |||
| unit = typeof unit == 'string' ? unit : '$'; | |||
| decimalSeparator = typeof decimalSeparator == 'string' ? decimalSeparator : '.'; | |||
| groupingSeparator = typeof groupingSeparator == 'string' ? groupingSeparator : ','; | |||
| decimals = typeof decimals == 'number' ? Math.abs(decimals) : 2; | |||
| let formatted = formatNumber(num, decimals, decimalSeparator, groupingSeparator); | |||
| let minus = formatted[0] == '-'; | |||
| return (minus ? '-' : '') + unit + formatted.replace(/^\-/, ''); | |||
| }, | |||
| formatSize: function(num, decimals, binary) { | |||
| num = Number(num); | |||
| if (isNaN(num) || !isFinite(num)) return 'NaN'; | |||
| decimals = typeof decimals == 'number' ? Math.abs(decimals) : 2; | |||
| let base = binary ? 1024 : 1000; | |||
| let suffix = binary ? ['KiB', 'MiB', 'GiB', 'TiB'] : ['kB', 'MB', 'GB', 'TB']; | |||
| for (let i = 3; i >= 0; i--) { | |||
| let n = Math.pow(base, i + 1); | |||
| if (num >= n) { | |||
| return formatNumber(num / n, decimals) + suffix[i]; | |||
| } | |||
| } | |||
| return num + 'B'; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,59 @@ | |||
| module.exports = { | |||
| months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], | |||
| monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], | |||
| days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], | |||
| daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], | |||
| validator: { | |||
| core: { | |||
| required: 'This field is required.', | |||
| email: 'Please enter a valid email address.', | |||
| url: 'Please enter a valid URL.', | |||
| date: 'Please enter a valid date.', | |||
| time: 'Please enter a valid time.', | |||
| month: 'Please enter a valid month.', | |||
| week: 'Please enter a valid week.', | |||
| color: 'Please enter a color in the format #xxxxxx.', | |||
| pattern: 'Invalid format.', | |||
| number: 'Please enter a valid number.', | |||
| digits: 'Please enter only digits.', | |||
| alphanumeric: 'Letters, numbers and underscores only please.', | |||
| creditcard: 'Please enter a valid credit card number.', | |||
| bic: 'Please specify a valid BIC code.', | |||
| iban: 'Please specify a valid IBAN.', | |||
| vat: 'Please specify a valid VAT number.', | |||
| integer: 'A positive or negative non-decimal number please.', | |||
| ipv4: 'Please enter a valid IP v4 address.', | |||
| ipv6: 'Please enter a valid IP v6 address.', | |||
| lettersonly: 'Letters only please.', | |||
| unicodelettersonly: 'Letters only please.', | |||
| letterswithbasicpunc: 'Letters or punctuation only please.', | |||
| nowhitespace: 'No whitespace please.', | |||
| minlength: 'Please enter at least {0} characters.', | |||
| maxlength: 'Please enter no more than {0} characters.', | |||
| minitems: 'Please select at least {0} items.', | |||
| maxitems: 'Please select no more than {0} items.', | |||
| min: 'Please enter a value greater than or equal to {0}.', | |||
| max: 'Please enter a value less than or equal to {0}.', | |||
| equalTo: 'Please enter the same value again.', | |||
| notEqualTo: 'Please enter a different value, values must not be the same.', | |||
| }, | |||
| db: { | |||
| exists: 'Value does not exist in database.', | |||
| notexists: 'Value already exists in database.', | |||
| }, | |||
| unicode: { | |||
| unicodelettersonly: 'Entered characters are not allowed.', | |||
| unicodescripts: 'Entered characters are not allowed.', | |||
| }, | |||
| upload: { | |||
| accept: 'Please select a file with a valid file type.', | |||
| minsize: 'Please select a file of at least {0} bytes.', | |||
| maxsize: 'Please select a file of no more than {0} bytes.', | |||
| mintotalsize: 'Total size of selected files should be at least {0} bytes.', | |||
| maxtotalsize: 'Total size of selected files should be no more than {0} bytes.', | |||
| minfiles: 'Please select at least {0} files.', | |||
| maxfiles: 'Please select no more than {0} files.', | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,117 @@ | |||
| const { http, https } = require('follow-redirects'); | |||
| const querystring = require('querystring'); | |||
| const zlib = require('zlib'); | |||
| const pkg = require('../../package.json'); | |||
| module.exports = { | |||
| send: async function(options) { | |||
| let url = this.parseRequired(options.url, 'string', 'api.send: url is required.'); | |||
| let method = this.parseOptional(options.method, 'string', 'GET'); | |||
| let data = this.parseOptional(options.data, '*', ''); | |||
| let dataType = this.parseOptional(options.dataType, 'string', 'auto'); | |||
| let verifySSL = this.parseOptional(options.verifySSL, 'boolean', false); | |||
| let params = this.parseOptional(options.params, 'object', null); | |||
| let headers = this.parseOptional(options.headers, 'object', {}); | |||
| let username = this.parseOptional(options.username, 'string', ''); | |||
| let password = this.parseOptional(options.password, 'string', ''); | |||
| let oauth = this.parseOptional(options.oauth, 'string', ''); | |||
| let passErrors = this.parseOptional(options.passErrors, 'boolean', true); | |||
| let timeout = this.parseOptional(options.timeout, 'number', 0); | |||
| if (params) { | |||
| url += '?' + querystring.stringify(params); | |||
| } | |||
| if (dataType == 'auto' && method == 'POST') { | |||
| dataType = 'x-www-form-urlencoded'; | |||
| } | |||
| if (dataType != 'auto' && !headers['Content-Type']) { | |||
| headers['Content-Type'] = `application/${dataType}`; | |||
| } | |||
| if (dataType == 'x-www-form-urlencoded') { | |||
| data = querystring.stringify(data); | |||
| } else if (typeof data != 'string') { | |||
| data = JSON.stringify(data); | |||
| } | |||
| if (data) { | |||
| headers['Content-Length'] = Buffer.byteLength(data); | |||
| } | |||
| const Url = new URL(url); | |||
| const opts = { method, headers, rejectUnauthorized: !!verifySSL, maxBodyLength: 1000000000 }; | |||
| if (timeout > 0) { | |||
| opts.timeout = timeout; | |||
| } | |||
| if (username || password) { | |||
| opts.auth = `${username}:${password}`; | |||
| } | |||
| if (oauth) { | |||
| //const provider = this.oauth[oauth]; | |||
| const provider = await this.getOAuthProvider(oauth); | |||
| if (provider && provider.access_token) { | |||
| headers['Authorization'] = 'Bearer ' + provider.access_token; | |||
| } | |||
| } | |||
| if (!headers['User-Agent']) headers['User-Agent'] = `${pkg.name}/${pkg.version}`; | |||
| if (!headers['Accept']) headers['Accept'] = 'application/json'; | |||
| return new Promise((resolve, reject) => { | |||
| const req = (Url.protocol == 'https:' ? https : http).request(Url, opts, res => { | |||
| let body = ''; | |||
| let output = res; | |||
| if (res.headers['content-encoding'] == 'br') { | |||
| output = res.pipe(zlib.createBrotliDecompress()); | |||
| } | |||
| if (res.headers['content-encoding'] == 'gzip') { | |||
| output = res.pipe(zlib.createGunzip()); | |||
| } | |||
| if (res.headers['content-encoding'] == 'deflate') { | |||
| output = res.pipe(zlib.createInflate()); | |||
| } | |||
| output.setEncoding('utf8'); | |||
| output.on('data', chunk => body += chunk); | |||
| output.on('end', () => { | |||
| if (passErrors && res.statusCode >= 400) { | |||
| if (this.res.status) { | |||
| this.res.status(res.statusCode).send(body); | |||
| } | |||
| return reject(body); | |||
| } | |||
| if (body.charCodeAt(0) === 0xFEFF) { | |||
| body = body.slice(1); | |||
| } | |||
| if (res.headers['content-type'] && res.headers['content-type'].includes('json')) { | |||
| try { | |||
| body = JSON.parse(body); | |||
| } catch(e) { | |||
| console.error(e); | |||
| } | |||
| } | |||
| resolve({ | |||
| status: res.statusCode, | |||
| headers: res.headers, | |||
| data: body | |||
| }); | |||
| }); | |||
| }); | |||
| req.on('error', reject); | |||
| req.write(data); | |||
| req.end(); | |||
| }); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,173 @@ | |||
| // ArrayList | |||
| const { clone } = require('../core/util'); | |||
| // Create a new ArrayList | |||
| exports.create = function(options, name) { | |||
| const value = this.parseOptional(options.value, 'object', []); | |||
| if (!name) throw Error('arraylist.create: name is required.'); | |||
| if (!this.req.arrays) { | |||
| this.req.arrays = {}; | |||
| } | |||
| this.req.arrays[name] = Array.isArray(value) ? clone(value) : []; | |||
| }; | |||
| // Return the ArrayList as array value | |||
| exports.value = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.value: ref is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.value: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.value: ArrayList ${ref} not found.`); | |||
| return clone(this.req.arrays[ref]); | |||
| }; | |||
| // Return the size of the ArrayList | |||
| exports.size = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.size: ref is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.size: No arraylists are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.size: ArrayList ${ref} not found.`); | |||
| return this.req.arrays[ref].length; | |||
| }; | |||
| // Return the value at a specific index | |||
| exports.get = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.get: ref is required.'); | |||
| const index = this.parseRequired(options.index, 'number', 'arraylist.get: index is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.get: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.get: ArrayList ${ref} not found.`); | |||
| return clone(this.req.arrays[ref][index]); | |||
| }; | |||
| // Add a value to the ArrayList | |||
| exports.add = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.add: ref is required.'); | |||
| const value = this.parseRequired(options.value, '*', 'arraylist.add: value is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.add: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.add: ArrayList ${ref} not found.`); | |||
| this.req.arrays[ref].push(clone(value)); | |||
| }; | |||
| // Add an array of values to the ArrayList | |||
| exports.addAll = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.addAll: ref is required.'); | |||
| const value = this.parseRequired(options.value, 'object', 'arraylist.addAll: value is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.addAll: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.addAll: ArrayList ${ref} not found.`); | |||
| if (!Array.isArray(value)) throw Error('arraylist.addAll: Value must be an array.') | |||
| for (let val of value) { | |||
| this.req.arrays[ref].push(clone(val)); | |||
| } | |||
| }; | |||
| // Update a value at a specific index | |||
| exports.set = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.set: ref is required.'); | |||
| const index = this.parseRequired(options.index, 'number', 'arraylist.set: index is required.'); | |||
| const value = this.parseRequired(options.value, '*', 'arraylist.set: value is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.set: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.set: ArrayList ${ref} not found.`); | |||
| this.req.arrays[ref][index] = clone(value); | |||
| }; | |||
| // Remove the first occurrence of a specific value | |||
| exports.remove = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.remove: ref is required.'); | |||
| const value = this.parseRequired(options.value, '*', 'arraylist.remove: value is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.remove: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.remove: ArrayList ${ref} not found.`); | |||
| const index = this.req.arrays[ref].indexOf(value); | |||
| if (index == -1) throw Error('arraylist.remove: Value does not exist in the ArrayList.'); | |||
| this.req.arrays[ref].splice(index, 1); | |||
| }; | |||
| // Remove a value from the ArrayList at a specific index | |||
| exports.removeAt = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.removeAt: ref is required.'); | |||
| const index = this.parseRequired(options.index, 'number', 'arraylist.removeAt: index is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.removeAt: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.removeAt: ArrayList ${ref} not found.`); | |||
| this.req.arrays[ref].splice(index, 1); | |||
| }; | |||
| // Clear all values from the ArrayList | |||
| exports.clear = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.clear: ref is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.clear: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.clear: ArrayList ${ref} not found.`); | |||
| this.req.arrays[ref] = []; | |||
| }; | |||
| // Sort the ArrayList | |||
| exports.sort = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.sort: ref is required.'); | |||
| const prop = this.parseOptional(options.prop, 'string', null); | |||
| const desc = this.parseOptional(options.desc, 'boolean', false); | |||
| if (!this.req.arrays) throw Error('arraylist.sort: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.sort: ArrayList ${ref} not found.`); | |||
| this.req.arrays[ref].sort((a, b) => { | |||
| if (prop) { | |||
| a = a[prop]; | |||
| b = b[prop]; | |||
| } | |||
| if (a == b) return 0; | |||
| if (a < b) return desc ? 1 : -1; | |||
| return desc ? -1 : 1; | |||
| }); | |||
| }; | |||
| // Return the index of the first occurrence of a specific value | |||
| exports.indexOf = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.indexOf: ref is required.'); | |||
| const value = this.parseRequired(options.value, '*', 'arraylist.indexOf: value is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.indexOf: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.indexOf: ArrayList ${ref} not found.`); | |||
| return this.req.arrays[ref].indexOf(value); | |||
| }; | |||
| // Return true if the ArrayList contains a specific value | |||
| exports.contains = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.contains: ref is required.'); | |||
| const value = this.parseRequired(options.value, '*', 'arraylist.contains: value is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.contains: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.contains: ArrayList ${ref} not found.`); | |||
| return this.req.arrays[ref].includes(value); | |||
| }; | |||
| // Return true when ArrayList is empty | |||
| exports.isEmpty = function(options) { | |||
| const ref = this.parseRequired(options.ref, 'string', 'arraylist.isEmpty: ref is required.'); | |||
| if (!this.req.arrays) throw Error('arraylist.isEmpty: No ArrayList are created.'); | |||
| if (!this.req.arrays[ref]) throw Error(`arraylist.isEmpty: ArrayList ${ref} not found.`); | |||
| return !this.req.arrays[ref].length; | |||
| }; | |||
| @@ -0,0 +1,53 @@ | |||
| module.exports = { | |||
| provider: async function(options, name) { | |||
| const provider = await this.setAuthProvider(name, options); | |||
| return { identity: provider.identity }; | |||
| }, | |||
| identify: async function(options) { | |||
| const provider = await this.getAuthProvider(this.parseRequired(options.provider, 'string', 'auth.validate: provider is required.')); | |||
| return provider.identity; | |||
| }, | |||
| validate: async function(options) { | |||
| const provider = await this.getAuthProvider(this.parseRequired(options.provider, 'string', 'auth.validate: provider is required.')); | |||
| const { action, username, password, remember } = this.req.body; | |||
| if (action == 'login') { | |||
| return provider.login(username, password, remember); | |||
| } | |||
| if (action == 'logout') { | |||
| return provider.logout(); | |||
| } | |||
| if (!provider.identity) { | |||
| return provider.unauthorized(); | |||
| } | |||
| }, | |||
| login: async function(options) { | |||
| const provider = await this.getAuthProvider(this.parseRequired(options.provider, 'string', 'auth.login: provider is required.')); | |||
| const username = this.parseOptional(options.username, 'string', this.parse('{{$_POST.username}}')); | |||
| const password = this.parseOptional(options.password, 'string', this.parse('{{$_POST.password}}')); | |||
| const remember = this.parseOptional(options.remember, '*', this.parse('{{$_POST.remember}}')); | |||
| return provider.login(username, password, remember); | |||
| }, | |||
| logout: async function(options) { | |||
| const provider = await this.getAuthProvider(this.parseRequired(options.provider, 'string', 'auth.logout: provider is required.')); | |||
| return provider.logout(); | |||
| }, | |||
| restrict: async function(options) { | |||
| const provider = await this.getAuthProvider(this.parseRequired(options.provider, 'string', 'auth.restrict: provider is required.')); | |||
| return provider.restrict(this.parse(options)); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,185 @@ | |||
| const { clone } = require('../core/util'); | |||
| module.exports = { | |||
| addColumns: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.addColumns: collection is required.'); | |||
| let add = this.parseRequired(options.add, 'object', 'collections.addColumns: add is required.'); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let output = []; | |||
| for (let row of collection) { | |||
| let newRow = clone(row); | |||
| for (let column in add) { | |||
| if (overwrite || newRow[column] == null) { | |||
| newRow[column] = add[column]; | |||
| } | |||
| } | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| filterColumns: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.filterColumns: collection is required.'); | |||
| let columns = this.parseRequired(options.columns, 'object'/*array[string]*/, 'collections.filterColumns: columns is required.'); | |||
| let keep = this.parseOptional(options.keep, 'boolean', false); | |||
| let output = []; | |||
| for (let row of collection) { | |||
| let newRow = {}; | |||
| for (let column in row) { | |||
| if (columns.includes(column)) { | |||
| if (keep) { | |||
| newRow[column] = clone(row[column]); | |||
| } | |||
| } else if (!keep) { | |||
| newRow[column] = clone(row[column]); | |||
| } | |||
| } | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| renameColumns: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.renamecolumns: collection is required.'); | |||
| let rename = this.parseRequired(options.rename, 'object', 'collections.renamecolumns: rename is required.'); | |||
| let output = []; | |||
| for (let row of collection) { | |||
| let newRow = {}; | |||
| for (let column in row) { | |||
| newRow[rename[column] || column] = clone(row[column]); | |||
| } | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| fillDown: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.fillDown: collection is required.'); | |||
| let columns = this.parseRequired(options.columns, 'object'/*array[string]*/, 'collections.fillDown: columns is required.'); | |||
| let output = []; | |||
| let values = {}; | |||
| for (let column of columns) { | |||
| values[column] = null; | |||
| } | |||
| for (let row of collection) { | |||
| let newRow = clone(row); | |||
| for (let column in values) { | |||
| if (newRow[column] == null) { | |||
| newRow[column] = values[column]; | |||
| } else { | |||
| values[column] = newRow[column]; | |||
| } | |||
| } | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| addRows: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.addRows: collection is required.'); | |||
| let rows = this.parseRequired(options.rows, 'object'/*array[object]*/, 'collections.addRows: rows is required.'); | |||
| return clone(collection).concat(clone(rows)); | |||
| }, | |||
| addRownumbers: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.addRownumbers: collection is required.'); | |||
| let column = this.parseOptional(options.column, 'string', 'nr'); | |||
| let startAt = this.parseOptional(options.startAt, 'number', 1); | |||
| let desc = this.parseOptional(options.desc, 'boolean', false); | |||
| let output = []; | |||
| let nr = desc ? collection.length + startAt - 1 : startAt; | |||
| for (let row of collection) { | |||
| let newRow = clone(row); | |||
| newRow[column] = desc ? nr-- : nr++; | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| join: function(options) { | |||
| let collection1 = this.parseRequired(options.collection1, 'object'/*array[object]*/, 'collections.join: collection1 is required.'); | |||
| let collection2 = this.parseRequired(options.collection2, 'object'/*array[object]*/, 'collections.join: collection2 is required.'); | |||
| let matches = this.parseRequired(options.matches, 'object', 'collections.join: matches is required.'); | |||
| let matchAll = this.parseOptional(options.matchAll, 'boolean', false); | |||
| let output = []; | |||
| for (let row1 of collection1) { | |||
| let newRow = clone(row1); | |||
| for (let row2 of collection2) { | |||
| let join = false; | |||
| for (let match in matches) { | |||
| if (row1[match] == row2[matches[match]]) { | |||
| join = true; | |||
| if (!matchAll) break; | |||
| } else if (matchAll) { | |||
| join = false; | |||
| break; | |||
| } | |||
| } | |||
| if (join) { | |||
| for (let column in row2) { | |||
| newRow[column] = clone(row2[column]); | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| normalize: function(options) { | |||
| let collection = this.parseRequired(options.collection, 'object'/*array[object]*/, 'collections.normalize: collection is required.'); | |||
| let columns = []; | |||
| let output = []; | |||
| for (let row of collection) { | |||
| for (let column in row) { | |||
| if (!columns.includes(column)) { | |||
| columns.push(column); | |||
| } | |||
| } | |||
| } | |||
| for (let row of collection) { | |||
| let newRow = {}; | |||
| for (let column of columns) { | |||
| newRow[column] = row[column] == null ? null : clone(row[column]); | |||
| } | |||
| output.push(newRow); | |||
| } | |||
| return output; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,269 @@ | |||
| const fs = require('fs-extra'); | |||
| const { clone } = require('../core/util'); | |||
| module.exports = { | |||
| wait: function(options) { | |||
| let delay = this.parseOptional(options.delay, 'number', 1000); | |||
| return new Promise(resolve => setTimeout(resolve, delay)) | |||
| }, | |||
| log: function(options) { | |||
| let message = this.parse(options.message); | |||
| console.log(message); | |||
| return message; | |||
| }, | |||
| repeat: async function(options) { | |||
| let repeater = this.parseRequired(options.repeat, '*', 'core.repeater: repeat is required.'); | |||
| let outputFilter = this.parseOptional(options.outputFilter, 'string', 'include'); // include/exclude | |||
| let outputFields = this.parseOptional(options.outputFields, 'object'/*array[string]*/, []); | |||
| let index = 0, data = [], parentData = this.data; | |||
| switch (typeof repeater) { | |||
| case 'boolean': | |||
| repeater = repeater ? [0] : []; | |||
| break; | |||
| case 'string': | |||
| repeater = repeater.split(','); | |||
| break; | |||
| case 'number': | |||
| repeater = (n => { | |||
| let a = [], i = 0; | |||
| while (i < n) a.push(i++); | |||
| return a; | |||
| })(repeater); | |||
| break; | |||
| } | |||
| if (!(Array.isArray(repeater) || repeater instanceof Object)) { | |||
| throw new Error('Repeater data is not an array or object'); | |||
| } | |||
| for (let key in repeater) { | |||
| if (repeater.hasOwnProperty(key)) { | |||
| let scope = {}; | |||
| this.data = {}; | |||
| if (repeater[key] instanceof Object) { | |||
| for (var prop in repeater[key]) { | |||
| if (repeater[key].hasOwnProperty(prop)) { | |||
| scope[prop] = repeater[key][prop]; | |||
| if (outputFilter == 'exclude') { | |||
| if (!outputFields.includes(prop)) { | |||
| this.data[prop] = repeater[key][prop]; | |||
| } | |||
| } else { | |||
| if (outputFields.includes(prop)) { | |||
| this.data[prop] = repeater[key][prop]; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| scope.$key = key; | |||
| scope.$name = key; | |||
| scope.$value = clone(repeater[key]); | |||
| scope.$index = index; | |||
| scope.$number = index + 1; | |||
| scope.$oddeven = index % 2; | |||
| if (repeater[key] == null) { | |||
| repeater[key] = {}; | |||
| } | |||
| this.scope = this.scope.create(scope, clone(repeater[key])); | |||
| await this.exec(options.exec, true); | |||
| this.scope = this.scope.parent; | |||
| data.push({ ...this.data }); | |||
| index++; | |||
| } | |||
| } | |||
| this.data = parentData; | |||
| return data; | |||
| }, | |||
| while: async function(options) { | |||
| let max = this.parseOptional(options.max, 'number', Number.MAX_SAFE_INTEGER); | |||
| let i = 0; | |||
| while (this.parse(options.while)) { | |||
| await this.exec(options.exec, true); | |||
| if (++i == max) break; | |||
| } | |||
| }, | |||
| condition: async function(options) { | |||
| let condition = this.parse(options.if); | |||
| if (!!condition) { | |||
| if (options.then) { | |||
| await this.exec(options.then, true); | |||
| } | |||
| } else if (options.else) { | |||
| await this.exec(options.else, true); | |||
| } | |||
| }, | |||
| conditions: async function(options) { | |||
| if (Array.isArray(options.conditions)) { | |||
| for (let condition of options.conditions) { | |||
| let when = this.parse(condition.when); | |||
| if (!!when) { | |||
| return this.exec(condition.then, true); | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| select: async function(options) { | |||
| let expression = this.parse(options.expression); | |||
| if (Array.isArray(options.cases)) { | |||
| for (let item of options.cases) { | |||
| let value = this.parse(item.value); | |||
| if (expression === value) { | |||
| return this.exec(item.exec, true); | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| setvalue: function(options) { | |||
| let key = this.parseOptional(options.key, 'string', ''); | |||
| let value = this.parse(options.value); | |||
| if (key) this.set(key, value); | |||
| return value; | |||
| }, | |||
| setsession: function(options, name) { | |||
| let value = this.parse(options.value); | |||
| this.req.session[name] = value; | |||
| return value; | |||
| }, | |||
| removesession: function(options, name) { | |||
| delete this.req.session[name]; | |||
| }, | |||
| setcookie: function(options, name) { | |||
| options = this.parse(options); | |||
| let cookieOptions = { | |||
| domain: options.domain || undefined, | |||
| httpOnly: !!options.httpOnly, | |||
| maxAge: options.expires === 0 ? undefined : (options.expires || 30) * 24 * 60 * 60 * 1000, // from days to ms | |||
| path: options.path || '/', | |||
| secure: !!options.secure, | |||
| sameSite: options.sameSite || false | |||
| }; | |||
| this.setCookie(name, options.value, cookieOptions); | |||
| }, | |||
| removecookie: function(options, name) { | |||
| options = this.parse(options); | |||
| let cookieOptions = { | |||
| domain: options.domain || undefined, | |||
| httpOnly: !!options.httpOnly, | |||
| maxAge: options.expires === 0 ? undefined : (options.expires || 30) * 24 * 60 * 60 * 1000, // from days to ms | |||
| path: options.path || '/', | |||
| secure: !!options.secure, | |||
| sameSite: !!options.sameSite | |||
| }; | |||
| this.removeCookie(name, cookieOptions); | |||
| }, | |||
| response: function(options) { | |||
| let data = this.parseOptional(options.data, '*', null); | |||
| let status = this.parseOptional(options.status, 'number', 200); | |||
| let contentType = this.parseOptional(options.contentType, 'string', 'application/json'); | |||
| if (contentType != 'application/json') { | |||
| this.res.set('Content-Type', contentType); | |||
| this.res.status(status).send(data); | |||
| } else { | |||
| this.res.status(status).json(data); | |||
| } | |||
| }, | |||
| error: function(options) { | |||
| let message = this.parseRequired(options.message, 'string', 'core.error: message is required.'); | |||
| throw new Error(message); | |||
| }, | |||
| redirect: function(options) { | |||
| let url = this.parseRequired(options.url, 'string', 'core.redirect: url is required.'); | |||
| let status = this.parseOptional(options.status, 'number', 302); | |||
| this.res.redirect(status == 301 ? 301 : 302, url); | |||
| }, | |||
| trycatch: async function(options) { | |||
| try { | |||
| await this.exec(options.try, true); | |||
| } catch (error) { | |||
| this.scope.set('$_ERROR', error.message || error); | |||
| this.error = false; | |||
| if (options.catch) { | |||
| await this.exec(options.catch, true); | |||
| } | |||
| } | |||
| }, | |||
| exec: async function(options) { | |||
| var data = {}; | |||
| if (options.exec && fs.existsSync(`app/modules/lib/${options.exec}.json`)) { | |||
| let parentData = this.data; | |||
| this.data = {}; | |||
| this.scope = this.scope.create({ $_PARAM: this.parse(options.params) }); | |||
| await this.exec(await fs.readJSON(`app/modules/lib/${options.exec}.json`), true); | |||
| data = this.data; | |||
| this.scope = this.scope.parent; | |||
| this.data = parentData; | |||
| } else { | |||
| throw new Error(`There is no action called '${options.exec}' found in the library.`); | |||
| } | |||
| return data; | |||
| }, | |||
| group: async function(options, name) { | |||
| if (name) { | |||
| return this.sub(options.exec); | |||
| } else { | |||
| return this.exec(options.exec, true); | |||
| } | |||
| }, | |||
| parallel: async function(options, name) { | |||
| let actions = options.exec.steps || options.exec; | |||
| if (!Array.isArray(actions)) actions = [actions]; | |||
| return await Promise.all(actions.map(exec => { | |||
| if (name) { | |||
| return this.sub(exec); | |||
| } else { | |||
| return this.exec(exec, true); | |||
| } | |||
| })); | |||
| }, | |||
| randomUUID: function(options) { | |||
| const { randomUUID } = require('crypto'); | |||
| const { v4: uuidv4 } = require('uuid'); | |||
| return randomUUID ? randomUUID() : uuidv4(); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,31 @@ | |||
| // Perhaps use hashy as reference to implement bcrypt and needRehash action | |||
| // https://github.com/JsCommunity/hashy | |||
| // supports argon2i, argon2d and argon2id | |||
| exports.passwordHash = async function(options) { | |||
| const password = this.parseRequired(options.password, 'string', 'crypto.passwordHash: password is required.') | |||
| const algo = this.parseOptional(options.algo, 'string', 'argon2i') | |||
| const argon2 = require('argon2') | |||
| const type = argon2[algo] | |||
| return argon2.hash(password, { type }) | |||
| } | |||
| exports.passwordVerify = async function(options) { | |||
| const password = this.parseRequired(options.password, 'string', 'crypto.passwordVerify: password is required.') | |||
| const hash = this.parseRequired(options.hash, 'string', 'crypto.passwordVerify: hash is required.') | |||
| const argon2 = require('argon2') | |||
| return argon2.verify(hash, password) | |||
| } | |||
| exports.passwordNeedsRehash = async function(options) { | |||
| const hash = this.parseRequired(options.hash, 'string', 'crypto.passwordNeedsRehash: hash is required.') | |||
| const algo = this.parseOptional(options.algo, 'string', 'argon2i') | |||
| const argon2 = require('argon2') | |||
| return argon2.needsRehash(hash, {}) | |||
| } | |||
| exports.uuid = async function(options) { | |||
| const { randomUUID } = require('crypto') | |||
| const { v4: uuidv4 } = require('uuid') | |||
| return randomUUID ? randomUUID() : uuidv4() | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| const { readFileSync } = require('fs'); | |||
| const { resolve } = require('path'); | |||
| module.exports = { | |||
| json: function(options) { | |||
| return JSON.parse(this.parse(options.data)); | |||
| }, | |||
| local: function(options) { | |||
| let path = this.parse(options.path); | |||
| if (typeof path !== 'string') throw new Error('dataset.local: path is required.'); | |||
| let data = readFileSync(resolve('public', path)); | |||
| return JSON.parse(data); | |||
| }, | |||
| remote: function(options) { | |||
| throw new Error('dataset.remote: not implemented, use api instead.'); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,765 @@ | |||
| const db = require('../core/db'); | |||
| const { where } = require('../formatters'); | |||
| const debug = require('debug')('server-connect:db'); | |||
| module.exports = { | |||
| connect: function(options, name) { | |||
| if (!name) throw new Error('dbconnector.connect has no name.'); | |||
| this.setDbConnection(name, options); | |||
| }, | |||
| select: async function(options, name, meta) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.select: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.select: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.select: sql.table is required.'); | |||
| if (typeof sql.sort != 'string') sql.sort = this.parseOptional('{{ $_GET.sort }}', 'string', null); | |||
| if (typeof sql.dir != 'string') sql.dir = this.parseOptional('{{ $_GET.dir }}', 'string', 'asc'); | |||
| if (sql.sort && sql.columns) { | |||
| if (!sql.orders) sql.orders = []; | |||
| for (let column of sql.columns) { | |||
| if (column.column == sql.sort || column.alias == sql.sort) { | |||
| let order = { | |||
| column: column.alias || column.column, | |||
| direction: sql.dir.toLowerCase() == 'desc' ? 'desc' : 'asc' | |||
| }; | |||
| if (column.table) order.table = column.table; | |||
| sql.orders.unshift(order); | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| sql.type = 'select'; | |||
| if (db.client == 'couchdb') { | |||
| let table = sql.table.name || sql.table; | |||
| let { rows } = await db.list({ include_docs: true, startkey: table + '/', endkey: table + '0' }); | |||
| rows = rows.map(row => row.doc); | |||
| if (sql.wheres) { | |||
| const validate = (row, rule) => { | |||
| if (rule.operator) { | |||
| let a = row[rule.data.column]; | |||
| let b = rule.value; | |||
| switch (rule.operator) { | |||
| case 'equal': return a == b; | |||
| case 'not_equal': return a != b; | |||
| case 'in': return b.includes(a); | |||
| case 'not_in': return !b.includes(a); | |||
| case 'less': return a < b; | |||
| case 'less_or_equal': return a <= b; | |||
| case 'greater': return a > b; | |||
| case 'greater_or_equal': return a >= b; | |||
| case 'between': return b[0] <= a <= b[1]; | |||
| case 'not_between': return !(b[0] <= a <= b[1]); | |||
| case 'begins_with': return String(a).startsWith(String(b)); | |||
| case 'not_begins_with': return !String(a).startsWith(String(b)); | |||
| case 'contains': return String(a).includes(String(b)); | |||
| case 'not_contains': return !String(a).includes(String(b)); | |||
| case 'ends_with': return String(a).endsWith(String(b)); | |||
| case 'not_ends_with': return !String(a).endsWith(String(b)); | |||
| case 'is_empty': return a == null || a == ''; | |||
| case 'is_not_empty': return a != null && a != ''; | |||
| case 'is_null': return a == null; | |||
| case 'is_not_null': return a != null; | |||
| } | |||
| } | |||
| if (rule.condition && rule.rules.length) { | |||
| for (const _rule of rule.rules) { | |||
| const valid = validate(row, _rule); | |||
| if (!valid && rule.condition == 'AND') return false; | |||
| if (valid && rule.condition == 'OR') return true; | |||
| } | |||
| return rule.condition == 'OR' ? false : true; | |||
| } | |||
| return true; | |||
| }; | |||
| rows = rows.filter(row => { | |||
| return validate(row, sql.wheres); | |||
| }); | |||
| } | |||
| if (sql.orders && sql.orders.length) { | |||
| rows.sort((a, b) => { | |||
| for (let order of sql.orders) { | |||
| if (a[order.column] == b[order.column]) continue; | |||
| let desc = order.direction && order.direction.toLowerCase() == 'desc'; | |||
| if (a[order.column] < b[order.column]) { | |||
| return desc ? 1 : -1; | |||
| } else { | |||
| return desc ? -1 : 1; | |||
| } | |||
| } | |||
| return 0; | |||
| }); | |||
| } | |||
| if (sql.columns && sql.columns.length) { | |||
| // we can also skip if user want just all columns | |||
| if (!(sql.columns.length == 1 && sql.columns[0].column == '*')) { | |||
| rows = rows.map(doc => { | |||
| const row = {}; | |||
| for (let column of sql.columns) { | |||
| if (column.column == '*') { | |||
| Object.assign(row, doc); | |||
| } else { | |||
| // only support single level for now | |||
| row[column.alias || column.column || column] = doc[column.column || column]; | |||
| } | |||
| } | |||
| return row; | |||
| }); | |||
| } | |||
| } | |||
| if (sql.distinct) { | |||
| rows = [...new Set(rows)]; | |||
| } | |||
| let offset = Number(sql.offset || 0); | |||
| let limit = Number(sql.limit || 0); | |||
| return rows.slice(offset, limit ? offset + limit : undefined); | |||
| } | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: db.fromJSON(sql, meta).toSQL().toNative() | |||
| }; | |||
| } | |||
| if (hasSubs(sql)) { | |||
| prepareColumns(sql); | |||
| const results = await db.fromJSON(sql, meta); | |||
| if (results.length) { | |||
| if (sql.sub) { | |||
| await _processSubQueries.call(this, db, results, sql.sub, meta); | |||
| } | |||
| if (sql.joins && sql.joins.length) { | |||
| for (const join of sql.joins) { | |||
| if (join.sub) { | |||
| await _processSubQueries.call(this, db, results, join.sub, meta, '_' + (join.alias || join.table)); | |||
| } | |||
| } | |||
| } | |||
| cleanupResults(results); | |||
| } | |||
| return results; | |||
| } | |||
| return db.fromJSON(sql, meta); | |||
| }, | |||
| count: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.count: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.count: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.count: sql.table is required.'); | |||
| sql.type = 'count'; | |||
| if (db.client == 'couchdb') { | |||
| let table = sql.table.name || sql.table; | |||
| let { rows } = await db.list({ include_docs: true, startkey: table + '/', endkey: table + '0' }); | |||
| rows = rows.map(row => row.doc); | |||
| if (sql.wheres) { | |||
| const validate = (row, rule) => { | |||
| if (rule.operator) { | |||
| let a = row[rule.data.column]; | |||
| let b = rule.value; | |||
| switch (rule.operator) { | |||
| case 'equal': return a == b; | |||
| case 'not_equal': return a != b; | |||
| case 'in': return b.includes(a); | |||
| case 'not_in': return !b.includes(a); | |||
| case 'less': return a < b; | |||
| case 'less_or_equal': return a <= b; | |||
| case 'greater': return a > b; | |||
| case 'greater_or_equal': return a >= b; | |||
| case 'between': return b[0] <= a <= b[1]; | |||
| case 'not_between': return !(b[0] <= a <= b[1]); | |||
| case 'begins_with': return String(a).startsWith(String(b)); | |||
| case 'not_begins_with': return !String(a).startsWith(String(b)); | |||
| case 'contains': return String(a).includes(String(b)); | |||
| case 'not_contains': return !String(a).includes(String(b)); | |||
| case 'ends_with': return String(a).endsWith(String(b)); | |||
| case 'not_ends_with': return !String(a).endsWith(String(b)); | |||
| case 'is_empty': return a == null || a == ''; | |||
| case 'is_not_empty': return a != null && a != ''; | |||
| case 'is_null': return a == null; | |||
| case 'is_not_null': return a != null; | |||
| } | |||
| } | |||
| if (rule.condition && rule.rules.length) { | |||
| for (const _rule of rule.rules) { | |||
| const valid = validate(row, _rule); | |||
| if (!valid && rule.condition == 'AND') return false; | |||
| if (valid && rule.condition == 'OR') return true; | |||
| } | |||
| return rule.condition == 'OR' ? false : true; | |||
| } | |||
| return true; | |||
| }; | |||
| rows = rows.filter(row => { | |||
| return validate(row, sql.wheres); | |||
| }); | |||
| } | |||
| if (sql.distinct) { | |||
| rows = [...new Set(rows)]; | |||
| } | |||
| return rows.length; | |||
| } | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: db.fromJSON(sql, meta).toSQL().toNative() | |||
| }; | |||
| } | |||
| return (await db.fromJSON(sql)).Total; | |||
| }, | |||
| single: async function(options, name, meta) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.single: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.single: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.single: sql.table is required.'); | |||
| if (typeof sql.sort != 'string') sql.sort = this.parseOptional('{{ $_GET.sort }}', 'string', null); | |||
| if (typeof sql.dir != 'string') sql.dir = this.parseOptional('{{ $_GET.dir }}', 'string', 'asc'); | |||
| sql.type = 'first'; | |||
| if (db.client == 'couchdb') { | |||
| let table = sql.table.name || sql.table; | |||
| let { rows } = await db.list({ include_docs: true, startkey: table + '/', endkey: table + '0' }); | |||
| rows = rows.map(row => row.doc); | |||
| if (sql.wheres) { | |||
| const validate = (row, rule) => { | |||
| if (rule.operator) { | |||
| let a = row[rule.data.column]; | |||
| let b = rule.value; | |||
| switch (rule.operator) { | |||
| case 'equal': return a == b; | |||
| case 'not_equal': return a != b; | |||
| case 'in': return b.includes(a); | |||
| case 'not_in': return !b.includes(a); | |||
| case 'less': return a < b; | |||
| case 'less_or_equal': return a <= b; | |||
| case 'greater': return a > b; | |||
| case 'greater_or_equal': return a >= b; | |||
| case 'between': return b[0] <= a <= b[1]; | |||
| case 'not_between': return !(b[0] <= a <= b[1]); | |||
| case 'begins_with': return String(a).startsWith(String(b)); | |||
| case 'not_begins_with': return !String(a).startsWith(String(b)); | |||
| case 'contains': return String(a).includes(String(b)); | |||
| case 'not_contains': return !String(a).includes(String(b)); | |||
| case 'ends_with': return String(a).endsWith(String(b)); | |||
| case 'not_ends_with': return !String(a).endsWith(String(b)); | |||
| case 'is_empty': return a == null || a == ''; | |||
| case 'is_not_empty': return a != null && a != ''; | |||
| case 'is_null': return a == null; | |||
| case 'is_not_null': return a != null; | |||
| } | |||
| } | |||
| if (rule.condition && rule.rules.length) { | |||
| for (const _rule of rule.rules) { | |||
| const valid = validate(row, _rule); | |||
| if (!valid && rule.condition == 'AND') return false; | |||
| if (valid && rule.condition == 'OR') return true; | |||
| } | |||
| return rule.condition == 'OR' ? false : true; | |||
| } | |||
| return true; | |||
| }; | |||
| rows = rows.filter(row => { | |||
| return validate(row, sql.wheres); | |||
| }); | |||
| } | |||
| if (sql.orders && sql.orders.length) { | |||
| rows.sort((a, b) => { | |||
| for (let order of sql.orders) { | |||
| if (a[order.column] == b[order.column]) continue; | |||
| let desc = order.direction && order.direction.toLowerCase() == 'desc'; | |||
| if (a[order.column] < b[order.column]) { | |||
| return desc ? 1 : -1; | |||
| } else { | |||
| return desc ? -1 : 1; | |||
| } | |||
| } | |||
| return 0; | |||
| }); | |||
| } | |||
| if (sql.columns && sql.columns.length) { | |||
| // we can also skip if user want just all columns | |||
| if (!(sql.columns.length == 1 && sql.columns[0].column == '*')) { | |||
| rows = rows.map(doc => { | |||
| const row = {}; | |||
| for (let column of sql.columns) { | |||
| if (column.column == '*') { | |||
| Object.assign(row, doc); | |||
| } else { | |||
| // only support single level for now | |||
| row[column.alias || column.column || column] = doc[column.column || column]; | |||
| } | |||
| } | |||
| return row; | |||
| }); | |||
| } | |||
| } | |||
| if (sql.distinct) { | |||
| rows = [...new Set(rows)]; | |||
| } | |||
| return rows.length ? rows[0] : null; | |||
| } | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: db.fromJSON(sql, meta).toSQL().toNative() | |||
| }; | |||
| } | |||
| if (hasSubs(sql)) { | |||
| prepareColumns(sql); | |||
| const result = await db.fromJSON(sql, meta); | |||
| if (!result) return null; | |||
| if (sql.sub) { | |||
| await _processSubQueries.call(this, db, [result], sql.sub, meta); | |||
| } | |||
| if (sql.joins && sql.joins.length) { | |||
| for (const join of sql.joins) { | |||
| if (join.sub) { | |||
| await _processSubQueries.call(this, db, [result], join.sub, meta, '_' + (join.alias || join.table)); | |||
| } | |||
| } | |||
| } | |||
| cleanupResults([result]); | |||
| return result; | |||
| } | |||
| return db.fromJSON(sql, meta) || null; | |||
| }, | |||
| paged: async function(options, name, meta) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.paged: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.paged: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.paged: sql.table is required.'); | |||
| if (typeof sql.offset != 'number') sql.offset = Number(this.parseOptional('{{ $_GET.offset }}', '*', 0)); | |||
| if (typeof sql.limit != 'number') sql.limit = Number(this.parseOptional('{{ $_GET.limit }}', '*', 25)); | |||
| if (typeof sql.sort != 'string') sql.sort = this.parseOptional('{{ $_GET.sort }}', 'string', null); | |||
| if (typeof sql.dir != 'string') sql.dir = this.parseOptional('{{ $_GET.dir }}', 'string', 'asc'); | |||
| if (sql.sort && sql.columns) { | |||
| if (!sql.orders) sql.orders = []; | |||
| for (let column of sql.columns) { | |||
| if (column.column == sql.sort || column.alias == sql.sort) { | |||
| let order = { | |||
| column: column.alias || column.column, | |||
| direction: sql.dir.toLowerCase() == 'desc' ? 'desc' : 'asc' | |||
| }; | |||
| if (column.table) order.table = column.table; | |||
| sql.orders.unshift(order); | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| if (db.client == 'couchdb') { | |||
| let table = sql.table.name || sql.table; | |||
| let { rows } = await db.list({ include_docs: true, startkey: table + '/', endkey: table + '0' }); | |||
| rows = rows.map(row => row.doc); | |||
| if (sql.wheres) { | |||
| const validate = (row, rule) => { | |||
| if (rule.operator) { | |||
| let a = row[rule.data.column]; | |||
| let b = rule.value; | |||
| switch (rule.operator) { | |||
| case 'equal': return a == b; | |||
| case 'not_equal': return a != b; | |||
| case 'in': return b.includes(a); | |||
| case 'not_in': return !b.includes(a); | |||
| case 'less': return a < b; | |||
| case 'less_or_equal': return a <= b; | |||
| case 'greater': return a > b; | |||
| case 'greater_or_equal': return a >= b; | |||
| case 'between': return b[0] <= a <= b[1]; | |||
| case 'not_between': return !(b[0] <= a <= b[1]); | |||
| case 'begins_with': return String(a).startsWith(String(b)); | |||
| case 'not_begins_with': return !String(a).startsWith(String(b)); | |||
| case 'contains': return String(a).includes(String(b)); | |||
| case 'not_contains': return !String(a).includes(String(b)); | |||
| case 'ends_with': return String(a).endsWith(String(b)); | |||
| case 'not_ends_with': return !String(a).endsWith(String(b)); | |||
| case 'is_empty': return a == null || a == ''; | |||
| case 'is_not_empty': return a != null && a != ''; | |||
| case 'is_null': return a == null; | |||
| case 'is_not_null': return a != null; | |||
| } | |||
| } | |||
| if (rule.condition && rule.rules.length) { | |||
| for (const _rule of rule.rules) { | |||
| const valid = validate(row, _rule); | |||
| if (!valid && rule.condition == 'AND') return false; | |||
| if (valid && rule.condition == 'OR') return true; | |||
| } | |||
| return rule.condition == 'OR' ? false : true; | |||
| } | |||
| return true; | |||
| }; | |||
| rows = rows.filter(row => { | |||
| return validate(row, sql.wheres); | |||
| }); | |||
| } | |||
| if (sql.orders && sql.orders.length) { | |||
| rows.sort((a, b) => { | |||
| for (let order of sql.orders) { | |||
| if (a[order.column] == b[order.column]) continue; | |||
| let desc = order.direction && order.direction.toLowerCase() == 'desc'; | |||
| if (a[order.column] < b[order.column]) { | |||
| return desc ? 1 : -1; | |||
| } else { | |||
| return desc ? -1 : 1; | |||
| } | |||
| } | |||
| return 0; | |||
| }); | |||
| } | |||
| if (sql.columns && sql.columns.length) { | |||
| // we can also skip if user want just all columns | |||
| if (!(sql.columns.length == 1 && sql.columns[0].column == '*')) { | |||
| rows = rows.map(doc => { | |||
| const row = {}; | |||
| for (let column of sql.columns) { | |||
| if (column.column == '*') { | |||
| Object.assign(row, doc); | |||
| } else { | |||
| // only support single level for now | |||
| row[column.alias || column.column || column] = doc[column.column || column]; | |||
| } | |||
| } | |||
| return row; | |||
| }); | |||
| } | |||
| } | |||
| if (sql.distinct) { | |||
| rows = [...new Set(rows)]; | |||
| } | |||
| let offset = Number(sql.offset || 0); | |||
| let limit = Number(sql.limit || 0); | |||
| let total = rows.length; | |||
| return { | |||
| offset, | |||
| limit, | |||
| total, | |||
| page: { | |||
| offset: { | |||
| first: 0, | |||
| prev: offset - limit > 0 ? offset - limit : 0, | |||
| next: offset + limit < total ? offset + limit : offset, | |||
| last: (Math.ceil(total / limit) - 1) * limit | |||
| }, | |||
| current: Math.floor(offset / limit) + 1, | |||
| total: Math.ceil(total / limit) | |||
| }, | |||
| data: rows.slice(offset, limit ? offset + limit : undefined) | |||
| }; | |||
| } | |||
| sql.type = 'count'; | |||
| let total = +(await db.fromJSON(sql, meta))['Total']; | |||
| sql.type = 'select'; | |||
| let data = []; | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: db.fromJSON(sql, meta).toSQL().toNative() | |||
| }; | |||
| } | |||
| if (hasSubs(sql)) { | |||
| prepareColumns(sql); | |||
| const results = await db.fromJSON(sql, meta); | |||
| if (results.length) { | |||
| if (sql.sub) { | |||
| await _processSubQueries.call(this, db, results, sql.sub, meta); | |||
| } | |||
| if (sql.joins && sql.joins.length) { | |||
| for (const join of sql.joins) { | |||
| if (join.sub) { | |||
| await _processSubQueries.call(this, db, results, join.sub, meta, '_' + (join.alias || join.table)); | |||
| } | |||
| } | |||
| } | |||
| cleanupResults(results); | |||
| } | |||
| data = results; | |||
| } else { | |||
| data = await db.fromJSON(sql, meta); | |||
| } | |||
| return { | |||
| offset: sql.offset, | |||
| limit: sql.limit, | |||
| total, | |||
| page: { | |||
| offset: { | |||
| first: 0, | |||
| prev: sql.offset - sql.limit > 0 ? sql.offset - sql.limit : 0, | |||
| next: sql.offset + sql.limit < total ? sql.offset + sql.limit : sql.offset, | |||
| last: (Math.ceil(total / sql.limit) - 1) * sql.limit | |||
| }, | |||
| current: Math.floor(sql.offset / sql.limit) + 1, | |||
| total: Math.ceil(total / sql.limit) | |||
| }, | |||
| data | |||
| } | |||
| }, | |||
| }; | |||
| async function _processSubQueries(db, results, sub, meta, prefix = '') { | |||
| const lookup = new Map(); | |||
| const keys = new Set(); | |||
| // get keys from results and create lookup table | |||
| // add initial sub field to results (empty array) | |||
| for (const result of results) { | |||
| const key = String(result['__dmxPrimary' + prefix]); | |||
| if (lookup.has(key)) { | |||
| lookup.get(key).push(result); | |||
| } else { | |||
| lookup.set(key, [result]); | |||
| } | |||
| keys.add(key); | |||
| for (const field in sub) { | |||
| result[field] = []; | |||
| } | |||
| } | |||
| for (const field in sub) { | |||
| const sql = this.parseSQL(sub[field]); | |||
| sql.type = 'select'; | |||
| prepareColumns(sql); | |||
| let submeta = meta && meta.find(data => data.name == field); | |||
| if (submeta && submeta.sub) submeta = submeta.sub; | |||
| // get all subresults with a single query | |||
| const subResults = await db.fromJSON(sql, submeta).whereIn(sql.key, Array.from(keys)); | |||
| if (subResults.length) { | |||
| if (sql.sub) { | |||
| await _processSubQueries.call(this, db, subResults, sql.sub, submeta); | |||
| } | |||
| if (sql.joins && sql.joins.length) { | |||
| for (const join of sql.joins) { | |||
| if (join.sub) { | |||
| await _processSubQueries.call(this, db, subResults, join.sub, submeta, '_' + (join.alias || join.table)); | |||
| } | |||
| } | |||
| } | |||
| // map the sub results to the parent recordset | |||
| for (const subResult of subResults) { | |||
| const results = lookup.get(String(subResult['__dmxForeign'])); | |||
| if (results) { | |||
| for (const result of results) { | |||
| result[field].push(subResult); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // we don't need to return anything since all is updated by reference | |||
| } | |||
| function hasSubs(sql) { | |||
| if (sql.sub) return true; | |||
| if (sql.joins && sql.joins.length) { | |||
| for (const join of sql.joins) { | |||
| if (join.sub) return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| function prepareColumns(sql) { | |||
| const table = sql.table.alias || sql.table.name || sql.table; | |||
| if (!Array.isArray(sql.columns) || !sql.columns.length) { | |||
| sql.columns = [{ | |||
| table: table, | |||
| column: '*' | |||
| }]; | |||
| if (Array.isArray(sql.joins) && sql.joins.length) { | |||
| for (join of sql.joins) { | |||
| sql.columns.push({ | |||
| table: join.alias || join.table, | |||
| column: '*' | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| if (sql.sub && sql.primary) { | |||
| sql.columns.push({ | |||
| table: table, | |||
| column: sql.primary, | |||
| alias: '__dmxPrimary' | |||
| }); | |||
| if (sql.groupBy && sql.groupBy.length) { | |||
| sql.groupBy.push({ | |||
| table: table, | |||
| column: sql.primary | |||
| }); | |||
| } | |||
| } | |||
| if (sql.key) { | |||
| sql.columns.push({ | |||
| table: table, | |||
| column: sql.key, | |||
| alias: '__dmxForeign' | |||
| }); | |||
| if (sql.groupBy && sql.groupBy.length) { | |||
| sql.groupBy.push({ | |||
| table: table, | |||
| column: sql.key | |||
| }); | |||
| } | |||
| } | |||
| if (sql.joins && sql.joins.length) { | |||
| for (const join of sql.joins) { | |||
| if (join.sub && join.primary) { | |||
| sql.columns.push({ | |||
| table: join.alias || join.table, | |||
| column: join.primary, | |||
| alias: '__dmxPrimary_' + (join.alias || join.table) | |||
| }); | |||
| if (sql.groupBy && sql.groupBy.length) { | |||
| sql.groupBy.push({ | |||
| table: join.alias || join.table, | |||
| column: join.primary | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| function cleanupResults(results) { | |||
| for (const result of results) { | |||
| for (const field of Object.keys(result)) { | |||
| if (field.startsWith('__dmx')) { | |||
| delete result[field]; | |||
| } else if (Array.isArray(result[field])) { | |||
| cleanupResults(result[field]); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,412 @@ | |||
| module.exports = { | |||
| insert: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.insert: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.insert: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.insert: sql.table is required.'); | |||
| sql.type = 'insert'; | |||
| if (db.client == 'couchdb') { | |||
| const doc = {}; | |||
| for (const value of sql.values) { | |||
| doc[value.column] = value.value; | |||
| } | |||
| const result = await db.insert(doc, sql.table + '/' + Date.now()); | |||
| if (result.ok) { | |||
| return { affected: 1, identity: result.id }; | |||
| } else { | |||
| //throw new Error('dbconnector.insert: error inserting document into couchdb.'); | |||
| return { affected: 0 }; | |||
| } | |||
| } | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: sql.toString() | |||
| }; | |||
| } | |||
| if (sql.sub) { | |||
| return db.transaction(async trx => { | |||
| // TODO: test how identity is returned for each database | |||
| // main insert, returns inserted id | |||
| const [identity] = (await trx.fromJSON(sql)).map(value => value[sql.returning] || value); | |||
| // loop sub (relation table) | |||
| for (let { table, key, value, values } of Object.values(sql.sub)) { | |||
| if (!Array.isArray(value)) break; | |||
| for (const current of value) { | |||
| if (typeof current == 'object') { | |||
| current[key] = identity; | |||
| await trx(table).insert(current); | |||
| } else { | |||
| if (values.length != 1) throw new Error('Invalid value mapping'); | |||
| await trx(table).insert({ | |||
| [key]: identity, | |||
| [values[0].column]: current | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| return { affected: 1, identity }; | |||
| }); | |||
| } | |||
| let identity = await db.fromJSON(sql); | |||
| if (identity) { | |||
| if (Array.isArray(identity)) { | |||
| identity = identity[0]; | |||
| } | |||
| if (typeof identity == 'object') { | |||
| identity = identity[Object.keys(identity)[0]]; | |||
| } | |||
| } | |||
| return { affected: 1, identity }; | |||
| }, | |||
| update: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.update: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.update: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.update: sql.table is required.'); | |||
| sql.type = 'update'; | |||
| if (db.client == 'couchdb') { | |||
| let { rows } = await db.list({ include_docs: true, startkey: sql.table + '/', endkey: sql.table + '0' }); | |||
| rows = rows.map(row => row.doc); | |||
| if (sql.wheres) { | |||
| const validate = (row, rule) => { | |||
| if (rule.operator) { | |||
| let a = row[rule.data.column]; | |||
| let b = rule.value; | |||
| switch (rule.operator) { | |||
| case 'equal': return a == b; | |||
| case 'not_equal': return a != b; | |||
| case 'in': return b.includes(a); | |||
| case 'not_in': return !b.includes(a); | |||
| case 'less': return a < b; | |||
| case 'less_or_equal': return a <= b; | |||
| case 'greater': return a > b; | |||
| case 'greater_or_equal': return a >= b; | |||
| case 'between': return b[0] <= a <= b[1]; | |||
| case 'not_between': return !(b[0] <= a <= b[1]); | |||
| case 'begins_with': return String(a).startsWith(String(b)); | |||
| case 'not_begins_with': return !String(a).startsWith(String(b)); | |||
| case 'contains': return String(a).includes(String(b)); | |||
| case 'not_contains': return !String(a).includes(String(b)); | |||
| case 'ends_with': return String(a).endsWith(String(b)); | |||
| case 'not_ends_with': return !String(a).endsWith(String(b)); | |||
| case 'is_empty': return a == null || a == ''; | |||
| case 'is_not_empty': return a != null && a != ''; | |||
| case 'is_null': return a == null; | |||
| case 'is_not_null': return a != null; | |||
| } | |||
| } | |||
| if (rule.condition && rule.rules.length) { | |||
| for (const _rule of rule.rules) { | |||
| const valid = validate(row, _rule); | |||
| if (!valid && rule.condition == 'AND') return false; | |||
| if (valid && rule.condition == 'OR') return true; | |||
| } | |||
| return rule.condition == 'OR' ? false : true; | |||
| } | |||
| return true; | |||
| }; | |||
| rows = rows.filter(row => { | |||
| return validate(row, sql.wheres); | |||
| }); | |||
| } | |||
| const result = await db.bulk(rows.map(doc => { | |||
| for (const value of sql.values) { | |||
| doc[value.column] = value.value; | |||
| } | |||
| return doc; | |||
| })); | |||
| return { affected: Array.isArray(result) ? result.filter(result => result.ok).length : rows.length }; | |||
| } | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: sql.toString() | |||
| }; | |||
| } | |||
| if (sql.sub) { | |||
| return db.transaction(async trx => { | |||
| let updated = await trx.fromJSON(sql); | |||
| if (!Array.isArray(updated)) { | |||
| // check if is single update | |||
| const single = ( | |||
| sql.wheres && | |||
| sql.wheres.rules && | |||
| sql.wheres.rules.length == 1 && | |||
| sql.wheres.rules[0].field == sql.returning && | |||
| sql.wheres.rules[0].operation == '=' | |||
| ); | |||
| if (single) { | |||
| // get id from where condition | |||
| updated = [sql.wheres.rules[0].value]; | |||
| } else { | |||
| // create a select with same where conditions | |||
| updated = await trx.fromJSON({ | |||
| ...sql, | |||
| type: 'select', | |||
| columns: [sql.returning] | |||
| }); | |||
| updated = updated.map(value => value[sql.returning]); | |||
| } | |||
| } else { | |||
| updated = updated.map(value => value[sql.returning]); | |||
| } | |||
| // loop sub | |||
| for (let { table, key, value, values } of Object.values(sql.sub)) { | |||
| if (!Array.isArray(value)) continue; | |||
| // delete old related data first | |||
| await trx(table).whereIn(key, updated).del(); | |||
| // for each updated item | |||
| for (const identity of updated) { | |||
| // insert value | |||
| for (const current of value) { | |||
| if (typeof current == 'object') { | |||
| current[key] = identity; | |||
| await trx(table).insert(current); | |||
| } else { | |||
| if (values.length != 1) throw new Error('Invalid value mapping'); | |||
| await trx(table).insert({ | |||
| [key]: identity, | |||
| [values[0].column]: current | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return { affected: updated.length }; | |||
| }); | |||
| } | |||
| let affected = await db.fromJSON(sql); | |||
| return { affected }; | |||
| }, | |||
| delete: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbconnector.delete: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.delete: sql is required.'); | |||
| if (!sql.table) throw new Error('dbconnector.delete: sql.table is required.'); | |||
| sql.type = 'del'; | |||
| if (db.client == 'couchdb') { | |||
| let { rows } = await db.list({ include_docs: true, startkey: sql.table + '/', endkey: sql.table + '0' }); | |||
| rows = rows.map(row => row.doc); | |||
| if (sql.wheres) { | |||
| const validate = (row, rule) => { | |||
| if (rule.operator) { | |||
| let a = row[rule.data.column]; | |||
| let b = rule.value; | |||
| switch (rule.operator) { | |||
| case 'equal': return a == b; | |||
| case 'not_equal': return a != b; | |||
| case 'in': return b.includes(a); | |||
| case 'not_in': return !b.includes(a); | |||
| case 'less': return a < b; | |||
| case 'less_or_equal': return a <= b; | |||
| case 'greater': return a > b; | |||
| case 'greater_or_equal': return a >= b; | |||
| case 'between': return b[0] <= a <= b[1]; | |||
| case 'not_between': return !(b[0] <= a <= b[1]); | |||
| case 'begins_with': return String(a).startsWith(String(b)); | |||
| case 'not_begins_with': return !String(a).startsWith(String(b)); | |||
| case 'contains': return String(a).includes(String(b)); | |||
| case 'not_contains': return !String(a).includes(String(b)); | |||
| case 'ends_with': return String(a).endsWith(String(b)); | |||
| case 'not_ends_with': return !String(a).endsWith(String(b)); | |||
| case 'is_empty': return a == null || a == ''; | |||
| case 'is_not_empty': return a != null && a != ''; | |||
| case 'is_null': return a == null; | |||
| case 'is_not_null': return a != null; | |||
| } | |||
| } | |||
| if (rule.condition && rule.rules.length) { | |||
| for (const _rule of rule.rules) { | |||
| const valid = validate(row, _rule); | |||
| if (!valid && rule.condition == 'AND') return false; | |||
| if (valid && rule.condition == 'OR') return true; | |||
| } | |||
| return rule.condition == 'OR' ? false : true; | |||
| } | |||
| return true; | |||
| }; | |||
| rows = rows.filter(row => { | |||
| return validate(row, sql.wheres); | |||
| }); | |||
| } | |||
| const result = await db.bulk(rows.map(doc => { | |||
| doc._deleted = true; | |||
| return doc; | |||
| })); | |||
| return { affected: Array.isArray(result) ? result.filter(result => result.ok).length : rows.length }; | |||
| } | |||
| if (options.test) { | |||
| return { | |||
| options: options, | |||
| query: sql.toString() | |||
| }; | |||
| } | |||
| if (sql.sub) { | |||
| return db.transaction(async trx => { | |||
| const deleted = (await trx.fromJSON(sql)).map(value => value[sql.returning] || value); | |||
| // loop sub | |||
| for (let { table, key } of Object.values(sql.sub)) { | |||
| // delete related data | |||
| await trx(table).whereIn(key, deleted).del(); | |||
| } | |||
| return { affected: deleted.length }; | |||
| }); | |||
| } | |||
| let affected = await db.fromJSON(sql); | |||
| return { affected }; | |||
| }, | |||
| custom: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbupdater.custom: connection is required.'); | |||
| const sql = this.parseSQL(options.sql); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (!sql) throw new Error('dbconnector.custom: sql is required.'); | |||
| if (typeof sql.query != 'string') throw new Error('dbupdater.custom: sql.query is required.'); | |||
| if (!Array.isArray(sql.params)) throw new Error('dbupdater.custom: sql.params is required.'); | |||
| if (db.client == 'couchdb') { | |||
| throw new Error('dbupdater.custom: couchdb is not supported.'); | |||
| } | |||
| const params = []; | |||
| const query = sql.query.replace(/([:@][a-zA-Z_]\w*|\?)/g, param => { | |||
| if (param == '?') { | |||
| params.push(sql.params[params.length].value); | |||
| return '?'; | |||
| } | |||
| let p = sql.params.find(p => p.name == param); | |||
| if (p) { | |||
| params.push(p.value); | |||
| return '?'; | |||
| } | |||
| return param; | |||
| }); | |||
| let results = await db.raw(query, params); | |||
| if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') { | |||
| results = results[0]; | |||
| } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') { | |||
| results = results.rows; | |||
| } | |||
| return results; | |||
| }, | |||
| execute: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbupdater.execute: connection is required.'); | |||
| const query = this.parseRequired(options.query, 'string', 'dbupdater.execute: query is required.'); | |||
| const params = this.parseOptional(options.params, 'object', []); | |||
| const db = this.getDbConnection(connection); | |||
| if (!db) throw new Error(`Connection "${connection}" doesn't exist.`); | |||
| if (db.client == 'couchdb') { | |||
| throw new Error('dbupdater.execute: couchdb is not supported.'); | |||
| } | |||
| let results = await db.raw(query, params); | |||
| if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') { | |||
| results = results[0]; | |||
| } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') { | |||
| results = results.rows; | |||
| } | |||
| return results; | |||
| }, | |||
| // bulk insert | |||
| // values is array of objects with column(key) and value | |||
| // batchSize is number of inserts per query (multi insert statement) | |||
| bulkinsert: async function(options) { | |||
| const connection = this.parseRequired(options.connection, 'string', 'dbupdater.bulkinsert: connection is required.'); | |||
| const table = this.parseRequired(options.table, 'string', 'dbupdater.bulkinsert: table is required.'); | |||
| const values = this.parseRequired(options.values, 'object', 'dbupdater.bulkinsert: values is required.'); | |||
| const batchSize = this.parseOptional(options.batchSize, 'number', 100); | |||
| const db = this.getDbConnection(connection); | |||
| if (db.client == 'couchdb') { | |||
| throw new Error('dbupdater.bulkinsert: couchdb is not supported.'); | |||
| } | |||
| await db.transaction(async transaction => { | |||
| for (let i = 0; i < values.length; i += batchSize) { | |||
| let batch = values.slice(i, i + batchSize); | |||
| await transaction(table).insert(batch); | |||
| } | |||
| }); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,109 @@ | |||
| const fs = require('fs-extra'); | |||
| const { toSystemPath } = require('../core/path'); | |||
| module.exports = { | |||
| csv: async function(options) { | |||
| let path = this.parse(options.path); | |||
| let data = this.parse(options.data); | |||
| let header = this.parse(options.header); | |||
| let delimiter = this.parse(options.delimiter); | |||
| let overwrite = this.parse(options.overwrite); | |||
| if (typeof path != 'string') throw new Error('export.csv: path is required.'); | |||
| if (!Array.isArray(data) || !data.length) throw new Error ('export.csv: data is required.'); | |||
| delimiter = typeof delimiter == 'string' ? delimiter : ','; | |||
| if (delimiter == '\\t') delimiter = '\t'; | |||
| const fd = await fs.open(toSystemPath(path), overwrite ? 'w' : 'wx'); | |||
| if (header) { | |||
| await putcsv(fd, Object.keys(data[0]), delimiter); | |||
| } | |||
| for (let row of data) { | |||
| await putcsv(fd, row, delimiter); | |||
| } | |||
| await fs.close(fd); | |||
| return path; | |||
| }, | |||
| xml: async function(options) { | |||
| let path = this.parse(options.path); | |||
| let data = this.parse(options.data); | |||
| let root = this.parse(options.root); | |||
| let item = this.parse(options.item); | |||
| let overwrite = this.parse(options.overwrite); | |||
| if (typeof path != 'string') throw new Error('export.xml: path is required.'); | |||
| if (!Array.isArray(data) || !data.length) throw new Error('export.xml: data is required.'); | |||
| root = typeof root == 'string' ? root : 'export'; | |||
| item = typeof item == 'string' ? item : 'item'; | |||
| const fd = await fs.open(toSystemPath(path), overwrite ? 'w' : 'wx'); | |||
| await fs.write(fd, `<?xml version="1.0" encoding="UTF-8" ?><${root}>`); | |||
| for (let row of data) { | |||
| await fs.write(fd, `<${item}>`); | |||
| for (let prop in row) { | |||
| await fs.write(fd, `<${prop}><![CDATA[${row[prop]}]]></${prop}>`); | |||
| } | |||
| await fs.write(fd, `</${item}>`); | |||
| } | |||
| await fs.write(fd, `</${root}>`); | |||
| await fs.close(fd); | |||
| return path; | |||
| }, | |||
| }; | |||
| async function putcsv(fd, data, delimiter) { | |||
| let str = ''; | |||
| if (typeof data != 'object') { | |||
| throw new Error('putcsv: Invalid data.'); | |||
| } | |||
| for (let prop in data) { | |||
| if (data.hasOwnProperty(prop)) { | |||
| let value = String(data[prop]); | |||
| if (/["\n\r\t\s]/.test(value) || value.includes(delimiter)) { | |||
| let escaped = false; | |||
| str += '"'; | |||
| for (let i = 0; i < value.length; i++) { | |||
| if (value.charAt(i) == '\\') { | |||
| escaped = true; | |||
| } else if (!escaped && value.charAt(i) == '"') { | |||
| str += '"'; | |||
| } else { | |||
| escaped = false; | |||
| } | |||
| str += value.charAt(i); | |||
| } | |||
| str += '"'; | |||
| } else { | |||
| str += value; | |||
| } | |||
| str += delimiter; | |||
| } | |||
| } | |||
| if (!str) { | |||
| throw new Error('putcsv: No data.'); | |||
| } | |||
| return fs.write(fd, str.substr(0, str.length - delimiter.length) + '\r\n'); | |||
| } | |||
| @@ -0,0 +1,258 @@ | |||
| const fs = require('fs-extra'); | |||
| const { join, dirname, basename, extname } = require('path'); | |||
| const { toSystemPath, toAppPath, toSiteUrl, getUniqFile, parseTemplate } = require('../core/path'); | |||
| const { map } = require('../core/async'); | |||
| module.exports = { | |||
| download: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.download: path is required.')); | |||
| let filename = this.parseOptional(options.filename, 'string', basename(path)); | |||
| if (fs.existsSync(path)) { | |||
| this.res.download(path, filename); | |||
| this.noOutput = true; | |||
| } else { | |||
| this.res.sendStatus(404); | |||
| } | |||
| }, | |||
| exists: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.exists: path is required.')); | |||
| if (await isFile(path)) { | |||
| if (options.then) { | |||
| await this.exec(options.then, true); | |||
| } | |||
| return true; | |||
| } else { | |||
| if (options.else) { | |||
| await this.exec(options.else, true); | |||
| } | |||
| return false; | |||
| } | |||
| }, | |||
| direxists: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.direxists: path is required.')); | |||
| if (await isDirectory(path)) { | |||
| if (options.then) { | |||
| await this.exec(options.then, true); | |||
| } | |||
| return true; | |||
| } else { | |||
| if (options.else) { | |||
| await this.exec(options.else, true); | |||
| } | |||
| return false; | |||
| } | |||
| }, | |||
| createdir: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.createdir: path is required.')); | |||
| await fs.ensureDir(path); | |||
| return toAppPath(path); | |||
| }, | |||
| removedir: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.removedir: path is required.')); | |||
| await fs.remove(path); | |||
| return toAppPath(path); | |||
| }, | |||
| emptydir: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.emptydir: path is required.')); | |||
| await fs.emptyDir(path) | |||
| return toAppPath(path); | |||
| }, | |||
| move: async function(options) { | |||
| let from = toSystemPath(this.parseRequired(options.from, 'string', 'fs.move: from is required.')); | |||
| let to = toSystemPath(this.parseRequired(options.to, 'string', 'fs.move: to is required.')); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let createdir = this.parseOptional(options.createdir, 'boolean', true); | |||
| if (!fs.existsSync(to)) { | |||
| if (createdir) { | |||
| await fs.ensureDir(to); | |||
| } else { | |||
| throw new Error(`Destination path doesn't exists.`); | |||
| } | |||
| } | |||
| to = join(to, basename(from)); | |||
| if (!overwrite) { | |||
| to = getUniqFile(to); | |||
| } | |||
| await fs.move(from, to, { overwrite: true }); | |||
| return toAppPath(to); | |||
| }, | |||
| rename: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.rename: path is required.')); | |||
| let template = this.parseRequired(options.template, 'string', 'fs.rename: template is required.'); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let to = parseTemplate(path, template); | |||
| if (!overwrite && fs.existsSync(to)) { | |||
| throw new Error(`fs.rename: file "${to}" already exists.`); | |||
| } | |||
| await fs.rename(path, to); | |||
| return toAppPath(to); | |||
| }, | |||
| copy: async function(options) { | |||
| let from = toSystemPath(this.parseRequired(options.from, 'string', 'fs.copy: from is required.')); | |||
| let to = toSystemPath(this.parseRequired(options.to, 'string', 'fs.copy: to is required.')); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let createdir = this.parseOptional(options.createdir, 'boolean', true); | |||
| if (!fs.existsSync(to)) { | |||
| if (createdir) { | |||
| await fs.ensureDir(to); | |||
| } else { | |||
| throw new Error(`Destination path doesn't exist.`); | |||
| } | |||
| } | |||
| to = join(to, basename(from)); | |||
| if (!overwrite && fs.existsSync(to)) { | |||
| to = getUniqFile(to); | |||
| } | |||
| await fs.copy(from, to); | |||
| return toAppPath(to); | |||
| }, | |||
| remove: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.remove: path is required.')); | |||
| await fs.unlink(path); | |||
| return true; | |||
| }, | |||
| dir: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.dir: path is required.')); | |||
| let allowedExtensions = this.parseOptional(options.allowedExtensions, 'string', ''); | |||
| let showHidden = this.parseOptional(options.showHidden, 'boolean', false); | |||
| let includeFolders = this.parseOptional(options.includeFolders, 'boolean', false); | |||
| let folderSize = this.parseOptional(options.folderSize, 'string', 'none'); | |||
| let concurrency = this.parseOptional(options.concurrency, 'number', 4); | |||
| folderSize = ['none', 'files', 'recursive'].includes(folderSize) ? folderSize : 'none'; | |||
| allowedExtensions = allowedExtensions ? allowedExtensions.split(/\s*,\s*/).map(ext => lowercase(ext[0] == '.' ? ext : '.' + ext)) : []; | |||
| let files = await fs.readdir(path, { withFileTypes: true }); | |||
| files = files.filter(entry => { | |||
| if (!includeFolders && entry.isDirectory()) return false; | |||
| if (!showHidden && entry.name[0] == '.') return false; | |||
| if (allowedExtensions.length && entry.isFile() && !allowedExtensions.includes(lowercase(extname(entry.name)))) return false; | |||
| return entry.isFile() || entry.isDirectory(); | |||
| }); | |||
| // Fast parallel map | |||
| return map(files, async (entry) => { | |||
| let curr = join(path, entry.name); | |||
| let stat = await fs.stat(curr); | |||
| if (folderSize != 'none' && entry.isDirectory()) { | |||
| stat.size = await calcSize(curr, folderSize == 'recursive', concurrency); | |||
| } | |||
| return { | |||
| type: entry.isFile() ? 'file' : 'dir', | |||
| name: entry.name, | |||
| folder: toAppPath(dirname(curr)), | |||
| basename: basename(curr, extname(curr)), | |||
| extension: extname(curr), | |||
| path: toAppPath(curr), | |||
| url: toSiteUrl(curr), | |||
| size: stat.size, | |||
| created: stat.ctime, | |||
| accessed: stat.atime, | |||
| modified: stat.mtime | |||
| }; | |||
| }, concurrency); | |||
| }, | |||
| stat: async function(options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'fs.stat: path is required.')); | |||
| let folderSize = this.parseOptional(options.folderSize, 'string', 'none'); | |||
| let concurrency = this.parseOptional(options.concurrency, 'number', 4); | |||
| folderSize = ['none', 'files', 'recursive'].includes(folderSize) ? folderSize : 'none'; | |||
| let stat = await fs.stat(path); | |||
| if (folderSize != 'none' && stat.isDirectory()) { | |||
| stat.size = await calcSize(path, folderSize == 'recursive', concurrency); | |||
| } | |||
| return { | |||
| type: stat.isFile() ? 'file' : 'dir', | |||
| name: basename(path), | |||
| folder: toAppPath(dirname(path)), | |||
| basename: basename(path, extname(path)), | |||
| extension: extname(path), | |||
| path: toAppPath(path), | |||
| url: toSiteUrl(path), | |||
| size: stat.size, | |||
| created: stat.ctime, | |||
| accessed: stat.atime, | |||
| modified: stat.mtime | |||
| }; | |||
| }, | |||
| }; | |||
| function lowercase(str) { | |||
| return str.toLowerCase(); | |||
| } | |||
| async function calcSize(folder, recursive, concurrency) { | |||
| let entries = await fs.readdir(folder); | |||
| return map(entries, async (entry) => { | |||
| let stat = await fs.stat(join(folder, entry)); | |||
| if (stat.isDirectory() && recursive) { | |||
| return calcSize(join(folder, entry), recursive, concurrency); | |||
| } | |||
| return stat.size; | |||
| }, concurrency).then(arr => arr.reduce((size, curr) => size + curr, 0)); | |||
| }; | |||
| async function isFile(path) { | |||
| try { | |||
| let stats = await fs.stat(path); | |||
| return stats.isFile(); | |||
| } catch (err) { | |||
| return false; | |||
| } | |||
| } | |||
| async function isDirectory(path) { | |||
| try { | |||
| let stats = await fs.stat(path); | |||
| return stats.isDirectory(); | |||
| } catch (err) { | |||
| return false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,505 @@ | |||
| const fs = require('fs-extra'); | |||
| const Sharp = require('sharp'); | |||
| const debug = require('debug')('server-connect:image'); | |||
| const { basename, extname, join } = require('path'); | |||
| const { toAppPath, toSystemPath, parseTemplate, getUniqFile } = require('../core/path'); | |||
| const positions = { | |||
| 'center': 0, | |||
| 'centre': 0, | |||
| 'top': 1, | |||
| 'north': 1, | |||
| 'right': 2, | |||
| 'east': 2, | |||
| 'bottom': 3, | |||
| 'south': 3, | |||
| 'left': 4, | |||
| 'west': 4, | |||
| 'top right': 5, | |||
| 'right top': 5, | |||
| 'northeast': 5, | |||
| 'bottom right': 6, | |||
| 'right bottom': 6, | |||
| 'southeast': 6, | |||
| 'bottom left': 7, | |||
| 'left bottom': 7, | |||
| 'southwest': 7, | |||
| 'top left': 8, | |||
| 'left top': 8, | |||
| 'northwest': 8, | |||
| 'entropy': 16, | |||
| 'attention': 17 | |||
| }; | |||
| function cw(w, meta) { | |||
| if (typeof w == 'string') { | |||
| if (/%$/.test(w)) { | |||
| w = meta.width * parseFloat(w) / 100; | |||
| } | |||
| } | |||
| if (w < 0) { | |||
| w = meta.width + w; | |||
| } | |||
| return parseInt(w); | |||
| } | |||
| function ch(h, meta) { | |||
| if (typeof h == 'string') { | |||
| if (/%$/.test(h)) { | |||
| h = meta.height * parseFloat(h) / 100; | |||
| } | |||
| } | |||
| if (h < 0) { | |||
| h = meta.height + h; | |||
| } | |||
| return parseInt(h); | |||
| } | |||
| function cx(x, w, meta) { | |||
| if (typeof x == 'string') { | |||
| switch (x) { | |||
| case 'left': | |||
| x = 0; | |||
| break; | |||
| case 'center': | |||
| x = (meta.width - w) / 2; | |||
| break; | |||
| case 'right': | |||
| x = meta.width - w; | |||
| break; | |||
| default: | |||
| if (/%$/.test(x)) { | |||
| x = (meta.width - w) * parseFloat(x) / 100; | |||
| } | |||
| } | |||
| } | |||
| if (x < 0) { | |||
| x = meta.width - w + x; | |||
| } | |||
| return parseInt(x); | |||
| } | |||
| function cy(y, h, meta) { | |||
| if (typeof y == 'string') { | |||
| switch (y) { | |||
| case 'top': | |||
| y = 0; | |||
| break; | |||
| case 'middle': | |||
| y = (meta.height - h) / 2; | |||
| break; | |||
| case 'bottom': | |||
| y = meta.height - h; | |||
| break; | |||
| default: | |||
| if (/%$/.test(y)) { | |||
| y = (meta.height - h) * parseFloat(y) / 100; | |||
| } | |||
| } | |||
| } | |||
| if (y < 0) { | |||
| y = meta.height - h + y; | |||
| } | |||
| return parseInt(y); | |||
| } | |||
| async function updateImage(sharp) { | |||
| sharp.image = Sharp(await sharp.image.toBuffer()); | |||
| sharp.metadata = await sharp.image.metadata(); | |||
| } | |||
| module.exports = { | |||
| getImageSize: async function (options) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.getImageSize: path is required.')); | |||
| const image = Sharp(path); | |||
| const metadata = await image.metadata(); | |||
| return { | |||
| width: metadata.width, | |||
| height: metadata.height | |||
| }; | |||
| }, | |||
| load: async function (options, name) { | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.load: path is required.')); | |||
| let orient = this.parseOptional(options.autoOrient, 'boolean', false); | |||
| this.req.image = this.req.image || {}; | |||
| this.req.image[name] = { name: basename(path), image: Sharp(path), metadata: null }; | |||
| const sharp = this.req.image[name]; | |||
| if (orient) sharp.image.rotate(); | |||
| await updateImage(sharp); | |||
| return { | |||
| name: basename(path), | |||
| width: sharp.metadata.width, | |||
| height: sharp.metadata.height | |||
| }; | |||
| }, | |||
| save: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.save: instance "${options.instance} doesn't exist.`); | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.save: path is required.')); | |||
| let format = this.parseOptional(options.format, 'string', 'jpeg').toLowerCase(); | |||
| let template = this.parseOptional(options.template, 'string', '{name}{ext}'); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let createPath = this.parseOptional(options.createPath, 'boolean', true); | |||
| let background = this.parseOptional(options.background, 'string', '#FFFFFF'); | |||
| let quality = this.parseOptional(options.quality, 'number', 75); | |||
| if (!fs.existsSync(path)) { | |||
| if (createPath) { | |||
| await fs.ensureDir(path); | |||
| } else { | |||
| throw new Error(`image.save: path "${path}" doesn't exist.`); | |||
| } | |||
| } | |||
| let file = join(path, sharp.name); | |||
| if (template) { | |||
| file = parseTemplate(file, template); | |||
| } | |||
| if (format == 'auto') { | |||
| switch (extname(file).toLowerCase()) { | |||
| case '.png': format = 'png'; break; | |||
| case '.gif': format = 'gif'; break; | |||
| case '.webp': format = 'webp'; break; | |||
| default: format = 'jpeg'; | |||
| } | |||
| } | |||
| if (format == 'jpeg') { | |||
| sharp.image.flatten({ background }); | |||
| sharp.image.toFormat(format, { quality }); | |||
| } else if (format == 'webp') { | |||
| sharp.image.toFormat(format, { quality }); | |||
| } else { | |||
| sharp.image.toFormat(format); | |||
| } | |||
| const data = await sharp.image.toBuffer(); | |||
| file = file.replace(extname(file), '.' + format.replace('jpeg', 'jpg')); | |||
| if (fs.existsSync(file)) { | |||
| if (overwrite) { | |||
| await fs.unlink(file); | |||
| } else { | |||
| file = getUniqFile(file); | |||
| } | |||
| } | |||
| await fs.writeFile(file, data) //.toFile(file); | |||
| return toAppPath(file); | |||
| }, | |||
| resize: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.resize: instance "${options.instance} doesn't exist.`); | |||
| let width = this.parseOptional(cw(this.parse(options.width), sharp.metadata), 'number', null); | |||
| let height = this.parseOptional(ch(this.parse(options.height), sharp.metadata), 'number', null); | |||
| let upscale = this.parseOptional(options.upscale, 'boolean', false); | |||
| if (isNaN(width)) width = null; | |||
| if (isNaN(height)) height = null; | |||
| sharp.image.resize(width, height, { fit: width && height ? 'fill' : 'cover', withoutEnlargement: !upscale }); | |||
| await updateImage(sharp); | |||
| }, | |||
| crop: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.crop: instance "${options.instance} doesn't exist.`); | |||
| let width = this.parseRequired(cw(this.parse(options.width)), 'number', 'image.crop: width is required.'); | |||
| let height = this.parseRequired(ch(this.parse(options.height)), 'number', 'image.crop: height is required.'); | |||
| if (width > sharp.metadata.width) width = sharp.metadata.width; | |||
| if (height > sharp.metadata.height) height = sharp.metadata.height; | |||
| let left = this.parseRequired(cx(this.parse(options.x), width, sharp.metadata), 'number', 'image.crop: x is required.'); | |||
| let top = this.parseRequired(cy(this.parse(options.y), height, sharp.metadata), 'number', 'image.crop: y is required.'); | |||
| sharp.image.extract({ left, top, width, height }); | |||
| await updateImage(sharp); | |||
| }, | |||
| cover: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.cover: instance "${options.instance}" doesn't exist.`); | |||
| let width = this.parseRequired(options.width, 'number', 'image.cover: width is required.'); | |||
| let height = this.parseRequired(options.height, 'number', 'image.cover: height is required.'); | |||
| // position: see positions object for options | |||
| let position = this.parseOptional(options.position, 'string', 'center'); | |||
| // kernel: 'nearest', 'cubic', 'mitchell', 'lanczos2', 'lanczos3' | |||
| let kernel = this.parseOptional(options.kernel, 'string', 'lanczos3'); | |||
| position = positions[position] || 0; | |||
| sharp.image.resize({ width, height, position, kernel }); | |||
| await updateImage(sharp); | |||
| }, | |||
| watermark: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.watermark: instance "${options.instance} doesn't exist.`); | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.watermark: path is required.')); | |||
| let image = Sharp(path); | |||
| let metadata = await image.metadata(); | |||
| let input = await image.toBuffer(); | |||
| let left = this.parseRequired(cx(this.parse(options.x), metadata.width, sharp.metadata), 'number', 'image.watermark: x is required.'); | |||
| let top = this.parseRequired(cy(this.parse(options.y), metadata.height, sharp.metadata), 'number', 'image.watermark: y is required.'); | |||
| sharp.image.composite([{ input, left, top }]); | |||
| }, | |||
| text: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.text: instance "${options.instance} doesn't exist.`); | |||
| let x = this.parse(options.x); | |||
| let y = this.parse(options.y); | |||
| let text = this.parseRequired(options.text, 'string', 'image.text: text is required.'); | |||
| let font = this.parseOptional(options.font, 'string', 'Verdana'); | |||
| let size = this.parseOptional(options.size, 'number', 24); | |||
| let color = this.parseOptional(options.color, 'string', '#ffffff'); | |||
| let width = sharp.metadata.width; | |||
| let height = sharp.metadata.height; | |||
| let anchor = 'start'; | |||
| switch (x) { | |||
| case 'left': | |||
| x = '0%'; | |||
| anchor = 'start'; | |||
| break; | |||
| case 'center': | |||
| x = '50%'; | |||
| anchor = 'middle'; | |||
| break; | |||
| case 'right': | |||
| x = '100%'; | |||
| anchor = 'end'; | |||
| break; | |||
| default: | |||
| if (x < 0) { | |||
| x = width - x; | |||
| anchor = 'end'; | |||
| } | |||
| } | |||
| switch (y) { | |||
| case 'top': | |||
| y = size; | |||
| break; | |||
| case 'middle': | |||
| y = (height / 2) - (size / 2); | |||
| break; | |||
| case 'bottom': | |||
| y = height; | |||
| break; | |||
| default: | |||
| if (y < 0) { | |||
| y = height - size - y; | |||
| } | |||
| } | |||
| let svg = ` | |||
| <svg width="${width}" height="${height}"> | |||
| <style> | |||
| .text { | |||
| fill: ${color}; | |||
| font-family: "${font}"; | |||
| font-size: ${size}px; | |||
| line-height: 1; | |||
| } | |||
| </style> | |||
| <text x="${x}" y="${y}" text-anchor="${anchor}" class="text">${text}</text> | |||
| </svg> | |||
| `; | |||
| const input = await Sharp(Buffer.from(svg)).toBuffer(); | |||
| sharp.image.composite([{ input, left: 0, top: 0 }]); | |||
| }, | |||
| tiled: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.tiled: instance "${options.instance} doesn't exist.`); | |||
| let input = toSystemPath(this.parseRequired(options.path, 'string', 'image.tiled: path is required.')); | |||
| let padding = this.parseOptional(options.padding, 'number', 0); | |||
| if (padding) { | |||
| input = await Sharp(input).extend({ | |||
| top: padding, left: padding, bottom: 0, right: 0, background: { r: 0, g: 0, b: 0, alpha: 0 } | |||
| }).toBuffer(); | |||
| } | |||
| sharp.image.composite([{ input, left: 0, top: 0, tile: true }]); | |||
| }, | |||
| flip: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.flip: instance "${options.instance} doesn't exist.`); | |||
| let horizontal = this.parseOptional(options.horizontal, 'boolean', false); | |||
| let vertical = this.parseOptional(options.vertical, 'boolean', false); | |||
| if (horizontal) sharp.image.flop(); | |||
| if (vertical) sharp.image.flip(); | |||
| }, | |||
| rotateLeft: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.rotateLeft: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.rotate(-90); | |||
| await updateImage(sharp); | |||
| }, | |||
| rotateRight: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.rotateRight: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.rotate(90); | |||
| await updateImage(sharp); | |||
| }, | |||
| smooth: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.smooth: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.convolve({ | |||
| width: 3, | |||
| height: 3, | |||
| kernel: [ | |||
| 1, 1, 1, | |||
| 1, 1, 1, | |||
| 1, 1, 1 | |||
| ] | |||
| }); | |||
| }, | |||
| blur: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.blur: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.convolve({ | |||
| width: 3, | |||
| height: 3, | |||
| kernel: [ | |||
| 1, 2, 1, | |||
| 2, 4, 2, | |||
| 1, 2, 1 | |||
| ] | |||
| }); | |||
| }, | |||
| sharpen: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.sharpen: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.convolve({ | |||
| width: 3, | |||
| height: 3, | |||
| kernel: [ | |||
| 0, -2, 0, | |||
| -2, 15, -2, | |||
| 0, -2, 0 | |||
| ] | |||
| }); | |||
| }, | |||
| meanRemoval: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.meanRemoval: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.convolve({ | |||
| width: 3, | |||
| height: 3, | |||
| kernel: [ | |||
| -1, -1, -1, | |||
| -1, 9, -1, | |||
| -1, -1, -1 | |||
| ] | |||
| }); | |||
| }, | |||
| emboss: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.emboss: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.convolve({ | |||
| width: 3, | |||
| height: 3, | |||
| kernel: [ | |||
| -1, 0, -1, | |||
| 0, 4, 0, | |||
| -1, 0, -1 | |||
| ], | |||
| offset: 127 | |||
| }); | |||
| }, | |||
| edgeDetect: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.edgeDetect: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.convolve({ | |||
| width: 3, | |||
| height: 3, | |||
| kernel: [ | |||
| -1, -1, -1, | |||
| 0, 0, 0, | |||
| 1, 1, 1 | |||
| ], | |||
| offset: 127 | |||
| }); | |||
| }, | |||
| grayscale: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.grayscale: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.grayscale(); | |||
| }, | |||
| sepia: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.sepia: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.tint({ r: 112, g: 66, b: 20 }); | |||
| }, | |||
| invert: async function (options) { | |||
| const sharp = this.req.image[options.instance]; | |||
| if (!sharp) throw new Error(`image.invert: instance "${options.instance} doesn't exist.`); | |||
| sharp.image.negate(); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,129 @@ | |||
| const fs = require('fs-extra'); | |||
| const { toSystemPath } = require('../core/path'); | |||
| const { keysToLowerCase } = require('../core/util'); | |||
| // simple inital implementation | |||
| // better implementation at: | |||
| // https://github.com/adaltas/node-csv-parse | |||
| // https://github.com/mafintosh/csv-parser | |||
| function parseCSV(csv, options) { | |||
| if (!csv) return []; | |||
| if (csv.charCodeAt(0) === 0xFEFF) { | |||
| csv = csv.slice(1); | |||
| } | |||
| let delimiter = options.delimiter.replace('\\t', '\t'); | |||
| let keys = options.fields; | |||
| let line = 1; | |||
| let data = []; | |||
| if (options.header) { | |||
| keys = getcsv(); | |||
| options.fields.forEach(field => { | |||
| if (keys.indexOf(field) == -1) { | |||
| throw new Error('parseCSV: ' + field + ' is missing in ' + options.path); | |||
| } | |||
| }); | |||
| line++; | |||
| } | |||
| let size = keys.length; | |||
| while (csv.length) { | |||
| let values = getcsv(); | |||
| let o = {}; | |||
| if (values.length != size) { | |||
| throw new Error('parseCSV: colomns do not match. keys: ' + size + ', values: ' + values.length + ' at line ' + line); | |||
| } | |||
| for (let i = 0; i < size; i++) { | |||
| o[keys[i]] = values[i]; | |||
| } | |||
| data.push(o); | |||
| line++; | |||
| } | |||
| return data; | |||
| function getcsv() { | |||
| let data = [''], l = csv.length, | |||
| esc = false, escesc = false, | |||
| n = 0, i = 0; | |||
| while (i < l) { | |||
| let s = csv.charAt(i); | |||
| if (s == '\n') { | |||
| if (esc) { | |||
| data[n] += s; | |||
| } else { | |||
| i++; | |||
| break; | |||
| } | |||
| } else if (s == '\r') { | |||
| if (esc) { | |||
| data[n] += s; | |||
| } | |||
| } else if (s == delimiter) { | |||
| if (esc) { | |||
| data[n] += s; | |||
| } else { | |||
| data[++n] = ''; | |||
| esc = false; | |||
| escesc = false; | |||
| } | |||
| } else if (s == '"') { | |||
| if (escesc) { | |||
| data[n] += s; | |||
| escesc = false; | |||
| } | |||
| if (esc) { | |||
| esc = false; | |||
| escesc = true; | |||
| } else { | |||
| esc = true; | |||
| escesc = false; | |||
| } | |||
| } else { | |||
| if (escesc) { | |||
| data[n] += '"'; | |||
| escesc = false; | |||
| } | |||
| data[n] += s; | |||
| } | |||
| i++; | |||
| } | |||
| csv = csv.substr(i); | |||
| return data; | |||
| } | |||
| } | |||
| module.exports = { | |||
| csv: async function(options) { | |||
| let path = this.parseRequired(options.path, 'string', 'export.csv: path is required.'); | |||
| let fields = this.parseOptional(options.fields, 'object', []); | |||
| let header = this.parseOptional(options.header, 'boolean', false); | |||
| let delimiter = this.parseOptional(options.delimiter, 'string', ','); | |||
| let csv = await fs.readFile(toSystemPath(path), 'utf8'); | |||
| return parseCSV(csv, { fields, header, delimiter }); | |||
| }, | |||
| xml: async function(options) { | |||
| // TODO: import.xml | |||
| throw new Error('import.xml: not implemented.'); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,22 @@ | |||
| exports.sign = function(options, name) { | |||
| return this.setJSONWebToken(name, options) | |||
| }; | |||
| exports.decode = function(options) { | |||
| const jwt = require('jsonwebtoken'); | |||
| return jwt.decode(this.parse(options.token), { complete: true }); | |||
| }; | |||
| exports.verify = function(options) { | |||
| const jwt = require('jsonwebtoken'); | |||
| options = this.parse(options); | |||
| try { | |||
| return jwt.verify(options.token, options.key, options); | |||
| } catch (error) { | |||
| const debug = require('debug')('server-connect:jwt'); | |||
| debug('jwt verify failed: %o', error); | |||
| if (options.throw) throw error; | |||
| return { error }; | |||
| } | |||
| }; | |||
| @@ -0,0 +1,92 @@ | |||
| const fs = require('fs-extra'); | |||
| const { getFilesArray, toSystemPath } = require('../core/path'); | |||
| const { basename, posix } = require('path'); | |||
| const { v4: uuidv4 } = require('uuid'); | |||
| const IMPORTANCE = { 0: 'low', 1: 'normal', 2: 'high' }; | |||
| module.exports = { | |||
| setup: function(options, name) { | |||
| if (!name) throw new Error('mail.setup has no name.'); | |||
| this.setMailer(name, options); | |||
| }, | |||
| send: async function(options) { | |||
| let setup = this.getMailer(this.parseOptional(options.instance, 'string', 'system')); | |||
| let subject = this.parseRequired(options.subject, 'string', 'mail.send: subject is required.'); | |||
| let fromEmail = this.parseRequired(options.fromEmail, 'string', 'mail.send: fromEmail is required.'); | |||
| let fromName = this.parseOptional(options.fromName, 'string', ''); | |||
| let toEmail = this.parseRequired(options.toEmail, 'string', 'mail.send: toEmail is required.'); | |||
| let toName = this.parseOptional(options.toName, 'string', ''); | |||
| let replyTo = this.parseOptional(options.replyTo, 'string', ''); | |||
| let cc = this.parseOptional(options.cc, 'string', ''); | |||
| let bcc = this.parseOptional(options.bcc, 'string', ''); | |||
| let source = this.parseOptional(options.source, 'string', 'static'); // static, file | |||
| let contentType = this.parseOptional(options.contentType, 'string', 'text'); // text / html | |||
| let body = this.parseOptional(options.body, 'string', ''); | |||
| let bodyFile = this.parseOptional(options.bodyFile, 'string', ''); | |||
| let embedImages = this.parseOptional(options.embedImages, 'boolean', false); | |||
| let priority = IMPORTANCE[this.parseOptional(options.importance, 'number', 1)]; | |||
| let attachments = this.parseOptional(options.attachments, '*', []); // "/file.ext" / ["/file.ext"] / {path:"/file.ext"} / [{path:"/file.ext"}] | |||
| let from = fromName ? `"${fromName}" <${fromEmail}>` : fromEmail; | |||
| let to = toName ? `"${toName}" <${toEmail}>` : toEmail; | |||
| let text = body; | |||
| let html = null; | |||
| if (source == 'file') { | |||
| body = this.parse(await fs.readFile(toSystemPath(bodyFile), 'utf8')); | |||
| } | |||
| if (attachments) { | |||
| attachments = getFilesArray(attachments).map((path) => ({ filename: basename(path), path })); | |||
| } | |||
| if (contentType == 'html') { | |||
| html = body; | |||
| if (embedImages) { | |||
| let cid = {}; | |||
| html = html.replace(/(?:"|')([^"']+\.(jpg|png|gif))(?:"|')/gi, (m, url) => { | |||
| let path = toSystemPath(url); | |||
| if (fs.existsSync(path)) { | |||
| if (!cid[path]) { | |||
| cid[path] = uuidv4(); | |||
| attachments.push({ | |||
| filename: basename(path), | |||
| path: path, | |||
| cid: cid[path] | |||
| }); | |||
| } | |||
| return `"cid:${cid[path]}"`; | |||
| } else { | |||
| console.warn(`${path} not found`); | |||
| } | |||
| return `"${url}"`; | |||
| }); | |||
| } | |||
| if (this.req.get) { // we can only do this if we have a request to get our hostname | |||
| const hasProxy = !!this.req.get('x-forwarded-host'); | |||
| const host = hasProxy ? `${this.req.protocol}://${this.req.hostname}` : this.req.get('host'); | |||
| html = html.replace(/(href|src)(?:\s*=\s*)(?:"|')([^"']+)(?:"|')/gi, (m, attr, url) => { | |||
| if (!url.includes(':')) { | |||
| url = posix.join(host, url); | |||
| } | |||
| return `${attr}="${url}"`; | |||
| }); | |||
| } | |||
| } | |||
| const nodemailer = require('nodemailer'); | |||
| let transport = nodemailer.createTransport(setup); | |||
| return transport.sendMail({ from, to, cc, bcc, replyTo, subject, html, text, priority, attachments }); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,484 @@ | |||
| const fs = require('fs-extra'); | |||
| const imageTypes = ['PNG', 'GIF', 'BMP', 'JPEG', 'TIFF']; | |||
| const videoTypes = ['AVI', 'MP4', 'MOV', 'MKV', 'WEBM', 'OGV']; | |||
| const soundTypes = ['OGG', 'WAV', 'MP3', 'FLAC']; | |||
| const read = async (path, offset, length) => { | |||
| const fp = await fs.open(path); | |||
| const buff = Buffer.alloc(length); | |||
| await fs.read(fd, buff, 0, length, offset); | |||
| await fs.close(); | |||
| return buff; | |||
| }; | |||
| const parser = { | |||
| PNG: async (path, result) => { | |||
| const buff = await read(path, 18, 6); | |||
| result.width = buff.readUInt16BE(0); | |||
| result.height = buff.readUInt16BE(4); | |||
| }, | |||
| GIF: async (path, result) => { | |||
| const buff = await read(path, 6, 4); | |||
| result.width = buff.readUInt16LE(0); | |||
| result.height = buff.readUInt16LE(2); | |||
| }, | |||
| BMP: async (path, result) => { | |||
| const buff = await read(path, 18, 8); | |||
| result.width = buff.readUInt32LE(0); | |||
| result.height = buff.readUInt32LE(4); | |||
| }, | |||
| JPEG: async (path, result) => { | |||
| const sof = [0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf, 0xde]; | |||
| const buff = await read(path, 2, 64000); | |||
| let pos = 0; | |||
| while (buff[pos++] == 0xff) { | |||
| let marker = buff[pos++]; | |||
| let size = buff.readUInt16BE(pos); | |||
| if (marker == 0xda) break; | |||
| if (sof.includes(marker)) { | |||
| result.height = buff.readUInt16BE(pos + 3); | |||
| result.width = buff.readUInt16BE(pos + 5); | |||
| break; | |||
| } | |||
| pos += size; | |||
| } | |||
| }, | |||
| TIFF: async (path, result) => { | |||
| const buff = await read(path, 0, 64000); | |||
| const le = buff.toString('ascii', 0, 2) == 'II'; | |||
| let pos = 0; | |||
| const readUInt16 = () => { pos += 2; return buff[le ? 'readUInt16LE' : 'readUInt16BE'](pos - 2); } | |||
| const readUInt32 = () => { pos += 4; return buff[le ? 'readUInt32LE' : 'readUInt32BE'](pos - 4); } | |||
| let offset = readUInt32(); | |||
| while (pos < buff.length && offset > 0) { | |||
| let entries = readUInt16(offset); | |||
| let start = pos; | |||
| for (let i = 0; i < entries; i++) { | |||
| let tag = readUInt16(); | |||
| let type = readUInt16(); | |||
| let length = readUInt32(); | |||
| let data = (type == 3) ? readUInt16() : readUInt32(); | |||
| if (type == 3) pos += 2; | |||
| if (tag == 256) { | |||
| result.width = data; | |||
| } else if (tag == 257) { | |||
| result.height = data; | |||
| } | |||
| if (result.width > 0 && result.height > 0) { | |||
| return; | |||
| } | |||
| } | |||
| offset = readUInt32(); | |||
| pos += offset; | |||
| } | |||
| }, | |||
| AVI: async (path, result) => { | |||
| const buff = await read(path, 0, 144); | |||
| result.width = buff.readUInt32LE(64); | |||
| result.height = buff.readUInt32LE(68); | |||
| result.duration = ~~(buff.readUInt32LE(128) / buff.readUInt32LE(132) * buff.readUInt32LE(140)); | |||
| }, | |||
| MP4: async (path, result) => { | |||
| return parser.MOV(path, result); | |||
| }, | |||
| MOV: async (path, result, pos = 0) => { | |||
| const buff = await read(path, 0, 64000); | |||
| while (pos < buff.length) { | |||
| let size = buff.readUInt32BE(pos); | |||
| let name = buff.toString('ascii', pos + 4, 4); | |||
| if (name == 'mvhd') { | |||
| let scale = buff.readUInt32BE(pos + 20); | |||
| let duration = buff.readUInt32BE(pos + 24); | |||
| result.duration = ~~(duration / scale); | |||
| } | |||
| if (name == 'tkhd') { | |||
| let m0 = buff.readUInt32BE(pos + 48); | |||
| let m4 = buff.readUInt32BE(pos + 64); | |||
| let w = buff.readUInt32BE(pos + 84); | |||
| let h = buff.readUInt32BE(pos + 88); | |||
| if (w > 0 && h > 0) { | |||
| result.width = w / m0; | |||
| result.height = h / m4; | |||
| return; | |||
| } | |||
| } | |||
| if (name == 'moov' || name == 'trak') { | |||
| await parser.MOV(path, pos + 8); | |||
| } | |||
| pos += size; | |||
| } | |||
| }, | |||
| WEBM: async (path, result) => { | |||
| return parser.EBML(path, result); | |||
| }, | |||
| MKV: async (path, result) => { | |||
| return parser.EBML(path, result); | |||
| }, | |||
| EBML: async (path, result) => { | |||
| const containers = ['\x1a\x45\xdf\xa3', '\x18\x53\x80\x67', '\x15\x49\xa9\x66', '\x16\x54\xae\x6b', '\xae', '\xe0']; | |||
| const buff = await read(path, 0, 64000); | |||
| // TODO parse EBML | |||
| }, | |||
| OGV: async (path, result) => { | |||
| return parser.OGG(path, result); | |||
| }, | |||
| OGG: async (path, result) => { | |||
| const buff = await read(apth, 0, 64000); | |||
| let pos = 0, vorbis; | |||
| while (buff.toString('ascii', pos, pos + 4) == 'OggS') { | |||
| let version = buff[pos + 4]; | |||
| let b = buff[pos + 5]; | |||
| let continuation = !!(b & 0x01); | |||
| let bos = !!(b & 0x02); | |||
| let eos = !!(b & 0x04); | |||
| let position = Number(buff.readBigUInt64LE(pos + 6)); | |||
| let serial = buff.readUInt32LE(pos + 14); | |||
| let pageNumber = buff.readUInt32LE(pos + 18); | |||
| let checksum = buff.readUInt32LE(pos + 22); | |||
| let pageSegments = buff[path + 26]; | |||
| let lacing = buff.slice(pos + 27, pos + 27 + pageSegments); | |||
| let pageSize = lacing.reduce((p, v) => p + v, 0); | |||
| let start = pos + 27 + pageSegments; | |||
| let pageHeader = buff.slice(start, start + 7); | |||
| if (pageHeader.compare(Buffer.from([0x01, 'v', 'o', 'r', 'b', 'i', 's']))) { | |||
| vorbis = { serial, sampleRate: buff.readUInt32LE(start + 12) }; | |||
| } | |||
| if (pageHeader.compare(Buffer.from([0x80, 't', 'h', 'e', 'o', 'r', 'a']))) { | |||
| let version = buff.slice(start + 7, start + 10); | |||
| result.width = buff.readUInt16BE(start + 10) << 4; | |||
| result.height = buff.readUInt16BE(start + 12) << 4; | |||
| if (version >= 0x030200) { | |||
| let width = buff.slice(start + 14, start + 17); | |||
| let height = buff.slice(start + 17, start + 20); | |||
| if (width <= result.width && width > result.width - 16 && height <= result.height && height > result.height - 16) { | |||
| result.width = width; | |||
| result.height = height; | |||
| } | |||
| } | |||
| } | |||
| if (eos && vorbis && serail == vorbis.serial) { | |||
| result.duration = ~~(position / vorbis.sampleRate); | |||
| } | |||
| pos = start + pageSize; | |||
| } | |||
| }, | |||
| WAV: async (path, result) => { | |||
| const buff = await read(path, 0, 32); | |||
| let size = buff.readUInt32LE(4); | |||
| let rate = buff.readUInt32LE(28); | |||
| result.duration = ~~(size / rate); | |||
| }, | |||
| MP3: async (path, result) => { | |||
| const versions = [2.5, 0, 2, 1]; | |||
| const layers = [0, 3, 2, 1]; | |||
| const bitrates = [ | |||
| [ // version 2.5 | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved | |||
| [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3 | |||
| [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2 | |||
| [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256] // layer 1 | |||
| ], | |||
| [ // reserved | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // reserved | |||
| ], | |||
| [ // version 2 | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved | |||
| [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3 | |||
| [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2 | |||
| [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256] // layer 1 | |||
| ], | |||
| [ // version 1 | |||
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved | |||
| [0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320], // layer 3 | |||
| [0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384], // layer 2 | |||
| [0,32,64,96,128,160,192,224,256,288,320,352,384,416,448] // layer 1 | |||
| ] | |||
| ] | |||
| const srates = [ | |||
| [11025, 12000, 8000, 0], // mpeg 2.5 | |||
| [ 0, 0, 0, 0], // reserved | |||
| [22050, 24000, 16000, 0], // mpeg 2 | |||
| [44100, 48000, 32000, 0] // mpeg 1 | |||
| ] | |||
| const tsamples = [ | |||
| [0, 576, 1152, 384], // mpeg 2.5 | |||
| [0, 0, 0, 0], // reserved | |||
| [0, 576, 1152, 384], // mpeg 2 | |||
| [0, 1152, 1152, 384] // mpeg 1 | |||
| ]; | |||
| const slotSizes = [0, 1, 1, 4]; | |||
| const modes = ['stereo', 'joint_stereo', 'dual_channel', 'mono']; | |||
| const buff = await read(path, 0, 64000); | |||
| let duration = 0; | |||
| let count = 0; | |||
| let skip = 0; | |||
| let pos = 0; | |||
| while (pos < buff.length) { | |||
| let start = pos; | |||
| if (buff.toString('ascii', pos, 4) == 'TAG+') { | |||
| skip += 227; | |||
| pos += 227; | |||
| } else if (buff.toString('ascii', pos, 3) == 'TAG') { | |||
| skip += 128; | |||
| pos += 128; | |||
| } else if (buff.toString('ascii', pos, 3) == 'ID3') { | |||
| let bytes = buff.readUInt32BE(pos + 6); | |||
| let size = 10 + (bytes[0] << 21 | bytes[1] << 14 | bytes[2] << 7 | bytes[3]); | |||
| skip += size; | |||
| pos += size; | |||
| } else { | |||
| let hdr = buff.slice(pos, pos + 4); | |||
| while (pos < buff.length && !(hdr[0] == 0xff && (hdr[1] & 0xe0) == 0xe0)) { | |||
| pos++; | |||
| hdr = buff.slice(pos, pos + 4); | |||
| } | |||
| let ver = (hdr[1] & 0x18) >> 3; | |||
| let lyr = (hdr[1] & 0x06) >> 1; | |||
| let pad = (hdr[2] & 0x02) >> 1; | |||
| let brx = (hdr[2] & 0xf0) >> 4; | |||
| let srx = (hdr[2] & 0x0c) >> 2; | |||
| let mdx = (hdr[3] & 0xc0) >> 6; | |||
| let version = versions[ver]; | |||
| let layer = layers[lyr]; | |||
| let bitrate = bitrates[ver][lyr][brx] * 1000; | |||
| let samprate = srates[ver][srx]; | |||
| let samples = tsamples[ver][lyr]; | |||
| let slotSize = slotSizes[lyr]; | |||
| let mode = modes[mdx]; | |||
| let fsize = ~~(((samples / 8 * bitrate) / samprate) + (pad ? slotSize : 0)); | |||
| count++; | |||
| if (count == 1) { | |||
| if (layer != 3) { | |||
| pos += 2; | |||
| } else { | |||
| if (mode != 'mono') { | |||
| if (version == 1) { | |||
| pos += 32; | |||
| } else { | |||
| pos += 17; | |||
| } | |||
| } else { | |||
| if (version == 1) { | |||
| pos += 17; | |||
| } else { | |||
| pos += 9; | |||
| } | |||
| } | |||
| } | |||
| if (buff.toString('ascii', pos, pos + 4) == 'Xing' && (buff.readUInt32BE(pos + 4) & 0x0001) == 0x0001) { | |||
| let totalFrames = buff.readUInt32BE(pos + 8); | |||
| duration = totalFrames * samples / samprate; | |||
| break; | |||
| } | |||
| } | |||
| if (fsize < 1) break; | |||
| pos = start + fsize; | |||
| duration += (samples / samprate); | |||
| } | |||
| } | |||
| result.duration = ~~duration; | |||
| }, | |||
| FLAC: async (path, result) => { | |||
| const buff = await read(path, 18, 8); | |||
| let rate = (buff[0] << 12) | (buff[1] << 4) | ((buff[2] & 0xf0) >> 4); | |||
| let size = ((buff[3] & 0x0f) << 32) | (buff[4] << 24) | (buff[5] << 16) | (buff[6] << 8) | buff[7]; | |||
| result.duration = ~~(size / rate); | |||
| }, | |||
| }; | |||
| async function detect(path) { | |||
| const buff = await read(path, 0, 12); | |||
| if (buff.slice(0, 8).compare(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) { | |||
| return 'PNG'; | |||
| } | |||
| if (buff.toString('ascii', 0, 3) == 'GIF') { | |||
| return 'GIF'; | |||
| } | |||
| if (buff.toString('ascii', 0, 2) == 'BM') { | |||
| return 'BMP'; | |||
| } | |||
| if (buff.slice(0, 2).compare(Buffer.from([0xff, 0xd8]))) { | |||
| return 'JPEG'; | |||
| } | |||
| if (buff.toString('ascii', 0, 2) == 'II' && buff.readUInt16LE(2) == 42) { | |||
| return 'TIFF'; | |||
| } | |||
| if (buff.toString('ascii', 0, 2) == 'MM' && buff.readUInt16BE(2) == 42) { | |||
| return 'TIFF'; | |||
| } | |||
| if (buff.toString('ascii', 0, 4) == 'RIFF' && buff.toString('ascii', 8, 4) == 'AVI ') { | |||
| return 'AVI'; | |||
| } | |||
| if (buff.toString('ascii', 4, 4) == 'ftyp') { | |||
| return 'MP4'; | |||
| } | |||
| if (buff.toString('ascii', 4, 4) == 'moov') { | |||
| return 'MOV'; | |||
| } | |||
| if (buff.slice(0, 4).compare(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]))) { | |||
| // TODO detect MKV | |||
| return 'EBML'; | |||
| } | |||
| if (buff.toString('ascii', 0, 4) == 'OggS') { | |||
| // TODO detect OGV | |||
| return 'OGG' | |||
| } | |||
| if (buff.toString('ascii', 0, 4) == 'RIFF', buff.toString('ascii', 8, 4) == 'WAVE') { | |||
| return 'WAV'; | |||
| } | |||
| if (buff.toString('ascii', 0, 3) == 'ID3' || (buf[0] == 0xff && (buff[1] & 0xe0))) { | |||
| return 'MP3'; | |||
| } | |||
| if (buff.toString('ascii', 0, 4) == 'fLaC') { | |||
| return 'FLAC'; | |||
| } | |||
| return null | |||
| } | |||
| module.exports = { | |||
| detect: async function(options) { | |||
| let path = this.parseRequired(options.path, 'string', 'metadata.detect: path is required.'); | |||
| return detect(path); | |||
| }, | |||
| isImage: async function(options) { | |||
| let path = this.parseRequired(options.path, 'string', 'metadata.isImage: path is required.'); | |||
| let type = await detect(path); | |||
| let cond = imageTypes.includes(type); | |||
| if (cond) { | |||
| if (options.then) { | |||
| await this.exec(options.then, true); | |||
| } | |||
| } else if (options.else) { | |||
| await this.exec(options.else, true); | |||
| } | |||
| return cond; | |||
| }, | |||
| isVideo: async function(options) { | |||
| let path = this.parseRequired(options.path, 'string', 'metadata.isVideo: path is required.'); | |||
| let type = await detect(path); | |||
| let cond = videoTypes.includes(type); | |||
| if (cond) { | |||
| if (options.then) { | |||
| await this.exec(options.then, true); | |||
| } | |||
| } else if (options.else) { | |||
| await this.exec(options.else, true); | |||
| } | |||
| return cond; | |||
| }, | |||
| isSound: async function(options) { | |||
| let path = this.parseRequired(options.path, 'string', 'metadata.isSound: path is required.'); | |||
| let type = await detect(path); | |||
| let cond = soundTypes.includes(type); | |||
| if (cond) { | |||
| if (options.then) { | |||
| await this.exec(options.then, true); | |||
| } | |||
| } else if (options.else) { | |||
| await this.exec(options.else, true); | |||
| } | |||
| return cond; | |||
| }, | |||
| fileinfo: async function(options) { | |||
| let path = this.parseRequired(options.path, 'string', 'metadata.fileinfo: path is required.'); | |||
| let type = await detect(path); | |||
| let result = { type, width: null, height: null, duration: null }; | |||
| if (parser[type]) { | |||
| await parser[type](path, result); | |||
| } | |||
| return result; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,24 @@ | |||
| module.exports = { | |||
| provider: async function(options, name) { | |||
| const oauth = await this.setOAuthProvider(name, options) | |||
| return { | |||
| access_token: oauth.access_token, | |||
| refresh_token: oauth.refresh_token | |||
| }; | |||
| }, | |||
| authorize: async function(options) { | |||
| const oauth = await this.getOAuthProvider(options.provider); | |||
| return oauth.authorize(this.parse(options.scopes), this.parse(options.params)); | |||
| }, | |||
| refresh: async function(options) { | |||
| const oauth = await this.getOAuthProvider(options.provider); | |||
| return oauth.refreshToken(this.parse(options.refresh_token)); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,47 @@ | |||
| const querystring = require('querystring'); | |||
| const https = require('https'); | |||
| exports.validate = function(options) { | |||
| const secret = this.parseRequired(options.secret, 'string', 'recaptcha.validate: secret is required.'); | |||
| const msg = this.parseOptional(options.msg, 'string', 'Recaptcha check failed.'); | |||
| const response = this.req.body['g-recaptcha-response']; | |||
| const remoteip = this.req.ip; | |||
| const data = querystring.stringify({ secret, response, remoteip }); | |||
| return new Promise((resolve, reject) => { | |||
| const req = https.request('https://www.google.com/recaptcha/api/siteverify', { | |||
| method: 'POST', | |||
| headers: { | |||
| 'Content-Type': 'application/x-www-form-urlencoded', | |||
| 'Content-Length': data.length | |||
| } | |||
| }, (res) => { | |||
| let body = ''; | |||
| res.setEncoding('utf8'); | |||
| res.on('data', (chunk) => body += chunk); | |||
| res.on('end', () => { | |||
| if (res.statusCode >= 400) return reject(body); | |||
| if (body.charCodeAt(0) === 0xFEFF) { | |||
| body = body.slice(1); | |||
| } | |||
| body = JSON.parse(body); | |||
| if (!body.success) { | |||
| this.res.status(400).json({ | |||
| form: { 'g-recaptcha-response': msg } | |||
| }); | |||
| } | |||
| resolve(body); | |||
| }); | |||
| }); | |||
| req.on('error', reject); | |||
| req.write(data); | |||
| req.end(); | |||
| }); | |||
| }; | |||
| @@ -0,0 +1,237 @@ | |||
| const client = global.redisClient; | |||
| if (client) { | |||
| const { promisify } = require('util'); | |||
| const commands = {}; | |||
| [ | |||
| 'append', 'decr', 'decrby', 'del', 'exists', 'get', 'getset', 'incr', 'incrby', | |||
| 'mget', 'set', 'setex', 'setnx', 'strlen', 'lindex', 'linsert', 'llen', 'lpop', | |||
| 'lpos', 'lpush', 'lpushx', 'lrange', 'lrem', 'lset', 'ltrim', 'rpop', 'rpush', | |||
| 'rpushx', 'copy', 'del', 'expire', 'expireat', 'keys', 'persist', 'pexpire', | |||
| 'pexpireat', 'pttl', 'randomkey', 'rename', 'renamenx', 'touch', 'ttl', 'type', | |||
| 'unlink' | |||
| ].forEach(command => { | |||
| commands[command] = promisify(client[command]).bind(client); | |||
| }); | |||
| exports.append = function(options) { | |||
| options = this.parse(options); | |||
| return commands.append(options.key, options.value); | |||
| }; | |||
| exports.decr = function(options) { | |||
| options = this.parse(options); | |||
| return commands.decr(options.key); | |||
| }; | |||
| exports.decrby = function(options) { | |||
| options = this.parse(options); | |||
| return commands.decrby(options.key, options.decrement); | |||
| }; | |||
| exports.del = function(options) { | |||
| options = this.parse(options); | |||
| return commands.del(options.key); | |||
| }; | |||
| exports.exists = function(options) { | |||
| options = this.parse(options); | |||
| return commands.exists(options.key); | |||
| }; | |||
| exports.get = function(options) { | |||
| options = this.parse(options); | |||
| return commands.get(options.key); | |||
| }; | |||
| exports.getset = function(options) { | |||
| options = this.parse(options); | |||
| return commands.getset(options.key, options.value); | |||
| }; | |||
| exports.incr = function(options) { | |||
| options = this.parse(options); | |||
| return commands.incr(options.key); | |||
| }; | |||
| exports.incrby = function(options) { | |||
| options = this.parse(options); | |||
| return commands.incrby(options.key, options.increment); | |||
| }; | |||
| exports.mget = function(options) { | |||
| options = this.parse(options); | |||
| return commands.mget(options.keys); | |||
| }; | |||
| exports.set = function(options) { | |||
| options = this.parse(options); | |||
| return commands.set(options.key, options.value); | |||
| }; | |||
| exports.setex = function(options) { | |||
| options = this.parse(options); | |||
| return commands.setex(options.key, options.seconds, options.value); | |||
| }; | |||
| exports.setnx = function(options) { | |||
| options = this.parse(options); | |||
| return commands.setnx(options.key, options.value); | |||
| }; | |||
| exports.strlen = function(options) { | |||
| options = this.parse(options); | |||
| return commands.strlen(options.key); | |||
| }; | |||
| exports.lindex = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lindex(options.key, options.index); | |||
| }; | |||
| exports.linsert = function(options) { | |||
| options = this.parse(options); // position: BEFORE|AFTER | |||
| return commands.linsert(options.key, options.position, options.pivot, options.element); | |||
| }; | |||
| exports.llen = function(options) { | |||
| options = this.parse(options); | |||
| return commands.llen(options.key); | |||
| }; | |||
| exports.lpop = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lpop(options.key, options.count); | |||
| }; | |||
| exports.lpos = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lpos(options.key, options.element); | |||
| }; | |||
| exports.lpush = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lpush(options.key, options.element); | |||
| }; | |||
| exports.lpushx = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lpushx(options.key, options.element); | |||
| }; | |||
| exports.lrange = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lrange(options.key, options.start, options.stop); | |||
| }; | |||
| exports.lrem = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lrem(options.key, options.count, options.element); | |||
| }; | |||
| exports.lset = function(options) { | |||
| options = this.parse(options); | |||
| return commands.lset(options.key, options.index, options.element); | |||
| }; | |||
| exports.ltrim = function(options) { | |||
| options = this.parse(options); | |||
| return commands.ltrim(options.key, options.start, options.stop); | |||
| }; | |||
| exports.rpop = function(options) { | |||
| options = this.parse(options); | |||
| return commands.rpop(options.key, options.count); | |||
| }; | |||
| exports.rpush = function(options) { | |||
| options = this.parse(options); | |||
| return commands.rpush(options.key, options.element); | |||
| }; | |||
| exports.rpushx = function(options) { | |||
| options = this.parse(options); | |||
| return commands.rpushx(options.key, options.element); | |||
| }; | |||
| exports.copy = function(options) { | |||
| options = this.parse(options); | |||
| return commands.copy(options.source, options.destination); | |||
| }; | |||
| exports.del = function(options) { | |||
| options = this.parse(options); | |||
| return commands.del(options.key); | |||
| }; | |||
| exports.expire = function(options) { | |||
| options = this.parse(options); | |||
| return commands.expire(options.key, options.seconds); | |||
| }; | |||
| exports.expireat = function(options) { | |||
| options = this.parse(options); | |||
| return commands.expireat(options.key, options.timestamp); | |||
| }; | |||
| exports.keys = function(options) { | |||
| options = this.parse(options); | |||
| return commands.keys(options.pattern); | |||
| }; | |||
| exports.persist = function(options) { | |||
| options = this.parse(options); | |||
| return commands.persist(options.key); | |||
| }; | |||
| exports.pexpire = function(options) { | |||
| options = this.parse(options); | |||
| return commands.pexpire(options.key, options.milliseconds); | |||
| }; | |||
| exports.pexpireat = function(options) { | |||
| options = this.parse(options); | |||
| return commands.pexpireat(options.key, options.mstimestamp) | |||
| }; | |||
| exports.pttl = function(options) { | |||
| options = this.parse(options); | |||
| return commands.pttl(options.key); | |||
| }; | |||
| exports.randomkey = function(options) { | |||
| options = this.parse(options); | |||
| return commands.randomkey(); | |||
| }; | |||
| exports.rename = function(options) { | |||
| options = this.parse(options); | |||
| return commands.rename(options.key, options.newkey); | |||
| }; | |||
| exports.renamenx = function(options) { | |||
| options = this.parse(options); | |||
| return commands.renamenx(options.key, options.newkey); | |||
| }; | |||
| exports.touch = function(options) { | |||
| options = this.parse(options); | |||
| return commands.touch(options.key); | |||
| }; | |||
| exports.ttl = function(options) { | |||
| options = this.parse(options); | |||
| return commands.ttl(options.key); | |||
| }; | |||
| exports.type = function(options) { | |||
| options = this.parse(options); | |||
| return commands.type(options.key); | |||
| }; | |||
| exports.unlink = function(options) { | |||
| options = this.parse(options); | |||
| return commands.unlink(options.key); | |||
| }; | |||
| } | |||
| @@ -0,0 +1,174 @@ | |||
| const fs = require('fs-extra'); | |||
| const mime = require('mime-types'); | |||
| const { join, basename, dirname } = require('path'); | |||
| const { toSystemPath } = require('../core/path'); | |||
| module.exports = { | |||
| provider: function (options, name) { | |||
| this.setS3Provider(name, options); | |||
| }, | |||
| createBucket: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.createBucket: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.createBucket: bucket is required.'); | |||
| const ACL = this.parseOptional(options.acl, 'string', undefined); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| return s3.createBucket({ Bucket, ACL }); | |||
| }, | |||
| listBuckets: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.listBuckets: provider is required.'); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| return s3.listBuckets({}); | |||
| }, | |||
| deleteBucket: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.deleteBucket: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.deleteBucket: bucket is required.'); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| return s3.deleteBucket({ Bucket }); | |||
| }, | |||
| listFiles: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.listFiles: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.listFiles: bucket is required.'); | |||
| const MaxKeys = this.parseOptional(options.maxKeys, 'number', undefined); | |||
| const Prefix = this.parseOptional(options.prefix, 'string', undefined); | |||
| const ContinuationToken = this.parseOptional(options.continuationToken, 'string', undefined); | |||
| const StartAfter = this.parseOptional(options.startAfter, 'string', undefined); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| return s3.listObjectsV2({ Bucket, MaxKeys, Prefix, ContinuationToken, StartAfter }); | |||
| }, | |||
| putFile: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.uploadFile: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.uploadFile: bucket is required.'); | |||
| const path = toSystemPath(this.parseRequired(options.path, 'string', 's3.uploadFile: path is required.')); | |||
| const Key = this.parseRequired(options.key, 'string', 's3.uploadFile: key is required.'); | |||
| const ContentType = this.parseOptional(options.contentType, 'string', mime.lookup(Key) || 'application/octet-stream'); | |||
| const ContentDisposition = this.parseOptional(options.contentDisposition, 'string', undefined); | |||
| const ACL = this.parseOptional(options.acl, 'string', undefined); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| let Body = fs.createReadStream(path); | |||
| const result = await s3.putObject({ Bucket, ACL, Key, ContentType, ContentDisposition, Body }); | |||
| try { | |||
| const endpoint = await s3.config.endpoint(); | |||
| result.Location = `https://${Bucket}.${endpoint.hostname}/${Key}`; | |||
| } catch (e) {} | |||
| return result; | |||
| }, | |||
| getFile: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.getFile: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.getFile: bucket is required.'); | |||
| const Key = this.parseRequired(options.key, 'string', 's3.getFile: key is required.'); | |||
| const path = toSystemPath(this.parseRequired(options.path, 'string', 's3.getFile: path is required.')); | |||
| const stripKeyPath = this.parseOptional(options.stripKeyPath, 'boolean', false); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| const file = Key; | |||
| if (stripKeyPath) file = basename(file); | |||
| const destination = join(path, file); | |||
| await fs.ensureDir(dirname(destination)); | |||
| const writer = fs.createWriteStream(destination); | |||
| const { Body } = await s3.getObject({ Bucket, Key }); | |||
| Body.pipe(writer); | |||
| return new Promise((resolve, reject) => { | |||
| writer.on('finish', resolve); | |||
| writer.on('error', reject); | |||
| }); | |||
| }, | |||
| deleteFile: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.deleteFile: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.deleteFile: bucket is required.'); | |||
| const Key = this.parseRequired(options.key, 'string', 's3.deleteFile: key is required.'); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| return s3.deleteObject({ Bucket, Key }); | |||
| }, | |||
| downloadFile: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.downloadFile: profider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.downloadFile: bucket is required.'); | |||
| const Key = this.parseRequired(options.key, 'string', 's3.downloadFile: key is required.'); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| const data = await s3.headObject({ Bucket, Key }); | |||
| this.res.set('Content-Length', data.ContentLength); | |||
| this.res.attachment(basename(Key)); | |||
| const { Body } = await s3.getObject({ Bucket, Key }); | |||
| Body.pipe(this.res); | |||
| this.noOutput = true; | |||
| }, | |||
| signDownloadUrl: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.signDownloadUrl: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.signDownloadUrl: bucket is required.'); | |||
| const Key = this.parseRequired(options.key, 'string', 's3.signDownloadUrl: key is required.'); | |||
| const expiresIn = this.parseOptional(options.expires, 'number', 300); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); | |||
| const { GetObjectCommand } = require('@aws-sdk/client-s3'); | |||
| const command = new GetObjectCommand({ Bucket, Key }); | |||
| return getSignedUrl(s3, command, { expiresIn }); | |||
| }, | |||
| signUploadUrl: async function (options) { | |||
| const provider = this.parseRequired(options.provider, 'string', 's3.signUploadUrl: provider is required.'); | |||
| const Bucket = this.parseRequired(options.bucket, 'string', 's3.signUploadUrl: bucket is required.'); | |||
| const Key = this.parseRequired(options.key, 'string', 's3.signUploadUrl: key is required.'); | |||
| const ContentType = this.parseOptional(options.contentType, 'string', mime.lookup(Key) || 'application/octet-stream'); | |||
| const expiresIn = this.parseOptional(options.expires, 'number', 300); | |||
| const ACL = this.parseOptional(options.acl, 'string', undefined); | |||
| const s3 = this.getS3Provider(provider); | |||
| if (!s3) throw new Error(`S3 provider "${provider}" doesn't exist.`); | |||
| const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); | |||
| const { PutObjectCommand } = require('@aws-sdk/client-s3'); | |||
| const command = new PutObjectCommand({ Bucket, Key, ContentType, ACL }); | |||
| return getSignedUrl(s3, command, { expiresIn }); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,172 @@ | |||
| /** | |||
| * emits an event to all clients filtered by the given options | |||
| * @param {string} namespace - only to the specified namespace | |||
| * @param {string} room - emit only to the specified room (can be combined with namespace) | |||
| * @param {string} eventName - the name of the event | |||
| * @param {object} params - any parameters/data to send with the event | |||
| */ | |||
| exports.emit = function(options) { | |||
| if (this.io) { | |||
| options = this.parse(options); | |||
| if (options.namespace) { | |||
| if (options.room) { | |||
| this.io.of(options.namespace).to(options.room).emit(options.eventName, options.params); | |||
| } else { | |||
| this.io.of(options.namespace).emit(options.eventName, options.params); | |||
| } | |||
| } else { | |||
| if (options.room) { | |||
| this.io.in(options.room).emit(options.eventName, options.params); | |||
| } else { | |||
| this.io.emit(options.eventName, options.params); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * emit an event to all clients except the sender | |||
| * @param {string} room - broadcast in a specific room | |||
| * @param {string} eventName - the name of the event | |||
| * @param {object} params - the parameters/data to send with the event | |||
| */ | |||
| exports.broadcast = function(options) { | |||
| if (this.socket) { | |||
| options = this.parse(options); | |||
| if (options.room) { | |||
| this.socket.to(options.room).emit(options.eventName, options.params); | |||
| } else { | |||
| this.socket.broadcast.emit(options.eventName, options.params); | |||
| } | |||
| } | |||
| }; | |||
| /** | |||
| * emit an event to client and wait for an answer | |||
| * @param {string} room - broadcast in a specific room | |||
| * @param {string} eventName - the name of the event | |||
| * @param {object} params - the parameters/data to send with the event | |||
| */ | |||
| exports.request = function(options) { | |||
| if (this.socket) { | |||
| options = this.parse(options); | |||
| return new Promise((resolve) => { | |||
| this.socket.emit(options.eventName, options.params, resolve); | |||
| }); | |||
| } | |||
| return null; | |||
| }; | |||
| /** | |||
| * send a private meessage to a socket | |||
| * @param {string} socketId - the socket id to send the message to | |||
| * @param {string} eventName - the name of the event | |||
| * @param {object} params - the parameters/data to send with the event | |||
| */ | |||
| exports.message = function(options) { | |||
| if (this.io) { | |||
| options = this.parse(options); | |||
| this.io.to(options.socketId).emit(options.eventName, options.params); | |||
| } | |||
| }; | |||
| /** | |||
| * special serverconnect refresh broadcast | |||
| * @param {string} action - the action that require refresh | |||
| */ | |||
| exports.refresh = async function(options) { | |||
| if (this.io) { | |||
| options = this.parse(options); | |||
| // Do we have a global redis client? | |||
| if (global.redisClient) { | |||
| try { // ignore any errors here | |||
| let wsKeys = await global.redisClient.keys('ws:' + options.action + ':*'); | |||
| if (wsKeys.length) await global.redisClient.del(wsKeys); | |||
| let scKeys = await global.redisClient.keys('erc:' + '/api/' + options.action + '*'); | |||
| if (scKeys.length) await global.redisClient.del(scKeys); | |||
| } catch (e) { | |||
| console.error(e); | |||
| } | |||
| } | |||
| this.io.of('/api').emit(options.action, options.params); | |||
| } | |||
| }; | |||
| /** | |||
| * let current client join a room | |||
| * @param {string} room - the room to join | |||
| */ | |||
| exports.join = function(options) { | |||
| if (this.socket) { | |||
| this.socket.join(this.parse(options.room)); | |||
| } | |||
| }; | |||
| /** | |||
| * let current client leave a room | |||
| * @param {string} room - the room to leave | |||
| */ | |||
| exports.leave = function(options) { | |||
| if (this.socket) { | |||
| this.socket.leave(this.parse(options.room)); | |||
| } | |||
| }; | |||
| /** | |||
| * get the socket id of the current client | |||
| */ | |||
| exports.identify = function(options) { | |||
| return this.socket ? this.socket.id : null; | |||
| }; | |||
| /** | |||
| * get the rooms the current client has joined | |||
| */ | |||
| exports.rooms = function(options) { | |||
| return this.socket ? Array.from(this.socket.rooms) : []; | |||
| }; | |||
| /** | |||
| * get all the rooms | |||
| * @param {string} namespace - the namespace | |||
| */ | |||
| exports.allRooms = async function(options) { | |||
| if (this.io) { | |||
| let adapter = io.of(options.namespace || '/').adapter; | |||
| if (typeof adapter.allRooms == 'function') { | |||
| return Array.from(await adapter.allRooms()); | |||
| } else if (adapter.rooms) { | |||
| return Array.from(adapter.rooms.keys()); | |||
| } | |||
| } | |||
| return []; | |||
| }; | |||
| /** | |||
| * get all the connected sockets | |||
| * @param {string} namespace - the namespace | |||
| * @param {string} room - return only clients in a specific room | |||
| */ | |||
| exports.allSockets = async function(options) { | |||
| if (this.io) { | |||
| options = this.parse(options); | |||
| if (options.room) { | |||
| return Array.from(await this.io.of(options.namespace || '/').in(options.room).allSockets()); | |||
| } else { | |||
| return Array.from(await this.io.of(options.namespace || '/').allSockets()); | |||
| } | |||
| } | |||
| return []; | |||
| }; | |||
| @@ -0,0 +1,118 @@ | |||
| const fs = require('fs-extra'); | |||
| const debug = require('debug')('server-connect:upload'); | |||
| const { join, basename, extname } = require('path'); | |||
| const { toAppPath, toSystemPath, toSiteUrl, parseTemplate, getUniqFile } = require('../core/path'); | |||
| const diacritics = require('../core/diacritics'); | |||
| module.exports = { | |||
| upload: async function(options) { | |||
| let self = this; | |||
| let fields = this.parse(options.fields || this.parse('{{$_POST}}')); | |||
| let path = this.parseOptional(options.path, 'string', '/uploads'); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let createPath = this.parseOptional(options.createPath, 'boolean', true); | |||
| let throwErrors = this.parseOptional(options.throwErrors, 'boolean', false); | |||
| let template = typeof options.template == 'string' ? options.template : false; //this.parseOptional(options.template, 'string', ''); | |||
| let replaceSpace = this.parseOptional(options.replaceSpace, 'boolean', false); | |||
| let asciiOnly = this.parseOptional(options.asciiOnly, 'boolean', false); | |||
| let replaceDiacritics = this.parseOptional(options.replaceDiacritics, 'boolean', false); | |||
| if (throwErrors) { | |||
| for (let field in this.req.files) { | |||
| if (Array.isArray(this.req.files[field])) { | |||
| for (let file of this.req.files[field]) { | |||
| if (file.truncated) { | |||
| throw new Error('Some files failed to upload.'); | |||
| } | |||
| } | |||
| } else { | |||
| let file = this.req.files[field]; | |||
| if (file.truncated) { | |||
| throw new Error('Some files failed to upload.'); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| path = toSystemPath(path); | |||
| if (!fs.existsSync(path)) { | |||
| if (createPath) { | |||
| await fs.ensureDir(path); | |||
| } else { | |||
| throw new Error(`Upload path doesn't exist.`); | |||
| } | |||
| } | |||
| let files = this.req.files; | |||
| let uploaded = []; | |||
| if (files) { | |||
| await processFields(fields); | |||
| } | |||
| return typeof fields == 'string' ? (uploaded.length ? uploaded[0] : null) : uploaded; | |||
| async function processFields(fields) { | |||
| debug('Process fields: %O', fields); | |||
| if (typeof fields == 'object') { | |||
| for (let i in fields) { | |||
| await processFields(fields[i]); | |||
| } | |||
| } else if (typeof fields == 'string' && files[fields]) { | |||
| let processing = files[fields]; | |||
| if (!Array.isArray(processing)) processing = [processing]; | |||
| for (let file of processing) { | |||
| debug('Processing file: %O', file); | |||
| if (!file.processed) { | |||
| let name = file.name.replace(/[\x00-\x1f\x7f!%&#@$*()?:,;"'<>^`|+={}\[\]\\\/]/g, ''); | |||
| if (replaceSpace) name = name.replace(/\s+/g, '_'); | |||
| if (replaceDiacritics) name = diacritics.replace(name); | |||
| if (asciiOnly) name = name.replace(/[^\x00-\x7e]/g, ''); | |||
| let filepath = join(path, name); | |||
| if (template) { | |||
| let _template = self.parse(template, self.scope.create({ | |||
| file: basename(filepath), | |||
| name: basename(filepath, extname(filepath)), | |||
| ext: extname(filepath) | |||
| })); | |||
| filepath = parseTemplate(filepath, _template); | |||
| } | |||
| if (fs.existsSync(filepath)) { | |||
| if (overwrite) { | |||
| await fs.unlink(filepath); | |||
| } else { | |||
| filepath = getUniqFile(filepath); | |||
| } | |||
| } | |||
| await file.mv(filepath); | |||
| uploaded.push({ | |||
| name: basename(filepath), | |||
| path: toAppPath(filepath), | |||
| url: toSiteUrl(filepath), | |||
| type: file.mimetype, | |||
| size: file.size | |||
| }); | |||
| file.processed = true; | |||
| } | |||
| } | |||
| } | |||
| return uploaded; | |||
| } | |||
| } | |||
| }; | |||
| @@ -0,0 +1,18 @@ | |||
| const validator = require('../validator'); | |||
| module.exports = { | |||
| validate: function(options) { | |||
| const data = this.parse(options.data); | |||
| const noError = this.parseOptional(options.noError, 'boolean', false); | |||
| return validator.validateData(this, data, noError); | |||
| }, | |||
| error: function(options, name) { | |||
| const message = this.parseRequired(options.message, 'string', 'validator.error: message is required.'); | |||
| const error = { [options.fieldName ? 'form' : 'data']: {[options.fieldName || name]: message }}; | |||
| this.res.status(400).json(error); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,114 @@ | |||
| const fs = require('fs-extra'); | |||
| const debug = require('debug')('server-connect:zip'); | |||
| const { basename } = require('path'); | |||
| const { toSystemPath, toAppPath, getFilesArray, getUniqFile } = require('../core/path'); | |||
| const openZip = (zipfile) => require('unzipper').Open.file(zipfile); | |||
| const Zip = function(zipfile, options) { | |||
| this.output = fs.createWriteStream(zipfile); | |||
| this.archive = require('archiver')('zip', options); | |||
| this.archive.on('warning', (err) => { | |||
| if (err.code == 'ENOENT') { | |||
| debug('error: %O', err); | |||
| } else { | |||
| throw err; | |||
| } | |||
| }); | |||
| this.archive.on('error', (err) => { | |||
| throw err; | |||
| }); | |||
| this.archive.pipe(this.output); | |||
| }; | |||
| Zip.prototype.addFile = function(file) { | |||
| this.archive.file(file, { name: basename(file) }); | |||
| }; | |||
| Zip.prototype.addDir = function(dir, recursive) { | |||
| if (recursive) { | |||
| this.archive.directory(dir, false); | |||
| } else { | |||
| this.archive.glob('*', { cwd: dir }); | |||
| } | |||
| }; | |||
| Zip.prototype.save = function() { | |||
| return new Promise((resolve, reject) => { | |||
| this.output.on('close', resolve); | |||
| this.archive.on('error', reject); | |||
| this.archive.finalize(); | |||
| }); | |||
| }; | |||
| module.exports = { | |||
| zip: async function(options) { | |||
| let zipfile = toSystemPath(this.parseRequired(options.zipfile, 'string', 'zip.zip: zipfile is required.')); | |||
| let files = getFilesArray(this.parseRequired(options.files, 'object', 'zip.zip: files is requires.')); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let comment = this.parseOptional(options.comment, 'string', ''); | |||
| if (!overwrite) zipfile = getUniqFile(zipfile); | |||
| const zip = new Zip(zipfile, { comment }); | |||
| for (let file of files) { | |||
| zip.addFile(file); | |||
| } | |||
| await zip.save(); | |||
| return toAppPath(zipfile); | |||
| }, | |||
| zipdir: async function(options) { | |||
| let zipfile = toSystemPath(this.parseRequired(options.zipfile, 'string', 'zip.zipdir: zipfile is required.')); | |||
| let path = toSystemPath(this.parseRequired(options.path, 'string', 'zip.zipdir: path is required.')); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', false); | |||
| let recursive = this.parseOptional(options.recursive, 'boolean', false); | |||
| let comment = this.parseOptional(options.comment, 'string', ''); | |||
| if (!overwrite) zipfile = getUniqFile(zipfile); | |||
| const zip = new Zip(zipfile, { comment }); | |||
| zip.addDir(path, recursive); | |||
| await zip.save(); | |||
| return toAppPath(zipfile); | |||
| }, | |||
| unzip: async function(options) { | |||
| let zipfile = toSystemPath(this.parseRequired(options.zipfile, 'string', 'zip.unzip: zipfile is required.')); | |||
| let dest = toSystemPath(this.parseRequired(options.destination, 'string', 'zip.unzip: destination is required.')); | |||
| let overwrite = this.parseOptional(options.overwrite, 'boolean', true); | |||
| // TODO: overwrite option | |||
| await openZip(zipfile).then(d => d.extract({ path: dest, concurrency: 4 })); | |||
| return true; | |||
| }, | |||
| dir: async function(options) { | |||
| let zipfile = toSystemPath(this.parseRequired(options.zipfile, 'string', 'zip.dir: zipfile is required.')); | |||
| return openZip(zipfile).then(d => { | |||
| return d.files.map(file => ({ | |||
| type: file.type == 'Directory' ? 'dir': 'file', | |||
| path: file.path, | |||
| size: file.uncompressedSize, | |||
| compressedSize: file.compressedSize, | |||
| compressionMethod: file.compressionMethod == 8 ? 'Deflate' : 'None', | |||
| lastModified: file.lastModifiedDateTime | |||
| })); | |||
| }); | |||
| }, | |||
| comment: async function(options) { | |||
| let zipfile = toSystemPath(this.parseRequired(options.zipfile, 'string', 'zip.comment: zipfile is required.')); | |||
| return openZip(zipfile).then(d => d.comment); | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,170 @@ | |||
| const { http, https } = require('follow-redirects'); | |||
| const querystring = require('querystring'); | |||
| const services = require('./services'); | |||
| const crypto = require('crypto'); | |||
| class OAuth2 { | |||
| constructor(app, opts, name) { | |||
| this.app = app; | |||
| this.name = name; | |||
| this.opts = opts; | |||
| if (opts.service) { | |||
| Object.assign(this.opts, services[opts.service]); | |||
| } | |||
| this.access_token = opts.access_token || app.getSession(`${name}_access_token`); | |||
| this.refresh_token = opts.refresh_token || app.getSession(`${name}_refresh_token`); | |||
| if (this.access_token) { | |||
| let expires = app.getSession(`${name}_expires`); | |||
| if (expires && expires < expires_in(-10)) { | |||
| this.access_token = null; | |||
| app.removeSession(`${name}_access_token`); | |||
| app.removeSession(`${name}_expires`); | |||
| if (this.refresh_token) { | |||
| this.refreshToken(this.refresh_token); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| async init() { | |||
| if (this.opts.jwt_bearer && !this.access_token) { | |||
| const assertion = this.app.getJSONWebToken(this.opts.jwt_bearer); | |||
| let response = await this.grant('urn:ietf:params:oauth:grant-type:jwt-bearer', { assertion }); | |||
| this.access_token = response.access_token; | |||
| } | |||
| if (this.opts.client_credentials && !this.access_token) { | |||
| let response = await this.grant('client_credentials'); | |||
| this.access_token = response.access_token; | |||
| } | |||
| } | |||
| async authorize(scopes = [], params = {}) { | |||
| let query = this.app.req.query; | |||
| if (query.state && query.state == this.app.getSession(`${this.name}_state`)) { | |||
| this.app.removeSession(`${this.name}_state`); | |||
| if (query.error) { | |||
| throw new Error(query.error_message || query.error); | |||
| } | |||
| if (query.code) { | |||
| let params = { | |||
| redirect_uri: redirect_uri(this.app.req), | |||
| code: query.code | |||
| }; | |||
| if (this.app.getSession(`${this.name}_code_verifier`)) { | |||
| params.code_verifier = this.app.getSession(`${this.name}_code_verifier`); | |||
| } | |||
| return this.grant('authorization_code', params); | |||
| } | |||
| } | |||
| if (this.opts.pkce || params.code_verifier) { | |||
| const code_verifier = params.code_verifier || crypto.randomBytes(40).toString('hex'); | |||
| this.app.setSession(`${this.name}_code_verifier`, code_verifier); | |||
| params.code_challenge_method = 'S256'; | |||
| params.code_challenge = crypto.createHash('sha256').update(code_verifier).digest('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); | |||
| } | |||
| params = Object.assign({}, { | |||
| response_type: 'code', | |||
| client_id: this.opts.client_id, | |||
| scope: scopes.join(this.opts.scope_separator), | |||
| redirect_uri: redirect_uri(this.app.req), | |||
| state: generate_state() | |||
| }, this.opts.params, params); | |||
| this.app.setSession(`${this.name}_state`, params.state); | |||
| this.app.res.redirect(build_url(this.opts.auth_endpoint, params)); | |||
| } | |||
| async refreshToken(refresh_token) { | |||
| return this.grant('refresh_token', { refresh_token }); | |||
| } | |||
| async grant(type, params) { | |||
| const endpoint = new URL(this.opts.token_endpoint); | |||
| return new Promise((resolve, reject) => { | |||
| const req = (endpoint.protocol == 'https:' ? https : http).request(endpoint, { | |||
| method: 'POST' | |||
| }, res => { | |||
| let body = ''; | |||
| res.setEncoding('utf8'); | |||
| res.on('data', chunk => body += chunk); | |||
| res.on('end', () => { | |||
| if (res.statusCode >= 400) { | |||
| return reject(new Error(`Http status code ${res.statusCode}. ${body}`)); | |||
| } | |||
| try { | |||
| body = JSON.parse(body); | |||
| } catch (e) { } | |||
| if (!body.access_token) { | |||
| return reject(new Error(`Http response has no access_token. ${JSON.stringify(body)}`)) | |||
| } | |||
| this.app.setSession(`${this.name}_access_token`, body.access_token); | |||
| this.access_token = body.access_token; | |||
| if (body.expires_in) { | |||
| this.app.setSession(`${this.name}_expires`, expires_in(body.expires_in)); | |||
| } | |||
| if (body.refresh_token) { | |||
| this.app.setSession(`${this.name}_refresh_token`, body.refresh_token); | |||
| this.refresh_token = body.refresh_token; | |||
| } | |||
| resolve(body); | |||
| }); | |||
| }); | |||
| const body = querystring.stringify(Object.assign({ | |||
| grant_type: type, | |||
| client_id: this.opts.client_id, | |||
| client_secret: this.opts.client_secret | |||
| }, params)); | |||
| req.on('error', reject); | |||
| req.setHeader('Content-Type', 'application/x-www-form-urlencoded'); | |||
| req.setHeader('Content-Length', body.length); // required by azure endpoint | |||
| req.write(body); | |||
| req.end(); | |||
| }); | |||
| } | |||
| } | |||
| function build_url(url, params) { | |||
| return url + (url.indexOf('?') != -1 ? '&' : '?') + querystring.stringify(params); | |||
| } | |||
| function redirect_uri(req) { | |||
| let hasProxy = !!req.get('x-forwarded-host'); | |||
| return `${req.protocol}://${hasProxy ? req.hostname : req.get('host')}${req.path}`; | |||
| } | |||
| function expires_in(s) { | |||
| return ~~(Date.now() / 1000) + s; | |||
| } | |||
| function generate_state() { | |||
| const { v4: uuidv4 } = require('uuid'); | |||
| return uuidv4(); | |||
| } | |||
| module.exports = OAuth2; | |||
| @@ -0,0 +1,98 @@ | |||
| module.exports = { | |||
| 'google': { | |||
| auth_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth', | |||
| token_endpoint: 'https://www.googleapis.com/oauth2/v4/token', | |||
| params: { access_type: 'offline' } | |||
| }, | |||
| 'facebook': { | |||
| auth_endpoint: 'https://www.facebook.com/v3.2/dialog/oauth', | |||
| token_endpoint: 'https://graph.facebook.com/v3.2/oauth/access_token' | |||
| }, | |||
| 'linkedin': { | |||
| auth_endpoint: 'https://www.linkedin.com/oauth/v2/authorization', | |||
| token_endpoint: 'https://www.linkedin.com/oauth/v2/accessToken' | |||
| }, | |||
| 'github': { | |||
| auth_endpoint: 'https://github.com/login/oauth/authorize', | |||
| token_endpoint: 'https://github.com/login/oauth/access_token' | |||
| }, | |||
| 'instagram': { | |||
| auth_endpoint: 'https://api.instagram.com/oauth/authorize/', | |||
| token_endpoint: 'https://api.instagram.com/oauth/access_token' | |||
| }, | |||
| 'amazon': { | |||
| auth_endpoint: 'https://www.amazon.com/ap/oa', | |||
| token_endpoint: 'https://api.amazon.com/auth/o2/token' | |||
| }, | |||
| 'dropbox': { | |||
| auth_endpoint: 'https://www.dropbox.com/oauth2/authorize', | |||
| token_endpoint: 'https://api.dropbox.com/oauth2/token', | |||
| scope_separator: ',' | |||
| }, | |||
| 'foursquare': { | |||
| auth_endpoint: 'https://foursquare.com/oauth2/authenticate', | |||
| token_endpoint: 'https://foursquare.com/oauth2/access_token' | |||
| }, | |||
| 'imgur': { | |||
| auth_endpoint: 'https://api.imgur.com/oauth2/authorize', | |||
| token_endpoint: 'https://api.imgur.com/oauth2/token' | |||
| }, | |||
| 'wordpress': { | |||
| auth_endpoint: 'https://public-api.wordpress.com/oauth2/authorize', | |||
| token_endpoint: 'https://public-api.wordpress.com/oauth2/token' | |||
| }, | |||
| 'spotify': { | |||
| auth_endpoint: 'https://accounts.spotify.com/authorize', | |||
| token_endpoint: 'https://accounts.spotify.com/api/token' | |||
| }, | |||
| 'slack': { | |||
| auth_endpoint: 'https://slack.com/oauth/authorize', | |||
| token_endpoint: 'https://slack.com/api/oauth.access' | |||
| }, | |||
| 'reddit': { | |||
| auth_endpoint: 'https://ssl.reddit.com/api/v1/authorize', | |||
| token_endpoint: 'https://ssl.reddit.com/api/v1/access_token', | |||
| scope_separator: ',' | |||
| }, | |||
| 'twitch': { | |||
| auth_endpoint: 'https://api.twitch.tv/kraken/oauth2/authorize', | |||
| token_endpoint: 'https://api.twitch.tv/kraken/oauth2/token' | |||
| }, | |||
| 'paypal': { | |||
| auth_endpoint: 'https://identity.x.com/xidentity/resources/authorize', | |||
| token_endpoint: 'https://identity.x.com/xidentity/oauthtokenservice' | |||
| }, | |||
| 'pinterest': { | |||
| auth_endpoint: 'https://api.pinterest.com/oauth/', | |||
| token_endpoint: 'https://api.pinterest.com/v1/oauth/token', | |||
| scope_separator: ',' | |||
| }, | |||
| 'stripe': { | |||
| auth_endpoint: 'https://connect.stripe.com/oauth/authorize', | |||
| token_endpoint: 'https://connect.stripe.com/oauth/token', | |||
| scope_separator: ',' | |||
| }, | |||
| 'coinbase': { | |||
| auth_endpoint: 'https://www.coinbase.com/oauth/authorize', | |||
| token_endpoint: 'https://www.coinbase.com/oauth/token' | |||
| } | |||
| }; | |||
| @@ -0,0 +1,94 @@ | |||
| if (process.env.NODE_ENV !== 'production') { | |||
| require('dotenv').config(); | |||
| } | |||
| process.on('uncaughtException', (e) => { | |||
| // prevent errors from killing the server and just log them | |||
| console.error(e); | |||
| }); | |||
| const config = require('./setup/config'); | |||
| const debug = require('debug')('server-connect:server'); | |||
| const routes = require('./setup/routes'); | |||
| const sockets = require('./setup/sockets'); | |||
| const upload = require('./setup/upload'); | |||
| const cron = require('./setup/cron'); | |||
| const http = require('http'); | |||
| const express = require('express'); | |||
| const endmw = require('express-end'); | |||
| const cookieParser = require('cookie-parser'); | |||
| const session = require('./setup/session'); //require('express-session')(Object.assign({ secret: config.secret }, config.session)); | |||
| const cors = require('cors'); | |||
| const app = express(); | |||
| app.set('trust proxy', true); | |||
| app.set('view engine', 'ejs'); | |||
| app.set('view options', { root: 'views', async: true }); | |||
| app.disable('x-powered-by') | |||
| if (config.compression) { | |||
| const compression = require('compression'); | |||
| app.use(compression()); | |||
| } | |||
| if (config.abortOnDisconnect) { | |||
| app.use((req, res, next) => { | |||
| req.isDisconnected = false; | |||
| req.on('close', () => { | |||
| req.isDisconnected = true; | |||
| }); | |||
| next(); | |||
| }); | |||
| } | |||
| app.use(cors(config.cors)); | |||
| app.use(express.static('public', config.static)); | |||
| app.use(express.urlencoded({ extended: true })); | |||
| app.use(express.json({ | |||
| verify: (req, res, buf) => { | |||
| req.rawBody = buf.toString() | |||
| } | |||
| })); | |||
| app.use(cookieParser(config.secret)); | |||
| app.use(session); | |||
| app.use(endmw); | |||
| upload(app); | |||
| routes(app); | |||
| const server = http.createServer(app); | |||
| const io = sockets(server, session); | |||
| // Make sockets global available | |||
| global.io = io; | |||
| module.exports = { | |||
| server, app, io, | |||
| start: function(port) { | |||
| // We add the 404 and 500 routes as last | |||
| app.use((req, res) => { | |||
| res.status(404).json({ | |||
| status: '404', | |||
| message: `${req.url} not found.` | |||
| }); | |||
| }); | |||
| app.use((err, req, res, next) => { | |||
| debug(`Got error? %O`, err); | |||
| res.status(500).json({ | |||
| status: '500', | |||
| code: config.debug ? err.code : undefined, | |||
| message: config.debug ? err.message || err : 'A server error occured, to see the error enable the DEBUG flag.', | |||
| stack: config.debug ? err.stack : undefined, | |||
| }); | |||
| }); | |||
| cron.start(); | |||
| server.listen(port || config.port, () => { | |||
| console.log(`App listening at http://localhost:${config.port}`); | |||
| }); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,107 @@ | |||
| const package = require('../../package.json'); | |||
| const fs = require('fs-extra'); | |||
| const debug = require('debug')('server-connect:setup:config'); | |||
| const { toSystemPath } = require('../core/path'); | |||
| const { mergeDeep } = require('../core/util'); | |||
| const Parser = require('../core/parser'); | |||
| const Scope = require('../core/scope'); | |||
| const config = { | |||
| port: process.env.PORT || 3000, | |||
| debug: false, | |||
| secret: 'Need to be set', | |||
| tmpFolder: '/tmp', | |||
| abortOnDisconnect: false, | |||
| createApiRoutes: true, | |||
| compression: true, | |||
| redis: false, | |||
| cron: true, | |||
| static: { | |||
| index: false | |||
| }, | |||
| session: { | |||
| name: package.name + '.sid', | |||
| resave: false, | |||
| saveUninitialized: false, | |||
| store: { $type: 'memory', ttl: 86400000 } | |||
| }, | |||
| cors: { // see https://github.com/expressjs/cors | |||
| origin: false, | |||
| methods: 'GET,POST', | |||
| allowedHeaders: '*', | |||
| credentials: true | |||
| }, | |||
| globals: {}, | |||
| mail: {}, | |||
| auth: {}, | |||
| oauth: {}, | |||
| db: {}, | |||
| s3: {}, | |||
| jwt: {}, | |||
| stripe: {}, | |||
| env: {} | |||
| }; | |||
| if (fs.existsSync('app/config/config.json')) { | |||
| mergeDeep(config, fs.readJSONSync('app/config/config.json')) | |||
| } | |||
| if (fs.existsSync('app/config/user_config.json')) { | |||
| mergeDeep(config, fs.readJSONSync('app/config/user_config.json')); | |||
| } | |||
| // folders are site relative | |||
| config.tmpFolder = toSystemPath(config.tmpFolder); | |||
| if (config.env) { | |||
| for (let key in config.env) { | |||
| if (!Object.prototype.hasOwnProperty.call(process.env, key)) { | |||
| process.env[key] = config.env[key]; | |||
| } else if (config.debug) { | |||
| debug(`"${key}" is already defined in \`process.env\` and will not be overwritten`); | |||
| } | |||
| } | |||
| } | |||
| Parser.parseValue(config, new Scope({ | |||
| $_ENV: process.env | |||
| })); | |||
| // we change the cors config a bit, * will become true | |||
| // and we split string on comma for multiple origins | |||
| if (typeof config.cors?.origin == 'string') { | |||
| if (config.cors.origin === '*') { | |||
| config.cors.origin = true; | |||
| } else if (config.cors.origin.includes(',')) { | |||
| config.cors.origin = config.cors.origin.split(/\s*,\s*/); | |||
| } | |||
| } | |||
| if (config.debug) { | |||
| require('debug').enable(typeof config.debug == 'string' ? config.debug : 'server-connect:*'); | |||
| } | |||
| if (config.redis) { | |||
| const { createClient } = require('redis'); | |||
| const client = createClient(typeof config.redis == 'object' ? config.redis : { | |||
| url: config.redis === true ? 'redis://redis' : config.redis, | |||
| socket: { connectTimeout: 60000 }, | |||
| pingInterval: 10000, | |||
| }); | |||
| client.on('error', err => console.error(err)); | |||
| client.on('connect', () => debug('Redis Connected')); | |||
| client.on('reconnecting', () => debug('Redis Reconnecting')); | |||
| client.on('ready', () => debug('Redis Ready')); | |||
| client.connect().catch(err => { | |||
| // we don't want to crash the server if redis is not available | |||
| }); | |||
| global.redisClient = client; | |||
| } | |||
| debug(config); | |||
| module.exports = config; | |||
| @@ -0,0 +1,46 @@ | |||
| const fs = require('fs-extra'); | |||
| const { isEmpty } = require('./util'); | |||
| const config = require('./config'); | |||
| const debug = require('debug')('server-connect:cron'); | |||
| exports.start = () => { | |||
| if (!config.cron || isEmpty('app/schedule')) return; | |||
| debug('Start schedule'); | |||
| processEntries('app/schedule'); | |||
| }; | |||
| function processEntries(path) { | |||
| const schedule = require('node-schedule'); | |||
| const entries = fs.readdirSync(path, { withFileTypes: true }); | |||
| for (let entry of entries) { | |||
| if (entry.isFile() && entry.name.endsWith('.json')) { | |||
| try { | |||
| const job = fs.readJSONSync(`${path}/${entry.name}`); | |||
| const rule = job.settings.options.rule; | |||
| debug(`Adding schedule ${entry.name}`); | |||
| if (rule == '@reboot') { | |||
| setImmediate(exec(job.exec)); | |||
| } else { | |||
| schedule.scheduleJob(rule, exec(job.exec)) | |||
| } | |||
| } catch (e) { | |||
| console.error(e); | |||
| } | |||
| } else if (entry.isDirectory()) { | |||
| processEntries(`${path}/${entry.name}`); | |||
| } | |||
| } | |||
| } | |||
| function exec(action) { | |||
| return async () => { | |||
| const App = require('../core/app'); | |||
| const app = new App({ params: {}, session: {}, cookies: {}, signedCookies: {}, query: {}, headers: {} }); | |||
| return app.define(action, true); | |||
| } | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| // allow Wappler to run queries on the server | |||
| const crypto = require('crypto'); | |||
| const App = require("../core/app"); | |||
| const config = require('./config'); | |||
| module.exports = function (app) { | |||
| app.post('/_db_', async (req, res) => { | |||
| const time = req.get('auth-time'); | |||
| const hash = req.get('auth-hash'); | |||
| if (!time || !hash || isNaN(time)) return res.status(400).json({ error: 'Auth headers missing' }); | |||
| const diff = Math.abs(Date.now() - time) / 1000; | |||
| if (diff > 60) return res.status(400).json({ error: 'Auth time diff to high' }); | |||
| if (hash !== crypto.createHmac('sha256', config.secret).update(req.rawBody).digest('hex')) return res.status(400).json({ error: 'Auth hash invalid' }); | |||
| const sc = new App(req, res); | |||
| const db = sc.getDbConnection(req.body.name); | |||
| let results = []; | |||
| try { | |||
| results = await db.raw(req.body.query); | |||
| } catch (error) { | |||
| return res.json({ error: error.sqlMessage }); | |||
| } | |||
| if (db.client.config.client == 'mysql' || db.client.config.client == 'mysql2') { | |||
| results = results[0]; | |||
| } else if (db.client.config.client == 'postgres' || db.client.config.client == 'redshift') { | |||
| results = results.rows; | |||
| } | |||
| res.json({ results }); | |||
| }); | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| const config = require('./config'); | |||
| if (config.redis) { | |||
| const redis = require('redis'); | |||
| global.redisClient = redis.createClient(config.redis === true ? 'redis://redis' : config.redis); | |||
| } | |||
| module.exports = global.redisClient; | |||
| @@ -0,0 +1,249 @@ | |||
| const fs = require('fs-extra'); | |||
| const debug = require('debug')('server-connect:setup:routes'); | |||
| const config = require('./config'); | |||
| const { map } = require('../core/async'); | |||
| const { posix, extname } = require('path'); | |||
| const { cache, serverConnect, templateView } = require('../core/middleware'); | |||
| const database = require('./database'); | |||
| const webhooks = require('./webhooks'); | |||
| module.exports = async function (app) { | |||
| app.use((req, res, next) => { | |||
| req.fragment = (req.headers['accept'] || '*/*').includes('fragment'); | |||
| next(); | |||
| }); | |||
| if (fs.existsSync('extensions/server_connect/routes')) { | |||
| const entries = fs.readdirSync('extensions/server_connect/routes', { withFileTypes: true }); | |||
| for (let entry of entries) { | |||
| if (entry.isFile() && extname(entry.name) == '.js') { | |||
| let hook = require(`../../extensions/server_connect/routes/${entry.name}`); | |||
| if (hook.before) hook.before(app); | |||
| if (hook.handler) hook.handler(app); | |||
| debug(`Custom router ${entry.name} loaded`); | |||
| } | |||
| } | |||
| } | |||
| if (config.createApiRoutes) { | |||
| fs.ensureDirSync('app/api'); | |||
| createApiRoutes('app/api'); | |||
| } | |||
| if (fs.existsSync('app/config/routes.json')) { | |||
| const { routes, layouts } = fs.readJSONSync('app/config/routes.json'); | |||
| parseRoutes(routes, null); | |||
| function parseRoutes(routes, parent) { | |||
| for (let route of routes) { | |||
| if (!route.path) continue; | |||
| createRoute(route, parent); | |||
| if (Array.isArray(route.routes)) { | |||
| parseRoutes(route.routes, route); | |||
| } | |||
| } | |||
| } | |||
| function createRoute({ auth, path, method, redirect, url, page, layout, exec, data, ttl, status, proxy }, parent) { | |||
| method = method || 'all'; | |||
| data = data || {}; | |||
| if (page) page = page.replace(/^\//, ''); | |||
| if (layout) layout = layout.replace(/^\//, ''); | |||
| if (parent && parent.path) path = parent.path + path; | |||
| if (auth) { | |||
| app.use(path, (req, res, next) => { | |||
| if (typeof auth == 'string' && req.session && req.session[auth + 'Id']) { | |||
| next(); | |||
| } else if (typeof auth == 'object' && auth.user) { | |||
| const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; | |||
| const [user, password] = Buffer.from(b64auth, 'base64').toString().split(':'); | |||
| if (user && password && user === auth.user && password === auth.password) { | |||
| next(); | |||
| } else { | |||
| res.set('WWW-Authenticate', 'Basic realm="401"'); | |||
| res.status(401).json({ error: 'Unauthorized' }); | |||
| } | |||
| } else { | |||
| res.status(401).json({ error: 'Unauthorized' }); | |||
| } | |||
| }); | |||
| } | |||
| if (proxy) { | |||
| const httpProxy = require('http-proxy'); | |||
| const proxyServer = httpProxy.createProxyServer(proxy); | |||
| app.use(path, (req, res) => { | |||
| proxyServer.web(req, res); | |||
| }); | |||
| } else if (redirect) { | |||
| app.get(path, (req, res) => res.redirect(status == 302 ? 302 : 301, redirect)); | |||
| } else if (url) { | |||
| app[method](path, (req, res, next) => { | |||
| next(parent && !req.fragment ? 'route' : null); | |||
| }, (req, res) => { | |||
| res.sendFile(url, { root: 'public' }) | |||
| }); | |||
| if (parent) { | |||
| createRoute({ | |||
| path, | |||
| method: parent.method, | |||
| redirect: parent.redirect, | |||
| url: parent.url, | |||
| page: parent.page, | |||
| layout: parent.layout, | |||
| exec: parent.exec, | |||
| data: parent.data | |||
| }); | |||
| } | |||
| } else if (page) { | |||
| if (exec) { | |||
| if (fs.existsSync(`app/${exec}.json`)) { | |||
| let json = fs.readJSONSync(`app/${exec}.json`); | |||
| if (json.exec && json.exec.steps) { | |||
| json = json.exec.steps; | |||
| } else if (json.steps) { | |||
| json = json.steps; | |||
| } | |||
| if (!Array.isArray(json)) { | |||
| json = [json]; | |||
| } | |||
| if (layout && layouts && layouts[layout]) { | |||
| if (layouts[layout].data) { | |||
| data = Object.assign({}, layouts[layout].data, data); | |||
| } | |||
| if (layouts[layout].exec) { | |||
| if (fs.existsSync(`app/${layouts[layout].exec}.json`)) { | |||
| let _json = fs.readJSONSync(`app/${layouts[layout].exec}.json`); | |||
| if (_json.exec && _json.exec.steps) { | |||
| _json = _json.exec.steps; | |||
| } else if (_json.steps) { | |||
| _json = _json.steps; | |||
| } | |||
| if (!Array.isArray(_json)) { | |||
| _json = [_json]; | |||
| } | |||
| json = _json.concat(json); | |||
| } else { | |||
| debug(`Route ${path} skipped, "app/${exec}.json" not found`); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| app[method](path, (req, res, next) => { | |||
| next(parent && !req.fragment ? 'route' : null); | |||
| }, cache({ttl}), templateView(layout, page, data, json)); | |||
| } else { | |||
| debug(`Route ${path} skipped, "app/${exec}.json" not found`); | |||
| return; | |||
| } | |||
| } else { | |||
| let json = []; | |||
| if (layout && layouts && layouts[layout]) { | |||
| if (layouts[layout].data) { | |||
| data = Object.assign({}, layouts[layout].data, data); | |||
| } | |||
| if (layouts[layout].exec) { | |||
| if (fs.existsSync(`app/${layouts[layout].exec}.json`)) { | |||
| let _json = fs.readJSONSync(`app/${layouts[layout].exec}.json`); | |||
| if (_json.exec && _json.exec.steps) { | |||
| _json = _json.exec.steps; | |||
| } else if (_json.steps) { | |||
| _json = _json.steps; | |||
| } | |||
| if (!Array.isArray(_json)) { | |||
| _json = [_json]; | |||
| } | |||
| json = _json.concat(json); | |||
| } else { | |||
| debug(`Route ${path} skipped, "app/${exec}.json" not found`); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| app[method](path, (req, res, next) => { | |||
| next(parent && !req.fragment ? 'route' : null); | |||
| }, cache({ttl}), templateView(layout, page, data, json)); | |||
| } | |||
| if (parent) { | |||
| createRoute({ | |||
| path, | |||
| method: parent.method, | |||
| redirect: parent.redirect, | |||
| url: parent.url, | |||
| page: parent.page, | |||
| layout: parent.layout, | |||
| exec: parent.exec, | |||
| data: parent.data | |||
| }); | |||
| } | |||
| } else if (exec) { | |||
| if (fs.existsSync(`app/${exec}.json`)) { | |||
| let json = fs.readJSONSync(`app/${exec}.json`); | |||
| app[method](path, cache({ttl}), serverConnect(json)); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| database(app); | |||
| webhooks(app); | |||
| if (fs.existsSync('extensions/server_connect/routes')) { | |||
| const entries = fs.readdirSync('extensions/server_connect/routes', { withFileTypes: true }); | |||
| for (let entry of entries) { | |||
| if (entry.isFile() && extname(entry.name) == '.js') { | |||
| let hook = require(`../../extensions/server_connect/routes/${entry.name}`); | |||
| if (hook.after) hook.after(app); | |||
| debug(`Custom router ${entry.name} loaded`); | |||
| } | |||
| } | |||
| } | |||
| function createApiRoutes(dir) { | |||
| const entries = fs.readdirSync(dir, { withFileTypes: true }); | |||
| return map(entries, async (entry) => { | |||
| let path = posix.join(dir, entry.name); | |||
| if (entry.isFile() && extname(path) == '.json') { | |||
| let json = fs.readJSONSync(path); | |||
| let routePath = path.replace(/^app/i, '').replace(/.json$/, '(.json)?'); | |||
| let ttl = (json.settings && json.settings.options && json.settings.options.ttl) ? json.settings.options.ttl : 0; | |||
| app.all(routePath, cache({ttl}), serverConnect(json)); | |||
| debug(`Api route ${routePath} created`); | |||
| } | |||
| if (entry.isDirectory()) { | |||
| return createApiRoutes(path); | |||
| } | |||
| }); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,87 @@ | |||
| const session = require('express-session'); //(Object.assign({ secret: config.secret }, config.session)); | |||
| const debug = require('debug')('server-connect:setup:session'); | |||
| const Parser = require('../core/parser'); | |||
| const Scope = require('../core/scope'); | |||
| const db = require('../core/db'); | |||
| const { toSystemPath } = require('../core/path'); | |||
| const config = require('./config'); | |||
| const options = config.session; | |||
| if (!options.secret) { | |||
| options.secret = config.secret; | |||
| } | |||
| debug('init session store %o', options.store); | |||
| if (options.store.$type == 'redis') { // https://www.npmjs.com/package/connect-redis | |||
| const RedisStore = require('connect-redis').default; | |||
| options.store = new RedisStore(Object.assign({ | |||
| client: global.redisClient | |||
| }, options.store)); | |||
| } else if (options.store.$type == 'file') { // https://www.npmjs.com/package/session-file-store | |||
| const FileStore = require('session-file-store')(session); | |||
| options.store = new FileStore(options.store); | |||
| } else if (options.store.$type == 'database') { // https://www.npmjs.com/package/connect-session-knex | |||
| const KnexStore = require('connect-session-knex')(session); | |||
| if (typeof options.store.knex == 'string') { | |||
| if (!global.db) global.db = {}; | |||
| if (!global.db[options.store.knex]) { | |||
| const fs = require('fs-extra'); | |||
| const action = fs.readJSONSync(`app/modules/connections/${options.store.knex}.json`); | |||
| const knex_options = Parser.parseValue(action.options, new Scope({ $_ENV: process.env })); | |||
| if (knex_options.connection && knex_options.connection.filename) { | |||
| knex_options.connection.filename = toSystemPath(knex_options.connection.filename); | |||
| } | |||
| if (knex_options.connection && knex_options.connection.ssl) { | |||
| if (knex_options.connection.ssl.key) { | |||
| knex_options.connection.ssl.key = fs.readFileSync(toSystemPath(knex_options.connection.ssl.key)); | |||
| } | |||
| if (knex_options.connection.ssl.ca) { | |||
| knex_options.connection.ssl.ca = fs.readFileSync(toSystemPath(knex_options.connection.ssl.ca)); | |||
| } | |||
| if (knex_options.connection.ssl.cert) { | |||
| knex_options.connection.ssl.cert = fs.readFileSync(toSystemPath(knex_options.connection.ssl.cert)); | |||
| } | |||
| } | |||
| knex_options.useNullAsDefault = true; | |||
| knex_options.postProcessResponse = function(result) { | |||
| if (Array.isArray(result)) { | |||
| return result.map(row => { | |||
| for (column in row) { | |||
| if (row[column] && row[column].toJSON) { | |||
| row[column] = row[column].toJSON(); | |||
| } | |||
| } | |||
| return row; | |||
| }); | |||
| } else { | |||
| for (column in result) { | |||
| if (result[column] && result[column].toJSON) { | |||
| result[column] = result[column].toJSON(); | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| }; | |||
| global.db[options.store.knex] = db(knex_options); | |||
| } | |||
| options.store.knex = global.db[options.store.knex]; | |||
| } | |||
| options.store = new KnexStore(options.store); | |||
| } else { | |||
| const MemoryStore = require('../core/memoryStore')(session); | |||
| options.store = new MemoryStore(options.store); | |||
| } | |||
| module.exports = session(options); | |||
| @@ -0,0 +1,198 @@ | |||
| const fs = require('fs-extra'); | |||
| const { basename, extname } = require('path'); | |||
| const debug = require('debug')('server-connect:sockets'); | |||
| const { isEmpty } = require('./util'); | |||
| const config = require('./config'); | |||
| const cookieParser = require('cookie-parser'); | |||
| const { promisify } = require('util'); | |||
| module.exports = function (server, appSession) { | |||
| //if (isEmpty('app/sockets')) return null; | |||
| const io = require('socket.io')(); | |||
| if (global.redisClient) { | |||
| const { createAdapter } = require('@socket.io/redis-adapter'); | |||
| const pubClient = global.redisClient.duplicate(); | |||
| const subClient = global.redisClient.duplicate(); | |||
| Promise.all([pubClient.connect(), subClient.connect()]).then(() => { | |||
| io.adapter(createAdapter(pubClient, subClient)); | |||
| }); | |||
| } | |||
| // user hooks | |||
| if (fs.existsSync('extensions/server_connect/sockets')) { | |||
| const entries = fs.readdirSync('extensions/server_connect/sockets', { withFileTypes: true }); | |||
| for (let entry of entries) { | |||
| if (entry.isFile() && extname(entry.name) == '.js') { | |||
| const hook = require(`../../extensions/server_connect/sockets/${entry.name}`); | |||
| if (hook.handler) hook.handler(io); | |||
| debug(`Custom sockets hook ${entry.name} loaded`); | |||
| } | |||
| } | |||
| } | |||
| // create socket connections for api endpoints | |||
| if (fs.existsSync('app/api')) { | |||
| io.of('/api').on('connection', async (socket) => { | |||
| socket.onAny(async (event, params, cb) => { | |||
| try { | |||
| if (typeof cb == 'function' && global.redisClient && global.redisClient.isReady) { | |||
| const cached = await global.redisClient.get('ws:' + event + ':' + JSON.stringify(params)); | |||
| if (cached) return cb(JSON.parse(cached)); | |||
| } | |||
| const req = Object.assign({}, socket.handshake); | |||
| const res = { | |||
| statusCode: 200, | |||
| getHeader: () => { }, | |||
| setHeader: () => { }, | |||
| sendStatus: (statusCode) => { res.statusCode = statusCode; }, | |||
| write: () => { }, | |||
| end: () => { } | |||
| }; | |||
| cookieParser(config.secret)(req, res, () => { | |||
| appSession(req, res, async () => { | |||
| const App = require('../core/app'); | |||
| const app = new App(req, res); | |||
| const action = await fs.readJSON(`app/api/${event}.json`); | |||
| app.set('$_PARAM', params); | |||
| app.set('$_GET', params); // fake query params | |||
| app.socket = socket; | |||
| await app.define(action, true); | |||
| if (typeof cb == 'function') { | |||
| cb({ | |||
| status: res.statusCode, | |||
| data: res.statusCode == 200 ? app.data : null | |||
| }); | |||
| if (global.redisClient && global.redisClient.isReady) { | |||
| let ttl = (action.settings && action.settings.options && action.settings.options.ttl) ? action.settings.options.ttl : null; | |||
| if (ttl && res.statusCode < 400) { // only cache valid response, not error response | |||
| global.redisClient.setEx('ws:' + event + ':' + JSON.stringify(params), ttl, JSON.stringify({ | |||
| status: res.statusCode, | |||
| data: res.statusCode == 200 ? app.data : null | |||
| })); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| }); | |||
| } catch (e) { | |||
| debug(`ERROR: ${e.message}`); | |||
| console.error(e); | |||
| } | |||
| }); | |||
| }); | |||
| } | |||
| if (fs.existsSync('app/sockets')) { | |||
| parseSockets(); | |||
| } | |||
| function parseSockets(namespace = '') { | |||
| const entries = fs.readdirSync('app/sockets' + namespace, { withFileTypes: true }); | |||
| io.of(namespace || '/').on('connection', async (socket) => { | |||
| if (fs.existsSync(`app/sockets${namespace}/connect.json`)) { | |||
| try { | |||
| const req = Object.assign({}, socket.handshake); | |||
| const res = { | |||
| statusCode: 200, | |||
| getHeader: () => { }, | |||
| setHeader: () => { }, | |||
| sendStatus: (statusCode) => { res.statusCode = statusCode; }, | |||
| write: () => { }, | |||
| end: () => { } | |||
| }; | |||
| cookieParser(config.secret)(req, res, () => { | |||
| appSession(req, res, async () => { | |||
| const App = require('../core/app'); | |||
| const app = new App(req, res); | |||
| const action = await fs.readJSON(`app/sockets${namespace}/connect.json`); | |||
| app.socket = socket; | |||
| await app.define(action, true); | |||
| }); | |||
| }); | |||
| } catch (e) { | |||
| debug(`ERROR: ${e.message}`); | |||
| console.error(e); | |||
| } | |||
| } | |||
| if (fs.existsSync(`app/sockets${namespace}/disconnect.json`)) { | |||
| socket.on('disconnect', async (event) => { | |||
| try { | |||
| const req = Object.assign({}, socket.handshake); | |||
| const res = { | |||
| statusCode: 200, | |||
| getHeader: () => { }, | |||
| setHeader: () => { }, | |||
| sendStatus: (statusCode) => { res.statusCode = statusCode; }, | |||
| write: () => { }, | |||
| end: () => { } | |||
| }; | |||
| cookieParser(config.secret)(req, res, () => { | |||
| appSession(req, res, async () => { | |||
| const App = require('../core/app'); | |||
| const app = new App(req, res); | |||
| const action = await fs.readJSON(`app/sockets${namespace}/disconnect.json`); | |||
| app.socket = socket; | |||
| await app.define(action, true); | |||
| }); | |||
| }); | |||
| } catch (e) { | |||
| debug(`ERROR: ${e.message}`); | |||
| console.error(e); | |||
| } | |||
| }); | |||
| } | |||
| socket.onAny(async (event, params, cb) => { | |||
| try { | |||
| const req = Object.assign({}, socket.handshake); | |||
| const res = { | |||
| statusCode: 200, | |||
| getHeader: () => { }, | |||
| setHeader: () => { }, | |||
| sendStatus: (statusCode) => { res.statusCode = statusCode; }, | |||
| write: () => { }, | |||
| end: () => { } | |||
| }; | |||
| cookieParser(config.secret)(req, res, () => { | |||
| appSession(req, res, async () => { | |||
| const App = require('../core/app'); | |||
| const app = new App(req, res); | |||
| const action = await fs.readJSON(`app/sockets${namespace}/${event}.json`); | |||
| app.set('$_PARAM', params); | |||
| app.socket = socket; | |||
| await app.define(action, true); | |||
| if (typeof cb == 'function') cb(app.data); | |||
| }); | |||
| }); | |||
| } catch (e) { | |||
| debug(`ERROR: ${e.message}`); | |||
| console.error(e); | |||
| } | |||
| }); | |||
| }); | |||
| for (let entry of entries) { | |||
| if (entry.isDirectory()) { | |||
| parseSockets(namespace + '/' + entry.name); | |||
| } | |||
| } | |||
| } | |||
| io.attach(server); | |||
| return io; | |||
| }; | |||
| @@ -0,0 +1,61 @@ | |||
| const config = require('./config'); | |||
| const debug = require('debug')('server-connect:setup:upload'); | |||
| const fs = require('fs-extra'); | |||
| const qs = require('qs'); | |||
| module.exports = function(app) { | |||
| const fileupload = require('express-fileupload'); | |||
| const isEligibleRequest = require('express-fileupload/lib/isEligibleRequest'); | |||
| // Make sure tmp folder exists and make it empty | |||
| fs.ensureDirSync(config.tmpFolder); | |||
| fs.emptyDirSync(config.tmpFolder); | |||
| // Always use tmp folder | |||
| app.use(fileupload(Object.assign({}, config.fileupload, { | |||
| useTempFiles: true, | |||
| tempFileDir: config.tmpFolder | |||
| }))); | |||
| app.use((req, res, next) => { | |||
| if (!isEligibleRequest(req)) { | |||
| return next(); | |||
| } | |||
| let encoded = qs.stringify(req.body); | |||
| if (req.files) { | |||
| for (let field in req.files) { | |||
| encoded += '&' + field + '=' + field; | |||
| } | |||
| } | |||
| req.body = qs.parse(encoded, { | |||
| arrayLimit: 1000 | |||
| }); | |||
| // Cleanup | |||
| res.once('close', () => { | |||
| if (req.files) { | |||
| for (let field in req.files) { | |||
| let file = req.files[field]; | |||
| if (Array.isArray(file)) { | |||
| file.forEach((file) => { | |||
| if (file.tempFilePath && fs.existsSync(file.tempFilePath)) { | |||
| debug('delete %s', file.tempFilePath); | |||
| fs.unlink(file.tempFilePath); | |||
| } | |||
| }); | |||
| } else if (file.tempFilePath && fs.existsSync(file.tempFilePath)) { | |||
| debug('delete %s', file.tempFilePath); | |||
| fs.unlink(file.tempFilePath); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| next(); | |||
| }); | |||
| debug('Upload middleware configured.'); | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| const fs = require('fs-extra'); | |||
| exports.isEmpty = (path) => { | |||
| try { | |||
| let stat = fs.statSync(path); | |||
| if (!stat.isDirectory()) return true; | |||
| let items = fs.readdirSync(path); | |||
| return !items || !items.length; | |||
| } catch (e) { | |||
| return true; | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| const fs = require('fs-extra'); | |||
| module.exports = function(app) { | |||
| app.all('/webhooks/:name', (req, res, next) => { | |||
| const name = req.params.name; | |||
| if (!/^[a-zA-Z0-9-_]+$/.test(name)) { | |||
| res.status(400).json({error: `Invalid webhook name.`}); | |||
| } else if (fs.existsSync(`lib/webhooks/${name}.js`)) { | |||
| const webhook = require(`../webhooks/${name}`); | |||
| if (webhook.handler) { | |||
| webhook.handler(req, res, next); | |||
| } else { | |||
| res.status(400).json({error: `Webhook ${name} has no handler.`}); | |||
| } | |||
| } else { | |||
| const webhook = require('../core/webhook'); | |||
| const handler = webhook.createHandler(name); | |||
| handler(req, res, next); | |||
| } | |||
| }); | |||
| }; | |||
| @@ -0,0 +1,243 @@ | |||
| const isValidString = (s) => typeof s == 'string' && s.length > 0; | |||
| const getLength = (s) => s && s.length || 0;; | |||
| const testRegexp = (re) => (v) => !isValidString(v) || re.test(v); | |||
| const reEmail = /^(?!\.)((?!.*\.{2})[a-zA-Z0-9\u0080-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u137F\u1380-\u139F\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1B00-\u1B7F\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u1E00-\u1EFF\u1F00-\u1FFFu20D0-\u20FF\u2100-\u214F\u2C00-\u2C5F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2F00-\u2FDF\u2FF0-\u2FFF\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA700-\uA71F\uA800-\uA82F\uA840-\uA87F\uAC00-\uD7AF\uF900-\uFAFF\.!#$%&'*+-/=?^_`{|}~\-\d]+)@(?!\.)([a-zA-Z0-9\u0080-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u137F\u1380-\u139F\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1B00-\u1B7F\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u1E00-\u1EFF\u1F00-\u1FFF\u20D0-\u20FF\u2100-\u214F\u2C00-\u2C5F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2F00-\u2FDF\u2FF0-\u2FFF\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA700-\uA71F\uA800-\uA82F\uA840-\uA87F\uAC00-\uD7AF\uF900-\uFAFF\-\.\d]+)((\.([a-zA-Z\u0080-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u0300-\u036F\u0370-\u03FF\u0400-\u04FF\u0500-\u052F\u0530-\u058F\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F\u0D80-\u0DFF\u0E00-\u0E7F\u0E80-\u0EFF\u0F00-\u0FFF\u1000-\u109F\u10A0-\u10FF\u1100-\u11FF\u1200-\u137F\u1380-\u139F\u13A0-\u13FF\u1400-\u167F\u1680-\u169F\u16A0-\u16FF\u1700-\u171F\u1720-\u173F\u1740-\u175F\u1760-\u177F\u1780-\u17FF\u1800-\u18AF\u1900-\u194F\u1950-\u197F\u1980-\u19DF\u19E0-\u19FF\u1A00-\u1A1F\u1B00-\u1B7F\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF\u1E00-\u1EFF\u1F00-\u1FFF\u20D0-\u20FF\u2100-\u214F\u2C00-\u2C5F\u2C60-\u2C7F\u2C80-\u2CFF\u2D00-\u2D2F\u2D30-\u2D7F\u2D80-\u2DDF\u2F00-\u2FDF\u2FF0-\u2FFF\u3040-\u309F\u30A0-\u30FF\u3100-\u312F\u3130-\u318F\u3190-\u319F\u31C0-\u31EF\u31F0-\u31FF\u3200-\u32FF\u3300-\u33FF\u3400-\u4DBF\u4DC0-\u4DFF\u4E00-\u9FFF\uA000-\uA48F\uA490-\uA4CF\uA700-\uA71F\uA800-\uA82F\uA840-\uA87F\uAC00-\uD7AF\uF900-\uFAFF]){2,63})+)$/i; | |||
| const reUrl = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i; | |||
| const reDateTime = /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])[T ]([01][0-9]|2[0-4]):[0-5][0-9](:([0-5][0-9]|60))?Z?$/; | |||
| const reDate = /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/; | |||
| const reTime = /^([01][0-9]|2[0-4]):[0-5][0-9](:([0-5][0-9]|60))?$/; | |||
| const reMonth = /^\d{4}-(0[1-9]|1[012])$/; | |||
| const reWeek = /^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/; | |||
| const reColor = /^#[a-fA-F0-9]{6}$/; | |||
| const reNumber = /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/; | |||
| const reDigits = /^\d+$/; | |||
| const reAlphanumeric = /^\w+$/; | |||
| const reBic = /^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/; | |||
| const reVat = /^((AT)?U[0-9]{8}|(BE)?0[0-9]{9}|(BG)?[0-9]{9,10}|(CY)?[0-9]{8}L|(CZ)?[0-9]{8,10}|(DE)?[0-9]{9}|(DK)?[0-9]{8}|(EE)?[0-9]{9}|(EL|GR)?[0-9]{9}|(ES)?[0-9A-Z][0-9]{7}[0-9A-Z]|(FI)?[0-9]{8}|(FR)?[0-9A-Z]{2}[0-9]{9}|(GB)?([0-9]{9}([0-9]{3})?|[A-Z]{2}[0-9]{3})|(HU)?[0-9]{8}|(IE)?[0-9]S[0-9]{5}L|(IT)?[0-9]{11}|(LT)?([0-9]{9}|[0-9]{12})|(LU)?[0-9]{8}|(LV)?[0-9]{11}|(MT)?[0-9]{8}|(NL)?[0-9]{9}B[0-9]{2}|(PL)?[0-9]{10}|(PT)?[0-9]{9}|(RO)?[0-9]{2,10}|(SE)?[0-9]{12}|(SI)?[0-9]{8}|(SK)?[0-9]{10})$/; | |||
| const reInteger = /^-?\d+$/; | |||
| const reIpv4 = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/; | |||
| const reIpv6 = /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/; | |||
| const reLettersonly = /^[a-z]+$/i; | |||
| const reUnicodelettersonly = /^[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]+$/; | |||
| const reLetterswithbasicpunc = /^[a-z\-.,()'"\s]+$/; | |||
| const reNowhitespace = /^\S+$/; | |||
| const ibanCountryPatterns = { | |||
| "AL": "\\d{8}[\\dA-Z]{16}", | |||
| "AD": "\\d{8}[\\dA-Z]{12}", | |||
| "AT": "\\d{16}", | |||
| "AZ": "[\\dA-Z]{4}\\d{20}", | |||
| "BE": "\\d{12}", | |||
| "BH": "[A-Z]{4}[\\dA-Z]{14}", | |||
| "BA": "\\d{16}", | |||
| "BR": "\\d{23}[A-Z][\\dA-Z]", | |||
| "BG": "[A-Z]{4}\\d{6}[\\dA-Z]{8}", | |||
| "CR": "\\d{17}", | |||
| "HR": "\\d{17}", | |||
| "CY": "\\d{8}[\\dA-Z]{16}", | |||
| "CZ": "\\d{20}", | |||
| "DK": "\\d{14}", | |||
| "DO": "[A-Z]{4}\\d{20}", | |||
| "EE": "\\d{16}", | |||
| "FO": "\\d{14}", | |||
| "FI": "\\d{14}", | |||
| "FR": "\\d{10}[\\dA-Z]{11}\\d{2}", | |||
| "GE": "[\\dA-Z]{2}\\d{16}", | |||
| "DE": "\\d{18}", | |||
| "GI": "[A-Z]{4}[\\dA-Z]{15}", | |||
| "GR": "\\d{7}[\\dA-Z]{16}", | |||
| "GL": "\\d{14}", | |||
| "GT": "[\\dA-Z]{4}[\\dA-Z]{20}", | |||
| "HU": "\\d{24}", | |||
| "IS": "\\d{22}", | |||
| "IE": "[\\dA-Z]{4}\\d{14}", | |||
| "IL": "\\d{19}", | |||
| "IT": "[A-Z]\\d{10}[\\dA-Z]{12}", | |||
| "KZ": "\\d{3}[\\dA-Z]{13}", | |||
| "KW": "[A-Z]{4}[\\dA-Z]{22}", | |||
| "LV": "[A-Z]{4}[\\dA-Z]{13}", | |||
| "LB": "\\d{4}[\\dA-Z]{20}", | |||
| "LI": "\\d{5}[\\dA-Z]{12}", | |||
| "LT": "\\d{16}", | |||
| "LU": "\\d{3}[\\dA-Z]{13}", | |||
| "MK": "\\d{3}[\\dA-Z]{10}\\d{2}", | |||
| "MT": "[A-Z]{4}\\d{5}[\\dA-Z]{18}", | |||
| "MR": "\\d{23}", | |||
| "MU": "[A-Z]{4}\\d{19}[A-Z]{3}", | |||
| "MC": "\\d{10}[\\dA-Z]{11}\\d{2}", | |||
| "MD": "[\\dA-Z]{2}\\d{18}", | |||
| "ME": "\\d{18}", | |||
| "NL": "[A-Z]{4}\\d{10}", | |||
| "NO": "\\d{11}", | |||
| "PK": "[\\dA-Z]{4}\\d{16}", | |||
| "PS": "[\\dA-Z]{4}\\d{21}", | |||
| "PL": "\\d{24}", | |||
| "PT": "\\d{21}", | |||
| "RO": "[A-Z]{4}[\\dA-Z]{16}", | |||
| "SM": "[A-Z]\\d{10}[\\dA-Z]{12}", | |||
| "SA": "\\d{2}[\\dA-Z]{18}", | |||
| "RS": "\\d{18}", | |||
| "SK": "\\d{20}", | |||
| "SI": "\\d{15}", | |||
| "ES": "\\d{20}", | |||
| "SE": "\\d{20}", | |||
| "CH": "\\d{5}[\\dA-Z]{12}", | |||
| "TN": "\\d{20}", | |||
| "TR": "\\d{5}[\\dA-Z]{17}", | |||
| "AE": "\\d{3}\\d{16}", | |||
| "GB": "[A-Z]{4}\\d{14}", | |||
| "VG": "[\\dA-Z]{4}\\d{16}" | |||
| }; | |||
| module.exports = { | |||
| required: function(value) { | |||
| return value && value.length > 0; | |||
| }, | |||
| email: testRegexp(reEmail), | |||
| url: testRegexp(reUrl), | |||
| datetime: testRegexp(reDateTime), | |||
| date: testRegexp(reDate), | |||
| time: testRegexp(reTime), | |||
| month: testRegexp(reMonth), | |||
| week: testRegexp(reWeek), | |||
| color: testRegexp(reColor), | |||
| number: testRegexp(reNumber), | |||
| digits: testRegexp(reDigits), | |||
| alphanumeric: testRegexp(reAlphanumeric), | |||
| bic: testRegexp(reBic), | |||
| vat: testRegexp(reVat), | |||
| integer: testRegexp(reInteger), | |||
| ipv4: testRegexp(reIpv4), | |||
| ipv6: testRegexp(reIpv6), | |||
| lettersonly: testRegexp(reLettersonly), | |||
| unicodelettersonly: testRegexp(reUnicodelettersonly), | |||
| letterswithbasicpunc: testRegexp(reLetterswithbasicpunc), | |||
| nowhitespace: testRegexp(reNowhitespace), | |||
| pattern: function(value, param) { | |||
| const re = new RegExp(`^(?:${param})$`); | |||
| return !isValidString(value) || re.test(value); | |||
| }, | |||
| creditcard: function(value) { | |||
| if (!isValidString(value)) { | |||
| return true; | |||
| } | |||
| if (/[^0-9 \-]+/.test(value)) { | |||
| return false; | |||
| } | |||
| value = value.replace(/\D/g, ''); | |||
| if (value.length < 13 || value.length > 19) { | |||
| return false; | |||
| } | |||
| let check = 0, digit = 0, even = false; | |||
| for (let i = value.length - 1; i >= 0; i--) { | |||
| digit = parseInt(value.charAt(i), 10); | |||
| if (even && (digit *= 2) > 9) { | |||
| digit -= 9; | |||
| } | |||
| check += digit; | |||
| even = !even; | |||
| } | |||
| return (check % 10) === 0; | |||
| }, | |||
| iban: function(value) { | |||
| if (!isValidString(value)) { | |||
| return true; | |||
| } | |||
| const iban = value.replace(/ /g, '').toUpperCase(); | |||
| const country = iban.substr(0, 2); | |||
| const pattern = ibanCountryPatterns[country]; | |||
| if (typeof patter !== 'undefined') { | |||
| const re = new RegExp(`^[A-Z]{2}\\d{2}${pattern}$`); | |||
| if (!re.test(iban)) { | |||
| return false; | |||
| } | |||
| } | |||
| let leadingZeroes = true, digits = '', rest = ''; | |||
| const lookup = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; | |||
| const check = iban.substr(4) + iban.substr(0, 4); | |||
| for (let i = 0; i < check.length; i++) { | |||
| const ch = check.charAt(i); | |||
| if (ch !== '0') { | |||
| leadingZeroes = false; | |||
| } | |||
| if (!leadingZeroes) { | |||
| digits += lookup.indexOf(ch); | |||
| } | |||
| } | |||
| for (let i = 0; i < digits.length; i++) { | |||
| const ch = digits.charAt(i); | |||
| const op = rest + ch; | |||
| rest = op % 97; | |||
| } | |||
| return rest === 1; | |||
| }, | |||
| minlength: function(value, param) { | |||
| const length = getLength(value); | |||
| return length == 0 || length >= param; | |||
| }, | |||
| maxlength: function(value, param) { | |||
| const length = getLength(value); | |||
| return length == 0 || length <= param; | |||
| }, | |||
| rangelength: function(value, param) { | |||
| const length = getLength(value); | |||
| return length == 0 || (length >= param['0'] && length <= param['1']); | |||
| }, | |||
| minitems: function(value, param) { | |||
| const length = getLength(value); | |||
| return length == 0 || (Array.isArray(value) && length >= param); | |||
| }, | |||
| maxitems: function(value, param) { | |||
| const length = getLength(value); | |||
| return length == 0 || (Array.isArray(value) && length <= param); | |||
| }, | |||
| rangeitems: function(value, param) { | |||
| const length = getLength(value); | |||
| return length == 0 || (Array.isArray(value) && length >= param['0'] && length <= param['1']); | |||
| }, | |||
| min: function(value, param) { | |||
| return value != null && value != '' && Number(value) >= Number(param); | |||
| }, | |||
| max: function(value, param) { | |||
| return value != null && value != '' && Number(value) <= Number(param); | |||
| }, | |||
| range: function(value, param) { | |||
| return value != null && value != '' && Number(value) >= Number(param['0']) && Number(value) <= Number(param['1']); | |||
| }, | |||
| equalTo: function(value, param) { | |||
| return this.parse(`{{ $_POST.${param.replace(/\[([^\]]+)\]/g, '.$1')} }}`) == value; | |||
| }, | |||
| notEqualTo: function(value, param) { | |||
| return this.parse(`{{ $_POST.${param.replace(/\[([^\]]+)\]/g, '.$1')} }}`) != value; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,21 @@ | |||
| module.exports = { | |||
| exists: async function(value, options) { | |||
| if (!value) return true; | |||
| const db = this.getDbConnection(options.connection); | |||
| const results = await db.from(options.table).where(options.column, value).limit(1); | |||
| return results.length > 0; | |||
| }, | |||
| notexists: async function(value, options) { | |||
| if (!value) return true; | |||
| const db = this.getDbConnection(options.connection); | |||
| const results = await db.from(options.table).where(options.column, value).limit(1); | |||
| return results.length == 0; | |||
| } | |||
| }; | |||
| @@ -0,0 +1,120 @@ | |||
| module.exports = { | |||
| async init(app, meta) { | |||
| const errors = {}; | |||
| if (meta['$_GET'] && Array.isArray(meta['$_GET'])) { | |||
| await this.validateFields(app, meta['$_GET'], app.scope.data['$_GET'], errors); | |||
| } | |||
| if (meta['$_POST'] && Array.isArray(meta['$_POST'])) { | |||
| await this.validateFields(app, meta['$_POST'], app.scope.data['$_POST'], errors); | |||
| } | |||
| if (Object.keys(errors).length) { | |||
| app.res.status(400).json(errors); | |||
| } | |||
| }, | |||
| async validateData(app, data, noError) { | |||
| const errors = {}; | |||
| if (Array.isArray(data)) { | |||
| for (let item of data) { | |||
| for (let rule in item.rules) { | |||
| const options = item.rules[rule]; | |||
| rule = this.getRule(rule); | |||
| if (!await this.validateRule(app, rule, item.value, options)) { | |||
| const t = item.fieldName ? 'form' : 'data'; | |||
| errors[t] = errors[t] || {}; | |||
| errors[t][item.fieldName || item.name] = this.errorMessage(rule, options); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| if (Object.keys(errors).length) { | |||
| if (noError) return false; | |||
| app.res.status(400).json(errors); | |||
| } | |||
| return true; | |||
| }, | |||
| async validateFields(app, fields, parent, errors, fieldname) { | |||
| if (parent == null) return; | |||
| for (let field of fields) { | |||
| let value = parent[field.name]; | |||
| let curFieldname = fieldname ? `${fieldname}[${field.name}]` : field.name; | |||
| if (field.type == 'array' && value == null) { | |||
| value = []; | |||
| } | |||
| if (field.options && field.options.rules) { | |||
| await this.validateField(app, field, value, errors, curFieldname); | |||
| } | |||
| if (field.type == 'object' && field.sub) { | |||
| await this.validateFields(app, field.sub, value, errors, curFieldname); | |||
| } | |||
| if (field.type == 'array' && field.sub && field.sub[0] && field.sub[0].sub) { | |||
| if (Array.isArray(value) && value.length) { | |||
| for (let i = 0; i < value.length; i++) { | |||
| await this.validateFields(app, field.sub[0].sub, value[i], errors, `${curFieldname}[${i}]`); | |||
| } | |||
| } else { | |||
| await this.validateFields(app, field.sub[0].sub, null, errors, `${curFieldname}[0]`); | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| async validateField(app, field, value, errors, fieldname) { | |||
| for (let rule in field.options.rules) { | |||
| const options = field.options.rules[rule]; | |||
| rule = this.getRule(rule); | |||
| if (!await this.validateRule(app, rule, value, options)) { | |||
| const t = field.fieldName ? 'form' : 'data'; | |||
| errors[t] = errors[t] || {}; | |||
| errors[t][fieldname] = this.errorMessage(rule, options); | |||
| } | |||
| } | |||
| }, | |||
| async validateRule(app, rule, value, options = {}) { | |||
| const module = require(`./${rule.module}`); | |||
| return module[rule.method].call(app, value, options.param); | |||
| }, | |||
| errorMessage(rule, options = {}) { | |||
| let message = options.message; | |||
| if (!message) { | |||
| const { validator: messages } = require('../locale/en-US'); | |||
| message = messages[rule.module][rule.method]; | |||
| } | |||
| if (typeof options.param != 'object') { | |||
| options.param = { '0': options.param }; | |||
| } | |||
| return message.replace(/{([^}]+)}/g, (m, i) => options.param[i]); | |||
| }, | |||
| getRule(rule) { | |||
| const colon = rule.indexOf(':'); | |||
| return { | |||
| module: colon > 0 ? rule.substr(0, colon) : 'core', | |||
| method: colon > 0 ? rule.substr(colon + 1) : rule | |||
| }; | |||
| } | |||
| }; | |||
| @@ -0,0 +1,466 @@ | |||
| const unicode = { | |||
| 'L': { | |||
| bmp: 'A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC', | |||
| astral: '\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD83A[\uDC00-\uDCC4]|\uD801[\uDC00-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD80D[\uDC00-\uDC2E]|\uD87E[\uDC00-\uDE1D]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD805[\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD809[\uDC80-\uDD43]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD808[\uDC00-\uDF99]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD811[\uDC00-\uDE46]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD82C[\uDC00\uDC01]|\uD873[\uDC00-\uDEA1]' | |||
| }, | |||
| 'M': { | |||
| bmp: '\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFC-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F', | |||
| astral: '\uD805[\uDCB0-\uDCC3\uDDAF-\uDDB5\uDDB8-\uDDC0\uDDDC\uDDDD\uDE30-\uDE40\uDEAB-\uDEB7\uDF1D-\uDF2B]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD804[\uDC00-\uDC02\uDC38-\uDC46\uDC7F-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD73\uDD80-\uDD82\uDDB3-\uDDC0\uDDCA-\uDDCC\uDE2C-\uDE37\uDEDF-\uDEEA\uDF00-\uDF03\uDF3C\uDF3E-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF57\uDF62\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD81B[\uDF51-\uDF7E\uDF8F-\uDF92]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD82F[\uDC9D\uDC9E]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD83A[\uDCD0-\uDCD6]|\uDB40[\uDD00-\uDDEF]' | |||
| }, | |||
| 'N': { | |||
| bmp: '0-9\xB2\xB3\xB9\xBC-\xBE\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u09F4-\u09F9\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0B72-\u0B77\u0BE6-\u0BF2\u0C66-\u0C6F\u0C78-\u0C7E\u0CE6-\u0CEF\u0D66-\u0D75\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F33\u1040-\u1049\u1090-\u1099\u1369-\u137C\u16EE-\u16F0\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1946-\u194F\u19D0-\u19DA\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\u2070\u2074-\u2079\u2080-\u2089\u2150-\u2182\u2185-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3007\u3021-\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\uA620-\uA629\uA6E6-\uA6EF\uA830-\uA835\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19', | |||
| astral: '\uD800[\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDEE1-\uDEFB\uDF20-\uDF23\uDF41\uDF4A\uDFD1-\uDFD5]|\uD801[\uDCA0-\uDCA9]|\uD803[\uDCFA-\uDCFF\uDE60-\uDE7E]|\uD835[\uDFCE-\uDFFF]|\uD83A[\uDCC7-\uDCCF]|\uD81A[\uDE60-\uDE69\uDF50-\uDF59\uDF5B-\uDF61]|\uD806[\uDCE0-\uDCF2]|\uD804[\uDC52-\uDC6F\uDCF0-\uDCF9\uDD36-\uDD3F\uDDD0-\uDDD9\uDDE1-\uDDF4\uDEF0-\uDEF9]|\uD834[\uDF60-\uDF71]|\uD83C[\uDD00-\uDD0C]|\uD809[\uDC00-\uDC6E]|\uD802[\uDC58-\uDC5F\uDC79-\uDC7F\uDCA7-\uDCAF\uDCFB-\uDCFF\uDD16-\uDD1B\uDDBC\uDDBD\uDDC0-\uDDCF\uDDD2-\uDDFF\uDE40-\uDE47\uDE7D\uDE7E\uDE9D-\uDE9F\uDEEB-\uDEEF\uDF58-\uDF5F\uDF78-\uDF7F\uDFA9-\uDFAF]|\uD805[\uDCD0-\uDCD9\uDE50-\uDE59\uDEC0-\uDEC9\uDF30-\uDF3B]' | |||
| }, | |||
| 'P': { | |||
| bmp: '\x21-\x23\x25-\\x2A\x2C-\x2F\x3A\x3B\\x3F\x40\\x5B-\\x5D\x5F\\x7B\x7D\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65', | |||
| astral: '\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD809[\uDC70-\uDC74]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD836[\uDE87-\uDE8B]|\uD801\uDD6F|\uD82F\uDC9F|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]' | |||
| }, | |||
| 'S': { | |||
| bmp: '\\x24\\x2B\x3C-\x3E\\x5E\x60\\x7C\x7E\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20BE\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u23FA\u2400-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B98-\u2BB9\u2BBD-\u2BC8\u2BCA-\u2BD1\u2BEC-\u2BEF\u2CE5-\u2CEA\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uFB29\uFBB2-\uFBC1\uFDFC\uFDFD\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD', | |||
| astral: '\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDD10-\uDD18\uDD80-\uDD84\uDDC0]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD10-\uDD2E\uDD30-\uDD6B\uDD70-\uDD9A\uDDE6-\uDE02\uDE10-\uDE3A\uDE40-\uDE48\uDE50\uDE51\uDF00-\uDFFF]|\uD83D[\uDC00-\uDD79\uDD7B-\uDDA3\uDDA5-\uDED0\uDEE0-\uDEEC\uDEF0-\uDEF3\uDF00-\uDF73\uDF80-\uDFD4]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C\uDD90-\uDD9B\uDDA0\uDDD0-\uDDFC]|\uD82F\uDC9C|\uD805\uDF3F|\uD802[\uDC77\uDC78\uDEC8]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDE8\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD83B[\uDEF0\uDEF1]' | |||
| }, | |||
| 'Z': { | |||
| bmp: '\x20\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000' | |||
| }, | |||
| 'Ahom': { | |||
| astral: '\uD805[\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF3F]' | |||
| }, | |||
| 'Anatolian_Hieroglyphs' :{ | |||
| astral: '\uD811[\uDC00-\uDE46]' | |||
| }, | |||
| 'Arabic': { | |||
| bmp: '\u0600-\u0604\u0606-\u060B\u060D-\u061A\u061E\u0620-\u063F\u0641-\u064A\u0656-\u066F\u0671-\u06DC\u06DE-\u06FF\u0750-\u077F\u08A0-\u08B4\u08E3-\u08FF\uFB50-\uFBC1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFD\uFE70-\uFE74\uFE76-\uFEFC', | |||
| astral: '\uD803[\uDE60-\uDE7E]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB\uDEF0\uDEF1]' | |||
| }, | |||
| 'Armenian': { | |||
| bmp: '\u0531-\u0556\u0559-\u055F\u0561-\u0587\u058A\u058D-\u058F\uFB13-\uFB17' | |||
| }, | |||
| 'Avestan': { | |||
| astral: '\uD802[\uDF00-\uDF35\uDF39-\uDF3F]' | |||
| }, | |||
| 'Balinese': { | |||
| bmp: '\u1B00-\u1B4B\u1B50-\u1B7C' | |||
| }, | |||
| 'Bamum': { | |||
| bmp: '\uA6A0-\uA6F7', | |||
| astral: '\uD81A[\uDC00-\uDE38]' | |||
| }, | |||
| 'Bassa_Vah': { | |||
| astral: '\uD81A[\uDED0-\uDEED\uDEF0-\uDEF5]' | |||
| }, | |||
| 'Batak': { | |||
| bmp: '\u1BC0-\u1BF3\u1BFC-\u1BFF' | |||
| }, | |||
| 'Bengali': { | |||
| bmp: '\u0980-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09FB' | |||
| }, | |||
| 'Bopomofo': { | |||
| bmp: '\u02EA\u02EB\u3105-\u312D\u31A0-\u31BA' | |||
| }, | |||
| 'Brahmi': { | |||
| astral: '\uD804[\uDC00-\uDC4D\uDC52-\uDC6F\uDC7F]' | |||
| }, | |||
| 'Braille': { | |||
| bmp: '\u2800-\u28FF' | |||
| }, | |||
| 'Buginese': { | |||
| bmp: '\u1A00-\u1A1B\u1A1E\u1A1F' | |||
| }, | |||
| 'Buhid': { | |||
| bmp: '\u1740-\u1753' | |||
| }, | |||
| 'Canadian_Aboriginal': { | |||
| bmp: '\u1400-\u167F\u18B0-\u18F5' | |||
| }, | |||
| 'Carian': { | |||
| astral: '\uD800[\uDEA0-\uDED0]' | |||
| }, | |||
| 'Caucasian_Albanian': { | |||
| astral: '\uD801[\uDD30-\uDD63\uDD6F]' | |||
| }, | |||
| 'Chakma': { | |||
| astral: '\uD804[\uDD00-\uDD34\uDD36-\uDD43]' | |||
| }, | |||
| 'Cham': { | |||
| bmp: '\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA5C-\uAA5F' | |||
| }, | |||
| 'Cherokee': { | |||
| bmp: '\u13A0-\u13F5\u13F8-\u13FD\uAB70-\uABBF' | |||
| }, | |||
| 'Common': { | |||
| bmp: '\0-\x40\\x5B-\x60\\x7B-\xA9\xAB-\xB9\xBB-\xBF\xD7\xF7\u02B9-\u02DF\u02E5-\u02E9\u02EC-\u02FF\u0374\u037E\u0385\u0387\u0589\u0605\u060C\u061B\u061C\u061F\u0640\u06DD\u0964\u0965\u0E3F\u0FD5-\u0FD8\u10FB\u16EB-\u16ED\u1735\u1736\u1802\u1803\u1805\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u2000-\u200B\u200E-\u2064\u2066-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20BE\u2100-\u2125\u2127-\u2129\u212C-\u2131\u2133-\u214D\u214F-\u215F\u2189-\u218B\u2190-\u23FA\u2400-\u2426\u2440-\u244A\u2460-\u27FF\u2900-\u2B73\u2B76-\u2B95\u2B98-\u2BB9\u2BBD-\u2BC8\u2BCA-\u2BD1\u2BEC-\u2BEF\u2E00-\u2E42\u2FF0-\u2FFB\u3000-\u3004\u3006\u3008-\u3020\u3030-\u3037\u303C-\u303F\u309B\u309C\u30A0\u30FB\u30FC\u3190-\u319F\u31C0-\u31E3\u3220-\u325F\u327F-\u32CF\u3358-\u33FF\u4DC0-\u4DFF\uA700-\uA721\uA788-\uA78A\uA830-\uA839\uA92E\uA9CF\uAB5B\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFF70\uFF9E\uFF9F\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD', | |||
| astral: '\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDD10-\uDD18\uDD80-\uDD84\uDDC0]|\uD82F[\uDCA0-\uDCA3]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDFCB\uDFCE-\uDFFF]|\uDB40[\uDC01\uDC20-\uDC7F]|\uD83D[\uDC00-\uDD79\uDD7B-\uDDA3\uDDA5-\uDED0\uDEE0-\uDEEC\uDEF0-\uDEF3\uDF00-\uDF73\uDF80-\uDFD4]|\uD800[\uDD00-\uDD02\uDD07-\uDD33\uDD37-\uDD3F\uDD90-\uDD9B\uDDD0-\uDDFC\uDEE1-\uDEFB]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD66\uDD6A-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDE8\uDF00-\uDF56\uDF60-\uDF71]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD00-\uDD0C\uDD10-\uDD2E\uDD30-\uDD6B\uDD70-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE10-\uDE3A\uDE40-\uDE48\uDE50\uDE51\uDF00-\uDFFF]' | |||
| }, | |||
| 'Coptic': { | |||
| bmp: '\u03E2-\u03EF\u2C80-\u2CF3\u2CF9-\u2CFF' | |||
| }, | |||
| 'Cuneiform': { | |||
| astral: '\uD809[\uDC00-\uDC6E\uDC70-\uDC74\uDC80-\uDD43]|\uD808[\uDC00-\uDF99]' | |||
| }, | |||
| 'Cypriot': { | |||
| astral: '\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F]' | |||
| }, | |||
| 'Cyrillic': { | |||
| bmp: '\u0400-\u0484\u0487-\u052F\u1D2B\u1D78\u2DE0-\u2DFF\uA640-\uA69F\uFE2E\uFE2F' | |||
| }, | |||
| 'Deseret': { | |||
| astral: '\uD801[\uDC00-\uDC4F]' | |||
| }, | |||
| 'Devanagari': { | |||
| bmp: '\u0900-\u0950\u0953-\u0963\u0966-\u097F\uA8E0-\uA8FD' | |||
| }, | |||
| 'Duployan': { | |||
| astral: '\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9C-\uDC9F]' | |||
| }, | |||
| 'Egyptian_Hieroglyphs': { | |||
| astral: '\uD80C[\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]' | |||
| }, | |||
| 'Elbasan': { | |||
| astral: '\uD801[\uDD00-\uDD27]' | |||
| }, | |||
| 'Ethiopic': { | |||
| bmp: '\u1200-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u137C\u1380-\u1399\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E' | |||
| }, | |||
| 'Georgian': { | |||
| bmp: '\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u10FF\u2D00-\u2D25\u2D27\u2D2D' | |||
| }, | |||
| 'Glagolitic': { | |||
| bmp: '\u2C00-\u2C2E\u2C30-\u2C5E' | |||
| }, | |||
| 'Gothic': { | |||
| astral: '\uD800[\uDF30-\uDF4A]' | |||
| }, | |||
| 'Grantha': { | |||
| astral: '\uD804[\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]' | |||
| }, | |||
| 'Greek': { | |||
| bmp: '\u0370-\u0373\u0375-\u0377\u037A-\u037D\u037F\u0384\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03E1\u03F0-\u03FF\u1D26-\u1D2A\u1D5D-\u1D61\u1D66-\u1D6A\u1DBF\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FC4\u1FC6-\u1FD3\u1FD6-\u1FDB\u1FDD-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFE\u2126\uAB65', | |||
| astral: '\uD800[\uDD40-\uDD8C\uDDA0]|\uD834[\uDE00-\uDE45]' | |||
| }, | |||
| 'Gujarati': { | |||
| bmp: '\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AF1\u0AF9' | |||
| }, | |||
| 'Gurmukhi': { | |||
| bmp: '\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75' | |||
| }, | |||
| 'Han': { | |||
| bmp: '\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u3005\u3007\u3021-\u3029\u3038-\u303B\u3400-\u4DB5\u4E00-\u9FD5\uF900-\uFA6D\uFA70-\uFAD9', | |||
| astral: '\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD87E[\uDC00-\uDE1D]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD873[\uDC00-\uDEA1]' | |||
| }, | |||
| 'Hangul': { | |||
| bmp: '\u1100-\u11FF\u302E\u302F\u3131-\u318E\u3200-\u321E\u3260-\u327E\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC' | |||
| }, | |||
| 'Hanunoo': { | |||
| bmp: '\u1720-\u1734' | |||
| }, | |||
| 'Hatran': { | |||
| astral: '\uD802[\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDCFF]' | |||
| }, | |||
| 'Hebrew': { | |||
| bmp: '\u0591-\u05C7\u05D0-\u05EA\u05F0-\u05F4\uFB1D-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFB4F' | |||
| }, | |||
| 'Hiragana': { | |||
| bmp: '\u3041-\u3096\u309D-\u309F', | |||
| astral: '\uD82C\uDC01|\uD83C\uDE00' | |||
| }, | |||
| 'Imperial_Aramaic': { | |||
| astral: '\uD802[\uDC40-\uDC55\uDC57-\uDC5F]' | |||
| }, | |||
| 'Inherited': { | |||
| bmp: '\u0300-\u036F\u0485\u0486\u064B-\u0655\u0670\u0951\u0952\u1AB0-\u1ABE\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C\u200D\u20D0-\u20F0\u302A-\u302D\u3099\u309A\uFE00-\uFE0F\uFE20-\uFE2D', | |||
| astral: '\uD834[\uDD67-\uDD69\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD]|\uD800[\uDDFD\uDEE0]|\uDB40[\uDD00-\uDDEF]' | |||
| }, | |||
| 'Inscriptional_Pahlavi': { | |||
| astral: '\uD802[\uDF60-\uDF72\uDF78-\uDF7F]' | |||
| }, | |||
| 'Inscriptional_Parthian': { | |||
| astral: '\uD802[\uDF40-\uDF55\uDF58-\uDF5F]' | |||
| }, | |||
| 'Javanese': { | |||
| bmp: '\uA980-\uA9CD\uA9D0-\uA9D9\uA9DE\uA9DF' | |||
| }, | |||
| 'Kaithi': { | |||
| astral: '\uD804[\uDC80-\uDCC1]' | |||
| }, | |||
| 'Kannada': { | |||
| bmp: '\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2' | |||
| }, | |||
| 'Katakana': { | |||
| bmp: '\u30A1-\u30FA\u30FD-\u30FF\u31F0-\u31FF\u32D0-\u32FE\u3300-\u3357\uFF66-\uFF6F\uFF71-\uFF9D', | |||
| astral: '\uD82C\uDC00' | |||
| }, | |||
| 'Kayah_Li': { | |||
| bmp: '\uA900-\uA92D\uA92F' | |||
| }, | |||
| 'Kharoshthi': { | |||
| astral: '\uD802[\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F-\uDE47\uDE50-\uDE58]' | |||
| }, | |||
| 'Khmer': { | |||
| bmp: '\u1780-\u17DD\u17E0-\u17E9\u17F0-\u17F9\u19E0-\u19FF' | |||
| }, | |||
| 'Khojki': { | |||
| astral: '\uD804[\uDE00-\uDE11\uDE13-\uDE3D]' | |||
| }, | |||
| 'Khudawadi': { | |||
| astral: '\uD804[\uDEB0-\uDEEA\uDEF0-\uDEF9]' | |||
| }, | |||
| 'Lao': { | |||
| bmp: '\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF' | |||
| }, | |||
| 'Latin': { | |||
| bmp: 'A-Za-z\xAA\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02E0-\u02E4\u1D00-\u1D25\u1D2C-\u1D5C\u1D62-\u1D65\u1D6B-\u1D77\u1D79-\u1DBE\u1E00-\u1EFF\u2071\u207F\u2090-\u209C\u212A\u212B\u2132\u214E\u2160-\u2188\u2C60-\u2C7F\uA722-\uA787\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA7FF\uAB30-\uAB5A\uAB5C-\uAB64\uFB00-\uFB06\uFF21-\uFF3A\uFF41-\uFF5A' | |||
| }, | |||
| 'Lepcha': { | |||
| bmp: '\u1C00-\u1C37\u1C3B-\u1C49\u1C4D-\u1C4F' | |||
| }, | |||
| 'Limbu': { | |||
| bmp: '\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1940\u1944-\u194F' | |||
| }, | |||
| 'Linear_A': { | |||
| astral: '\uD801[\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]' | |||
| }, | |||
| 'Linear_B': { | |||
| astral: '\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA]' | |||
| }, | |||
| 'Lisu': { | |||
| bmp: '\uA4D0-\uA4FF' | |||
| }, | |||
| 'Lycian': { | |||
| astral: '\uD800[\uDE80-\uDE9C]' | |||
| }, | |||
| 'Lydian': { | |||
| astral: '\uD802[\uDD20-\uDD39\uDD3F]' | |||
| }, | |||
| 'Mahajani': { | |||
| astral: '\uD804[\uDD50-\uDD76]' | |||
| }, | |||
| 'Malayalam': { | |||
| bmp: '\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D5F-\u0D63\u0D66-\u0D75\u0D79-\u0D7F' | |||
| }, | |||
| 'Mandaic': { | |||
| bmp: '\u0840-\u085B\u085E' | |||
| }, | |||
| 'Manichaean': { | |||
| astral: '\uD802[\uDEC0-\uDEE6\uDEEB-\uDEF6]' | |||
| }, | |||
| 'Meetei_Mayek': { | |||
| bmp: '\uAAE0-\uAAF6\uABC0-\uABED\uABF0-\uABF9' | |||
| }, | |||
| 'Mende_Kikakui': { | |||
| astral: '\uD83A[\uDC00-\uDCC4\uDCC7-\uDCD6]' | |||
| }, | |||
| 'Meroitic_Cursive': { | |||
| astral: '\uD802[\uDDA0-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDDFF]' | |||
| }, | |||
| 'Meroitic_Hieroglyphs': { | |||
| astral: '\uD802[\uDD80-\uDD9F]' | |||
| }, | |||
| 'Miao': { | |||
| astral: '\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]' | |||
| }, | |||
| 'Modi': { | |||
| astral: '\uD805[\uDE00-\uDE44\uDE50-\uDE59]' | |||
| }, | |||
| 'Mongolian': { | |||
| bmp: '\u1800\u1801\u1804\u1806-\u180E\u1810-\u1819\u1820-\u1877\u1880-\u18AA' | |||
| }, | |||
| 'Mro': { | |||
| astral: '\uD81A[\uDE40-\uDE5E\uDE60-\uDE69\uDE6E\uDE6F]' | |||
| }, | |||
| 'Multani': { | |||
| astral: '\uD804[\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA9]' | |||
| }, | |||
| 'Myanmar': { | |||
| bmp: '\u1000-\u109F\uA9E0-\uA9FE\uAA60-\uAA7F' | |||
| }, | |||
| 'Nabataean': { | |||
| astral: '\uD802[\uDC80-\uDC9E\uDCA7-\uDCAF]' | |||
| }, | |||
| 'New_Tai_Lue': { | |||
| bmp: '\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u19DE\u19DF' | |||
| }, | |||
| 'Nko': { | |||
| bmp: '\u07C0-\u07FA' | |||
| }, | |||
| 'Ogham': { | |||
| bmp: '\u1680-\u169C' | |||
| }, | |||
| 'Ol_Chiki': { | |||
| bmp: '\u1C50-\u1C7F' | |||
| }, | |||
| 'Old_Hungarian': { | |||
| astral: '\uD803[\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDCFF]' | |||
| }, | |||
| 'Old_Italic': { | |||
| astral: '\uD800[\uDF00-\uDF23]' | |||
| }, | |||
| 'Old_North_Arabian': { | |||
| astral: '\uD802[\uDE80-\uDE9F]' | |||
| }, | |||
| 'Old_Permic': { | |||
| astral: '\uD800[\uDF50-\uDF7A]' | |||
| }, | |||
| 'Old_Persian': { | |||
| astral: '\uD800[\uDFA0-\uDFC3\uDFC8-\uDFD5]' | |||
| }, | |||
| 'Old_South_Arabian': { | |||
| astral: '\uD802[\uDE60-\uDE7F]' | |||
| }, | |||
| 'Old_Turkic': { | |||
| astral: '\uD803[\uDC00-\uDC48]' | |||
| }, | |||
| 'Oriya': { | |||
| bmp: '\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B77' | |||
| }, | |||
| 'Osmanya': { | |||
| astral: '\uD801[\uDC80-\uDC9D\uDCA0-\uDCA9]' | |||
| }, | |||
| 'Pahawh_Hmong': { | |||
| astral: '\uD81A[\uDF00-\uDF45\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]' | |||
| }, | |||
| 'Palmyrene': { | |||
| astral: '\uD802[\uDC60-\uDC7F]' | |||
| }, | |||
| 'Pau_Cin_Hau': { | |||
| astral: '\uD806[\uDEC0-\uDEF8]' | |||
| }, | |||
| 'Phags_Pa': { | |||
| bmp: '\uA840-\uA877' | |||
| }, | |||
| 'Phoenician': { | |||
| astral: '\uD802[\uDD00-\uDD1B\uDD1F]' | |||
| }, | |||
| 'Psalter_Pahlavi': { | |||
| astral: '\uD802[\uDF80-\uDF91\uDF99-\uDF9C\uDFA9-\uDFAF]' | |||
| }, | |||
| 'Rejang': { | |||
| bmp: '\uA930-\uA953\uA95F' | |||
| }, | |||
| 'Runic': { | |||
| bmp: '\u16A0-\u16EA\u16EE-\u16F8' | |||
| }, | |||
| 'Samaritan': { | |||
| bmp: '\u0800-\u082D\u0830-\u083E' | |||
| }, | |||
| 'Saurashtra': { | |||
| bmp: '\uA880-\uA8C4\uA8CE-\uA8D9' | |||
| }, | |||
| 'Sharada': { | |||
| astral: '\uD804[\uDD80-\uDDCD\uDDD0-\uDDDF]' | |||
| }, | |||
| 'Shavian': { | |||
| astral: '\uD801[\uDC50-\uDC7F]' | |||
| }, | |||
| 'Siddham': { | |||
| astral: '\uD805[\uDD80-\uDDB5\uDDB8-\uDDDD]' | |||
| }, | |||
| 'SignWriting': { | |||
| astral: '\uD836[\uDC00-\uDE8B\uDE9B-\uDE9F\uDEA1-\uDEAF]' | |||
| }, | |||
| 'Sinhala': { | |||
| bmp: '\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4', | |||
| astral: '\uD804[\uDDE1-\uDDF4]' | |||
| }, | |||
| 'Sora_Sompeng': { | |||
| astral: '\uD804[\uDCD0-\uDCE8\uDCF0-\uDCF9]' | |||
| }, | |||
| 'Sundanese': { | |||
| bmp: '\u1B80-\u1BBF\u1CC0-\u1CC7' | |||
| }, | |||
| 'Syloti_Nagri': { | |||
| bmp: '\uA800-\uA82B' | |||
| }, | |||
| 'Syriac': { | |||
| bmp: '\u0700-\u070D\u070F-\u074A\u074D-\u074F' | |||
| }, | |||
| 'Tagalog': { | |||
| bmp: '\u1700-\u170C\u170E-\u1714' | |||
| }, | |||
| 'Tagbanwa': { | |||
| bmp: '\u1760-\u176C\u176E-\u1770\u1772\u1773' | |||
| }, | |||
| 'Tai_Le': { | |||
| bmp: '\u1950-\u196D\u1970-\u1974' | |||
| }, | |||
| 'Tai_Tham': { | |||
| bmp: '\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD' | |||
| }, | |||
| 'Tai_Viet': { | |||
| bmp: '\uAA80-\uAAC2\uAADB-\uAADF' | |||
| }, | |||
| 'Takri': { | |||
| astral: '\uD805[\uDE80-\uDEB7\uDEC0-\uDEC9]' | |||
| }, | |||
| 'Tamil': { | |||
| bmp: '\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BFA' | |||
| }, | |||
| 'Telugu': { | |||
| bmp: '\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C78-\u0C7F' | |||
| }, | |||
| 'Thaana': { | |||
| bmp: '\u0780-\u07B1' | |||
| }, | |||
| 'Thai': { | |||
| bmp: '\u0E01-\u0E3A\u0E40-\u0E5B' | |||
| }, | |||
| 'Tibetan': { | |||
| bmp: '\u0F00-\u0F47\u0F49-\u0F6C\u0F71-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FD4\u0FD9\u0FDA' | |||
| }, | |||
| 'Tifinagh': { | |||
| bmp: '\u2D30-\u2D67\u2D6F\u2D70\u2D7F' | |||
| }, | |||
| 'Tirhuta': { | |||
| astral: '\uD805[\uDC80-\uDCC7\uDCD0-\uDCD9]' | |||
| }, | |||
| 'Ugaritic': { | |||
| astral: '\uD800[\uDF80-\uDF9D\uDF9F]' | |||
| }, | |||
| 'Vai': { | |||
| bmp: '\uA500-\uA62B' | |||
| }, | |||
| 'Warang_Citi': { | |||
| astral: '\uD806[\uDCA0-\uDCF2\uDCFF]' | |||
| }, | |||
| 'Yi': { | |||
| bmp: '\uA000-\uA48C\uA490-\uA4C6' | |||
| } | |||
| }; | |||
| const isValidString = (s) => typeof s == 'string' && s.length > 0; | |||
| const buildRegexp = (re) => new RegExp(re.replace(/\\p\{([^}]+)\}/g, function(a, b) { | |||
| if (!unicode[b]) throw Error('Invalid expression'); | |||
| var i = unicode[b], c = ''; | |||
| if (i.bmp) { | |||
| c = '[' + i.bmp + ']' + (i.astral ? '|' : ''); | |||
| } | |||
| if (i.astral) { | |||
| c += i.astral; | |||
| } | |||
| return '(?:' + c + ')'; | |||
| })); | |||
| module.exports = { | |||
| unicodelettersonly: function(value, param) { | |||
| const arr = ['L']; | |||
| for (let key in param) { | |||
| arr.push(key); | |||
| } | |||
| return !isValidString(value) || (buildRegexp('^(\\p{' + arr.join('}|\\p{') + '})+$')).test(value) | |||
| }, | |||
| unicodescript: function(value, param) { | |||
| const arr = param.scripts.slice(0); | |||
| for (let key in param) { | |||
| if (key != 'scripts') { | |||
| arr.push(key); | |||
| } | |||
| } | |||
| return !isValidString(value) || (buildRegexp('^(\\p{' + arr.join('}|\\p{') + '})+$')).test(value) | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,82 @@ | |||
| // IMPROVE: Improve this with the actual file field being checked, for now we check all uploaded files | |||
| function getFiles(app, value) { | |||
| const files = []; | |||
| if (app.req.files) { | |||
| for (let field in app.req.files) { | |||
| if (Array.isArray(app.req.files[field])) { | |||
| for (let file of app.req.files[field]) { | |||
| if (!file.truncated) { | |||
| files.push(file); | |||
| } | |||
| } | |||
| } else { | |||
| let file = app.req.files[field]; | |||
| if (!file.truncated) { | |||
| files.push(file); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return files; | |||
| } | |||
| module.exports = { | |||
| accept: function(value, param) { | |||
| const files = getFiles(this, value); | |||
| const allowed = param.replace(/\s/g, '').split(','); | |||
| if (!files.length) { | |||
| return true; | |||
| } | |||
| for (let file of files) { | |||
| if (!allowed.some(allow => { | |||
| if (allow[0] == '.') { | |||
| const re = new RegExp(`\\${allow}$`, 'i'); | |||
| if (re.test(file.name)) { | |||
| return true; | |||
| } | |||
| } else if (/(audio|video|image)\/\*/i.test(allow)) { | |||
| const re = new RegExp(`^${allow.replace('*', '.*')}$`, 'i'); | |||
| if (re.test(file.mimetype)) { | |||
| return true; | |||
| } | |||
| } else if (allow.toLowerCase() == file.mimetype.toLowerCase()) { | |||
| return true; | |||
| } | |||
| })) { | |||
| return false; | |||
| } | |||
| } | |||
| return true; | |||
| }, | |||
| minsize: function(value, param) { | |||
| return !getFiles(this, value).some(file => file.size < param); | |||
| }, | |||
| maxsize: function(value, param) { | |||
| return !getFiles(this, value).some(file => file.size > param); | |||
| }, | |||
| mintotalsize: function(value, param) { | |||
| return getFiles(this, value).reduce((size, file) => size + file.size, 0) >= param; | |||
| }, | |||
| maxtotalsize: function(value, param) { | |||
| return getFiles(this, value).reduce((size, file) => size + file.size, 0) <= param; | |||
| }, | |||
| minfiles: function(value, param) { | |||
| return getFiles(this, value).length >= param; | |||
| }, | |||
| maxfiles: function(value, param) { | |||
| return getFiles(this, value).length <= param; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,22 @@ | |||
| const webhook = require('../core/webhook'); | |||
| const config = require('../setup/config'); | |||
| const fs = require('fs-extra') | |||
| if (fs.existsSync('app/webhooks/stripe')) { | |||
| const stripe = require('stripe')(config.stripe.secretKey); | |||
| const endpointSecret = config.stripe.endpointSecret; | |||
| exports.handler = webhook.createHandler('stripe', (req, res, next) => { | |||
| const sig = req.headers['stripe-signature']; | |||
| try { | |||
| stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret); | |||
| } catch (err) { | |||
| res.status(400).send(`Webhook Error: ${err.message}`); | |||
| return false; | |||
| } | |||
| // return the action name to execute | |||
| return req.body.type; | |||
| }); | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| { | |||
| "name": "rpm_access", | |||
| "version": "1.6.0", | |||
| "private": true, | |||
| "description": "", | |||
| "main": "index.js", | |||
| "engines": { | |||
| "node": ">=14.15.0" | |||
| }, | |||
| "scripts": { | |||
| "start": "node ./index.js" | |||
| }, | |||
| "author": "Wappler", | |||
| "license": "ISC", | |||
| "dependencies": { | |||
| "archiver": "^5.3.0", | |||
| "compression": "^1.7.4", | |||
| "connect-session-knex": "^3.0.0", | |||
| "cookie-parser": "^1.4.6", | |||
| "cors": "^2.8.5", | |||
| "debug": "^4.3.2", | |||
| "dotenv": "^16.0.3", | |||
| "ejs": "^3.1.6", | |||
| "express": "^4.17.1", | |||
| "express-end": "0.0.8", | |||
| "express-fileupload": "^1.2.1", | |||
| "express-session": "^1.17.2", | |||
| "follow-redirects": "^1.14.5", | |||
| "fs-extra": "^10.0.0", | |||
| "http-proxy": "^1.18.1", | |||
| "knex": "^2.3.0", | |||
| "mime-types": "^2.1.34", | |||
| "node-schedule": "^2.0.0", | |||
| "nodemon": "^2.0.15", | |||
| "qs": "^6.10.1", | |||
| "session-file-store": "^1.5.0", | |||
| "socket.io": "^4.6.1", | |||
| "unzipper": "^0.10.11", | |||
| "uuid": "^9.0.0" | |||
| }, | |||
| "nodemonConfig": { | |||
| "watch": [ | |||
| "app", | |||
| "lib", | |||
| "views", | |||
| "extensions", | |||
| "tmp/**/restart.txt" | |||
| ], | |||
| "ext": "ejs,js,json" | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| dmx.config({ | |||
| "index": { | |||
| "repeat1": { | |||
| "meta": null, | |||
| "outputType": "text" | |||
| } | |||
| } | |||
| }); | |||
| @@ -0,0 +1,23 @@ | |||
| <!-- Wappler include head-page="layouts/main" fontawesome_5="cdn" bootstrap5="local" is="dmx-app" id="index" appConnect="local" --> | |||
| <meta name="ac:route" content="/"> | |||
| <div class="container"> | |||
| <div class="row"> | |||
| <div class="col"> | |||
| <form id="form1"> | |||
| <div class="row"> | |||
| <input id="txtSerial" name="serial" type="text" class="form-control" placeholder="Serial #"> | |||
| </div> | |||
| <div class="row"> | |||
| <button id="btn1" class="btn">Button</button> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <form is="dmx-api-form" id="apiform1" method="post"> | |||
| <div class="row"> | |||
| <small class="text-muted" id="lblResult" dmx-repeat:repeat1="form1.txtSerial.value">With faded secondary text</small> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,21 @@ | |||
| <!doctype html> | |||
| <html> | |||
| <head> | |||
| <base href="/"> | |||
| <script src="/dmxAppConnect/dmxAppConnect.js"></script> | |||
| <meta charset="UTF-8"> | |||
| <title>Untitled Document</title> | |||
| <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.14.0/css/all.css" integrity="sha384-HzLeBuhoNPvSl5KYnjx0BT+WB0QEEqLprO+NBkkk5gbc67FTaL7XIGa2w1L0Xbgc" crossorigin="anonymous" /> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |||
| <link rel="stylesheet" href="/bootstrap/5/css/bootstrap.min.css" /> | |||
| <link rel="stylesheet" href="/css/style.css" /> | |||
| <script src="/dmxAppConnect/dmxRouting/dmxRouting.js" defer></script> | |||
| </head> | |||
| <body is="dmx-app" id="main"> | |||
| <div is="dmx-view" id="content"> | |||
| <%- await include(content, locals); %> | |||
| </div> | |||
| <script src="/bootstrap/5/js/bootstrap.bundle.min.js"></script> | |||
| </body> | |||
| </html> | |||