Bläddra i källkod

Initial Commit

master
rpm-mcarman 2 år sedan
incheckning
ca3046e8de
87 ändrade filer med 15438 tillägg och 0 borttagningar
  1. +8
    -0
      .dockerignore
  2. +4
    -0
      .gitignore
  3. +1
    -0
      .npmrc
  4. +38
    -0
      .wappler/project.json
  5. Binär
      .wappler/thumb.png
  6. +10
    -0
      app/config/routes.json
  7. +3
    -0
      app/sockets/connect.json
  8. +3
    -0
      app/sockets/disconnect.json
  9. +3
    -0
      index.js
  10. +72
    -0
      lib/auth/database.js
  11. +149
    -0
      lib/auth/provider.js
  12. +26
    -0
      lib/auth/single.js
  13. +31
    -0
      lib/auth/static.js
  14. +713
    -0
      lib/core/app.js
  15. +97
    -0
      lib/core/async.js
  16. +43
    -0
      lib/core/basicauth.js
  17. +210
    -0
      lib/core/db.js
  18. +96
    -0
      lib/core/diacritics.js
  19. +92
    -0
      lib/core/memoryStore.js
  20. +151
    -0
      lib/core/middleware.js
  21. +752
    -0
      lib/core/parser.js
  22. +97
    -0
      lib/core/path.js
  23. +63
    -0
      lib/core/scope.js
  24. +185
    -0
      lib/core/util.js
  25. +24
    -0
      lib/core/webhook.js
  26. +169
    -0
      lib/formatters/collections.js
  27. +30
    -0
      lib/formatters/conditional.js
  28. +27
    -0
      lib/formatters/core.js
  29. +99
    -0
      lib/formatters/crypto.js
  30. +83
    -0
      lib/formatters/date.js
  31. +17
    -0
      lib/formatters/index.js
  32. +73
    -0
      lib/formatters/number.js
  33. +210
    -0
      lib/formatters/string.js
  34. +59
    -0
      lib/locale/en-US.js
  35. +117
    -0
      lib/modules/api.js
  36. +173
    -0
      lib/modules/arraylist.js
  37. +53
    -0
      lib/modules/auth.js
  38. +185
    -0
      lib/modules/collections.js
  39. +269
    -0
      lib/modules/core.js
  40. +31
    -0
      lib/modules/crypto.js
  41. +24
    -0
      lib/modules/dataset.js
  42. +765
    -0
      lib/modules/dbconnector.js
  43. +412
    -0
      lib/modules/dbupdater.js
  44. +109
    -0
      lib/modules/export.js
  45. +258
    -0
      lib/modules/fs.js
  46. +505
    -0
      lib/modules/image.js
  47. +129
    -0
      lib/modules/import.js
  48. +22
    -0
      lib/modules/jwt.js
  49. +92
    -0
      lib/modules/mail.js
  50. +484
    -0
      lib/modules/metadata.js
  51. +24
    -0
      lib/modules/oauth.js
  52. +47
    -0
      lib/modules/recaptcha.js
  53. +237
    -0
      lib/modules/redis.js
  54. +174
    -0
      lib/modules/s3.js
  55. +172
    -0
      lib/modules/sockets.js
  56. +4993
    -0
      lib/modules/stripe.js
  57. +118
    -0
      lib/modules/upload.js
  58. +18
    -0
      lib/modules/validator.js
  59. +114
    -0
      lib/modules/zip.js
  60. +170
    -0
      lib/oauth/index.js
  61. +98
    -0
      lib/oauth/services.js
  62. +94
    -0
      lib/server.js
  63. +107
    -0
      lib/setup/config.js
  64. +46
    -0
      lib/setup/cron.js
  65. +36
    -0
      lib/setup/database.js
  66. +8
    -0
      lib/setup/redis.js
  67. +249
    -0
      lib/setup/routes.js
  68. +87
    -0
      lib/setup/session.js
  69. +198
    -0
      lib/setup/sockets.js
  70. +61
    -0
      lib/setup/upload.js
  71. +12
    -0
      lib/setup/util.js
  72. +22
    -0
      lib/setup/webhooks.js
  73. +243
    -0
      lib/validator/core.js
  74. +21
    -0
      lib/validator/db.js
  75. +120
    -0
      lib/validator/index.js
  76. +466
    -0
      lib/validator/unicode.js
  77. +82
    -0
      lib/validator/upload.js
  78. +22
    -0
      lib/webhooks/stripe.js
  79. +51
    -0
      package.json
  80. +7
    -0
      public/bootstrap/5/css/bootstrap.min.css
  81. +7
    -0
      public/bootstrap/5/js/bootstrap.bundle.min.js
  82. +0
    -0
      public/css/style.css
  83. +8
    -0
      public/dmxAppConnect/config.js
  84. +8
    -0
      public/dmxAppConnect/dmxAppConnect.js
  85. +8
    -0
      public/dmxAppConnect/dmxRouting/dmxRouting.js
  86. +23
    -0
      views/index.ejs
  87. +21
    -0
      views/layouts/main.ejs

+ 8
- 0
.dockerignore Visa fil

@@ -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

+ 4
- 0
.gitignore Visa fil

@@ -0,0 +1,4 @@
node_modules
.svn
.env
**/.DS_Store

+ 1
- 0
.npmrc Visa fil

@@ -0,0 +1 @@
scripts-prepend-node-path=true

+ 38
- 0
.wappler/project.json Visa fil

@@ -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"
}

Binär
.wappler/thumb.png Visa fil

Före Efter
Bredd: 256  |  Höjd: 320  |  Storlek: 4.4 KiB

+ 10
- 0
app/config/routes.json Visa fil

@@ -0,0 +1,10 @@
{
"routes": [
{
"path": "/",
"page": "index",
"routeType": "page",
"layout": "main"
}
]
}

+ 3
- 0
app/sockets/connect.json Visa fil

@@ -0,0 +1,3 @@
{
"exec": {}
}

+ 3
- 0
app/sockets/disconnect.json Visa fil

@@ -0,0 +1,3 @@
{
"exec": {}
}

+ 3
- 0
index.js Visa fil

@@ -0,0 +1,3 @@
const server = require('./lib/server');
server.start();

+ 72
- 0
lib/auth/database.js Visa fil

@@ -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;

+ 149
- 0
lib/auth/provider.js Visa fil

@@ -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;

+ 26
- 0
lib/auth/single.js Visa fil

@@ -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;

+ 31
- 0
lib/auth/static.js Visa fil

@@ -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;

+ 713
- 0
lib/core/app.js Visa fil

@@ -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;

+ 97
- 0
lib/core/async.js Visa fil

@@ -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));
},
};

+ 43
- 0
lib/core/basicauth.js Visa fil

@@ -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] };
}

+ 210
- 0
lib/core/db.js Visa fil

@@ -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;

+ 96
- 0
lib/core/diacritics.js Visa fil

@@ -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;
}
};

+ 92
- 0
lib/core/memoryStore.js Visa fil

@@ -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
}

+ 151
- 0
lib/core/middleware.js Visa fil

@@ -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));
});
}
};
}
};

+ 752
- 0
lib/core/parser.js Visa fil

@@ -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;

+ 97
- 0
lib/core/path.js Visa fil

@@ -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);
}
};

+ 63
- 0
lib/core/scope.js Visa fil

@@ -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;

+ 185
- 0
lib/core/util.js Visa fil

@@ -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);
}
});
}
};

+ 24
- 0
lib/core/webhook.js Visa fil

@@ -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();
}
}
}
};

+ 169
- 0
lib/formatters/collections.js Visa fil

@@ -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);
},
};

+ 30
- 0
lib/formatters/conditional.js Visa fil

@@ -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;
},
};

+ 27
- 0
lib/formatters/core.js Visa fil

@@ -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);
},
};

+ 99
- 0
lib/formatters/crypto.js Visa fil

@@ -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);
}

+ 83
- 0
lib/formatters/date.js Visa fil

@@ -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();
},
};

+ 17
- 0
lib/formatters/index.js Visa fil

@@ -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
};

+ 73
- 0
lib/formatters/number.js Visa fil

@@ -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';
},
};

+ 210
- 0
lib/formatters/string.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 59
- 0
lib/locale/en-US.js Visa fil

@@ -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.',
},
},
};

+ 117
- 0
lib/modules/api.js Visa fil

@@ -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();
});
},
};

+ 173
- 0
lib/modules/arraylist.js Visa fil

@@ -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;
};

+ 53
- 0
lib/modules/auth.js Visa fil

@@ -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));
},
};

+ 185
- 0
lib/modules/collections.js Visa fil

@@ -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;
},
};

+ 269
- 0
lib/modules/core.js Visa fil

@@ -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();
},
};

+ 31
- 0
lib/modules/crypto.js Visa fil

@@ -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()
}

+ 24
- 0
lib/modules/dataset.js Visa fil

@@ -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.');
},
};

+ 765
- 0
lib/modules/dbconnector.js Visa fil

@@ -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]);
}
}
}
}

+ 412
- 0
lib/modules/dbupdater.js Visa fil

@@ -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);
}
});
},
};

+ 109
- 0
lib/modules/export.js Visa fil

@@ -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');
}

+ 258
- 0
lib/modules/fs.js Visa fil

@@ -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;
}
}

+ 505
- 0
lib/modules/image.js Visa fil

@@ -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();
},
};

+ 129
- 0
lib/modules/import.js Visa fil

@@ -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.');
},
};

+ 22
- 0
lib/modules/jwt.js Visa fil

@@ -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 };
}
};

+ 92
- 0
lib/modules/mail.js Visa fil

@@ -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 });
},
};

+ 484
- 0
lib/modules/metadata.js Visa fil

@@ -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;
},
};

+ 24
- 0
lib/modules/oauth.js Visa fil

@@ -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));
},
};

+ 47
- 0
lib/modules/recaptcha.js Visa fil

@@ -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();
});
};

+ 237
- 0
lib/modules/redis.js Visa fil

@@ -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);
};
}

+ 174
- 0
lib/modules/s3.js Visa fil

@@ -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 });
}
};

+ 172
- 0
lib/modules/sockets.js Visa fil

@@ -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 [];
};

+ 4993
- 0
lib/modules/stripe.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 118
- 0
lib/modules/upload.js Visa fil

@@ -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;
}
}
};

+ 18
- 0
lib/modules/validator.js Visa fil

@@ -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);
},
};

+ 114
- 0
lib/modules/zip.js Visa fil

@@ -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);
},
};

+ 170
- 0
lib/oauth/index.js Visa fil

@@ -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;

+ 98
- 0
lib/oauth/services.js Visa fil

@@ -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'
}
};

+ 94
- 0
lib/server.js Visa fil

@@ -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}`);
});
}
};

+ 107
- 0
lib/setup/config.js Visa fil

@@ -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;

+ 46
- 0
lib/setup/cron.js Visa fil

@@ -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);
}
}

+ 36
- 0
lib/setup/database.js Visa fil

@@ -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 });
});
}

+ 8
- 0
lib/setup/redis.js Visa fil

@@ -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;

+ 249
- 0
lib/setup/routes.js Visa fil

@@ -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);
}
});
}
};

+ 87
- 0
lib/setup/session.js Visa fil

@@ -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);

+ 198
- 0
lib/setup/sockets.js Visa fil

@@ -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;
};

+ 61
- 0
lib/setup/upload.js Visa fil

@@ -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.');
}

+ 12
- 0
lib/setup/util.js Visa fil

@@ -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;
}
}

+ 22
- 0
lib/setup/webhooks.js Visa fil

@@ -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);
}
});
};

+ 243
- 0
lib/validator/core.js Visa fil

@@ -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;
},
};

+ 21
- 0
lib/validator/db.js Visa fil

@@ -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;
}
};

+ 120
- 0
lib/validator/index.js Visa fil

@@ -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
};
}
};

+ 466
- 0
lib/validator/unicode.js Visa fil

@@ -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)
},
};

+ 82
- 0
lib/validator/upload.js Visa fil

@@ -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;
},
};

+ 22
- 0
lib/webhooks/stripe.js Visa fil

@@ -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;
});
}

+ 51
- 0
package.json Visa fil

@@ -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"
}
}

+ 7
- 0
public/bootstrap/5/css/bootstrap.min.css
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 7
- 0
public/bootstrap/5/js/bootstrap.bundle.min.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 0
- 0
public/css/style.css Visa fil


+ 8
- 0
public/dmxAppConnect/config.js Visa fil

@@ -0,0 +1,8 @@
dmx.config({
"index": {
"repeat1": {
"meta": null,
"outputType": "text"
}
}
});

+ 8
- 0
public/dmxAppConnect/dmxAppConnect.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 8
- 0
public/dmxAppConnect/dmxRouting/dmxRouting.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 23
- 0
views/index.ejs Visa fil

@@ -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>

+ 21
- 0
views/layouts/main.ejs Visa fil

@@ -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>

Laddar…
Avbryt
Spara